Merge "Fix cloud-init configuration file issue with duplicate headers"

This commit is contained in:
Zuul
2025-05-23 12:33:18 +00:00
committed by Gerrit Code Review
4 changed files with 70 additions and 22 deletions

View File

@@ -0,0 +1,6 @@
---
fixes:
- |
Fixes a bug where the cloud-init configuration file contained duplicate
headers when userdata was provided.
`LP#[2011309] <https://storyboard.openstack.org/#!/story/[2011309]>`__

View File

@@ -19,6 +19,7 @@ import base64
import json import json
import os.path import os.path
import re import re
import yaml
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
@@ -66,6 +67,8 @@ LOG = logging.getLogger(__name__)
AGENT_INVALID_STATUSES = ["BUILD", "REBOOT", "RESIZE", "PROMOTE", "EJECT", AGENT_INVALID_STATUSES = ["BUILD", "REBOOT", "RESIZE", "PROMOTE", "EJECT",
"UPGRADE"] "UPGRADE"]
CLOUDINIT_HEADER = "#cloud-config\n"
def ip_visible(ip, white_list_regex, black_list_regex): def ip_visible(ip, white_list_regex, black_list_regex):
if re.search(white_list_regex, ip) and not re.search(black_list_regex, ip): if re.search(white_list_regex, ip) and not re.search(black_list_regex, ip):
@@ -969,29 +972,26 @@ class BaseInstance(SimpleInstance):
def prepare_cloud_config(self, files): def prepare_cloud_config(self, files):
# This method returns None if the files argument is None # This method returns None if the files argument is None
userdata = None if not files:
return ""
if files: injected_config_owner = CONF.get('injected_config_owner')
userdata = ( injected_config_group = CONF.get('injected_config_group')
"#cloud-config\n"
"write_files:\n"
)
injected_config_owner = CONF.get('injected_config_owner') write_files = []
injected_config_group = CONF.get('injected_config_group') for filename, content in files.items():
for filename, content in files.items(): ud = encodeutils.safe_encode(content)
ud = encodeutils.safe_encode(content) write_files.append({
body_userdata = ( "encoding": "b64",
"- encoding: b64\n" "owner": f"{injected_config_owner}:{injected_config_group}",
" owner: %s:%s\n" "path": filename,
" path: %s\n" "content": encodeutils.safe_decode(base64.b64encode(ud))
" content: %s\n" % ( })
injected_config_owner, injected_config_group, filename,
encodeutils.safe_decode(base64.b64encode(ud)))
)
userdata = userdata + body_userdata
return userdata if userdata else "" cloud_config = {
"write_files": write_files
}
return CLOUDINIT_HEADER + yaml.dump(cloud_config)
@property @property
def datastore_registry_ext(self): def datastore_registry_ext(self):
@@ -1121,6 +1121,28 @@ class BaseInstance(SimpleInstance):
userdata = f.read() userdata = f.read()
return userdata return userdata
@staticmethod
def combine_cloudinit_userdata(cloudinit, userdata):
cloudinit = yaml.safe_load(cloudinit)
try:
# in case the userdata is not a valid yaml
userdata = yaml.safe_load(userdata)
except yaml.YAMLError as e:
LOG.error("Failed to parse userdata: %s. The error was: %s",
userdata,
str(e))
return CLOUDINIT_HEADER + yaml.dump(cloudinit)
if isinstance(userdata, dict):
# in case the userdata contains write_files directive
if userdata.get('write_files') and cloudinit.get('write_files'):
cloudinit['write_files'].extend(userdata['write_files'])
userdata.pop('write_files')
cloudinit.update(userdata)
else:
LOG.error("Userdata is not a valid cloudinit config: %s",
userdata)
return CLOUDINIT_HEADER + yaml.dump(cloudinit)
class FreshInstance(BaseInstance): class FreshInstance(BaseInstance):

View File

@@ -1110,8 +1110,9 @@ class FreshInstanceTasks(FreshInstance, NotifyMixin, ConfigurationMixin):
if not userdata: if not userdata:
userdata = self.prepare_cloud_config(files) userdata = self.prepare_cloud_config(files)
else: else:
userdata = userdata + self.prepare_cloud_config(files) userdata = self.combine_cloudinit_userdata(
self.prepare_cloud_config(files),
userdata)
files = {} files = {}
server = self.nova_client.servers.create( server = self.nova_client.servers.create(

View File

@@ -256,6 +256,25 @@ class FreshInstanceTasksTest(BaseFreshInstanceTasksTest):
user_data = self.freshinstancetasks.prepare_cloud_config(files) user_data = self.freshinstancetasks.prepare_cloud_config(files)
self.assertTrue(user_data.startswith('#cloud-config')) self.assertTrue(user_data.startswith('#cloud-config'))
def test_create_instance_combine_cloudinit_userdata(self):
cloudcfg = """
#cloud-config
write_files:
- path: /etc/myconfig.conf
content: This is the content of the file.
owner: root:root
encoding: b64
"""
userdata = """
#cloud-config
run_command:
- echo "hello world"
"""
cloudinit = self.freshinstancetasks.combine_cloudinit_userdata(
cloudcfg, userdata)
self.assertTrue(cloudinit.startswith('#cloud-config'))
self.assertEqual(cloudinit.count('cloud-config'), 1)
@patch.object(DBInstance, 'get_by') @patch.object(DBInstance, 'get_by')
def test_create_instance_guestconfig(self, patch_get_by): def test_create_instance_guestconfig(self, patch_get_by):
cfg.CONF.set_override('guest_config', self.guestconfig) cfg.CONF.set_override('guest_config', self.guestconfig)