blob: 9f07d8e10d8008ef5d29f13fc82d1800a6655a16 [file] [log] [blame]
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +02001# -*- coding: utf-8 -*-
2
Andrei Danin996e2092018-09-10 21:58:23 -07003import errno
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +02004import json
5import logging
6import os
7import shutil
8import six
Andrei Danin996e2092018-09-10 21:58:23 -07009import subprocess
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020010import tempfile
Andrei Danin996e2092018-09-10 21:58:23 -070011import uuid
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020012import yaml
13
Andrei Danin996e2092018-09-10 21:58:23 -070014LOG = logging.getLogger(__name__)
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020015
16class 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 Danin996e2092018-09-10 21:58:23 -070024 self._delete_if_exists(self.image_file)
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020025 return self
26
27 def __exit__(self, exctype, excval, exctb):
28 self.make_drive()
29
Andrei Danin996e2092018-09-10 21:58:23 -070030 @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 Stremkouski97927ee2018-08-23 23:20:38 +020048 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 Danin996e2092018-09-10 21:58:23 -070054 self._ensure_tree(dirname)
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020055 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 Danin996e2092018-09-10 21:58:23 -070065 cmd = ['mkisofs',
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020066 '-o', path,
67 '-ldots',
68 '-allow-lowercase',
69 '-allow-multidot',
70 '-l',
71 '-V', 'config-2',
72 '-r',
73 '-J',
74 '-quiet',
Andrei Danin996e2092018-09-10 21:58:23 -070075 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 Stremkouski97927ee2018-08-23 23:20:38 +020099
100 def make_drive(self):
101 """Make the config drive.
Andrei Danin996e2092018-09-10 21:58:23 -0700102 :raises CalledProcessError if a helper process has failed.
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +0200103 """
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 Danin996e2092018-09-10 21:58:23 -0700112def generate(dst, hostname, domainname, instance_id=None, user_data=None,
113 network_data=None):
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +0200114
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 Danin996e2092018-09-10 21:58:23 -0700121 :param user_data: custom user data dictionary.
122 :param network_data: custom network info dictionary.
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +0200123
124 '''
Andrei Danin996e2092018-09-10 21:58:23 -0700125 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 Stremkouski97927ee2018-08-23 23:20:38 +0200129
130 if user_data:
Andrei Danin996e2092018-09-10 21:58:23 -0700131 user_data = '#cloud-config\n\n' + yaml.dump(user_data, default_flow_style=False)
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +0200132
133 data = json.dumps(instance_md)
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +0200134 with ConfigDriveBuilder(dst) as cfgdrive:
Andrei Danin996e2092018-09-10 21:58:23 -0700135 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)