Use a stable hostname to render nova.conf

OVS introduced a new service called ovs-record-hostname.service which
records the hostname on the first start in the ovs database to identify
the ovn chassis, this is how it achieved a stable hostname and be
resilient to the changes in the FQDN when the DNS gets available.

This change introduces the same approach for nova-compute charm. In the
first run of the NovaComputeHostInfoContext the value passed in the
context as host_fqdn is stored in the unit's kv db, and re-used on every
subsequent call.

This change affects only new installs since the hint to store (or not)
the host fqdn is set in the install hook.

Change-Id: I2aa74442ec25b21201a47070077df27899465814
Closes-Bug: #1896630
This commit is contained in:
Felipe Reyes 2023-02-15 11:43:40 -03:00
parent 75a3dbd0ef
commit 2bad8a0522
6 changed files with 211 additions and 28 deletions

View File

@ -16,8 +16,14 @@ import json
import os
import platform
import shutil
import socket
import uuid
from typing import (
Dict,
Optional,
)
from charmhelpers.core.unitdata import kv
from charmhelpers.contrib.openstack import context
@ -36,6 +42,7 @@ from charmhelpers.core.hookenv import (
relation_ids,
related_units,
service_name,
DEBUG,
ERROR,
INFO,
)
@ -1031,3 +1038,91 @@ class NovaComputeSWTPMContext(context.OSContextGenerator):
}
return ctxt
class NovaComputeHostInfoContext(context.HostInfoContext):
USE_FQDN_KEY = 'nova-compute-charm-use-fqdn'
RECORD_FQDN_KEY = 'nova-compute-charm-record-fqdn'
FQDN_KEY = 'nova-compute-charm-fqdn'
def __init__(self):
super().__init__(use_fqdn_hint_cb=self._use_fqdn_hint)
@classmethod
def _use_fqdn_hint(cls):
"""Hint for whether FQDN should be used for agent registration
:returns: True or False
:rtype: bool
"""
db = kv()
return db.get(cls.USE_FQDN_KEY, False)
@classmethod
def set_fqdn_hint(cls, value: bool):
"""Set FQDN hint.
:param value: the value to set the FQDN hint to
"""
db = kv()
db.set(cls.USE_FQDN_KEY, value)
db.flush()
@classmethod
def set_record_fqdn_hint(cls, value: bool):
"""Set the hint to record the FQDN and reuse it on every call.
:param value: the value to the record FQDN hint to.
"""
db = kv()
db.set(cls.RECORD_FQDN_KEY, value)
db.flush()
@classmethod
def get_record_fqdn_hint(cls) -> bool:
"""Get the hint to record the FQDN."""
db = kv()
return db.get(cls.RECORD_FQDN_KEY, False)
def set_record_fqdn(self, fqdn: str):
"""Store in the unit's DB the FQDN.
:param fqdn: the FQDN to store.
"""
db = kv()
db.set(self.FQDN_KEY, fqdn)
db.flush()
def get_record_fqdn(self) -> Optional[str]:
"""Get the stored FQDN."""
db = kv()
return db.get(self.FQDN_KEY, None)
def __call__(self) -> Dict[str, str]:
"""Generate host info context.
Extends the __call__() method to save the host fqdn used in the first
run when the self.get_record_fqdn_hint() returns True, this allows to
give a stable hostname to the nova-compute service over its entire
life (see LP: #1896630).
:returns: context with host info
"""
name = socket.gethostname()
if self.get_record_fqdn_hint():
if not self.get_record_fqdn():
log('Saving host fqdn', level=DEBUG)
self.set_record_fqdn(self._get_canonical_name(name) or name)
host_fqdn = self.get_record_fqdn()
log('Re-using saved host fqdn stored: %s' % host_fqdn, level=DEBUG)
else:
host_fqdn = self._get_canonical_name(name) or name
ctxt = {
'host_fqdn': host_fqdn,
'host': name,
'use_fqdn_hint': (
self.use_fqdn_hint_cb() if self.use_fqdn_hint_cb else False)
}
return ctxt

View File

@ -125,7 +125,6 @@ from nova_compute_utils import (
resume_unit_helper,
remove_old_packages,
MULTIPATH_PACKAGES,
USE_FQDN_KEY,
SWTPM_PACKAGES,
)
@ -143,6 +142,7 @@ from nova_compute_context import (
NovaAPIAppArmorContext,
NovaComputeAppArmorContext,
NovaNetworkAppArmorContext,
NovaComputeHostInfoContext,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.core.sysctl import create as create_sysctl
@ -174,9 +174,8 @@ def install():
# units with OpenStack release Stein or newer.
release = os_release('nova-common')
if CompareOpenStackReleases(release) >= 'stein':
db = kv()
db.set(USE_FQDN_KEY, True)
db.flush()
NovaComputeHostInfoContext.set_fqdn_hint(True)
NovaComputeHostInfoContext.set_record_fqdn_hint(True) # LP: #1896630
install_vaultlocker()

View File

@ -116,6 +116,7 @@ from nova_compute_context import (
NovaComputePlacementContext,
NovaComputeSWTPMContext,
VirtMkfsContext,
NovaComputeHostInfoContext,
)
import charmhelpers.contrib.openstack.vaultlocker as vaultlocker
@ -206,18 +207,6 @@ MOUNT_DEPENDENCY_OVERRIDE = '99-mount.conf'
LIBVIRT_TYPES = ['kvm', 'qemu', 'lxc']
USE_FQDN_KEY = 'nova-compute-charm-use-fqdn'
def use_fqdn_hint():
"""Hint for whether FQDN should be used for agent registration
:returns: True or False
:rtype: bool
"""
db = kv()
return db.get(USE_FQDN_KEY, False)
BASE_RESOURCE_MAP = {
NOVA_CONF: {
@ -263,7 +252,7 @@ BASE_RESOURCE_MAP = {
vaultlocker.VAULTLOCKER_BACKEND),
context.IdentityCredentialsContext(
rel_name='cloud-credentials'),
context.HostInfoContext(use_fqdn_hint_cb=use_fqdn_hint),
NovaComputeHostInfoContext(),
VirtMkfsContext(),
],
},

View File

@ -1519,3 +1519,106 @@ class NovaComputeVirtMkfsContext(CharmTestCase):
self.assertEqual({'virt_mkfs': ('virt_mkfs = default=mkfs.ext4\n'
'virt_mkfs = windows=mkfs.ntfs')},
ctxt())
class TestNovaComputeHostInfoContext(CharmTestCase):
def setUp(self):
super().setUp(context, TO_PATCH)
self.config.side_effect = self.test_config.get
self.os_release.return_value = 'ussuri'
def test_use_fqdn_hint(self):
self.kv().get.return_value = False
ctxt = context.NovaComputeHostInfoContext
self.assertEqual(ctxt._use_fqdn_hint(), False)
self.kv().get.return_value = True
self.assertEqual(ctxt._use_fqdn_hint(), True)
def test_set_fqdn_hint(self):
context.NovaComputeHostInfoContext.set_fqdn_hint(True)
self.kv().set.assert_called_with(
context.NovaComputeHostInfoContext.USE_FQDN_KEY, True
)
def test_set_record_fqdn_hint(self):
context.NovaComputeHostInfoContext.set_record_fqdn_hint(True)
self.kv().set.assert_called_with(
context.NovaComputeHostInfoContext.RECORD_FQDN_KEY, True
)
def test_get_record_fqdn_hint(self):
self.kv().get.side_effect = dict().get
self.assertFalse(
context.NovaComputeHostInfoContext.get_record_fqdn_hint()
)
data = {context.NovaComputeHostInfoContext.RECORD_FQDN_KEY: True}
self.kv().get.side_effect = lambda x, y: data[x]
self.assertTrue(
context.NovaComputeHostInfoContext.get_record_fqdn_hint()
)
@patch('socket.getaddrinfo')
@patch('socket.gethostname')
def test_get_canonical_name_gethostname(self, gethostname, getaddrinfo):
gethostname.return_value = 'bar'
def raise_oserror(name, *args, **kwargs):
self.assertEqual(name, 'bar')
raise OSError()
getaddrinfo.side_effect = raise_oserror
data = {
context.NovaComputeHostInfoContext.USE_FQDN_KEY: True,
context.NovaComputeHostInfoContext.RECORD_FQDN_KEY: True,
}
self.kv().get.side_effect = lambda x, y: data.get(x, y)
self.kv().set.side_effect = lambda x, y: data.__setitem__(x, y)
ctxt = context.NovaComputeHostInfoContext()
self.assertEqual(ctxt._get_canonical_name('bar'), '')
def fake_getaddrinfo(name, *args, **kwargs):
self.assertEqual(name, 'foobar')
return [[0, 1, 2, 'bar.example.com']]
getaddrinfo.reset_mock()
getaddrinfo.side_effect = fake_getaddrinfo
self.assertEqual(ctxt._get_canonical_name('foobar'), 'bar.example.com')
gethostname.return_value = 'foobar'
self.assertEqual(ctxt(),
{'host_fqdn': 'bar.example.com',
'host': 'foobar',
'use_fqdn_hint': True})
@patch('socket.getaddrinfo')
@patch('socket.gethostname')
def test_call_unstable_hostname(self, gethostname, getaddrinfo):
def raise_oserror(name, *args, **kwargs):
raise OSError()
def fake_getaddrinfo(name, *args, **kwargs):
return [[0, 1, 2, 'bar.example.com']]
getaddrinfo.side_effect = raise_oserror
gethostname.return_value = 'bar'
data = {
context.NovaComputeHostInfoContext.USE_FQDN_KEY: True,
context.NovaComputeHostInfoContext.RECORD_FQDN_KEY: True,
}
self.kv().get.side_effect = lambda x, y: data.get(x, y)
self.kv().set.side_effect = lambda x, y: data.__setitem__(x, y)
ctxt = context.NovaComputeHostInfoContext()
self.assertEqual(ctxt(),
{'host_fqdn': 'bar',
'host': 'bar',
'use_fqdn_hint': True})
# After the first run socket.getaddrinfo() returns a valid fqdn
# provided by the DNS, but by this time the host_fqdn is stored and
# re-used.
getaddrinfo.reset_mock()
getaddrinfo.side_effect = fake_getaddrinfo
self.assertEqual(ctxt(),
{'host_fqdn': 'bar',
'host': 'bar',
'use_fqdn_hint': True})
getaddrinfo.assert_not_called()

View File

@ -25,7 +25,9 @@ from unittest.mock import (
from test_utils import CharmTestCase
from nova_compute_context import (
NovaComputeHostInfoContext
)
with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec:
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
lambda *args, **kwargs: f(*args, **kwargs))
@ -146,7 +148,7 @@ class NovaComputeRelationsTests(CharmTestCase):
self.is_container.return_value = False
@patch.object(hooks, 'configure_extra_repositories')
@patch.object(hooks, 'kv')
@patch('nova_compute_context.kv')
@patch.object(hooks, 'os_release')
def test_install_hook(self, _os_release, _kv, _configure_extra_repos):
repo = 'cloud:precise-grizzly'
@ -163,8 +165,10 @@ class NovaComputeRelationsTests(CharmTestCase):
kv = MagicMock()
_kv.return_value = kv
hooks.install()
kv.set.assert_called_once_with(hooks.USE_FQDN_KEY, True)
kv.flush.assert_called_once_with()
kv.set.assert_any_call(NovaComputeHostInfoContext.USE_FQDN_KEY, True)
kv.set.assert_any_call(
NovaComputeHostInfoContext.RECORD_FQDN_KEY, True)
kv.flush.assert_any_call()
def test_configure_extra_repositories(self):
"""Tests configuring of extra repositories"""

View File

@ -1477,13 +1477,6 @@ class NovaComputeUtilsTests(CharmTestCase):
call('foo'),
])
@patch.object(utils, 'kv')
def test_use_fqdn_hint(self, _kv):
_kv().get.return_value = False
self.assertEquals(utils.use_fqdn_hint(), False)
_kv().get.return_value = True
self.assertEquals(utils.use_fqdn_hint(), True)
@patch.object(utils, 'render')
def test_install_mount_override(self, render):
utils.install_mount_override('/srv/test')