Add virt/node module for stable uuids

Related to blueprint stable-compute-uuid

Change-Id: Ie8897a843fadf325c696b411923f075e237a7342
This commit is contained in:
Dan Smith 2022-11-02 09:19:35 -07:00
parent 6abbcc5033
commit 3b33b0938e
6 changed files with 274 additions and 0 deletions

View File

@ -2496,3 +2496,7 @@ class PlacementPciMixedTraitsException(PlacementPciException):
class ReimageException(NovaException):
msg_fmt = _("Reimaging volume failed.")
class InvalidNodeConfiguration(NovaException):
msg_fmt = _('Invalid node identity configuration: %(reason)s')

View File

@ -66,6 +66,7 @@ from nova.tests import fixtures as nova_fixtures
from nova.tests.unit import matchers
from nova import utils
from nova.virt import images
from nova.virt import node
CONF = cfg.CONF
@ -299,6 +300,11 @@ class TestCase(base.BaseTestCase):
# Reset the placement client singleton
report.PLACEMENTCLIENT = None
# Reset our local node uuid cache (and avoid writing to the
# local filesystem when we generate a new one).
node.LOCAL_NODE_UUID = None
self.useFixture(nova_fixtures.ComputeNodeIdFixture())
def _setup_cells(self):
"""Setup a normal cellsv2 environment.

View File

@ -1849,3 +1849,15 @@ class ImportModulePoisonFixture(fixtures.Fixture):
# will not work to cause a failure in the test.
if self.fail_message:
raise ImportError(self.fail_message)
class ComputeNodeIdFixture(fixtures.Fixture):
def setUp(self):
super().setUp()
self.useFixture(fixtures.MockPatch(
'nova.virt.node.read_local_node_uuid',
lambda: None))
self.useFixture(fixtures.MockPatch(
'nova.virt.node.write_local_node_uuid',
lambda uuid: None))

View File

@ -1230,6 +1230,8 @@ class _IntegratedTestBase(test.TestCase, PlacementInstanceHelperMixin):
self.glance = self.useFixture(nova_fixtures.GlanceFixture(self))
self.policy = self.useFixture(nova_fixtures.RealPolicyFixture())
self.useFixture(nova_fixtures.ComputeNodeIdFixture())
self.notifier = self.useFixture(
nova_fixtures.NotificationFixture(self))
@ -1301,6 +1303,8 @@ class ProviderUsageBaseTestCase(test.TestCase, PlacementInstanceHelperMixin):
self.placement = self.useFixture(func_fixtures.PlacementFixture()).api
self.useFixture(nova_fixtures.AllServicesCurrent())
self.useFixture(nova_fixtures.ComputeNodeIdFixture())
self.notifier = self.useFixture(
nova_fixtures.NotificationFixture(self))

View File

@ -0,0 +1,141 @@
# Copyright 2022 Red Hat, inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from unittest import mock
import uuid
import fixtures
from oslo_config import cfg
from oslo_utils.fixture import uuidsentinel as uuids
import testtools
from nova import exception
from nova import test
from nova.tests import fixtures as nova_fixtures
from nova.virt import node
CONF = cfg.CONF
# NOTE(danms): We do not inherit from test.TestCase because we need
# our node methods not stubbed out in order to exercise them.
class TestNodeIdentity(testtools.TestCase):
def flags(self, **kw):
"""Override flag variables for a test."""
group = kw.pop('group', None)
for k, v in kw.items():
CONF.set_override(k, v, group)
def setUp(self):
super().setUp()
self.useFixture(nova_fixtures.ConfFixture(CONF))
self.tempdir = self.useFixture(fixtures.TempDir()).path
self.identity_file = os.path.join(self.tempdir, node.COMPUTE_ID_FILE)
self.fake_config_files = ['%s/etc/nova.conf' % self.tempdir,
'%s/etc/nova/nova.conf' % self.tempdir,
'%s/opt/etc/nova/nova.conf' % self.tempdir]
for fn in self.fake_config_files:
os.makedirs(os.path.dirname(fn))
self.flags(state_path=self.tempdir,
config_file=self.fake_config_files)
node.LOCAL_NODE_UUID = None
def test_generate_local_node_uuid(self):
node_uuid = uuids.node
node.write_local_node_uuid(node_uuid)
e = self.assertRaises(exception.InvalidNodeConfiguration,
node.write_local_node_uuid, 'anything')
self.assertIn(
'Identity file %s appeared unexpectedly' % self.identity_file,
str(e))
def test_generate_local_node_uuid_unexpected_open_fail(self):
with mock.patch('builtins.open') as mock_open:
mock_open.side_effect = IndexError()
e = self.assertRaises(exception.InvalidNodeConfiguration,
node.write_local_node_uuid, 'foo')
self.assertIn('Unable to write uuid to %s' % (
self.identity_file), str(e))
def test_generate_local_node_uuid_unexpected_write_fail(self):
with mock.patch('builtins.open') as mock_open:
mock_open.return_value.write.side_effect = IndexError()
e = self.assertRaises(exception.InvalidNodeConfiguration,
node.write_local_node_uuid, 'foo')
self.assertIn('Unable to write uuid to %s' % (
self.identity_file), str(e))
def test_get_local_node_uuid_simple_exists(self):
node_uuid = uuids.node
with test.patch_open('%s/etc/nova/compute_id' % self.tempdir,
node_uuid):
self.assertEqual(node_uuid, node.get_local_node_uuid())
def test_get_local_node_uuid_simple_exists_whitespace(self):
node_uuid = uuids.node
# Make sure we strip whitespace from the file contents
with test.patch_open('%s/etc/nova/compute_id' % self.tempdir,
' %s \n' % node_uuid):
self.assertEqual(node_uuid, node.get_local_node_uuid())
def test_get_local_node_uuid_simple_generate(self):
self.assertIsNone(node.LOCAL_NODE_UUID)
node_uuid1 = node.get_local_node_uuid()
self.assertEqual(node_uuid1, node.LOCAL_NODE_UUID)
node_uuid2 = node.get_local_node_uuid()
self.assertEqual(node_uuid2, node.LOCAL_NODE_UUID)
# Make sure we got the same thing each time, and that it's a
# valid uuid. Since we provided no uuid, it must have been
# generated the first time and read/returned the second.
self.assertEqual(node_uuid1, node_uuid2)
uuid.UUID(node_uuid1)
# Try to read it directly to make sure the file was really
# created and with the right value.
self.assertEqual(node_uuid1, node.read_local_node_uuid())
def test_get_local_node_uuid_two(self):
node_uuid = uuids.node
# Write the uuid to two of our locations
for cf in (self.fake_config_files[0], self.fake_config_files[1]):
open(os.path.join(os.path.dirname(cf),
node.COMPUTE_ID_FILE), 'w').write(node_uuid)
# Make sure we got the expected uuid and that no exceptions
# were raised about the files disagreeing
self.assertEqual(node_uuid, node.get_local_node_uuid())
def test_get_local_node_uuid_two_mismatch(self):
node_uuids = [uuids.node1, uuids.node2]
# Write a different uuid to each file
for id, fn in zip(node_uuids, self.fake_config_files):
open(os.path.join(
os.path.dirname(fn),
node.COMPUTE_ID_FILE), 'w').write(id)
# Make sure we get an error that identifies the mismatching
# file with its uuid, as well as what we expected to find
e = self.assertRaises(exception.InvalidNodeConfiguration,
node.get_local_node_uuid)
expected = ('UUID %s in %s does not match %s' % (
node_uuids[1],
os.path.join(os.path.dirname(self.fake_config_files[1]),
'compute_id'),
node_uuids[0]))
self.assertIn(expected, str(e))

107
nova/virt/node.py Normal file
View File

@ -0,0 +1,107 @@
# Copyright 2022 Red Hat, inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import uuid
from oslo_utils import uuidutils
import nova.conf
from nova import exception
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
COMPUTE_ID_FILE = 'compute_id'
LOCAL_NODE_UUID = None
def write_local_node_uuid(node_uuid):
# We only ever write an identity file in the CONF.state_path
# location
fn = os.path.join(CONF.state_path, COMPUTE_ID_FILE)
# Try to create the identity file and write our uuid into it. Fail
# if the file exists (since it shouldn't if we made it here).
try:
open(fn, 'x').write(node_uuid)
except FileExistsError:
# If the file exists, we must either fail or re-survey all the
# potential files. If we just read and return it, it could be
# inconsistent with files in the other locations.
raise exception.InvalidNodeConfiguration(
reason='Identity file %s appeared unexpectedly' % fn)
except Exception as e:
raise exception.InvalidNodeConfiguration(
reason='Unable to write uuid to %s: %s' % (fn, e))
LOG.info('Wrote node identity %s to %s', node_uuid, fn)
def read_local_node_uuid():
locations = ([os.path.dirname(f) for f in CONF.config_file] +
[CONF.state_path])
uuids = []
found = []
for location in locations:
fn = os.path.join(location, COMPUTE_ID_FILE)
try:
# UUIDs should be 36 characters in canonical format. Read
# a little more to be graceful about whitespace in/around
# the actual value we want to read. However, it must parse
# to a legit UUID once we strip the whitespace.
with open(fn) as f:
content = f.read(40)
node_uuid = str(uuid.UUID(content.strip()))
except FileNotFoundError:
continue
except ValueError:
raise exception.InvalidNodeConfiguration(
reason='Unable to parse UUID from %s' % fn)
uuids.append(node_uuid)
found.append(fn)
if uuids:
# Any identities we found must be consistent, or we fail
first = uuids[0]
for i, (node_uuid, fn) in enumerate(zip(uuids, found)):
if node_uuid != first:
raise exception.InvalidNodeConfiguration(
reason='UUID %s in %s does not match %s' % (
node_uuid, fn, uuids[i - 1]))
LOG.info('Determined node identity %s from %s', first, found[0])
return first
else:
return None
def get_local_node_uuid():
"""Read or create local node uuid file.
:returns: UUID string read from file, or generated
"""
global LOCAL_NODE_UUID
if LOCAL_NODE_UUID is not None:
return LOCAL_NODE_UUID
node_uuid = read_local_node_uuid()
if not node_uuid:
node_uuid = uuidutils.generate_uuid()
LOG.info('Generated node identity %s', node_uuid)
write_local_node_uuid(node_uuid)
LOCAL_NODE_UUID = node_uuid
return node_uuid