mirror of
https://github.com/ansible/awx.git
synced 2026-03-11 14:39:30 -02:30
add per-inventory insights credential
This commit is contained in:
@@ -1112,7 +1112,8 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
fields = ('*', 'organization', 'kind', 'host_filter', 'variables', 'has_active_failures',
|
||||
'total_hosts', 'hosts_with_active_failures', 'total_groups',
|
||||
'groups_with_active_failures', 'has_inventory_sources',
|
||||
'total_inventory_sources', 'inventory_sources_with_failures')
|
||||
'total_inventory_sources', 'inventory_sources_with_failures',
|
||||
'insights_credential',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InventorySerializer, self).get_related(obj)
|
||||
@@ -1133,6 +1134,8 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}),
|
||||
instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}),
|
||||
))
|
||||
if obj.insights_credential:
|
||||
res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk})
|
||||
if obj.organization:
|
||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||
return res
|
||||
|
||||
@@ -14,7 +14,6 @@ import subprocess
|
||||
import sys
|
||||
import logging
|
||||
import requests
|
||||
import urlparse
|
||||
from base64 import b64encode
|
||||
from collections import OrderedDict
|
||||
|
||||
@@ -2088,37 +2087,36 @@ class HostInsights(GenericAPIView):
|
||||
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_500_INTERNAL_SERVER_ERROR)
|
||||
return (dict(error=_('SSLError while trying to connect to {}').format(url)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
except requests.exceptions.Timeout:
|
||||
return (dict(error='Request to {} timed out.'.format(url)), status.HTTP_504_GATEWAY_TIMEOUT)
|
||||
return (dict(error=_('Request to {} timed out.').format(url)), status.HTTP_504_GATEWAY_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (dict(error='Unkown exception {} while trying to GET {}'.format(e, url)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return (dict(error=_('Unkown exception {} while trying to GET {}').format(e, url)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
if res.status_code != 200:
|
||||
return (dict(error='Failed to gather reports and maintenance plans from Insights API. Server responded with {} status code and message {}'.format(res.status_code, res.content)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
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_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
try:
|
||||
return (dict(insights_content=res.json()), status.HTTP_200_OK)
|
||||
except ValueError:
|
||||
return (dict(error='Expected JSON response from Insights but instead got {}'.format(res.content)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return (dict(error=_('Expected JSON response from Insights but instead got {}').format(res.content)), status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
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)
|
||||
|
||||
creds = Credential.objects.filter(credential_type__name='Red Hat Satellite 6', credential_type__kind='cloud', credential_type__managed_by_tower=True)
|
||||
if creds.count() > 0:
|
||||
cred = creds[0]
|
||||
if host.inventory and host.inventory.insights_credential:
|
||||
cred = host.inventory.insights_credential
|
||||
else:
|
||||
return Response(dict(error='No Insights Credential found for the Inventory, "{}", that this host belongs to.'.format(host.inventory.name)), status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(dict(error=_('No Insights Credential found for the Inventory, "{}", that this host belongs to.').format(host.inventory.name)), status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
url = 'https://access.redhat.com/r/insights/v3/systems/{}/reports/'.format(host.insights_system_id)
|
||||
url = settings.INSIGHTS_URL_BASE + '/r/insights/v3/systems/{}/reports/'.format(host.insights_system_id)
|
||||
(username, password) = self._extract_insights_creds(cred)
|
||||
(msg, status) = self.get_insights(url, username, password)
|
||||
return Response(msg, status=status)
|
||||
(msg, err_code) = self.get_insights(url, username, password)
|
||||
return Response(msg, status=err_code)
|
||||
|
||||
|
||||
class GroupList(ListCreateAPIView):
|
||||
|
||||
@@ -243,4 +243,14 @@ class Migration(migrations.Migration):
|
||||
name='insights_system_id',
|
||||
field=models.TextField(default=None, help_text='Red Hat Insights host unique identifier.', null=True, db_index=True, blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventory',
|
||||
name='insights_credential',
|
||||
field=models.ForeignKey(related_name='insights_credential', default=None, blank=True, on_delete=models.deletion.SET_NULL, to='main.Credential', help_text='Credentials to be used by hosts belonging to this invtory when accessing Red Hat Insights API.', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventory',
|
||||
name='kind',
|
||||
field=models.CharField(default=b'', help_text='Kind of inventory being represented.', max_length=32, blank=True, choices=[(b'', 'Hosts have a direct link to this inventory.'), (b'smart', 'Hosts for inventory generated using the host_filter property.')]),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
('modified', models.DateTimeField(default=None, editable=False)),
|
||||
('description', models.TextField(default=b'', blank=True)),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('kind', models.CharField(max_length=32, choices=[(b'ssh', 'SSH'), (b'vault', 'Vault'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'cloud', 'Cloud')])),
|
||||
('kind', models.CharField(max_length=32, choices=[(b'ssh', 'SSH'), (b'vault', 'Vault'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'cloud', 'Cloud'), (b'insights', 'Insights')])),
|
||||
('managed_by_tower', models.BooleanField(default=False, editable=False)),
|
||||
('inputs', awx.main.fields.CredentialTypeInputField(default={}, blank=True)),
|
||||
('injectors', awx.main.fields.CredentialTypeInjectorField(default={}, blank=True)),
|
||||
|
||||
@@ -399,7 +399,8 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
('vault', _('Vault')),
|
||||
('net', _('Network')),
|
||||
('scm', _('Source Control')),
|
||||
('cloud', _('Cloud'))
|
||||
('cloud', _('Cloud')),
|
||||
('insights', _('Insights')),
|
||||
)
|
||||
|
||||
kind = models.CharField(
|
||||
@@ -919,3 +920,32 @@ def azure_rm(cls):
|
||||
}]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@CredentialType.default
|
||||
def insights(cls):
|
||||
return cls(
|
||||
kind='insights',
|
||||
name='Insights Basic Auth',
|
||||
managed_by_tower=True,
|
||||
inputs={
|
||||
'fields': [{
|
||||
'id': 'username',
|
||||
'label': 'Basic Auth Username',
|
||||
'type': 'string'
|
||||
}, {
|
||||
'id': 'password',
|
||||
'label': 'Basic Auth Password',
|
||||
'type': 'string',
|
||||
'secret': True
|
||||
}],
|
||||
'required': ['username', 'password'],
|
||||
},
|
||||
injectors={
|
||||
'extra_vars': {
|
||||
"scm_username": "{{username}}",
|
||||
"scm_password": "{{password}}",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -143,6 +143,16 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin):
|
||||
'use_role',
|
||||
'admin_role',
|
||||
])
|
||||
insights_credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='insights_credential',
|
||||
help_text=_('Credentials to be used by hosts belonging to this invtory when accessing Red Hat Insights API.'),
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:inventory_detail', kwargs={'pk': self.pk}, request=request)
|
||||
@@ -345,6 +355,11 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin):
|
||||
group_pks = self.groups.values_list('pk', flat=True)
|
||||
return self.groups.exclude(parents__pk__in=group_pks).distinct()
|
||||
|
||||
def clean_insights_credential(self):
|
||||
if self.insights_credential and self.insights_credential.credential_type.kind != 'insights':
|
||||
raise ValidationError(_('Invalid CredentialType'))
|
||||
return self.insights_credential
|
||||
|
||||
|
||||
class Host(CommonModelNameNotUnique):
|
||||
'''
|
||||
|
||||
@@ -141,7 +141,10 @@ class ProjectOptions(models.Model):
|
||||
return None
|
||||
cred = self.credential
|
||||
if cred:
|
||||
if cred.kind != 'scm':
|
||||
if self.scm_type == 'insights':
|
||||
if cred.kind != 'insights':
|
||||
raise ValidationError(_("Credential kind must be 'insights'."))
|
||||
elif cred.kind != 'scm':
|
||||
raise ValidationError(_("Credential kind must be 'scm'."))
|
||||
try:
|
||||
if self.scm_type == 'insights':
|
||||
|
||||
@@ -287,3 +287,17 @@ class TestControlledBySCM:
|
||||
r = options(reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}),
|
||||
admin_user, expect=200)
|
||||
assert 'POST' not in r.data['actions']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestInsightsCredential:
|
||||
def test_insights_credential(self, patch, insights_inventory, admin_user, insights_credential):
|
||||
patch(insights_inventory.get_absolute_url(),
|
||||
{'insights_credential': insights_credential.id}, admin_user,
|
||||
expect=200)
|
||||
|
||||
def test_non_insights_credential(self, patch, insights_inventory, admin_user, scm_credential):
|
||||
patch(insights_inventory.get_absolute_url(),
|
||||
{'insights_credential': scm_credential.id}, admin_user,
|
||||
expect=400)
|
||||
|
||||
|
||||
15
awx/main/tests/functional/api/test_project.py
Normal file
15
awx/main/tests/functional/api/test_project.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestInsightsCredential:
|
||||
def test_insights_credential(self, patch, insights_project, admin_user, insights_credential):
|
||||
patch(insights_project.get_absolute_url(),
|
||||
{'credential': insights_credential.id}, admin_user,
|
||||
expect=200)
|
||||
|
||||
def test_non_insights_credential(self, patch, insights_project, admin_user, scm_credential):
|
||||
patch(insights_project.get_absolute_url(),
|
||||
{'credential': scm_credential.id}, admin_user,
|
||||
expect=400)
|
||||
|
||||
@@ -178,6 +178,11 @@ def user_project(user):
|
||||
return Project.objects.create(name="test-user-project", created_by=owner, description="test-user-project-desc")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def insights_project():
|
||||
return Project.objects.create(name="test-insights-project", scm_type="insights")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance(settings):
|
||||
return Instance.objects.create(uuid=settings.SYSTEM_UUID, hostname="instance.example.org", capacity=100)
|
||||
@@ -216,6 +221,20 @@ def credentialtype_vault():
|
||||
return vault_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credentialtype_scm():
|
||||
scm_type = CredentialType.defaults['scm']()
|
||||
scm_type.save()
|
||||
return scm_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credentialtype_insights():
|
||||
insights_type = CredentialType.defaults['insights']()
|
||||
insights_type.save()
|
||||
return insights_type
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credential(credentialtype_aws):
|
||||
return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred',
|
||||
@@ -240,6 +259,18 @@ def machine_credential(credentialtype_ssh):
|
||||
inputs={'username': 'test_user', 'password': 'pas4word'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scm_credential(credentialtype_scm):
|
||||
return Credential.objects.create(credential_type=credentialtype_scm, name='scm-cred',
|
||||
inputs={'username': 'optimus', 'password': 'prime'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def insights_credential(credentialtype_insights):
|
||||
return Credential.objects.create(credential_type=credentialtype_insights, name='insights-cred',
|
||||
inputs={'username': 'morocco_mole', 'password': 'secret_squirrel'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def org_credential(organization, credentialtype_aws):
|
||||
return Credential.objects.create(credential_type=credentialtype_aws, name='test-cred',
|
||||
@@ -252,6 +283,13 @@ def inventory(organization):
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def insights_inventory(inventory):
|
||||
inventory.scm_type = 'insights'
|
||||
inventory.save()
|
||||
return inventory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scm_inventory_source(inventory, project):
|
||||
inv_src = InventorySource(
|
||||
|
||||
@@ -19,6 +19,7 @@ def test_default_cred_types():
|
||||
'azure_rm',
|
||||
'cloudforms',
|
||||
'gce',
|
||||
'insights',
|
||||
'net',
|
||||
'openstack',
|
||||
'satellite6',
|
||||
|
||||
@@ -12,6 +12,11 @@ from awx.api.views import (
|
||||
HostInsights,
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
Host,
|
||||
Inventory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response_new(mocker):
|
||||
@@ -143,10 +148,10 @@ class TestHostInsights():
|
||||
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, 'hello world!'))
|
||||
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. Server responded with 500 status code and message hello world!'
|
||||
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_malformed_json_content(self, patch_parent, mocker):
|
||||
view = HostInsights()
|
||||
@@ -164,3 +169,36 @@ class TestHostInsights():
|
||||
assert msg['error'] == 'Expected JSON response from Insights but instead got booo!'
|
||||
assert code == 500
|
||||
|
||||
#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'] == 'No Insights Credential found for the Inventory, "inventory_name_here", that this host belongs to.'
|
||||
assert resp.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user