Add virt/node module for stable uuids
Related to blueprint stable-compute-uuid Change-Id: Ie8897a843fadf325c696b411923f075e237a7342
This commit is contained in:
parent
6abbcc5033
commit
3b33b0938e
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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))
|
|
@ -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
|
Loading…
Reference in New Issue