Ondrej Smola | 86bf61a | 2016-11-28 10:20:16 +0100 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | ''' |
| 3 | Virtual machine image management tools |
| 4 | ''' |
| 5 | |
| 6 | from __future__ import absolute_import |
| 7 | |
| 8 | # Import python libs |
| 9 | import os |
| 10 | import shutil |
| 11 | import logging |
| 12 | import tempfile |
| 13 | |
| 14 | # Import salt libs |
| 15 | import salt.crypt |
| 16 | import salt.utils |
| 17 | import salt.utils.cloud |
| 18 | import salt.config |
| 19 | import salt.syspaths |
| 20 | import uuid |
| 21 | |
| 22 | |
| 23 | # Set up logging |
| 24 | log = logging.getLogger(__name__) |
| 25 | |
| 26 | # Don't shadow built-in's. |
| 27 | __func_alias__ = { |
| 28 | 'apply_': 'apply' |
| 29 | } |
| 30 | |
| 31 | |
| 32 | def _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 | |
| 39 | def 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 | |
| 64 | def _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 | |
| 86 | def _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 Stremkouski | 97927ee | 2018-08-23 23:20:38 +0200 | [diff] [blame] | 94 | def 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 Smola | 86bf61a | 2016-11-28 10:20:16 +0100 | [diff] [blame] | 104 | ''' |
| 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 | |
| 198 | def 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 | |
| 257 | def _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 Avadanii | 398e69f | 2017-08-21 02:03:01 +0200 | [diff] [blame] | 267 | arg = 'stable {0}'.format('.'.join(salt.version.__version__.split('.')[:2])) |
Ondrej Smola | 86bf61a | 2016-11-28 10:20:16 +0100 | [diff] [blame] | 268 | cmd = 'if type salt-minion; then exit 0; ' |
Alexandru Avadanii | 398e69f | 2017-08-21 02:03:01 +0200 | [diff] [blame] | 269 | cmd += 'else sh {0} -c /tmp {1}; fi'.format( |
| 270 | os.path.join(tmppath, 'bootstrap-salt.sh'), arg) |
Ondrej Smola | 86bf61a | 2016-11-28 10:20:16 +0100 | [diff] [blame] | 271 | return not __salt__['cmd.run_chroot'](mpt, cmd, python_shell=True)['retcode'] |
| 272 | |
| 273 | |
| 274 | def _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 | |
| 295 | def _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) |