Add metalsmith_instances instance option config_drive

This has sub-options ``cloud_config` and ``meta_data`` and allows
customization of data packaged in the config-drive to be consumed by
cloud-init.

This is needed by TripleO to provide an equivalent to the current
heat/nova based firstboot customization support.

Change-Id: Ie384292a3310cb06e908dd9027e9300ca108e7dd
This commit is contained in:
Steve Baker 2020-11-09 14:58:04 +13:00
parent 1c06881c32
commit 22a4e4071c
7 changed files with 86 additions and 18 deletions

View File

@ -35,11 +35,16 @@ class GenericConfig(object):
:ivar ssh_keys: List of SSH public keys. :ivar ssh_keys: List of SSH public keys.
:ivar user_data: User data as a string. :ivar user_data: User data as a string.
:ivar meta_data: Dict of data to add to the generated ``meta_data``
""" """
def __init__(self, ssh_keys=None, user_data=None): def __init__(self, ssh_keys=None, user_data=None, meta_data=None):
self.ssh_keys = ssh_keys or [] self.ssh_keys = ssh_keys or []
self.user_data = user_data self.user_data = user_data
if meta_data and not isinstance(meta_data, dict):
raise TypeError('Custom meta_data must be a dictionary, '
'got %r' % meta_data)
self.meta_data = meta_data
def generate(self, node, hostname=None): def generate(self, node, hostname=None):
"""Generate the config drive information. """Generate the config drive information.
@ -62,17 +67,24 @@ class GenericConfig(object):
else: else:
ssh_keys = self.ssh_keys ssh_keys = self.ssh_keys
metadata = {'public_keys': ssh_keys, meta_data = {}
'uuid': node.id, if self.meta_data:
'name': node.name, meta_data.update(self.meta_data)
'hostname': hostname,
'launch_index': 0, meta_data.update({
'availability_zone': '', 'public_keys': ssh_keys,
'files': [], 'uuid': node.id,
'meta': {}} 'name': node.name,
'hostname': hostname
})
meta_data.setdefault('launch_index', 0)
meta_data.setdefault('availability_zone', '')
meta_data.setdefault('files', [])
meta_data.setdefault('meta', {})
user_data = self.populate_user_data() user_data = self.populate_user_data()
return {'meta_data': metadata, return {'meta_data': meta_data,
'user_data': user_data} 'user_data': user_data}
def populate_user_data(self): def populate_user_data(self):
@ -91,15 +103,16 @@ class CloudInitConfig(GenericConfig):
Compared to :class:`GenericConfig`, this adds support for managing users. Compared to :class:`GenericConfig`, this adds support for managing users.
:ivar ssh_keys: List of SSH public keys. :ivar ssh_keys: List of SSH public keys.
:ivar user_data: Cloud-init script as a dictionary. :ivar user_data: Cloud-init cloud-config data as a dictionary.
:ivar users: Users to add on first boot. :ivar meta_data: Dict of data to add to the generated ``meta_data``
""" """
def __init__(self, ssh_keys=None, user_data=None): def __init__(self, ssh_keys=None, user_data=None, meta_data=None):
if user_data is not None and not isinstance(user_data, dict): if user_data is not None and not isinstance(user_data, dict):
raise TypeError('Custom user data must be a dictionary for ' raise TypeError('Custom user data must be a dictionary for '
'CloudInitConfig, got %r' % user_data) 'CloudInitConfig, got %r' % user_data)
super(CloudInitConfig, self).__init__(ssh_keys, user_data or {}) super(CloudInitConfig, self).__init__(ssh_keys, user_data or {},
meta_data=meta_data)
self.users = [] self.users = []
def add_user(self, name, admin=True, password_hash=None, sudo=False, def add_user(self, name, admin=True, password_hash=None, sudo=False,

View File

@ -78,6 +78,13 @@ class TestGenericConfig(unittest.TestCase):
config = self.CLASS(user_data='{"answer": 42}') config = self.CLASS(user_data='{"answer": 42}')
self._check(config, {}, {"answer": 42}, cloud_init=False) self._check(config, {}, {"answer": 42}, cloud_init=False)
def test_custom_metadata(self):
config = self.CLASS(meta_data={"foo": "bar"})
self._check(config, {"foo": "bar"}, cloud_init=False)
def test_custom_metadata_not_dict(self):
self.assertRaises(TypeError, self.CLASS, meta_data="foobar")
class TestCloudInitConfig(TestGenericConfig): class TestCloudInitConfig(TestGenericConfig):
CLASS = instance_config.CloudInitConfig CLASS = instance_config.CloudInitConfig

View File

@ -126,7 +126,11 @@ class TestMetalsmithInstances(unittest.TestCase):
'netboot': True, 'netboot': True,
'ssh_public_keys': 'abcd', 'ssh_public_keys': 'abcd',
'user_name': 'centos', 'user_name': 'centos',
'passwordless_sudo': False 'passwordless_sudo': False,
'config_drive': {
'meta_data': {'foo': 'bar'},
'cloud_config': {'bootcmd': ['echo henlo world']}
}
}, { }, {
'name': 'node-3', 'name': 'node-3',
'hostname': 'overcloud-controller-3', 'hostname': 'overcloud-controller-3',
@ -191,8 +195,10 @@ class TestMetalsmithInstances(unittest.TestCase):
), ),
]) ])
mock_config.assert_has_calls([ mock_config.assert_has_calls([
mock.call(ssh_keys=None), mock.call(ssh_keys=None, user_data=None, meta_data=None),
mock.call(ssh_keys='abcd'), mock.call(ssh_keys='abcd',
user_data={'bootcmd': ['echo henlo world']},
meta_data={'foo': 'bar'})
]) ])
config.add_user.assert_called_once_with( config.add_user.assert_called_once_with(
'centos', admin=True, sudo=False) 'centos', admin=True, sudo=False)

View File

@ -73,6 +73,7 @@ def transform(module, instances, defaults):
value(src, 'image_checksum', image, 'checksum') value(src, 'image_checksum', image, 'checksum')
value(src, 'image_kernel', image, 'kernel') value(src, 'image_kernel', image, 'kernel')
value(src, 'image_ramdisk', image, 'ramdisk') value(src, 'image_ramdisk', image, 'ramdisk')
value(src, 'config_drive', dest)
# keys in metalsmith_instances not currently in metalsmith_deployment: # keys in metalsmith_instances not currently in metalsmith_deployment:
# passwordless_sudo # passwordless_sudo

View File

@ -151,6 +151,23 @@ options:
- Allow password-less sudo for the user - Allow password-less sudo for the user
default: yes default: yes
type: bool type: bool
config_drive:
description:
- Extra data to add to the boot config-drive cloud-init
type: dict
suboptions:
cloud_config:
description:
- Dict of cloud-init cloud-config data for tasks to run on node
boot. The 'users' directive can be used to configure extra
users other than the 'user_name' admin user.
type: dict
meta_data:
description:
- Extra metadata to include with the config-drive cloud-init
metadata. This will be added to the generated metadata
'public_keys', 'uuid', 'name', and 'hostname'.
type: dict
clean_up: clean_up:
description: description:
- Clean up resources on failure - Clean up resources on failure
@ -330,7 +347,12 @@ def _provision_instance(provisioner, instance, nodes, timeout, wait):
image = _get_source(instance) image = _get_source(instance)
ssh_keys = instance.get('ssh_public_keys') ssh_keys = instance.get('ssh_public_keys')
config = instance_config.CloudInitConfig(ssh_keys=ssh_keys) config_drive = instance.get('config_drive', {})
cloud_config = config_drive.get('cloud_config')
meta_data = config_drive.get('meta_data')
config = instance_config.CloudInitConfig(ssh_keys=ssh_keys,
user_data=cloud_config,
meta_data=meta_data)
if instance.get('user_name'): if instance.get('user_name'):
config.add_user(instance.get('user_name'), admin=True, config.add_user(instance.get('user_name'), admin=True,
sudo=instance.get('passwordless_sudo', True)) sudo=instance.get('passwordless_sudo', True))

View File

@ -60,6 +60,18 @@ Each instances has the following attributes:
list of nodes (UUIDs or names) to be considered for deployment. list of nodes (UUIDs or names) to be considered for deployment.
``capabilities`` (defaults to ``metalsmith_capabilities``) ``capabilities`` (defaults to ``metalsmith_capabilities``)
node capabilities to request when scheduling. node capabilities to request when scheduling.
``config_drive``
extra data to add to the boot config-drive cloud-init:
``cloud_config``
Dict of cloud-init cloud-config data for tasks to run on node
boot. The 'users' directive can be used to configure extra
users other than the 'user_name' admin user.
``meta_data``
Extra metadata to include with the config-drive cloud-init
metadata. This will be added to the generated metadata
``public_keys``, ``uuid``, ``name``, and ``hostname``.
``conductor_group`` (defaults to ``metalsmith_conductor_group``) ``conductor_group`` (defaults to ``metalsmith_conductor_group``)
conductor group to pick nodes from. conductor group to pick nodes from.

View File

@ -0,0 +1,7 @@
---
features:
- |
Add new metalsmith_instances instance option ``config_drive``.
This has sub-options ``cloud_config` and ``meta_data`` and allows
customization of data packaged in the config-drive to be consumed by
cloud-init.