blob: 4d67e274f2ae3f7eeee5875f015275bd7fe14659 [file] [log] [blame]
Ondrej Smola86bf61a2016-11-28 10:20:16 +01001# -*- coding: utf-8 -*-
2'''
3Virtual machine image management tools
4'''
5
6from __future__ import absolute_import
7
8# Import python libs
9import os
10import shutil
11import logging
12import tempfile
13
14# Import salt libs
15import salt.crypt
16import salt.utils
17import salt.utils.cloud
18import salt.config
19import salt.syspaths
20import uuid
21
22
23# Set up logging
24log = logging.getLogger(__name__)
25
26# Don't shadow built-in's.
27__func_alias__ = {
28 'apply_': 'apply'
29}
30
31
32def _file_or_content(file_):
33 if os.path.exists(file_):
34 with salt.utils.fopen(file_) as fic:
35 return fic.read()
36 return file_
37
38
39def prep_bootstrap(mpt):
40 '''
41 Update and get the random script to a random place
42
43 CLI Example:
44
45 .. code-block:: bash
46
47 salt '*' seed.prep_bootstrap /tmp
48
49 '''
50 # Verify that the boostrap script is downloaded
51 bs_ = __salt__['config.gather_bootstrap_script']()
52 fpd_ = os.path.join(mpt, 'tmp', "{0}".format(
53 uuid.uuid4()))
54 if not os.path.exists(fpd_):
55 os.makedirs(fpd_)
56 os.chmod(fpd_, 0o700)
57 fp_ = os.path.join(fpd_, os.path.basename(bs_))
58 # Copy script into tmp
59 shutil.copy(bs_, fp_)
60 tmppath = fpd_.replace(mpt, '')
61 return fp_, tmppath
62
63
64def _mount(path, ftype, root=None):
65 mpt = None
66 if ftype == 'block':
67 mpt = tempfile.mkdtemp()
68 if not __salt__['mount.mount'](mpt, path):
69 os.rmdir(mpt)
70 return None
71 elif ftype == 'dir':
72 return path
73 elif ftype == 'file':
74 if 'guestfs.mount' in __salt__:
75 util = 'guestfs'
76 elif 'qemu_nbd.init' in __salt__:
77 util = 'qemu_nbd'
78 else:
79 return None
80 mpt = __salt__['mount.mount'](path, device=root, util=util)
81 if not mpt:
82 return None
83 return mpt
84
85
86def _umount(mpt, ftype):
87 if ftype == 'block':
88 __salt__['mount.umount'](mpt)
89 os.rmdir(mpt)
90 elif ftype == 'file':
91 __salt__['mount.umount'](mpt, util='qemu_nbd')
92
93
Dzmitry Stremkouski97927ee2018-08-23 23:20:38 +020094def apply_(
95 path, id_=None,
96 config=None,
97 approve_key=True,
98 install=True,
99 prep_install=False,
100 pub_key=None,
101 priv_key=None,
102 mount_point=None
103 ):
Ondrej Smola86bf61a2016-11-28 10:20:16 +0100104 '''
105 Seed a location (disk image, directory, or block device) with the
106 minion config, approve the minion's key, and/or install salt-minion.
107
108 CLI Example:
109
110 .. code-block:: bash
111
112 salt 'minion' seed.apply path id [config=config_data] \\
113 [gen_key=(true|false)] [approve_key=(true|false)] \\
114 [install=(true|false)]
115
116 path
117 Full path to the directory, device, or disk image on the target
118 minion's file system.
119
120 id
121 Minion id with which to seed the path.
122
123 config
124 Minion configuration options. By default, the 'master' option is set to
125 the target host's 'master'.
126
127 approve_key
128 Request a pre-approval of the generated minion key. Requires
129 that the salt-master be configured to either auto-accept all keys or
130 expect a signing request from the target host. Default: true.
131
132 install
133 Install salt-minion, if absent. Default: true.
134
135 prep_install
136 Prepare the bootstrap script, but don't run it. Default: false
137 '''
138 stats = __salt__['file.stats'](path, follow_symlinks=True)
139 if not stats:
140 return '{0} does not exist'.format(path)
141 ftype = stats['type']
142 path = stats['target']
143 log.debug('Mounting {0} at {1}'.format(ftype, path))
144 try:
145 os.makedirs(path)
146 except OSError:
147 # The directory already exists
148 pass
149
150 mpt = _mount(path, ftype, mount_point)
151
152 if not mpt:
153 return '{0} could not be mounted'.format(path)
154
155 tmp = os.path.join(mpt, 'tmp')
156 log.debug('Attempting to create directory {0}'.format(tmp))
157 try:
158 os.makedirs(tmp)
159 except OSError:
160 if not os.path.isdir(tmp):
161 raise
162 cfg_files = mkconfig(config, tmp=tmp, id_=id_, approve_key=approve_key,
163 pub_key=pub_key, priv_key=priv_key)
164
165 if _check_install(mpt):
166 # salt-minion is already installed, just move the config and keys
167 # into place
168 log.info('salt-minion pre-installed on image, '
169 'configuring as {0}'.format(id_))
170 minion_config = salt.config.minion_config(cfg_files['config'])
171 pki_dir = minion_config['pki_dir']
172 if not os.path.isdir(os.path.join(mpt, pki_dir.lstrip('/'))):
173 __salt__['file.makedirs'](
174 os.path.join(mpt, pki_dir.lstrip('/'), '')
175 )
176 os.rename(cfg_files['privkey'], os.path.join(
177 mpt, pki_dir.lstrip('/'), 'minion.pem'))
178 os.rename(cfg_files['pubkey'], os.path.join(
179 mpt, pki_dir.lstrip('/'), 'minion.pub'))
180 os.rename(cfg_files['config'], os.path.join(mpt, 'etc/salt/minion'))
181 res = True
182 elif install:
183 log.info('Attempting to install salt-minion to {0}'.format(mpt))
184 res = _install(mpt)
185 elif prep_install:
186 log.error('The prep_install option is no longer supported. Please use '
187 'the bootstrap script installed with Salt, located at {0}.'
188 .format(salt.syspaths.BOOTSTRAP))
189 res = False
190 else:
191 log.warning('No useful action performed on {0}'.format(mpt))
192 res = False
193
194 _umount(mpt, ftype)
195 return res
196
197
198def mkconfig(config=None,
199 tmp=None,
200 id_=None,
201 approve_key=True,
202 pub_key=None,
203 priv_key=None):
204 '''
205 Generate keys and config and put them in a tmp directory.
206
207 pub_key
208 absolute path or file content of an optional preseeded salt key
209
210 priv_key
211 absolute path or file content of an optional preseeded salt key
212
213 CLI Example:
214
215 .. code-block:: bash
216
217 salt 'minion' seed.mkconfig [config=config_data] [tmp=tmp_dir] \\
218 [id_=minion_id] [approve_key=(true|false)]
219 '''
220 if tmp is None:
221 tmp = tempfile.mkdtemp()
222 if config is None:
223 config = {}
224 if 'master' not in config and __opts__['master'] != 'salt':
225 config['master'] = __opts__['master']
226 if id_:
227 config['id'] = id_
228
229 # Write the new minion's config to a tmp file
230 tmp_config = os.path.join(tmp, 'minion')
231 with salt.utils.fopen(tmp_config, 'w+') as fp_:
232 fp_.write(salt.utils.cloud.salt_config_to_yaml(config))
233
234 # Generate keys for the minion
235 pubkeyfn = os.path.join(tmp, 'minion.pub')
236 privkeyfn = os.path.join(tmp, 'minion.pem')
237 preseeded = pub_key and priv_key
238 if preseeded:
239 log.debug('Writing minion.pub to {0}'.format(pubkeyfn))
240 log.debug('Writing minion.pem to {0}'.format(privkeyfn))
241 with salt.utils.fopen(pubkeyfn, 'w') as fic:
242 fic.write(_file_or_content(pub_key))
243 with salt.utils.fopen(privkeyfn, 'w') as fic:
244 fic.write(_file_or_content(priv_key))
245 os.chmod(pubkeyfn, 0o600)
246 os.chmod(privkeyfn, 0o600)
247 else:
248 salt.crypt.gen_keys(tmp, 'minion', 2048)
249 if approve_key and not preseeded:
250 with salt.utils.fopen(pubkeyfn) as fp_:
251 pubkey = fp_.read()
252 __salt__['pillar.ext']({'virtkey': [id_, pubkey]})
253
254 return {'config': tmp_config, 'pubkey': pubkeyfn, 'privkey': privkeyfn}
255
256
257def _install(mpt):
258 '''
259 Determine whether salt-minion is installed and, if not,
260 install it.
261 Return True if install is successful or already installed.
262 '''
263 _check_resolv(mpt)
264 boot_, tmppath = (prep_bootstrap(mpt)
265 or salt.syspaths.BOOTSTRAP)
266 # Exec the chroot command
Alexandru Avadanii398e69f2017-08-21 02:03:01 +0200267 arg = 'stable {0}'.format('.'.join(salt.version.__version__.split('.')[:2]))
Ondrej Smola86bf61a2016-11-28 10:20:16 +0100268 cmd = 'if type salt-minion; then exit 0; '
Alexandru Avadanii398e69f2017-08-21 02:03:01 +0200269 cmd += 'else sh {0} -c /tmp {1}; fi'.format(
270 os.path.join(tmppath, 'bootstrap-salt.sh'), arg)
Ondrej Smola86bf61a2016-11-28 10:20:16 +0100271 return not __salt__['cmd.run_chroot'](mpt, cmd, python_shell=True)['retcode']
272
273
274def _check_resolv(mpt):
275 '''
276 Check that the resolv.conf is present and populated
277 '''
278 resolv = os.path.join(mpt, 'etc/resolv.conf')
279 replace = False
280 if os.path.islink(resolv):
281 resolv = os.path.realpath(resolv)
282 if not os.path.isdir(os.path.dirname(resolv)):
283 os.makedirs(os.path.dirname(resolv))
284 if not os.path.isfile(resolv):
285 replace = True
286 if not replace:
287 with salt.utils.fopen(resolv, 'rb') as fp_:
288 conts = fp_.read()
289 if 'nameserver' not in conts:
290 replace = True
291 if replace:
292 shutil.copy('/etc/resolv.conf', resolv)
293
294
295def _check_install(root):
296 sh_ = '/bin/sh'
297 if os.path.isfile(os.path.join(root, 'bin/bash')):
298 sh_ = '/bin/bash'
299
300 cmd = ('if ! type salt-minion; then exit 1; fi')
301 cmd = 'chroot \'{0}\' {1} -c \'{2}\''.format(
302 root,
303 sh_,
304 cmd)
305
306 return not __salt__['cmd.retcode'](cmd,
307 output_loglevel='quiet',
308 python_shell=True)