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
Prev Previous commit
Next Next commit
chroot: add apply_, sls and highstate for state execution
Based on the code from the SSH client wrapper, add apply_, sls and
highstate for the execution of Salt states inside a chroot environment.
  • Loading branch information
aplanas committed Nov 21, 2019
commit 7b6b7aba0d8e878ff81b0108ac78a494cdae818a
218 changes: 211 additions & 7 deletions salt/modules/chroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@
:platform: Linux
'''
from __future__ import absolute_import, print_function, unicode_literals
import copy
import logging
import os
import sys
import tempfile

from salt.defaults.exitcodes import EX_OK
from salt.exceptions import CommandExecutionError
from salt.utils.args import clean_kwargs

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__)

Expand Down Expand Up @@ -94,10 +105,12 @@ def call(root, function, *args, **kwargs):
'''

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

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

# Create a temporary directory inside the chroot where we can
# untar salt-thin
Expand All @@ -122,7 +135,7 @@ def call(root, function, *args, **kwargs):
chroot_path = os.path.join(os.path.sep,
os.path.relpath(thin_dest_path, root))
try:
safe_kwargs = clean_kwargs(**kwargs)
safe_kwargs = salt.utils.args.clean_kwargs(**kwargs)
salt_argv = [
'python{}'.format(sys.version_info[0]),
os.path.join(chroot_path, 'salt-call'),
Expand All @@ -134,7 +147,9 @@ def call(root, function, *args, **kwargs):
'-l', 'quiet',
'--',
function
] + list(args) + ['{}={}'.format(k, v) for (k, v) in safe_kwargs.items()]
] + 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
Expand All @@ -151,3 +166,192 @@ def call(root, function, *args, **kwargs):
}
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)
44 changes: 44 additions & 0 deletions tests/unit/modules/test_chroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,47 @@ def test_call_success_parameters(self, mkdtemp, exist):
'--out', 'json', '-l', 'quiet',
'--', 'module.function', 'key=value'])
utils_mock['files.rm_rf'].assert_called_once()

@patch('salt.modules.chroot._create_and_execute_salt_state')
@patch('salt.client.ssh.state.SSHHighState')
@patch('salt.fileclient.get_file_client')
@patch('salt.utils.state.get_sls_opts')
def test_sls(self, get_sls_opts, get_file_client, SSHHighState,
_create_and_execute_salt_state):
'''
Test execution of Salt states in chroot.
'''
SSHHighState.return_value = SSHHighState
SSHHighState.render_highstate.return_value = (None, [])
SSHHighState.state.reconcile_extend.return_value = (None, [])
SSHHighState.state.requisite_in.return_value = (None, [])
SSHHighState.state.verify_high.return_value = []

_create_and_execute_salt_state.return_value = 'result'
opts_mock = {
'hash_type': 'md5',
}
get_sls_opts.return_value = opts_mock
with patch.dict(chroot.__opts__, opts_mock):
self.assertEqual(chroot.sls('/chroot', 'module'), 'result')
_create_and_execute_salt_state.assert_called_once()

@patch('salt.modules.chroot._create_and_execute_salt_state')
@patch('salt.client.ssh.state.SSHHighState')
@patch('salt.fileclient.get_file_client')
@patch('salt.utils.state.get_sls_opts')
def test_highstate(self, get_sls_opts, get_file_client, SSHHighState,
_create_and_execute_salt_state):
'''
Test execution of Salt states in chroot.
'''
SSHHighState.return_value = SSHHighState

_create_and_execute_salt_state.return_value = 'result'
opts_mock = {
'hash_type': 'md5',
}
get_sls_opts.return_value = opts_mock
with patch.dict(chroot.__opts__, opts_mock):
self.assertEqual(chroot.highstate('/chroot'), 'result')
_create_and_execute_salt_state.assert_called_once()