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:
parent
e2e94e1307
commit
9124a4a138
@ -167,6 +167,9 @@ exc_log_opts = [
|
|||||||
'an exception message (a programming error). If True, '
|
'an exception message (a programming error). If True, '
|
||||||
'raise an exception; if False, use the unformatted '
|
'raise an exception; if False, use the unformatted '
|
||||||
'message.')),
|
'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 = [
|
hash_opts = [
|
||||||
|
@ -12,11 +12,13 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
from oslo_utils import versionutils
|
from oslo_utils import versionutils
|
||||||
from oslo_versionedobjects import base as object_base
|
from oslo_versionedobjects import base as object_base
|
||||||
|
import six
|
||||||
|
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.common.i18n import _
|
from ironic.common.i18n import _
|
||||||
@ -29,6 +31,9 @@ from ironic.objects import notification
|
|||||||
|
|
||||||
REQUIRED_INT_PROPERTIES = ['local_gb', 'cpus', 'memory_mb']
|
REQUIRED_INT_PROPERTIES = ['local_gb', 'cpus', 'memory_mb']
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@base.IronicObjectRegistry.register
|
@base.IronicObjectRegistry.register
|
||||||
class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
||||||
@ -416,6 +421,16 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
|
|||||||
object, e.g.: Node(context)
|
object, e.g.: Node(context)
|
||||||
:raises: InvalidParameterValue if some property values are invalid.
|
: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()
|
updates = self.do_version_changes_for_db()
|
||||||
self._validate_property_values(updates.get('properties'))
|
self._validate_property_values(updates.get('properties'))
|
||||||
if 'driver' in updates and 'driver_internal_info' not in updates:
|
if 'driver' in updates and 'driver_internal_info' not in updates:
|
||||||
|
@ -23,6 +23,7 @@ from testtools import matchers
|
|||||||
from ironic.common import context
|
from ironic.common import context
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic import objects
|
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 base as db_base
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
from ironic.tests.unit.objects import utils as obj_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(test_time, res_updated_at)
|
||||||
self.assertEqual({}, n.driver_internal_info)
|
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):
|
def test_save_updated_at_field(self):
|
||||||
uuid = self.fake_node['uuid']
|
uuid = self.fake_node['uuid']
|
||||||
extra = {"test": 123}
|
extra = {"test": 123}
|
||||||
|
6
releasenotes/notes/bug-2005377-5c63357681a465ec.yaml
Normal file
6
releasenotes/notes/bug-2005377-5c63357681a465ec.yaml
Normal 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).
|
Loading…
x
Reference in New Issue
Block a user