Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 3 | import errno |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 4 | import json |
| 5 | import logging |
| 6 | import os |
| 7 | import shutil |
| 8 | import six |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 9 | import subprocess |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 10 | import tempfile |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 11 | import uuid |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 12 | import yaml |
| 13 | |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 14 | LOG = logging.getLogger(__name__) |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 15 | |
| 16 | class ConfigDriveBuilder(object): |
| 17 | """Build config drives, optionally as a context manager.""" |
| 18 | |
| 19 | def __init__(self, image_file): |
| 20 | self.image_file = image_file |
| 21 | self.mdfiles=[] |
| 22 | |
| 23 | def __enter__(self): |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 24 | self._delete_if_exists(self.image_file) |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 25 | return self |
| 26 | |
| 27 | def __exit__(self, exctype, excval, exctb): |
| 28 | self.make_drive() |
| 29 | |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 30 | @staticmethod |
| 31 | def _ensure_tree(path): |
| 32 | try: |
| 33 | os.makedirs(path) |
| 34 | except OSError as e: |
| 35 | if e.errno == errno.EEXIST and os.path.isdir(path): |
| 36 | pass |
| 37 | else: |
| 38 | raise |
| 39 | |
| 40 | @staticmethod |
| 41 | def _delete_if_exists(path): |
| 42 | try: |
| 43 | os.unlink(path) |
| 44 | except OSError as e: |
| 45 | if e.errno != errno.ENOENT: |
| 46 | raise |
| 47 | |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 48 | def add_file(self, path, data): |
| 49 | self.mdfiles.append((path, data)) |
| 50 | |
| 51 | def _add_file(self, basedir, path, data): |
| 52 | filepath = os.path.join(basedir, path) |
| 53 | dirname = os.path.dirname(filepath) |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 54 | self._ensure_tree(dirname) |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 55 | with open(filepath, 'wb') as f: |
| 56 | if isinstance(data, six.text_type): |
| 57 | data = data.encode('utf-8') |
| 58 | f.write(data) |
| 59 | |
| 60 | def _write_md_files(self, basedir): |
| 61 | for data in self.mdfiles: |
| 62 | self._add_file(basedir, data[0], data[1]) |
| 63 | |
| 64 | def _make_iso9660(self, path, tmpdir): |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 65 | cmd = ['mkisofs', |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 66 | '-o', path, |
| 67 | '-ldots', |
| 68 | '-allow-lowercase', |
| 69 | '-allow-multidot', |
| 70 | '-l', |
| 71 | '-V', 'config-2', |
| 72 | '-r', |
| 73 | '-J', |
| 74 | '-quiet', |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 75 | tmpdir] |
| 76 | try: |
| 77 | LOG.info('Running cmd (subprocess): %s', cmd) |
| 78 | _pipe = subprocess.PIPE |
| 79 | obj = subprocess.Popen(cmd, |
| 80 | stdin=_pipe, |
| 81 | stdout=_pipe, |
| 82 | stderr=_pipe, |
| 83 | close_fds=True) |
| 84 | (stdout, stderr) = obj.communicate() |
| 85 | obj.stdin.close() |
| 86 | _returncode = obj.returncode |
| 87 | LOG.debug('Cmd "%s" returned: %s', cmd, _returncode) |
| 88 | if _returncode != 0: |
| 89 | output = 'Stdout: %s\nStderr: %s' % (stdout, stderr) |
| 90 | LOG.error('The command "%s" failed. %s', |
| 91 | cmd, output) |
| 92 | raise subprocess.CalledProcessError(cmd=cmd, |
| 93 | returncode=_returncode, |
| 94 | output=output) |
| 95 | except OSError as err: |
| 96 | LOG.error('Got an OSError in the command: "%s". Errno: %s', cmd, |
| 97 | err.errno) |
| 98 | raise |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 99 | |
| 100 | def make_drive(self): |
| 101 | """Make the config drive. |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 102 | :raises CalledProcessError if a helper process has failed. |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 103 | """ |
| 104 | try: |
| 105 | tmpdir = tempfile.mkdtemp() |
| 106 | self._write_md_files(tmpdir) |
| 107 | self._make_iso9660(self.image_file, tmpdir) |
| 108 | finally: |
| 109 | shutil.rmtree(tmpdir) |
| 110 | |
| 111 | |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 112 | def generate(dst, hostname, domainname, instance_id=None, user_data=None, |
| 113 | network_data=None): |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 114 | |
| 115 | ''' Generate config drive |
| 116 | |
| 117 | :param dst: destination file to place config drive. |
| 118 | :param hostname: hostname of Instance. |
| 119 | :param domainname: instance domain. |
| 120 | :param instance_id: UUID of the instance. |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 121 | :param user_data: custom user data dictionary. |
| 122 | :param network_data: custom network info dictionary. |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 123 | |
| 124 | ''' |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 125 | instance_md = {} |
| 126 | instance_md['uuid'] = instance_id or str(uuid.uuid4()) |
| 127 | instance_md['hostname'] = '%s.%s' % (hostname, domainname) |
| 128 | instance_md['name'] = hostname |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 129 | |
| 130 | if user_data: |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 131 | user_data = '#cloud-config\n\n' + yaml.dump(user_data, default_flow_style=False) |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 132 | |
| 133 | data = json.dumps(instance_md) |
Dzmitry Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 134 | with ConfigDriveBuilder(dst) as cfgdrive: |
Andrei Danin | 996e209 | 2018-09-10 21:58:23 -0700 | [diff] [blame] | 135 | cfgdrive.add_file('openstack/latest/meta_data.json', data) |
| 136 | if user_data: |
| 137 | cfgdrive.add_file('openstack/latest/user_data', user_data) |
| 138 | if network_data: |
| 139 | cfgdrive.add_file('openstack/latest/network_data.json', json.dumps(network_data)) |
| 140 | |
| 141 | LOG.debug('Config drive was built %s' % dst) |