English string validation to error code validation

This commit is contained in:
David O Neill
2024-02-21 18:44:31 +00:00
committed by Dave
parent b076cb00a9
commit ca8085fe7e
4 changed files with 131 additions and 24 deletions

View File

@@ -1,6 +1,7 @@
# Python # Python
import contextlib import contextlib
import logging import logging
import psycopg
import threading import threading
import time import time
import os import os
@@ -13,7 +14,7 @@ from django.conf import settings, UserSettingsHolder
from django.core.cache import cache as django_cache from django.core.cache import cache as django_cache
from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation from django.core.exceptions import ImproperlyConfigured, SynchronousOnlyOperation
from django.db import transaction, connection from django.db import transaction, connection
from django.db.utils import Error as DBError, ProgrammingError from django.db.utils import DatabaseError, ProgrammingError
from django.utils.functional import cached_property from django.utils.functional import cached_property
# Django REST Framework # Django REST Framework
@@ -80,18 +81,26 @@ def _ctit_db_wrapper(trans_safe=False):
logger.debug('Obtaining database settings in spite of broken transaction.') logger.debug('Obtaining database settings in spite of broken transaction.')
transaction.set_rollback(False) transaction.set_rollback(False)
yield yield
except DBError as exc: except ProgrammingError as e:
# Exception raised for programming errors
# Examples may be table not found or already exists,
# this generally means we can't fetch Tower configuration
# because the database hasn't actually finished migrating yet;
# this is usually a sign that a service in a container (such as ws_broadcast)
# has come up *before* the database has finished migrating, and
# especially that the conf.settings table doesn't exist yet
# syntax error in the SQL statement, wrong number of parameters specified, etc.
if trans_safe: if trans_safe:
level = logger.warning logger.debug(f'Database settings are not available, using defaults. error: {str(e)}')
if isinstance(exc, ProgrammingError): else:
if 'relation' in str(exc) and 'does not exist' in str(exc): logger.exception('Error modifying something related to database settings.')
# this generally means we can't fetch Tower configuration except DatabaseError as e:
# because the database hasn't actually finished migrating yet; if trans_safe:
# this is usually a sign that a service in a container (such as ws_broadcast) cause = e.__cause__
# has come up *before* the database has finished migrating, and if cause and hasattr(cause, 'sqlstate'):
# especially that the conf.settings table doesn't exist yet sqlstate = cause.sqlstate
level = logger.debug sqlstate_str = psycopg.errors.lookup(sqlstate)
level(f'Database settings are not available, using defaults. error: {str(exc)}') logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
else: else:
logger.exception('Error modifying something related to database settings.') logger.exception('Error modifying something related to database settings.')
finally: finally:

View File

@@ -6,6 +6,7 @@ import itertools
import json import json
import logging import logging
import os import os
import psycopg
from io import StringIO from io import StringIO
from contextlib import redirect_stdout from contextlib import redirect_stdout
import shutil import shutil
@@ -630,10 +631,18 @@ def cluster_node_heartbeat(dispatch_time=None, worker_tasks=None):
logger.error("Host {} last checked in at {}, marked as lost.".format(other_inst.hostname, other_inst.last_seen)) logger.error("Host {} last checked in at {}, marked as lost.".format(other_inst.hostname, other_inst.last_seen))
except DatabaseError as e: except DatabaseError as e:
if 'did not affect any rows' in str(e): cause = e.__cause__
logger.debug('Another instance has marked {} as lost'.format(other_inst.hostname)) if cause and hasattr(cause, 'sqlstate'):
sqlstate = cause.sqlstate
sqlstate_str = psycopg.errors.lookup(sqlstate)
logger.debug('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
if sqlstate == psycopg.errors.NoData:
logger.debug('Another instance has marked {} as lost'.format(other_inst.hostname))
else:
logger.exception("Error marking {} as lost.".format(other_inst.hostname))
else: else:
logger.exception('Error marking {} as lost'.format(other_inst.hostname)) logger.exception('No SQL state available. Error marking {} as lost'.format(other_inst.hostname))
# Run local reaper # Run local reaper
if worker_tasks is not None: if worker_tasks is not None:
@@ -788,10 +797,19 @@ def update_inventory_computed_fields(inventory_id):
try: try:
i.update_computed_fields() i.update_computed_fields()
except DatabaseError as e: except DatabaseError as e:
if 'did not affect any rows' in str(e): # https://github.com/django/django/blob/eff21d8e7a1cb297aedf1c702668b590a1b618f3/django/db/models/base.py#L1105
logger.debug('Exiting duplicate update_inventory_computed_fields task.') # django raises DatabaseError("Forced update did not affect any rows.")
return
raise # if sqlstate is set then there was a database error and otherwise will re-raise that error
cause = e.__cause__
if cause and hasattr(cause, 'sqlstate'):
sqlstate = cause.sqlstate
sqlstate_str = psycopg.errors.lookup(sqlstate)
logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
raise
# otherwise
logger.debug('Exiting duplicate update_inventory_computed_fields task.')
def update_smart_memberships_for_inventory(smart_inventory): def update_smart_memberships_for_inventory(smart_inventory):

View File

@@ -0,0 +1,64 @@
import pytest
from unittest.mock import MagicMock, patch
from awx.main.tasks.system import update_inventory_computed_fields
from awx.main.models import Inventory
from django.db import DatabaseError
@pytest.fixture
def mock_logger():
with patch("awx.main.tasks.system.logger") as logger:
yield logger
@pytest.fixture
def mock_inventory():
return MagicMock(spec=Inventory)
def test_update_inventory_computed_fields_existing_inventory(mock_logger, mock_inventory):
# Mocking the Inventory.objects.filter method to return a non-empty queryset
with patch("awx.main.tasks.system.Inventory.objects.filter") as mock_filter:
mock_filter.return_value.exists.return_value = True
mock_filter.return_value.__getitem__.return_value = mock_inventory
# Mocking the update_computed_fields method
with patch.object(mock_inventory, "update_computed_fields") as mock_update_computed_fields:
update_inventory_computed_fields(1)
# Assertions
mock_filter.assert_called_once_with(id=1)
mock_update_computed_fields.assert_called_once()
# You can add more assertions based on your specific requirements
def test_update_inventory_computed_fields_missing_inventory(mock_logger):
# Mocking the Inventory.objects.filter method to return an empty queryset
with patch("awx.main.tasks.system.Inventory.objects.filter") as mock_filter:
mock_filter.return_value.exists.return_value = False
update_inventory_computed_fields(1)
# Assertions
mock_filter.assert_called_once_with(id=1)
mock_logger.error.assert_called_once_with("Update Inventory Computed Fields failed due to missing inventory: 1")
def test_update_inventory_computed_fields_database_error_nosqlstate(mock_logger, mock_inventory):
# Mocking the Inventory.objects.filter method to return a non-empty queryset
with patch("awx.main.tasks.system.Inventory.objects.filter") as mock_filter:
mock_filter.return_value.exists.return_value = True
mock_filter.return_value.__getitem__.return_value = mock_inventory
# Mocking the update_computed_fields method
with patch.object(mock_inventory, "update_computed_fields") as mock_update_computed_fields:
# Simulating the update_computed_fields method to explicitly raise a DatabaseError
mock_update_computed_fields.side_effect = DatabaseError("Some error")
update_inventory_computed_fields(1)
# Assertions
mock_filter.assert_called_once_with(id=1)
mock_update_computed_fields.assert_called_once()
mock_inventory.update_computed_fields.assert_called_once()

View File

@@ -7,6 +7,7 @@ import json
import yaml import yaml
import logging import logging
import time import time
import psycopg
import os import os
import subprocess import subprocess
import re import re
@@ -23,7 +24,7 @@ from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.db import connection, transaction, ProgrammingError, IntegrityError from django.db import connection, DatabaseError, transaction, ProgrammingError, IntegrityError
from django.db.models.fields.related import ForeignObjectRel, ManyToManyField from django.db.models.fields.related import ForeignObjectRel, ManyToManyField
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor, ManyToManyDescriptor from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor, ManyToManyDescriptor
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@@ -1155,11 +1156,26 @@ def create_partition(tblname, start=None):
f'ALTER TABLE {tblname} ATTACH PARTITION {tblname}_{partition_label} ' f'ALTER TABLE {tblname} ATTACH PARTITION {tblname}_{partition_label} '
f'FOR VALUES FROM (\'{start_timestamp}\') TO (\'{end_timestamp}\');' f'FOR VALUES FROM (\'{start_timestamp}\') TO (\'{end_timestamp}\');'
) )
except (ProgrammingError, IntegrityError) as e: except (ProgrammingError, IntegrityError) as e:
if 'already exists' in str(e): cause = e.__cause__
logger.info(f'Caught known error due to partition creation race: {e}') if cause and hasattr(cause, 'sqlstate'):
else: # 42P07 = DuplicateTable
raise sqlstate = cause.sqlstate
sqlstate_str = psycopg.errors.lookup(sqlstate)
if psycopg.errors.DuplicateTable == sqlstate:
logger.info(f'Caught known error due to partition creation race: {e}')
else:
logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
raise
except DatabaseError as e:
cause = e.__cause__
if cause and hasattr(cause, 'sqlstate'):
sqlstate = cause.sqlstate
sqlstate_str = psycopg.errors.lookup(sqlstate)
logger.error('SQL Error state: {} - {}'.format(sqlstate, sqlstate_str))
raise
def cleanup_new_process(func): def cleanup_new_process(func):