# -*- 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)