Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chroot: add apply_, sls and highstate for state execution #55345

Merged
merged 12 commits into from Dec 3, 2019
357 changes: 357 additions & 0 deletions salt/modules/chroot.py
@@ -0,0 +1,357 @@
# -*- coding: utf-8 -*-

'''
:maintainer: Alberto Planas <aplanas@suse.com>
:maturity: new
:depends: None
:platform: Linux
'''
from __future__ import absolute_import, print_function, unicode_literals
import copy
import logging
import os
import sys
import tempfile


import salt
import salt.client.ssh.state
import salt.client.ssh.wrapper.state
import salt.defaults.exitcodes
import salt.exceptions
import salt.ext.six as six
import salt.utils.args


__func_alias__ = {
'apply_': 'apply'
}

log = logging.getLogger(__name__)


def __virtual__():
'''
Chroot command is required.
'''
if __utils__['path.which']('chroot') is not None:
return True
else:
return (False, 'Module chroot requires the command chroot')


def exist(root):
'''
Return True if the chroot environment is present.
'''
dev = os.path.join(root, 'dev')
proc = os.path.join(root, 'proc')
sys = os.path.join(root, 'sys')
return all(os.path.isdir(i) for i in (root, dev, proc, sys))


def create(root):
'''
Create a basic chroot environment.

Note that this environment is not functional. The caller needs to
install the minimal required binaries, including Python if
chroot.call is called.

root
Path to the chroot environment

CLI Example:

.. code-block:: bash

salt myminion chroot.create /chroot

'''
if not exist(root):
dev = os.path.join(root, 'dev')
proc = os.path.join(root, 'proc')
sys = os.path.join(root, 'sys')
try:
os.makedirs(dev, mode=0o755)
os.makedirs(proc, mode=0o555)
os.makedirs(sys, mode=0o555)
except OSError as e:
log.error('Error when trying to create chroot directories: %s', e)
return False
return True


def call(root, function, *args, **kwargs):
'''
Executes a Salt function inside a chroot environment.

The chroot does not need to have Salt installed, but Python is
required.

root
Path to the chroot environment

function
Salt execution module function

CLI Example:

.. code-block:: bash

salt myminion chroot.call /chroot test.ping
salt myminion chroot.call /chroot ssh.set_auth_key user key=mykey

'''

if not function:
raise salt.exceptions.CommandExecutionError(
'Missing function parameter')

if not exist(root):
raise salt.exceptions.CommandExecutionError(
'Chroot environment not found')

# Create a temporary directory inside the chroot where we can
# untar salt-thin
thin_dest_path = tempfile.mkdtemp(dir=root)
thin_path = __utils__['thin.gen_thin'](
__opts__['cachedir'],
extra_mods=__salt__['config.option']('thin_extra_mods', ''),
so_mods=__salt__['config.option']('thin_so_mods', '')
)
# Some bug in Salt is preventing us to use `archive.tar` here. A
# AsyncZeroMQReqChannel is not closed at the end os the salt-call,
# and makes the client never exit.
#
# stdout = __salt__['archive.tar']('xzf', thin_path, dest=thin_dest_path)
#
stdout = __salt__['cmd.run'](['tar', 'xzf', thin_path,
'-C', thin_dest_path])
if stdout:
__utils__['files.rm_rf'](thin_dest_path)
return {'result': False, 'comment': stdout}

chroot_path = os.path.join(os.path.sep,
os.path.relpath(thin_dest_path, root))
try:
safe_kwargs = salt.utils.args.clean_kwargs(**kwargs)
salt_argv = [
'python{}'.format(sys.version_info[0]),
os.path.join(chroot_path, 'salt-call'),
'--metadata',
'--local',
'--log-file', os.path.join(chroot_path, 'log'),
'--cachedir', os.path.join(chroot_path, 'cache'),
'--out', 'json',
'-l', 'quiet',
'--',
function
] + list(args) + [
'{}={}'.format(k, v) for (k, v) in safe_kwargs.items()
]
ret = __salt__['cmd.run_chroot'](root, [str(x) for x in salt_argv])

# Process "real" result in stdout
try:
data = __utils__['json.find_json'](ret['stdout'])
local = data.get('local', data)
if isinstance(local, dict) and 'retcode' in local:
__context__['retcode'] = local['retcode']
return local.get('return', data)
except (KeyError, ValueError):
return {
'result': False,
'comment': "Can't parse container command output"
}
finally:
__utils__['files.rm_rf'](thin_dest_path)


def apply_(root, mods=None, **kwargs):
'''
Apply an state inside a chroot.

This function will call `chroot.highstate` or `chroot.sls` based
on the arguments passed to this function. It exists as a more
intuitive way of applying states.

root
Path to the chroot environment

For a formal description of the possible parameters accepted in
this function, check `state.apply_` documentation.

CLI Example:

.. code-block:: bash

salt myminion chroot.apply /chroot
salt myminion chroot.apply /chroot stuff
salt myminion chroot.apply /chroot stuff pillar='{"foo": "bar"}'

'''
if mods:
return sls(root, mods, **kwargs)
return highstate(root, **kwargs)


def _create_and_execute_salt_state(root, chunks, file_refs, test, hash_type):
'''
Create the salt_stage tarball, and execute in the chroot
'''
# Create the tar containing the state pkg and relevant files.
salt.client.ssh.wrapper.state._cleanup_slsmod_low_data(chunks)
trans_tar = salt.client.ssh.state.prep_trans_tar(
salt.fileclient.get_file_client(__opts__), chunks, file_refs,
__pillar__, root)
trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, hash_type)

ret = None

# Create a temporary directory inside the chroot where we can move
# the salt_stage.tgz
salt_state_path = tempfile.mkdtemp(dir=root)
salt_state_path = os.path.join(salt_state_path, 'salt_state.tgz')
salt_state_path_in_chroot = salt_state_path.replace(root, '', 1)
try:
salt.utils.files.copyfile(trans_tar, salt_state_path)
ret = call(root, 'state.pkg', salt_state_path_in_chroot,
test=test, pkg_sum=trans_tar_sum,
hash_type=hash_type)
finally:
__utils__['files.rm_rf'](salt_state_path)

return ret


def sls(root, mods, saltenv='base', test=None, exclude=None, **kwargs):
'''
Execute the states in one or more SLS files inside the chroot.

root
Path to the chroot environment

saltenv
Specify a salt fileserver environment to be used when applying
states

mods
List of states to execute

test
Run states in test-only (dry-run) mode

exclude
Exclude specific states from execution. Accepts a list of sls
names, a comma-separated string of sls names, or a list of
dictionaries containing ``sls`` or ``id`` keys. Glob-patterns
may be used to match multiple states.

For a formal description of the possible parameters accepted in
this function, check `state.sls` documentation.

CLI Example:

.. code-block:: bash

salt '*' chroot.sls /chroot stuff pillar='{"foo": "bar"}'
'''
# Get a copy of the pillar data, to avoid overwriting the current
# pillar, instead the one delegated
pillar = copy.deepcopy(__pillar__)
pillar.update(kwargs.get('pillar', {}))

# Clone the options data and apply some default values. May not be
# needed, as this module just delegate
opts = salt.utils.state.get_sls_opts(__opts__, **kwargs)
st_ = salt.client.ssh.state.SSHHighState(
opts, pillar, __salt__,
salt.fileclient.get_file_client(__opts__))

if isinstance(mods, six.string_types):
mods = mods.split(',')

high_data, errors = st_.render_highstate({saltenv: mods})
if exclude:
if isinstance(exclude, six.string_types):
exclude = exclude.split(',')
if '__exclude__' in high_data:
high_data['__exclude__'].extend(exclude)
else:
high_data['__exclude__'] = exclude

high_data, ext_errors = st_.state.reconcile_extend(high_data)
errors += ext_errors
errors += st_.state.verify_high(high_data)
if errors:
return errors

high_data, req_in_errors = st_.state.requisite_in(high_data)
errors += req_in_errors
if errors:
return errors

high_data = st_.state.apply_exclude(high_data)

# Compile and verify the raw chunks
chunks = st_.state.compile_high_data(high_data)
file_refs = salt.client.ssh.state.lowstate_file_refs(
chunks,
salt.client.ssh.wrapper.state._merge_extra_filerefs(
kwargs.get('extra_filerefs', ''),
opts.get('extra_filerefs', '')))

hash_type = opts['hash_type']
return _create_and_execute_salt_state(root, chunks, file_refs, test,
hash_type)


def highstate(root, **kwargs):
'''
Retrieve the state data from the salt master for this minion and
execute it inside the chroot.

root
Path to the chroot environment

For a formal description of the possible parameters accepted in
this function, check `state.highstate` documentation.

CLI Example:

.. code-block:: bash

salt myminion chroot.highstate /chroot
salt myminion chroot.highstate /chroot pillar='{"foo": "bar"}'

'''
# Get a copy of the pillar data, to avoid overwriting the current
# pillar, instead the one delegated
pillar = copy.deepcopy(__pillar__)
pillar.update(kwargs.get('pillar', {}))

# Clone the options data and apply some default values. May not be
# needed, as this module just delegate
opts = salt.utils.state.get_sls_opts(__opts__, **kwargs)
st_ = salt.client.ssh.state.SSHHighState(
opts, pillar, __salt__,
salt.fileclient.get_file_client(__opts__))

# Compile and verify the raw chunks
chunks = st_.compile_low_chunks()
file_refs = salt.client.ssh.state.lowstate_file_refs(
chunks,
salt.client.ssh.wrapper.state._merge_extra_filerefs(
kwargs.get('extra_filerefs', ''),
opts.get('extra_filerefs', '')))
# Check for errors
for chunk in chunks:
if not isinstance(chunk, dict):
__context__['retcode'] = 1
return chunks

test = kwargs.pop('test', False)
hash_type = opts['hash_type']
return _create_and_execute_salt_state(root, chunks, file_refs, test,
hash_type)