Refactor cloud-init support and VM Salt config seeding
Missing package dependancies added.
A missing "config" parameter for qemu-nbd based seeding
method added.
A new seeding method utilising Cloud-init added.
The qemu-nbd based method is still a default method
for backward compatibility.
To enable cloud-init, set the "seed" parameter on
a cluster or node level to "cloud-init".
To disable seeding, set this parameter to "false".
Setting this parameter to "true" will default to
the "qemu-nbd" method.
Salt Minion config file will be created automatically
and may be overrided via cluster or node level
metadata:
salt:
control:
cluster:
mycluster:
seed: cloud-init
cloud_init:
user_data:
salt_minion:
conf:
master: 10.1.1.1
or for qemu-nbd case:
salt:
control:
cluster:
mycluster:
seed: true
config:
host: 10.1.1.1
That may be useful when Salt Master has two IPs in
different networks and one of the networks isn't accessible
from a VM at the moment it's created. Setting a reachable
Salt master IP from metadata helps avoid potential problems.
Also, a liitle optimization has been done to parse/dump
an libvirt XML only once while modifying it.
Change-Id: I091cf409cb43ba2d0a18eaf2a08c11e88d0334e2
Closes-Bug: PROD-22191
diff --git a/_modules/cfgdrive.py b/_modules/cfgdrive.py
index bc76b77..9f07d8e 100644
--- a/_modules/cfgdrive.py
+++ b/_modules/cfgdrive.py
@@ -1,16 +1,17 @@
# -*- coding: utf-8 -*-
+import errno
import json
import logging
import os
import shutil
import six
+import subprocess
import tempfile
+import uuid
import yaml
-from oslo_utils import uuidutils
-from oslo_utils import fileutils
-from oslo_concurrency import processutils
+LOG = logging.getLogger(__name__)
class ConfigDriveBuilder(object):
"""Build config drives, optionally as a context manager."""
@@ -20,19 +21,37 @@
self.mdfiles=[]
def __enter__(self):
- fileutils.delete_if_exists(self.image_file)
+ self._delete_if_exists(self.image_file)
return self
def __exit__(self, exctype, excval, exctb):
self.make_drive()
+ @staticmethod
+ def _ensure_tree(path):
+ try:
+ os.makedirs(path)
+ except OSError as e:
+ if e.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else:
+ raise
+
+ @staticmethod
+ def _delete_if_exists(path):
+ try:
+ os.unlink(path)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
def add_file(self, path, data):
self.mdfiles.append((path, data))
def _add_file(self, basedir, path, data):
filepath = os.path.join(basedir, path)
dirname = os.path.dirname(filepath)
- fileutils.ensure_tree(dirname)
+ self._ensure_tree(dirname)
with open(filepath, 'wb') as f:
if isinstance(data, six.text_type):
data = data.encode('utf-8')
@@ -43,8 +62,7 @@
self._add_file(basedir, data[0], data[1])
def _make_iso9660(self, path, tmpdir):
-
- processutils.execute('mkisofs',
+ cmd = ['mkisofs',
'-o', path,
'-ldots',
'-allow-lowercase',
@@ -54,13 +72,34 @@
'-r',
'-J',
'-quiet',
- tmpdir,
- attempts=1,
- run_as_root=False)
+ tmpdir]
+ try:
+ LOG.info('Running cmd (subprocess): %s', cmd)
+ _pipe = subprocess.PIPE
+ obj = subprocess.Popen(cmd,
+ stdin=_pipe,
+ stdout=_pipe,
+ stderr=_pipe,
+ close_fds=True)
+ (stdout, stderr) = obj.communicate()
+ obj.stdin.close()
+ _returncode = obj.returncode
+ LOG.debug('Cmd "%s" returned: %s', cmd, _returncode)
+ if _returncode != 0:
+ output = 'Stdout: %s\nStderr: %s' % (stdout, stderr)
+ LOG.error('The command "%s" failed. %s',
+ cmd, output)
+ raise subprocess.CalledProcessError(cmd=cmd,
+ returncode=_returncode,
+ output=output)
+ except OSError as err:
+ LOG.error('Got an OSError in the command: "%s". Errno: %s', cmd,
+ err.errno)
+ raise
def make_drive(self):
"""Make the config drive.
- :raises ProcessExecuteError if a helper process has failed.
+ :raises CalledProcessError if a helper process has failed.
"""
try:
tmpdir = tempfile.mkdtemp()
@@ -70,15 +109,8 @@
shutil.rmtree(tmpdir)
-def generate(
- dst,
- hostname,
- domainname,
- instance_id=None,
- user_data=None,
- network_data=None,
- saltconfig=None
- ):
+def generate(dst, hostname, domainname, instance_id=None, user_data=None,
+ network_data=None):
''' Generate config drive
@@ -86,29 +118,24 @@
:param hostname: hostname of Instance.
:param domainname: instance domain.
:param instance_id: UUID of the instance.
- :param user_data: custom user data dictionary. type: json
- :param network_data: custom network info dictionary. type: json
- :param saltconfig: salt minion configuration. type: json
+ :param user_data: custom user data dictionary.
+ :param network_data: custom network info dictionary.
'''
-
- instance_md = {}
- instance_md['uuid'] = instance_id or uuidutils.generate_uuid()
- instance_md['hostname'] = '%s.%s' % (hostname, domainname)
- instance_md['name'] = hostname
+ instance_md = {}
+ instance_md['uuid'] = instance_id or str(uuid.uuid4())
+ instance_md['hostname'] = '%s.%s' % (hostname, domainname)
+ instance_md['name'] = hostname
if user_data:
- user_data = '#cloud-config\n\n' + yaml.dump(yaml.load(user_data), default_flow_style=False)
- if saltconfig:
- user_data += yaml.dump(yaml.load(str(saltconfig)), default_flow_style=False)
+ user_data = '#cloud-config\n\n' + yaml.dump(user_data, default_flow_style=False)
data = json.dumps(instance_md)
-
with ConfigDriveBuilder(dst) as cfgdrive:
- cfgdrive.add_file('openstack/latest/meta_data.json', data)
- if user_data:
- cfgdrive.add_file('openstack/latest/user_data', user_data)
- if network_data:
- cfgdrive.add_file('openstack/latest/network_data.json', network_data)
- cfgdrive.add_file('openstack/latest/vendor_data.json', '{}')
- cfgdrive.add_file('openstack/latest/vendor_data2.json', '{}')
+ cfgdrive.add_file('openstack/latest/meta_data.json', data)
+ if user_data:
+ cfgdrive.add_file('openstack/latest/user_data', user_data)
+ if network_data:
+ cfgdrive.add_file('openstack/latest/network_data.json', json.dumps(network_data))
+
+ LOG.debug('Config drive was built %s' % dst)