Support Ignition for userdata

Fedora CoreOS will replace Fedora Atomic being the next generation
container OS. So it would be nice to support Fedora CoreOS in Heat.
In Fedora CoreOS, the cloud-init will be replaced with Ignition[1],
so the changes proposed in this patch are mainly focusing on how to
support Ignition when using Heat SOFTWARE_CONFIG with Ignition.

Task: 36671
Story: 2006566

Change-Id: I11df2431634de7d8b584b1a2ac733d43959e34fc
This commit is contained in:
Feilong Wang 2019-09-20 21:09:57 +12:00
parent bb44f779fb
commit f6249c0110
4 changed files with 85 additions and 5 deletions

View File

@ -308,7 +308,7 @@ class NovaClientPlugin(microversion_mixin.MicroversionMixin,
def build_userdata(self, metadata, userdata=None, instance_user=None,
user_data_format='HEAT_CFNTOOLS'):
"""Build multipart data blob for CloudInit.
"""Build multipart data blob for CloudInit and Ignition.
Data blob includes user-supplied Metadata, user data, and the required
Heat in-instance configuration.
@ -330,6 +330,10 @@ class NovaClientPlugin(microversion_mixin.MicroversionMixin,
is_cfntools = user_data_format == 'HEAT_CFNTOOLS'
is_software_config = user_data_format == 'SOFTWARE_CONFIG'
if (is_software_config and
NovaClientPlugin.is_ignition_format(userdata)):
return NovaClientPlugin.build_ignition_data(metadata, userdata)
def make_subpart(content, filename, subtype=None):
if subtype is None:
subtype = os.path.splitext(filename)[0]
@ -428,6 +432,46 @@ echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
return mime_blob.as_string()
@staticmethod
def is_ignition_format(userdata):
try:
payload = jsonutils.loads(userdata)
ig = payload.get("ignition")
return True if ig and ig.get("version") else False
except Exception:
return False
@staticmethod
def build_ignition_data(metadata, userdata):
if not metadata:
return userdata
payload = jsonutils.loads(userdata)
encoded_metadata = urlparse.quote(jsonutils.dumps(metadata))
cfn_init_data = {
"filesystem": "root",
"group": {"name": "root"},
"path": "/var/lib/os-collect-config/local-data",
"user": {"name": "root"},
"contents": {
"source": "data:," + encoded_metadata,
"verification": {}},
"mode": 0o640
}
storage = payload.setdefault('storage', {})
try:
files = storage.setdefault('files', [])
except AttributeError:
raise ValueError('Ignition "storage" section must be a map')
else:
try:
files.append(cfn_init_data)
except AttributeError:
raise ValueError('Ignition "files" section must be a list')
return jsonutils.dumps(payload)
def check_delete_server_complete(self, server_id):
"""Wait for server to disappear from Nova."""
try:

View File

@ -520,7 +520,9 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
'the user_data is passed to Nova unmodified. '
'For SOFTWARE_CONFIG user_data is bundled as part of the '
'software config data, and metadata is derived from any '
'associated SoftwareDeployment resources.'),
'associated SoftwareDeployment resources. And if the '
'user_data is in CoreOS ignition(json) format, the metadata '
'will be injected into the user_data automatically by Heat.'),
default=cfg.CONF.default_user_data_format,
constraints=[
constraints.AllowedValues(_SOFTWARE_CONFIG_FORMATS),
@ -556,9 +558,10 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
),
USER_DATA: properties.Schema(
properties.Schema.STRING,
_('User data script to be executed by cloud-init. Changes cause '
'replacement of the resource by default, but can be ignored '
'altogether by setting the `user_data_update_policy` property.'),
_('User data script to be executed by cloud-init or CoreOS '
'ignition. Changes cause replacement of the resource '
'by default, but can be ignored altogether by setting the '
'`user_data_update_policy` property.'),
default='',
update_allowed=True
),

View File

@ -394,6 +394,32 @@ class NovaClientPluginUserdataTest(NovaClientPluginTestCase):
self.assertIn('useradd', data)
self.assertIn('ec2-user', data)
def test_build_userdata_with_ignition(self):
metadata = {"os-collect-config": {"heat": {"password": "***"}}}
userdata = '{"ignition": {"version": "3.0"}, "storage": {"files": []}}'
ud_format = 'SOFTWARE_CONFIG'
data = self.nova_plugin.build_userdata(metadata,
userdata=userdata,
user_data_format=ud_format)
ig = json.loads(data)
self.assertEqual("/var/lib/os-collect-config/local-data",
ig["storage"]["files"][0]["path"])
self.assertEqual("data:,%7B%22os-collect-config%22%3A%20%7B%22heat"
"%22%3A%20%7B%22password%22%3A%20%22%2A%2A%2A%22"
"%7D%7D%7D",
ig["storage"]["files"][0]["contents"]["source"])
def test_build_userdata_with_invalid_ignition(self):
metadata = {"os-collect-config": {"heat": {"password": "***"}}}
userdata = '{"ignition": {"version": "3.0"}, "storage": []}'
ud_format = 'SOFTWARE_CONFIG'
self.assertRaises(ValueError,
self.nova_plugin.build_userdata,
metadata,
userdata=userdata,
user_data_format=ud_format)
class NovaClientPluginMetadataTest(NovaClientPluginTestCase):

View File

@ -0,0 +1,7 @@
---
features:
- |
Heat can now support software deployments with CoreOS by passing
a CoreOS Ignition config in the ``user_data`` property for an
``OS::Nova::Server`` resource when the ``user_data_format`` is
set to ``SOFTWARE_CONFIG``.