Vasyl Saienko | aad112d | 2017-06-19 16:45:37 +0300 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | |
| 4 | import gzip |
| 5 | import json |
| 6 | import logging |
| 7 | import os |
| 8 | import StringIO |
| 9 | import shutil |
| 10 | import six |
| 11 | import tempfile |
| 12 | import yaml |
| 13 | |
| 14 | |
| 15 | HAS_LIBS = False |
| 16 | try: |
| 17 | from oslo_utils import uuidutils |
| 18 | from oslo_utils import fileutils |
| 19 | from oslo_concurrency import processutils |
| 20 | from oslo_serialization import base64 |
| 21 | HAS_LIBS = True |
| 22 | except ImportError: |
| 23 | pass |
| 24 | |
| 25 | LOG = logging.getLogger(__name__) |
| 26 | |
| 27 | |
| 28 | def __virtual__(): |
| 29 | ''' |
| 30 | Only load this module if mkisofs is installed on this minion. |
| 31 | ''' |
| 32 | if not HAS_LIBS: |
| 33 | return False |
| 34 | |
| 35 | for path in os.environ["PATH"].split(os.pathsep): |
| 36 | if os.access(os.path.join(path, 'mkisofs'), os.X_OK): |
| 37 | return True |
| 38 | |
| 39 | return False |
| 40 | |
| 41 | class ConfigDriveBuilder(object): |
| 42 | """Build config drives, optionally as a context manager.""" |
| 43 | |
| 44 | def __init__(self, image_file): |
| 45 | self.image_file = image_file |
| 46 | self.mdfiles=[] # List with (path, data) |
| 47 | |
| 48 | def __enter__(self): |
| 49 | fileutils.delete_if_exists(self.image_file) |
| 50 | return self |
| 51 | |
| 52 | def __exit__(self, exctype, excval, exctb): |
| 53 | self.make_drive() |
| 54 | |
| 55 | def add_file(self, path, data): |
| 56 | self.mdfiles.append((path, data)) |
| 57 | |
| 58 | def _add_file(self, basedir, path, data): |
| 59 | filepath = os.path.join(basedir, path) |
| 60 | dirname = os.path.dirname(filepath) |
| 61 | fileutils.ensure_tree(dirname) |
| 62 | with open(filepath, 'wb') as f: |
| 63 | # the given data can be either text or bytes. we can only write |
| 64 | # bytes into files. |
| 65 | if isinstance(data, six.text_type): |
| 66 | data = data.encode('utf-8') |
| 67 | f.write(data) |
| 68 | |
| 69 | def _write_md_files(self, basedir): |
| 70 | for data in self.mdfiles: |
| 71 | self._add_file(basedir, data[0], data[1]) |
| 72 | |
| 73 | def _make_iso9660(self, path, tmpdir): |
| 74 | |
| 75 | processutils.execute('mkisofs', |
| 76 | '-o', path, |
| 77 | '-ldots', |
| 78 | '-allow-lowercase', |
| 79 | '-allow-multidot', |
| 80 | '-l', |
| 81 | '-V', 'config-2', |
| 82 | '-r', |
| 83 | '-J', |
| 84 | '-quiet', |
| 85 | tmpdir, |
| 86 | attempts=1, |
| 87 | run_as_root=False) |
| 88 | |
| 89 | def make_drive(self): |
| 90 | """Make the config drive. |
| 91 | :raises ProcessExecuteError if a helper process has failed. |
| 92 | """ |
| 93 | try: |
| 94 | tmpdir = tempfile.mkdtemp() |
| 95 | self._write_md_files(tmpdir) |
| 96 | self._make_iso9660(self.image_file, tmpdir) |
| 97 | finally: |
| 98 | shutil.rmtree(tmpdir) |
| 99 | |
| 100 | |
| 101 | def generate(dst, hostname, domainname, instance_id=None, public_keys=None, |
| 102 | user_data=None, network_data=None, ironic_format=False): |
| 103 | ''' Generate config drive |
| 104 | |
| 105 | :param dst: destination file to place config drive. |
| 106 | :param hostname: hostname of Instance. |
| 107 | :param domainname: instance domain. |
| 108 | :param instance_id: UUID of the instance. |
| 109 | :param public_keys: dict of public keys. |
| 110 | :param user_data: custom user data dictionary. |
| 111 | :param network_data: custom network info dictionary. |
| 112 | :param ironic_format: create base64 of gzipped ISO format |
| 113 | |
| 114 | CLI Example: |
| 115 | .. code-block:: bash |
| 116 | salt '*' configdrive.generate dst=/tmp/my_cfgdrive.iso hostname=host1 |
| 117 | ''' |
| 118 | instance_md = {} |
| 119 | public_keys = public_keys or {} |
| 120 | |
| 121 | instance_md['uuid'] = instance_id or uuidutils.generate_uuid() |
| 122 | instance_md['hostname'] = '%s.%s' % (hostname, domainname) |
| 123 | instance_md['name'] = hostname |
| 124 | instance_md['public_keys'] = public_keys |
| 125 | |
| 126 | data = json.dumps(instance_md) |
| 127 | |
| 128 | if user_data: |
| 129 | user_data = '#cloud-config\n\n' + yaml.dump(user_data, default_flow_style=False) |
| 130 | |
| 131 | LOG.debug('Generating config drive for %s' % hostname) |
| 132 | |
| 133 | with ConfigDriveBuilder(dst) as cfgdrive: |
| 134 | cfgdrive.add_file('openstack/latest/meta_data.json', data) |
| 135 | if user_data: |
| 136 | cfgdrive.add_file('openstack/latest/user_data', user_data) |
| 137 | if network_data: |
| 138 | cfgdrive.add_file('openstack/latest/network_data.json', |
| 139 | json.dumps(network_data)) |
| 140 | cfgdrive.add_file('openstack/latest/vendor_data.json', '{}') |
| 141 | cfgdrive.add_file('openstack/latest/vendor_data2.json', '{}') |
| 142 | |
| 143 | b64_gzip = None |
| 144 | if ironic_format: |
| 145 | with open(dst) as f: |
| 146 | with tempfile.NamedTemporaryFile() as tmpzipfile: |
| 147 | g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb') |
| 148 | shutil.copyfileobj(f, g) |
| 149 | g.close() |
| 150 | tmpzipfile.seek(0) |
| 151 | b64_gzip = base64.encode_as_bytes(tmpzipfile.read()) |
| 152 | with open(dst, 'w') as f: |
| 153 | f.write(b64_gzip) |
| 154 | |
| 155 | LOG.debug('Config drive was built %s' % dst) |
| 156 | res = {} |
| 157 | res['meta-data'] = data |
| 158 | if user_data: |
| 159 | res['user-data'] = user_data |
| 160 | if b64_gzip: |
| 161 | res['base64_gzip'] = b64_gzip |
| 162 | return res |