Truncate node text fields when too long

Pushing too long messages in the node last_error and maintenance reason can
cause node.save() failures, leaving the node in a transient state, with no
conductor actually handling it anymore.

Change-Id: Id4db377781f83cf4d97564ced9622d5a8a8c67af
Story: #2005377
Task: #30359
This commit is contained in:
Raphael Glon 2019-04-05 16:28:11 +02:00
parent e2e94e1307
commit 9124a4a138
4 changed files with 84 additions and 1 deletions

View File

@ -167,6 +167,9 @@ exc_log_opts = [
'an exception message (a programming error). If True, '
'raise an exception; if False, use the unformatted '
'message.')),
cfg.IntOpt('log_in_db_max_size', default=4096,
help=_('Max number of characters of any node '
'last_error/maintenance_reason pushed to database.'))
]
hash_opts = [

View File

@ -12,11 +12,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from oslo_log import log
from oslo_utils import strutils
from oslo_utils import uuidutils
from oslo_utils import versionutils
from oslo_versionedobjects import base as object_base
import six
from ironic.common import exception
from ironic.common.i18n import _
@ -29,6 +31,9 @@ from ironic.objects import notification
REQUIRED_INT_PROPERTIES = ['local_gb', 'cpus', 'memory_mb']
CONF = cfg.CONF
LOG = log.getLogger(__name__)
@base.IronicObjectRegistry.register
class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
@ -416,6 +421,16 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
object, e.g.: Node(context)
:raises: InvalidParameterValue if some property values are invalid.
"""
for attr_name in ('last_error', 'maintenance_reason'):
attr_value = getattr(self, attr_name, '')
if (attr_value and isinstance(attr_value, six.string_types) and
len(attr_value) > CONF.log_in_db_max_size):
LOG.info('Truncating too long %s to %s characters for node %s',
attr_name, CONF.log_in_db_max_size, self.uuid)
setattr(self, attr_name,
attr_value[0:CONF.log_in_db_max_size])
updates = self.do_version_changes_for_db()
self._validate_property_values(updates.get('properties'))
if 'driver' in updates and 'driver_internal_info' not in updates:

View File

@ -23,6 +23,7 @@ from testtools import matchers
from ironic.common import context
from ironic.common import exception
from ironic import objects
from ironic.objects import node as node_objects
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
@ -171,6 +172,64 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
self.assertEqual(test_time, res_updated_at)
self.assertEqual({}, n.driver_internal_info)
@mock.patch.object(node_objects, 'LOG', autospec=True)
def test_save_truncated(self, log_mock):
uuid = self.fake_node['uuid']
test_time = datetime.datetime(2000, 1, 1, 0, 0)
with mock.patch.object(self.dbapi, 'get_node_by_uuid',
autospec=True) as mock_get_node:
mock_get_node.return_value = self.fake_node
with mock.patch.object(self.dbapi, 'update_node',
autospec=True) as mock_update_node:
mock_update_node.return_value = db_utils.get_test_node(
properties={'fake': 'property'}, driver='fake-driver',
driver_internal_info={}, updated_at=test_time)
n = objects.Node.get(self.context, uuid)
self.assertEqual({'private_state': 'secret value'},
n.driver_internal_info)
n.properties = {'fake': 'property'}
n.driver = 'fake-driver'
last_error = 'BOOM' * 2000
maintenance_reason = last_error
n.last_error = last_error
n.maintenance_reason = maintenance_reason
n.save()
self.assertEqual([
mock.call.info(
'Truncating too long %s to %s characters for node %s',
'last_error',
node_objects.CONF.log_in_db_max_size,
uuid),
mock.call.info(
'Truncating too long %s to %s characters for node %s',
'maintenance_reason',
node_objects.CONF.log_in_db_max_size,
uuid)],
log_mock.mock_calls)
mock_get_node.assert_called_once_with(uuid)
mock_update_node.assert_called_once_with(
uuid,
{
'properties': {'fake': 'property'},
'driver': 'fake-driver',
'driver_internal_info': {},
'version': objects.Node.VERSION,
'maintenance_reason':
maintenance_reason[
0:node_objects.CONF.log_in_db_max_size],
'last_error':
last_error[
0:node_objects.CONF.log_in_db_max_size]
}
)
self.assertEqual(self.context, n._context)
res_updated_at = (n.updated_at).replace(tzinfo=None)
self.assertEqual(test_time, res_updated_at)
self.assertEqual({}, n.driver_internal_info)
def test_save_updated_at_field(self):
uuid = self.fake_node['uuid']
extra = {"test": 123}

View File

@ -0,0 +1,6 @@
---
fixes:
- Fixes node last_error and maintenance_reason overflow with too long error
messages, preventing the object from being correctly committed to database.
The maximum message length can be customized through a new configuration
parameter, ``[DEFAULT]/log_in_db_max_size`` (default, 4096 characters).