mirror of
https://github.com/ansible/awx.git
synced 2026-03-25 21:05:03 -02:30
Compare commits
4 Commits
AAP-57614-
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7f6f602f | ||
|
|
310dd3e18f | ||
|
|
7c75788b0a | ||
|
|
ab294385ad |
2
Makefile
2
Makefile
@@ -581,7 +581,7 @@ detect-schema-change: genschema
|
|||||||
|
|
||||||
validate-openapi-schema: genschema
|
validate-openapi-schema: genschema
|
||||||
@echo "Validating OpenAPI schema from schema.json..."
|
@echo "Validating OpenAPI schema from schema.json..."
|
||||||
@python3 -c "from openapi_spec_validator import validate; import json; spec = json.load(open('schema.json')); validate(spec); print('✓ OpenAPI Schema is valid!')"
|
@python3 -c "from openapi_spec_validator import validate; import json; spec = json.load(open('schema.json')); validate(spec); print('✓ Schema is valid')"
|
||||||
|
|
||||||
docker-compose-clean: awx/projects
|
docker-compose-clean: awx/projects
|
||||||
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
$(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml rm -sf
|
||||||
|
|||||||
@@ -409,9 +409,11 @@ class Command(BaseCommand):
|
|||||||
del_child_group_pks = list(set(db_children_name_pk_map.values()))
|
del_child_group_pks = list(set(db_children_name_pk_map.values()))
|
||||||
for offset in range(0, len(del_child_group_pks), self._batch_size):
|
for offset in range(0, len(del_child_group_pks), self._batch_size):
|
||||||
child_group_pks = del_child_group_pks[offset : (offset + self._batch_size)]
|
child_group_pks = del_child_group_pks[offset : (offset + self._batch_size)]
|
||||||
for db_child in db_children.filter(pk__in=child_group_pks):
|
children_to_remove = list(db_children.filter(pk__in=child_group_pks))
|
||||||
group_group_count += 1
|
if children_to_remove:
|
||||||
db_group.children.remove(db_child)
|
group_group_count += len(children_to_remove)
|
||||||
|
db_group.children.remove(*children_to_remove)
|
||||||
|
for db_child in children_to_remove:
|
||||||
logger.debug('Group "%s" removed from group "%s"', db_child.name, db_group.name)
|
logger.debug('Group "%s" removed from group "%s"', db_child.name, db_group.name)
|
||||||
# FIXME: Inventory source group relationships
|
# FIXME: Inventory source group relationships
|
||||||
# Delete group/host relationships not present in imported data.
|
# Delete group/host relationships not present in imported data.
|
||||||
@@ -441,11 +443,11 @@ class Command(BaseCommand):
|
|||||||
del_host_pks = list(del_host_pks)
|
del_host_pks = list(del_host_pks)
|
||||||
for offset in range(0, len(del_host_pks), self._batch_size):
|
for offset in range(0, len(del_host_pks), self._batch_size):
|
||||||
del_pks = del_host_pks[offset : (offset + self._batch_size)]
|
del_pks = del_host_pks[offset : (offset + self._batch_size)]
|
||||||
for db_host in db_hosts.filter(pk__in=del_pks):
|
hosts_to_remove = list(db_hosts.filter(pk__in=del_pks))
|
||||||
group_host_count += 1
|
if hosts_to_remove:
|
||||||
if db_host not in db_group.hosts.all():
|
group_host_count += len(hosts_to_remove)
|
||||||
continue
|
db_group.hosts.remove(*hosts_to_remove)
|
||||||
db_group.hosts.remove(db_host)
|
for db_host in hosts_to_remove:
|
||||||
logger.debug('Host "%s" removed from group "%s"', db_host.name, db_group.name)
|
logger.debug('Host "%s" removed from group "%s"', db_host.name, db_group.name)
|
||||||
if settings.SQL_DEBUG:
|
if settings.SQL_DEBUG:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
existing = ct_class.objects.filter(name=default.name, kind=default.kind).first()
|
existing = ct_class.objects.filter(name=default.name, kind=default.kind).first()
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
existing.namespace = default.namespace
|
existing.namespace = default.namespace
|
||||||
|
existing.description = getattr(default, 'description', '')
|
||||||
existing.inputs = {}
|
existing.inputs = {}
|
||||||
existing.injectors = {}
|
existing.injectors = {}
|
||||||
existing.save()
|
existing.save()
|
||||||
@@ -570,7 +571,14 @@ class CredentialType(CommonModelNameNotUnique):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def load_plugin(cls, ns, plugin):
|
def load_plugin(cls, ns, plugin):
|
||||||
# TODO: User "side-loaded" credential custom_injectors isn't supported
|
# TODO: User "side-loaded" credential custom_injectors isn't supported
|
||||||
ManagedCredentialType.registry[ns] = SimpleNamespace(namespace=ns, name=plugin.name, kind='external', inputs=plugin.inputs, backend=plugin.backend)
|
ManagedCredentialType.registry[ns] = SimpleNamespace(
|
||||||
|
namespace=ns,
|
||||||
|
name=plugin.name,
|
||||||
|
kind='external',
|
||||||
|
inputs=plugin.inputs,
|
||||||
|
backend=plugin.backend,
|
||||||
|
description=getattr(plugin, 'plugin_description', ''),
|
||||||
|
)
|
||||||
|
|
||||||
def inject_credential(self, credential, env, safe_env, args, private_data_dir, container_root=None):
|
def inject_credential(self, credential, env, safe_env, args, private_data_dir, container_root=None):
|
||||||
from awx_plugins.interfaces._temporary_private_inject_api import inject_credential
|
from awx_plugins.interfaces._temporary_private_inject_api import inject_credential
|
||||||
@@ -582,7 +590,13 @@ class CredentialTypeHelper:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_creation_params(cls, cred_type):
|
def get_creation_params(cls, cred_type):
|
||||||
if cred_type.kind == 'external':
|
if cred_type.kind == 'external':
|
||||||
return dict(namespace=cred_type.namespace, kind=cred_type.kind, name=cred_type.name, managed=True)
|
return {
|
||||||
|
'namespace': cred_type.namespace,
|
||||||
|
'kind': cred_type.kind,
|
||||||
|
'name': cred_type.name,
|
||||||
|
'managed': True,
|
||||||
|
'description': getattr(cred_type, 'description', ''),
|
||||||
|
}
|
||||||
return dict(
|
return dict(
|
||||||
namespace=cred_type.namespace,
|
namespace=cred_type.namespace,
|
||||||
kind=cred_type.kind,
|
kind=cred_type.kind,
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ class RunnerCallback:
|
|||||||
def artifacts_handler(self, artifact_dir):
|
def artifacts_handler(self, artifact_dir):
|
||||||
success, query_file_contents = try_load_query_file(artifact_dir)
|
success, query_file_contents = try_load_query_file(artifact_dir)
|
||||||
if success:
|
if success:
|
||||||
|
self.delay_update(event_queries_processed=False)
|
||||||
collections_info = collect_queries(query_file_contents)
|
collections_info = collect_queries(query_file_contents)
|
||||||
for collection, data in collections_info.items():
|
for collection, data in collections_info.items():
|
||||||
version = data['version']
|
version = data['version']
|
||||||
@@ -300,24 +301,6 @@ class RunnerCallback:
|
|||||||
else:
|
else:
|
||||||
logger.warning(f'The file {COLLECTION_FILENAME} unexpectedly did not contain ansible_version')
|
logger.warning(f'The file {COLLECTION_FILENAME} unexpectedly did not contain ansible_version')
|
||||||
|
|
||||||
# Write event_queries_processed and installed_collections directly
|
|
||||||
# to the DB instead of using delay_update. delay_update defers
|
|
||||||
# writes until the final job status save, but
|
|
||||||
# events_processed_hook (called from both the task runner after
|
|
||||||
# the final save and the callback receiver after the wrapup
|
|
||||||
# event) needs event_queries_processed=False visible in the DB
|
|
||||||
# to dispatch save_indirect_host_entries. The field defaults to
|
|
||||||
# True, so without a direct write the hook would see True and
|
|
||||||
# skip the dispatch. installed_collections is also written
|
|
||||||
# directly so it is available if the callback receiver
|
|
||||||
# dispatches before the final save.
|
|
||||||
from awx.main.models import Job
|
|
||||||
|
|
||||||
db_updates = {'event_queries_processed': False}
|
|
||||||
if 'installed_collections' in query_file_contents:
|
|
||||||
db_updates['installed_collections'] = query_file_contents['installed_collections']
|
|
||||||
Job.objects.filter(id=self.instance.id).update(**db_updates)
|
|
||||||
|
|
||||||
self.artifacts_processed = True
|
self.artifacts_processed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -305,6 +305,47 @@ class TestINIImports:
|
|||||||
has_host_group = inventory.groups.get(name='has_a_host')
|
has_host_group = inventory.groups.get(name='has_a_host')
|
||||||
assert has_host_group.hosts.count() == 1
|
assert has_host_group.hosts.count() == 1
|
||||||
|
|
||||||
|
@mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
|
||||||
|
def test_overwrite_removes_stale_memberships(self, inventory):
|
||||||
|
"""When overwrite is enabled, host-group and group-group memberships
|
||||||
|
that are no longer in the imported data should be removed."""
|
||||||
|
# First import: parent_group has two children, host_group has two hosts
|
||||||
|
inventory_import.AnsibleInventoryLoader._data = {
|
||||||
|
"_meta": {"hostvars": {"host1": {}, "host2": {}}},
|
||||||
|
"all": {"children": ["ungrouped", "parent_group", "child_a", "child_b", "host_group"]},
|
||||||
|
"parent_group": {"children": ["child_a", "child_b"]},
|
||||||
|
"host_group": {"hosts": ["host1", "host2"]},
|
||||||
|
"ungrouped": {"hosts": []},
|
||||||
|
}
|
||||||
|
cmd = inventory_import.Command()
|
||||||
|
cmd.handle(inventory_id=inventory.pk, source=__file__, overwrite=True)
|
||||||
|
|
||||||
|
parent = inventory.groups.get(name='parent_group')
|
||||||
|
assert set(parent.children.values_list('name', flat=True)) == {'child_a', 'child_b'}
|
||||||
|
host_grp = inventory.groups.get(name='host_group')
|
||||||
|
assert set(host_grp.hosts.values_list('name', flat=True)) == {'host1', 'host2'}
|
||||||
|
|
||||||
|
# Second import: child_b removed from parent_group, host2 moved out of host_group
|
||||||
|
inventory_import.AnsibleInventoryLoader._data = {
|
||||||
|
"_meta": {"hostvars": {"host1": {}, "host2": {}}},
|
||||||
|
"all": {"children": ["ungrouped", "parent_group", "child_a", "child_b", "host_group"]},
|
||||||
|
"parent_group": {"children": ["child_a"]},
|
||||||
|
"host_group": {"hosts": ["host1"]},
|
||||||
|
"ungrouped": {"hosts": ["host2"]},
|
||||||
|
}
|
||||||
|
cmd = inventory_import.Command()
|
||||||
|
cmd.handle(inventory_id=inventory.pk, source=__file__, overwrite=True)
|
||||||
|
|
||||||
|
parent.refresh_from_db()
|
||||||
|
host_grp.refresh_from_db()
|
||||||
|
# child_b should be removed from parent_group
|
||||||
|
assert set(parent.children.values_list('name', flat=True)) == {'child_a'}
|
||||||
|
# host2 should be removed from host_group
|
||||||
|
assert set(host_grp.hosts.values_list('name', flat=True)) == {'host1'}
|
||||||
|
# host2 and child_b should still exist in the inventory, just not in those groups
|
||||||
|
assert inventory.hosts.filter(name='host2').exists()
|
||||||
|
assert inventory.groups.filter(name='child_b').exists()
|
||||||
|
|
||||||
@mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
|
@mock.patch.object(inventory_import, 'AnsibleInventoryLoader', MockLoader)
|
||||||
def test_recursive_group_error(self, inventory):
|
def test_recursive_group_error(self, inventory):
|
||||||
inventory_import.AnsibleInventoryLoader._data = {
|
inventory_import.AnsibleInventoryLoader._data = {
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from awx.main.models import Credential, CredentialType
|
from awx.main.models import Credential, CredentialType
|
||||||
|
from awx.main.models.credential import CredentialTypeHelper, ManagedCredentialType
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
@@ -78,3 +82,53 @@ def test_credential_context_property_independent_instances():
|
|||||||
assert cred1.context == {'key1': 'value1'}
|
assert cred1.context == {'key1': 'value1'}
|
||||||
assert cred2.context == {'key2': 'value2'}
|
assert cred2.context == {'key2': 'value2'}
|
||||||
assert cred1.context is not cred2.context
|
assert cred1.context is not cred2.context
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_plugin_passes_description():
|
||||||
|
plugin = SimpleNamespace(name='test_plugin', inputs={'fields': []}, backend=None, plugin_description='A test plugin')
|
||||||
|
CredentialType.load_plugin('test_ns', plugin)
|
||||||
|
entry = ManagedCredentialType.registry['test_ns']
|
||||||
|
assert entry.description == 'A test plugin'
|
||||||
|
del ManagedCredentialType.registry['test_ns']
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_plugin_missing_description():
|
||||||
|
plugin = SimpleNamespace(name='test_plugin', inputs={'fields': []}, backend=None)
|
||||||
|
CredentialType.load_plugin('test_ns', plugin)
|
||||||
|
entry = ManagedCredentialType.registry['test_ns']
|
||||||
|
assert entry.description == ''
|
||||||
|
del ManagedCredentialType.registry['test_ns']
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_creation_params_external_includes_description():
|
||||||
|
cred_type = SimpleNamespace(namespace='test_ns', kind='external', name='Test', description='My description')
|
||||||
|
params = CredentialTypeHelper.get_creation_params(cred_type)
|
||||||
|
assert params['description'] == 'My description'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_creation_params_external_missing_description():
|
||||||
|
cred_type = SimpleNamespace(namespace='test_ns', kind='external', name='Test')
|
||||||
|
params = CredentialTypeHelper.get_creation_params(cred_type)
|
||||||
|
assert params['description'] == ''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_setup_tower_managed_defaults_updates_description():
|
||||||
|
registry_entry = SimpleNamespace(
|
||||||
|
namespace='test_ns',
|
||||||
|
kind='external',
|
||||||
|
name='Test Plugin',
|
||||||
|
inputs={'fields': []},
|
||||||
|
backend=None,
|
||||||
|
description='Updated description',
|
||||||
|
)
|
||||||
|
# Create an existing credential type with no description
|
||||||
|
ct = CredentialType.objects.create(name='Test Plugin', kind='external', namespace='old_ns')
|
||||||
|
assert ct.description == ''
|
||||||
|
|
||||||
|
with mock.patch.dict(ManagedCredentialType.registry, {'test_ns': registry_entry}, clear=True):
|
||||||
|
CredentialType._setup_tower_managed_defaults()
|
||||||
|
|
||||||
|
ct.refresh_from_db()
|
||||||
|
assert ct.description == 'Updated description'
|
||||||
|
assert ct.namespace == 'test_ns'
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ cython==3.1.3
|
|||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
daphne==4.2.1
|
daphne==4.2.1
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
dispatcherd[pg-notify]==2026.02.26
|
dispatcherd[pg-notify]==2026.3.25
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
# via -r /awx_devel/requirements/requirements.in
|
# via -r /awx_devel/requirements/requirements.in
|
||||||
|
|||||||
Reference in New Issue
Block a user