blob: 9f07d8e10d8008ef5d29f13fc82d1800a6655a16 [file] [log] [blame]
# -*- coding: utf-8 -*-
import errno
import json
import logging
import os
import shutil
import six
import subprocess
import tempfile
import uuid
import yaml
LOG = logging.getLogger(__name__)
class ConfigDriveBuilder(object):
"""Build config drives, optionally as a context manager."""
def __init__(self, image_file):
self.image_file = image_file
self.mdfiles=[]
def __enter__(self):
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)
self._ensure_tree(dirname)
with open(filepath, 'wb') as f:
if isinstance(data, six.text_type):
data = data.encode('utf-8')
f.write(data)
def _write_md_files(self, basedir):
for data in self.mdfiles:
self._add_file(basedir, data[0], data[1])
def _make_iso9660(self, path, tmpdir):
cmd = ['mkisofs',
'-o', path,
'-ldots',
'-allow-lowercase',
'-allow-multidot',
'-l',
'-V', 'config-2',
'-r',
'-J',
'-quiet',
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 CalledProcessError if a helper process has failed.
"""
try:
tmpdir = tempfile.mkdtemp()
self._write_md_files(tmpdir)
self._make_iso9660(self.image_file, tmpdir)
finally:
shutil.rmtree(tmpdir)
def generate(dst, hostname, domainname, instance_id=None, user_data=None,
network_data=None):
''' Generate config drive
:param dst: destination file to place config drive.
:param hostname: hostname of Instance.
:param domainname: instance domain.
:param instance_id: UUID of the instance.
:param user_data: custom user data dictionary.
:param network_data: custom network info dictionary.
'''
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(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', json.dumps(network_data))
LOG.debug('Config drive was built %s' % dst)