blob: 1d93c5d83f287f62ca55ec597732288caab1c293 [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
94def apply_(path, id_=None, config=None, approve_key=True, install=True,
95 prep_install=False, pub_key=None, priv_key=None, mount_point=None):
96 '''
97 Seed a location (disk image, directory, or block device) with the
98 minion config, approve the minion's key, and/or install salt-minion.
99
100 CLI Example:
101
102 .. code-block:: bash
103
104 salt 'minion' seed.apply path id [config=config_data] \\
105 [gen_key=(true|false)] [approve_key=(true|false)] \\
106 [install=(true|false)]
107
108 path
109 Full path to the directory, device, or disk image on the target
110 minion's file system.
111
112 id
113 Minion id with which to seed the path.
114
115 config
116 Minion configuration options. By default, the 'master' option is set to
117 the target host's 'master'.
118
119 approve_key
120 Request a pre-approval of the generated minion key. Requires
121 that the salt-master be configured to either auto-accept all keys or
122 expect a signing request from the target host. Default: true.
123
124 install
125 Install salt-minion, if absent. Default: true.
126
127 prep_install
128 Prepare the bootstrap script, but don't run it. Default: false
129 '''
130 stats = __salt__['file.stats'](path, follow_symlinks=True)
131 if not stats:
132 return '{0} does not exist'.format(path)
133 ftype = stats['type']
134 path = stats['target']
135 log.debug('Mounting {0} at {1}'.format(ftype, path))
136 try:
137 os.makedirs(path)
138 except OSError:
139 # The directory already exists
140 pass
141
142 mpt = _mount(path, ftype, mount_point)
143
144 if not mpt:
145 return '{0} could not be mounted'.format(path)
146
147 tmp = os.path.join(mpt, 'tmp')
148 log.debug('Attempting to create directory {0}'.format(tmp))
149 try:
150 os.makedirs(tmp)
151 except OSError:
152 if not os.path.isdir(tmp):
153 raise
154 cfg_files = mkconfig(config, tmp=tmp, id_=id_, approve_key=approve_key,
155 pub_key=pub_key, priv_key=priv_key)
156
157 if _check_install(mpt):
158 # salt-minion is already installed, just move the config and keys
159 # into place
160 log.info('salt-minion pre-installed on image, '
161 'configuring as {0}'.format(id_))
162 minion_config = salt.config.minion_config(cfg_files['config'])
163 pki_dir = minion_config['pki_dir']
164 if not os.path.isdir(os.path.join(mpt, pki_dir.lstrip('/'))):
165 __salt__['file.makedirs'](
166 os.path.join(mpt, pki_dir.lstrip('/'), '')
167 )
168 os.rename(cfg_files['privkey'], os.path.join(
169 mpt, pki_dir.lstrip('/'), 'minion.pem'))
170 os.rename(cfg_files['pubkey'], os.path.join(
171 mpt, pki_dir.lstrip('/'), 'minion.pub'))
172 os.rename(cfg_files['config'], os.path.join(mpt, 'etc/salt/minion'))
173 res = True
174 elif install:
175 log.info('Attempting to install salt-minion to {0}'.format(mpt))
176 res = _install(mpt)
177 elif prep_install:
178 log.error('The prep_install option is no longer supported. Please use '
179 'the bootstrap script installed with Salt, located at {0}.'
180 .format(salt.syspaths.BOOTSTRAP))
181 res = False
182 else:
183 log.warning('No useful action performed on {0}'.format(mpt))
184 res = False
185
186 _umount(mpt, ftype)
187 return res
188
189
190def mkconfig(config=None,
191 tmp=None,
192 id_=None,
193 approve_key=True,
194 pub_key=None,
195 priv_key=None):
196 '''
197 Generate keys and config and put them in a tmp directory.
198
199 pub_key
200 absolute path or file content of an optional preseeded salt key
201
202 priv_key
203 absolute path or file content of an optional preseeded salt key
204
205 CLI Example:
206
207 .. code-block:: bash
208
209 salt 'minion' seed.mkconfig [config=config_data] [tmp=tmp_dir] \\
210 [id_=minion_id] [approve_key=(true|false)]
211 '''
212 if tmp is None:
213 tmp = tempfile.mkdtemp()
214 if config is None:
215 config = {}
216 if 'master' not in config and __opts__['master'] != 'salt':
217 config['master'] = __opts__['master']
218 if id_:
219 config['id'] = id_
220
221 # Write the new minion's config to a tmp file
222 tmp_config = os.path.join(tmp, 'minion')
223 with salt.utils.fopen(tmp_config, 'w+') as fp_:
224 fp_.write(salt.utils.cloud.salt_config_to_yaml(config))
225
226 # Generate keys for the minion
227 pubkeyfn = os.path.join(tmp, 'minion.pub')
228 privkeyfn = os.path.join(tmp, 'minion.pem')
229 preseeded = pub_key and priv_key
230 if preseeded:
231 log.debug('Writing minion.pub to {0}'.format(pubkeyfn))
232 log.debug('Writing minion.pem to {0}'.format(privkeyfn))
233 with salt.utils.fopen(pubkeyfn, 'w') as fic:
234 fic.write(_file_or_content(pub_key))
235 with salt.utils.fopen(privkeyfn, 'w') as fic:
236 fic.write(_file_or_content(priv_key))
237 os.chmod(pubkeyfn, 0o600)
238 os.chmod(privkeyfn, 0o600)
239 else:
240 salt.crypt.gen_keys(tmp, 'minion', 2048)
241 if approve_key and not preseeded:
242 with salt.utils.fopen(pubkeyfn) as fp_:
243 pubkey = fp_.read()
244 __salt__['pillar.ext']({'virtkey': [id_, pubkey]})
245
246 return {'config': tmp_config, 'pubkey': pubkeyfn, 'privkey': privkeyfn}
247
248
249def _install(mpt):
250 '''
251 Determine whether salt-minion is installed and, if not,
252 install it.
253 Return True if install is successful or already installed.
254 '''
255 _check_resolv(mpt)
256 boot_, tmppath = (prep_bootstrap(mpt)
257 or salt.syspaths.BOOTSTRAP)
258 # Exec the chroot command
Alexandru Avadanii398e69f2017-08-21 02:03:01 +0200259 arg = 'stable {0}'.format('.'.join(salt.version.__version__.split('.')[:2]))
Ondrej Smola86bf61a2016-11-28 10:20:16 +0100260 cmd = 'if type salt-minion; then exit 0; '
Alexandru Avadanii398e69f2017-08-21 02:03:01 +0200261 cmd += 'else sh {0} -c /tmp {1}; fi'.format(
262 os.path.join(tmppath, 'bootstrap-salt.sh'), arg)
Ondrej Smola86bf61a2016-11-28 10:20:16 +0100263 return not __salt__['cmd.run_chroot'](mpt, cmd, python_shell=True)['retcode']
264
265
266def _check_resolv(mpt):
267 '''
268 Check that the resolv.conf is present and populated
269 '''
270 resolv = os.path.join(mpt, 'etc/resolv.conf')
271 replace = False
272 if os.path.islink(resolv):
273 resolv = os.path.realpath(resolv)
274 if not os.path.isdir(os.path.dirname(resolv)):
275 os.makedirs(os.path.dirname(resolv))
276 if not os.path.isfile(resolv):
277 replace = True
278 if not replace:
279 with salt.utils.fopen(resolv, 'rb') as fp_:
280 conts = fp_.read()
281 if 'nameserver' not in conts:
282 replace = True
283 if replace:
284 shutil.copy('/etc/resolv.conf', resolv)
285
286
287def _check_install(root):
288 sh_ = '/bin/sh'
289 if os.path.isfile(os.path.join(root, 'bin/bash')):
290 sh_ = '/bin/bash'
291
292 cmd = ('if ! type salt-minion; then exit 1; fi')
293 cmd = 'chroot \'{0}\' {1} -c \'{2}\''.format(
294 root,
295 sh_,
296 cmd)
297
298 return not __salt__['cmd.retcode'](cmd,
299 output_loglevel='quiet',
300 python_shell=True)