Merge branch '2018.3' into 2018.3

This commit is contained in:
Mike Place 2018-10-16 11:34:04 +02:00 committed by GitHub
commit 06bb2453ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 451 additions and 192 deletions

View file

@ -2402,6 +2402,12 @@ Master will not be returned to the Minion.
------------------------------
.. versionadded:: 2014.1.0
.. deprecated:: 2018.3.4
This option is now ignored. Firstly, it only traversed
:conf_master:`file_roots`, which means it did not work for the other
fileserver backends. Secondly, since this option was added we have added
caching to the code that traverses the file_roots (and gitfs, etc.), which
greatly reduces the amount of traversal that is done.
Default: ``False``

View file

@ -25,6 +25,7 @@ execution modules
aix_group
aliases
alternatives
ansiblegate
apache
apcups
apf

View file

@ -0,0 +1,6 @@
========================
salt.modules.ansiblegate
========================
.. automodule:: salt.modules.ansiblegate
:members:

View file

@ -42,6 +42,7 @@ pillar modules
reclass_adapter
redismod
s3
saltclass
sql_base
sqlcipher
sqlite3
@ -52,4 +53,3 @@ pillar modules
venafi
virtkey
vmware_pillar

View file

@ -0,0 +1,6 @@
=====================
salt.pillar.saltclass
=====================
.. automodule:: salt.pillar.saltclass
:members:

View file

@ -13,6 +13,7 @@ state modules
acme
alias
alternatives
ansiblegate
apache
apache_conf
apache_module

View file

@ -0,0 +1,6 @@
=======================
salt.states.ansiblegate
=======================
.. automodule:: salt.states.ansiblegate
:members:

View file

@ -14,4 +14,5 @@ master tops modules
ext_nodes
mongo
reclass_adapter
saltclass
varstack

View file

@ -0,0 +1,6 @@
===================
salt.tops.saltclass
===================
.. automodule:: salt.tops.saltclass
:members:

View file

@ -114,6 +114,12 @@ Set up the provider cloud config at ``/etc/salt/cloud.providers`` or
driver: gce
.. note::
Empty strings as values for ``service_account_private_key`` and ``service_account_email_address``
can be used on GCE instances. This will result in the service account assigned to the GCE instance
being used.
.. note::
The value provided for ``project`` must not contain underscores or spaces and

View file

@ -35,8 +35,8 @@ NAPALM
NAPALM (Network Automation and Programmability Abstraction Layer with
Multivendor support) is an opensourced Python library that implements a set of
functions to interact with different router vendor devices using a unified API.
Begin vendor-agnostic simplifies the operations, as the configuration
and the interaction with the network device does not rely on a particular vendor.
Being vendor-agnostic simplifies operations, as the configuration and
interaction with the network device does not rely on a particular vendor.
.. image:: /_static/napalm_logo.png

View file

@ -4,3 +4,13 @@ In Progress: Salt 2017.7.9 Release Notes
Version 2017.7.9 is an **unreleased** bugfix release for :ref:`2017.7.0 <release-2017-7-0>`.
This release is still in progress and has not been released yet.
Salt Cloud Features
===================
GCE Driver
----------
The GCE salt cloud driver can now be used with GCE instance credentials by
setting the configuration paramaters ``service_account_private_key`` and
``service_account_private_email`` to an empty string.

View file

@ -1182,7 +1182,7 @@ class LocalClient(object):
# stop the iteration, since the jid is invalid
raise StopIteration()
except Exception as exc:
log.warning('Returner unavailable: %s', exc)
log.warning('Returner unavailable: %s', exc, exc_info_on_loglevel=logging.DEBUG)
# Wait for the hosts to check in
last_time = False
# iterator for this job's return

View file

@ -928,7 +928,7 @@ def create_interface(call=None, kwargs=None): # pylint: disable=unused-argument
)
if pub_ip_data.ip_address: # pylint: disable=no-member
ip_kwargs['public_ip_address'] = PublicIPAddress(
six.text_type(pub_ip_data.id), # pylint: disable=no-member
id=six.text_type(pub_ip_data.id), # pylint: disable=no-member
)
ip_configurations = [
NetworkInterfaceIPConfiguration(

View file

@ -134,7 +134,8 @@ def __virtual__():
parameters = details['gce']
pathname = os.path.expanduser(parameters['service_account_private_key'])
if salt.utils.cloud.check_key_path_and_mode(
# empty pathname will tell libcloud to use instance credentials
if pathname and salt.utils.cloud.check_key_path_and_mode(
provider, pathname
) is False:
return False

View file

@ -593,7 +593,7 @@ class SlackClient(object):
Run each of them through ``get_configured_target(('foo', f), 'pillar.get')`` and confirm a valid target
'''
# Default to targetting all minions with a type of glob
# Default to targeting all minions with a type of glob
null_target = {'target': '*', 'tgt_type': 'glob'}
def check_cmd_against_group(cmd):
@ -627,14 +627,12 @@ class SlackClient(object):
return checked
return null_target
# emulate the yaml_out output formatter. It relies on a global __opts__ object which we can't
# obviously pass in
def format_return_text(self, data, function, **kwargs): # pylint: disable=unused-argument
'''
Print out YAML using the block mode
'''
# emulate the yaml_out output formatter. It relies on a global __opts__ object which
# we can't obviously pass in
try:
# Format results from state runs with highstate output
if function.startswith('state'):

View file

@ -61,7 +61,7 @@ def get_file_client(opts, pillar=False):
return {
'remote': RemoteClient,
'local': FSClient,
'pillar': LocalClient,
'pillar': PillarClient,
}.get(client, RemoteClient)(opts)
@ -347,58 +347,17 @@ class Client(object):
Return a list of all available sls modules on the master for a given
environment
'''
limit_traversal = self.opts.get('fileserver_limit_traversal', False)
states = []
if limit_traversal:
if saltenv not in self.opts['file_roots']:
log.warning(
'During an attempt to list states for saltenv \'%s\', '
'the environment could not be found in the configured '
'file roots', saltenv
)
return states
for path in self.opts['file_roots'][saltenv]:
for root, dirs, files in os.walk(path, topdown=True): # future lint: disable=blacklisted-function
root = salt.utils.data.decode(root)
files = salt.utils.data.decode(files)
log.debug(
'Searching for states in dirs %s and files %s',
salt.utils.data.decode(dirs), files
)
if not [filename.endswith('.sls') for filename in files]:
# Use shallow copy so we don't disturb the memory used
# by os.walk. Otherwise this breaks!
del dirs[:]
else:
for found_file in files:
stripped_root = os.path.relpath(root, path)
if salt.utils.platform.is_windows():
stripped_root = stripped_root.replace('\\', '/')
stripped_root = stripped_root.replace('/', '.')
if found_file.endswith(('.sls')):
if found_file.endswith('init.sls'):
if stripped_root.endswith('.'):
stripped_root = stripped_root.rstrip('.')
states.append(stripped_root)
else:
if not stripped_root.endswith('.'):
stripped_root += '.'
if stripped_root.startswith('.'):
stripped_root = stripped_root.lstrip('.')
states.append(stripped_root + found_file[:-4])
else:
for path in self.file_list(saltenv):
if salt.utils.platform.is_windows():
path = path.replace('\\', '/')
if path.endswith('.sls'):
# is an sls module!
if path.endswith('/init.sls'):
states.append(path.replace('/', '.')[:-9])
else:
states.append(path.replace('/', '.')[:-4])
return states
states = set()
for path in self.file_list(saltenv):
if salt.utils.platform.is_windows():
path = path.replace('\\', '/')
if path.endswith('.sls'):
# is an sls module!
if path.endswith('/init.sls'):
states.add(path.replace('/', '.')[:-9])
else:
states.add(path.replace('/', '.')[:-4])
return sorted(states)
def get_state(self, sls, saltenv, cachedir=None):
'''
@ -844,13 +803,10 @@ class Client(object):
)
class LocalClient(Client):
class PillarClient(Client):
'''
Use the local_roots option to parse a local file root
Used by pillar to handle fileclient requests
'''
def __init__(self, opts):
Client.__init__(self, opts)
def _find_file(self, path, saltenv='base'):
'''
Locate the file path
@ -858,12 +814,12 @@ class LocalClient(Client):
fnd = {'path': '',
'rel': ''}
if saltenv not in self.opts['file_roots']:
if saltenv not in self.opts['pillar_roots']:
return fnd
if salt.utils.url.is_escaped(path):
# The path arguments are escaped
path = salt.utils.url.unescape(path)
for root in self.opts['file_roots'][saltenv]:
for root in self.opts['pillar_roots'][saltenv]:
full = os.path.join(root, path)
if os.path.isfile(full):
fnd['path'] = full
@ -896,10 +852,10 @@ class LocalClient(Client):
with optional relative prefix path to limit directory traversal
'''
ret = []
if saltenv not in self.opts['file_roots']:
if saltenv not in self.opts['pillar_roots']:
return ret
prefix = prefix.strip('/')
for path in self.opts['file_roots'][saltenv]:
for path in self.opts['pillar_roots'][saltenv]:
for root, dirs, files in salt.utils.path.os_walk(
os.path.join(path, prefix), followlinks=True
):
@ -912,14 +868,14 @@ class LocalClient(Client):
def file_list_emptydirs(self, saltenv='base', prefix=''):
'''
List the empty dirs in the file_roots
List the empty dirs in the pillar_roots
with optional relative prefix path to limit directory traversal
'''
ret = []
prefix = prefix.strip('/')
if saltenv not in self.opts['file_roots']:
if saltenv not in self.opts['pillar_roots']:
return ret
for path in self.opts['file_roots'][saltenv]:
for path in self.opts['pillar_roots'][saltenv]:
for root, dirs, files in salt.utils.path.os_walk(
os.path.join(path, prefix), followlinks=True
):
@ -931,14 +887,14 @@ class LocalClient(Client):
def dir_list(self, saltenv='base', prefix=''):
'''
List the dirs in the file_roots
List the dirs in the pillar_roots
with optional relative prefix path to limit directory traversal
'''
ret = []
if saltenv not in self.opts['file_roots']:
if saltenv not in self.opts['pillar_roots']:
return ret
prefix = prefix.strip('/')
for path in self.opts['file_roots'][saltenv]:
for path in self.opts['pillar_roots'][saltenv]:
for root, dirs, files in salt.utils.path.os_walk(
os.path.join(path, prefix), followlinks=True
):
@ -965,7 +921,7 @@ class LocalClient(Client):
def hash_file(self, path, saltenv='base'):
'''
Return the hash of a file, to get the hash of a file in the file_roots
Return the hash of a file, to get the hash of a file in the pillar_roots
prepend the path with salt://<file on server> otherwise, prepend the
file with / for a local file.
'''
@ -988,7 +944,7 @@ class LocalClient(Client):
def hash_and_stat_file(self, path, saltenv='base'):
'''
Return the hash of a file, to get the hash of a file in the file_roots
Return the hash of a file, to get the hash of a file in the pillar_roots
prepend the path with salt://<file on server> otherwise, prepend the
file with / for a local file.
@ -1034,7 +990,7 @@ class LocalClient(Client):
Return the available environments
'''
ret = []
for saltenv in self.opts['file_roots']:
for saltenv in self.opts['pillar_roots']:
ret.append(saltenv)
return ret
@ -1428,6 +1384,11 @@ class FSClient(RemoteClient):
self.auth = DumbAuth()
# Provide backward compatibility for anyone directly using LocalClient (but no
# one should be doing this).
LocalClient = FSClient
class DumbAuth(object):
'''
The dumbauth class is used to stub out auth calls fired from the FSClient

View file

@ -131,7 +131,10 @@ def check_file_list_cache(opts, form, list_cache, w_lock):
if os.path.exists(list_cache):
# calculate filelist age is possible
cache_stat = os.stat(list_cache)
age = time.time() - cache_stat.st_mtime
# st_time can have a greater precision than time, removing
# float precision makes sure age will never be a negative
# number.
age = int(time.time()) - int(cache_stat.st_mtime)
else:
# if filelist does not exists yet, mark it as expired
age = opts.get('fileserver_list_cache_time', 20) + 1

View file

@ -1589,7 +1589,7 @@ class Minion(MinionBase):
sdata = {'pid': os.getpid()}
sdata.update(data)
log.info('Starting a new job with PID %s', sdata['pid'])
log.info('Starting a new job %s with PID %s', data['jid'], sdata['pid'])
with salt.utils.files.fopen(fn_, 'w+b') as fp_:
fp_.write(minion_instance.serial.dumps(sdata))
ret = {'success': False}

View file

@ -214,7 +214,7 @@ def get(consul_url=None, key=None, token=None, recurse=False, decode=False, raw=
if ret['res']:
if decode:
for item in ret['data']:
if item['Value'] != None:
if item['Value'] is not None:
item['Value'] = base64.b64decode(item['Value'])
else:
item['Value'] = ""

View file

@ -420,7 +420,9 @@ def get_url(path, dest='', saltenv='base', makedirs=False, source_hash=None):
result = _client().get_url(
path, None, makedirs, saltenv, no_cache=True, source_hash=source_hash)
if not result:
log.error('Unable to fetch file %s from saltenv %s.', path, saltenv)
log.error('Unable to fetch file %s from saltenv %s.',
salt.utils.url.redact_http_basic_auth(path),
saltenv)
return result

View file

@ -4119,7 +4119,7 @@ def get_managed(
msg = (
'Unable to verify upstream hash of source file {0}, '
'please set source_hash or set skip_verify to True'
.format(source)
.format(salt.utils.url.redact_http_basic_auth(source))
)
return '', {}, msg
@ -4151,12 +4151,14 @@ def get_managed(
except Exception as exc:
# A 404 or other error code may raise an exception, catch it
# and return a comment that will fail the calling state.
return '', {}, 'Failed to cache {0}: {1}'.format(source, exc)
_source = salt.utils.url.redact_http_basic_auth(source)
return '', {}, 'Failed to cache {0}: {1}'.format(_source, exc)
# If cache failed, sfn will be False, so do a truth check on sfn first
# as invoking os.path.exists() on a bool raises a TypeError.
if not sfn or not os.path.exists(sfn):
return sfn, {}, 'Source file \'{0}\' not found'.format(source)
_source = salt.utils.url.redact_http_basic_auth(source)
return sfn, {}, 'Source file \'{0}\' not found'.format(_source)
if sfn == name:
raise SaltInvocationError(
'Source file cannot be the same as destination'

View file

@ -1035,11 +1035,8 @@ def _parse_conf(conf_file=None, in_mem=False, family='ipv4'):
if args[-1].startswith('-'):
args.append('')
parsed_args = []
if sys.version.startswith('2.6'):
(opts, leftover_args) = parser.parse_args(args)
parsed_args = vars(opts)
else:
parsed_args = vars(parser.parse_args(args))
opts, _ = parser.parse_known_args(args)
parsed_args = vars(opts)
ret_args = {}
chain = parsed_args['append']
for arg in parsed_args:

View file

@ -4689,7 +4689,7 @@ def get_pid(name, path=None):
if name not in list_(limit='running', path=path):
raise CommandExecutionError('Container {0} is not running, can\'t determine PID'.format(name))
info = __salt__['cmd.run']('lxc-info -n {0}'.format(name)).split("\n")
pid = [line.split(':')[1].strip() for line in info if re.match(r'\s*PID', line) != None][0]
pid = [line.split(':')[1].strip() for line in info if re.match(r'\s*PID', line) is not None][0]
return pid

View file

@ -50,7 +50,7 @@ def _atrun_enabled():
Check to see if atrun is enabled on the system
'''
name = 'com.apple.atrun'
services = salt.utils.mac_utils.available_services()
services = __utils__['mac_utils.available_services']()
label = None
if name in services:

View file

@ -1989,17 +1989,20 @@ def processlist(**connection_args):
"SHOW FULL PROCESSLIST".
Returns: a list of dicts, with each dict representing a process:
.. code-block:: python
{'Command': 'Query',
'Host': 'localhost',
'Id': 39,
'Info': 'SHOW FULL PROCESSLIST',
'Rows_examined': 0,
'Rows_read': 1,
'Rows_sent': 0,
'State': None,
'Time': 0,
'User': 'root',
'db': 'mysql'}
'Host': 'localhost',
'Id': 39,
'Info': 'SHOW FULL PROCESSLIST',
'Rows_examined': 0,
'Rows_read': 1,
'Rows_sent': 0,
'State': None,
'Time': 0,
'User': 'root',
'db': 'mysql'}
CLI Example:

View file

@ -4337,7 +4337,7 @@ def _writeAdminTemplateRegPolFile(admtemplate_data,
adml_policy_resources=None,
display_language='en-US',
registry_class='Machine'):
u'''
r'''
helper function to prep/write adm template data to the Registry.pol file
each file begins with REGFILE_SIGNATURE (u'\u5250\u6765') and

View file

@ -699,7 +699,6 @@ def refresh_db(**kwargs):
include_pat='*.sls',
exclude_pat=r'E@\/\..*?\/' # Exclude all hidden directories (.git)
)
return genrepo(saltenv=saltenv, verbose=verbose, failhard=failhard)

View file

@ -345,8 +345,6 @@ class Pillar(object):
if pillarenv is None:
if opts.get('pillarenv_from_saltenv', False):
opts['pillarenv'] = saltenv
# Store the file_roots path so we can restore later. Issue 5449
self.actual_file_roots = opts['file_roots']
# use the local file client
self.opts = self.__gen_opts(opts, grains, saltenv=saltenv, pillarenv=pillarenv)
self.saltenv = saltenv
@ -369,9 +367,6 @@ class Pillar(object):
self.matcher = salt.minion.Matcher(self.opts, self.functions)
self.rend = salt.loader.render(self.opts, self.functions)
ext_pillar_opts = copy.deepcopy(self.opts)
# Fix self.opts['file_roots'] so that ext_pillars know the real
# location of file_roots. Issue 5951
ext_pillar_opts['file_roots'] = self.actual_file_roots
# Keep the incoming opts ID intact, ie, the master id
if 'id' in opts:
ext_pillar_opts['id'] = opts['id']
@ -438,7 +433,6 @@ class Pillar(object):
The options need to be altered to conform to the file client
'''
opts = copy.deepcopy(opts_in)
opts['file_roots'] = opts['pillar_roots']
opts['file_client'] = 'local'
if not grains:
opts['grains'] = {}
@ -463,15 +457,15 @@ class Pillar(object):
opts['ext_pillar'].append(self.ext)
else:
opts['ext_pillar'] = [self.ext]
if '__env__' in opts['file_roots']:
if '__env__' in opts['pillar_roots']:
env = opts.get('pillarenv') or opts.get('saltenv') or 'base'
if env not in opts['file_roots']:
if env not in opts['pillar_roots']:
log.debug("pillar environment '%s' maps to __env__ pillar_roots directory", env)
opts['file_roots'][env] = opts['file_roots'].pop('__env__')
opts['pillar_roots'][env] = opts['pillar_roots'].pop('__env__')
else:
log.debug("pillar_roots __env__ ignored (environment '%s' found in pillar_roots)",
env)
opts['file_roots'].pop('__env__')
opts['pillar_roots'].pop('__env__')
return opts
def _get_envs(self):
@ -479,8 +473,8 @@ class Pillar(object):
Pull the file server environments out of the master options
'''
envs = set(['base'])
if 'file_roots' in self.opts:
envs.update(list(self.opts['file_roots']))
if 'pillar_roots' in self.opts:
envs.update(list(self.opts['pillar_roots']))
return envs
def get_tops(self):
@ -496,11 +490,11 @@ class Pillar(object):
if self.opts['pillarenv']:
# If the specified pillarenv is not present in the available
# pillar environments, do not cache the pillar top file.
if self.opts['pillarenv'] not in self.opts['file_roots']:
if self.opts['pillarenv'] not in self.opts['pillar_roots']:
log.debug(
'pillarenv \'%s\' not found in the configured pillar '
'environments (%s)',
self.opts['pillarenv'], ', '.join(self.opts['file_roots'])
self.opts['pillarenv'], ', '.join(self.opts['pillar_roots'])
)
else:
top = self.client.cache_file(self.opts['state_top'], self.opts['pillarenv'])
@ -1010,8 +1004,6 @@ class Pillar(object):
mopts = dict(self.opts)
if 'grains' in mopts:
mopts.pop('grains')
# Restore the actual file_roots path. Issue 5449
mopts['file_roots'] = self.actual_file_roots
mopts['saltversion'] = __version__
pillar['master'] = mopts
if 'pillar' in self.opts and self.opts.get('ssh_merge_pillar', False):
@ -1038,10 +1030,6 @@ class Pillar(object):
if decrypt_errors:
pillar.setdefault('_errors', []).extend(decrypt_errors)
# Reset the file_roots for the renderers
for mod_name in sys.modules:
if mod_name.startswith('salt.loaded.int.render.'):
sys.modules[mod_name].__opts__['file_roots'] = self.actual_file_roots
return pillar
def decrypt_pillar(self, pillar):

View file

@ -156,6 +156,7 @@ import salt.utils.files
import salt.utils.minions
import salt.utils.path
import salt.utils.stringio
import salt.utils.stringutils
import salt.template
from salt.ext import six
@ -251,7 +252,7 @@ def _construct_pillar(top_dir,
else:
data = contents
if template is True:
data = salt.template.compile_template_str(template=contents,
data = salt.template.compile_template_str(template=salt.utils.stringutils.to_unicode(contents),
renderers=renderers,
default=render_default,
blacklist=renderer_blacklist,

View file

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
'''
SaltClass Pillar Module
=======================
.. code-block:: yaml
ext_pillar:
- saltclass:
- path: /srv/saltclass
ext_pillar:
- saltclass:
- path: /srv/saltclass
For additional configuration instructions, see the :mod:`saltclass <salt.tops.saltclass>` module
'''
# import python libs

View file

@ -48,7 +48,7 @@ def present(name,
The keyId or keyIds to add to the GPG keychain.
user
Add GPG keys to the user's keychain
Add GPG keys to the specified user's keychain
keyserver
The keyserver to retrieve the keys from.
@ -151,7 +151,7 @@ def absent(name,
The keyId or keyIds to add to the GPG keychain.
user
Add GPG keys to the user's keychain
Remove GPG keys from the specified user's keychain
gnupghome
Override GNUPG Home directory

View file

@ -42,7 +42,7 @@ def __virtual__():
def pv_present(name, **kwargs):
'''
Set a physical device to be used as an LVM physical volume
Set a Physical Device to be used as an LVM Physical Volume
name
The device name to initialize.
@ -106,13 +106,13 @@ def pv_absent(name):
def vg_present(name, devices=None, **kwargs):
'''
Create an LVM volume group
Create an LVM Volume Group
name
The volume group name to create
The Volume Group name to create
devices
A list of devices that will be added to the volume group
A list of devices that will be added to the Volume Group
kwargs
Any supported options to vgcreate. See
@ -214,16 +214,16 @@ def lv_present(name,
force=False,
**kwargs):
'''
Create a new logical volume
Create a new Logical Volume
name
The name of the logical volume
The name of the Logical Volume
vgname
The volume group name for this logical volume
The name of the Volume Group on which the Logical Volume resides
size
The initial size of the logical volume
The initial size of the Logical Volume
extents
The number of logical extents to allocate
@ -232,7 +232,7 @@ def lv_present(name,
The name of the snapshot
pv
The physical volume to use
The Physical Volume to use
kwargs
Any supported options to lvcreate. See
@ -241,10 +241,10 @@ def lv_present(name,
.. versionadded:: to_complete
thinvolume
Logical volume is thinly provisioned
Logical Volume is thinly provisioned
thinpool
Logical volume is a thin pool
Logical Volume is a thin pool
.. versionadded:: 2018.3.0
@ -297,13 +297,13 @@ def lv_present(name,
def lv_absent(name, vgname=None):
'''
Remove a given existing logical volume from a named existing volume group
Remove a given existing Logical Volume from a named existing volume group
name
The logical volume to remove
The Logical Volume to remove
vgname
The volume group name
The name of the Volume Group on which the Logical Volume resides
'''
ret = {'changes': {},
'comment': '',

View file

@ -26,7 +26,7 @@ def absent(name, user=None, signal=None):
The pattern to match.
user
The user process belongs
The user to which the process belongs
signal
Signal to send to the process(es).

View file

@ -55,7 +55,7 @@ def managed(name, port, services=None, user=None, password=None, bypass_domains=
The username to use for the proxy server if required
password
The password to use if required by the server
The password to use for the proxy server if required
bypass_domains
An array of the domains that should bypass the proxy

View file

@ -334,7 +334,7 @@ def _check_key_type(key_str, key_type=None):
value = __salt__['pillar.get'](key_str, None)
if value is None:
return None
elif type(value) is not key_type and key_type is not None:
elif key_type is not None and not isinstance(value, key_type):
return False
else:
return True
@ -415,7 +415,7 @@ def check_pillar(name,
checks[int] = integer
# those should be str:
string = _if_str_then_list(string)
checks[str] = string
checks[six.string_types] = string
# those should be list:
listing = _if_str_then_list(listing)
checks[list] = listing

View file

@ -1,11 +1,211 @@
# -*- coding: utf-8 -*-
'''
SaltClass master_tops Module
r'''
Saltclass Configuration
=======================
.. code-block:: yaml
master_tops:
saltclass:
path: /srv/saltclass
master_tops:
saltclass:
path: /srv/saltclass
Description
===========
This module clones the behaviour of reclass (http://reclass.pantsfullofunix.net/),
without the need of an external app, and add several features to improve flexibility.
Saltclass lets you define your nodes from simple ``yaml`` files (``.yml``) through
hierarchical class inheritance with the possibility to override pillars down the tree.
Features
========
- Define your nodes through hierarchical class inheritance
- Reuse your reclass datas with minimal modifications
- applications => states
- parameters => pillars
- Use Jinja templating in your yaml definitions
- Access to the following Salt objects in Jinja
- ``__opts__``
- ``__salt__``
- ``__grains__``
- ``__pillars__``
- ``minion_id``
- Chose how to merge or override your lists using ^ character (see examples)
- Expand variables ${} with possibility to escape them if needed \${} (see examples)
- Ignores missing node/class and will simply return empty without breaking the pillar module completely - will be logged
An example subset of datas is available here: http://git.mauras.ch/salt/saltclass/src/master/examples
========================== ===========
Terms usable in yaml files Description
========================== ===========
classes A list of classes that will be processed in order
states A list of states that will be returned by master_tops function
pillars A yaml dictionnary that will be returned by the ext_pillar function
environment Node saltenv that will be used by master_tops
========================== ===========
A class consists of:
- zero or more parent classes
- zero or more states
- any number of pillars
A child class can override pillars from a parent class.
A node definition is a class in itself with an added ``environment`` parameter for ``saltenv`` definition.
Class names
===========
Class names mimic salt way of defining states and pillar files.
This means that ``default.users`` class name will correspond to one of these:
- ``<saltclass_path>/classes/default/users.yml``
- ``<saltclass_path>/classes/default/users/init.yml``
Saltclass file hierachy
=======================
A saltclass tree would look like this:
.. code-block:: text
<saltclass_path>
classes
app
borgbackup.yml
ssh
server.yml
default
init.yml
motd.yml
users.yml
roles
app.yml
nginx
init.yml
server.yml
subsidiaries
gnv.yml
qls.yml
zrh.yml
nodes
geneva
gnv.node1.yml
lausanne
qls.node1.yml
qls.node2.yml
node127.yml
zurich
zrh.node1.yml
zrh.node2.yml
zrh.node3.yml
Saltclass Examples
==================
``<saltclass_path>/nodes/lausanne/qls.node1.yml``
.. code-block:: yaml
environment: base
classes:
{% for class in ['default'] %}
- {{ class }}
{% endfor %}
- subsidiaries.{{ __grains__['id'].split('.')[0] }}
``<saltclass_path>/classes/default/init.yml``
.. code-block:: yaml
classes:
- default.users
- default.motd
states:
- openssh
pillars:
default:
network:
dns:
srv1: 192.168.0.1
srv2: 192.168.0.2
domain: example.com
ntp:
srv1: 192.168.10.10
srv2: 192.168.10.20
``<saltclass_path>/classes/subsidiaries/gnv.yml``
.. code-block:: yaml
pillars:
default:
network:
sub: Geneva
dns:
srv1: 10.20.0.1
srv2: 10.20.0.2
srv3: 192.168.1.1
domain: gnv.example.com
users:
adm1:
uid: 1210
gid: 1210
gecos: 'Super user admin1'
homedir: /srv/app/adm1
adm3:
uid: 1203
gid: 1203
gecos: 'Super user adm
Variable expansions
===================
Escaped variables are rendered as is: ``${test}``
Missing variables are rendered as is: ``${net:dns:srv2}``
.. code-block:: yaml
pillars:
app:
config:
dns:
srv1: ${default:network:dns:srv1}
srv2: ${net:dns:srv2}
uri: https://application.domain/call?\${test}
prod_parameters:
- p1
- p2
- p3
pkg:
- app-core
- app-backend
List override
=============
Not using ``^`` as the first entry will simply merge the lists
.. code-block:: yaml
pillars:
app:
pkg:
- ^
- app-frontend
.. note:: **Known limitation**
Currently you can't have both a variable and an escaped variable in the same string as the
escaped one will not be correctly rendered - '\${xx}' will stay as is instead of being rendered as '${xx}'
'''
# import python libs

View file

@ -59,16 +59,16 @@ class SaltCacheLoader(BaseLoader):
self.opts = opts
self.saltenv = saltenv
self.encoding = encoding
if self.opts['file_roots'] is self.opts['pillar_roots']:
if saltenv not in self.opts['file_roots']:
self.pillar_rend = pillar_rend
if self.pillar_rend:
if saltenv not in self.opts['pillar_roots']:
self.searchpath = []
else:
self.searchpath = opts['file_roots'][saltenv]
self.searchpath = opts['pillar_roots'][saltenv]
else:
self.searchpath = [os.path.join(opts['cachedir'], 'files', saltenv)]
log.debug('Jinja search path: %s', self.searchpath)
self.cached = []
self.pillar_rend = pillar_rend
self._file_client = None
# Instantiate the fileclient
self.file_client()

View file

@ -15,7 +15,8 @@ from tests.support.runtests import RUNTIME_VARS
import salt.utils.files
import salt.utils.platform
CURL = os.path.join(RUNTIME_VARS.FILES, 'file', 'base', 'win', 'repo-ng', 'curl.sls')
REPO_DIR = os.path.join(RUNTIME_VARS.FILES, 'file', 'base', 'win', 'repo-ng')
CURL = os.path.join(REPO_DIR, 'curl.sls')
@skipIf(not salt.utils.platform.is_windows(), 'windows test only')
@ -33,8 +34,10 @@ class WinPKGTest(ModuleCase):
Test add and removing a new pkg sls
in the windows software repository
'''
def _check_pkg(pkgs, exists=True):
self.run_function('pkg.refresh_db')
def _check_pkg(pkgs, check_refresh, exists=True):
refresh = self.run_function('pkg.refresh_db')
self.assertEqual(check_refresh, refresh['total'],
msg='total returned {0}. Expected return {1}'.format(refresh['total'], check_refresh))
repo_data = self.run_function('pkg.get_repo_data', timeout=300)
repo_cache = os.path.join(RUNTIME_VARS.TMP, 'rootdir', 'cache', 'files', 'base', 'win', 'repo-ng')
for pkg in pkgs:
@ -51,7 +54,7 @@ class WinPKGTest(ModuleCase):
pkgs = ['putty', '7zip']
# check putty and 7zip are in cache and repo query
_check_pkg(pkgs)
_check_pkg(pkgs, 2)
# now add new sls
with salt.utils.files.fopen(CURL, 'w') as fp_:
@ -74,11 +77,13 @@ class WinPKGTest(ModuleCase):
'''))
# now check if curl is also in cache and repo query
pkgs.append('curl')
_check_pkg(pkgs)
for pkg in pkgs:
self.assertIn(pkg + '.sls', os.listdir(REPO_DIR))
_check_pkg(pkgs, 3)
# remove curl sls and check its not in cache and repo query
os.remove(CURL)
_check_pkg(['curl'], exists=False)
_check_pkg(['curl'], 2, exists=False)
def tearDown(self):
if os.path.isfile(CURL):

View file

@ -101,13 +101,13 @@ from salt.utils.gitfs import (
# Check for requisite components
try:
HAS_GITPYTHON = GITPYTHON_VERSION >= GITPYTHON_MINVER
except ImportError:
except Exception:
HAS_GITPYTHON = False
try:
HAS_PYGIT2 = PYGIT2_VERSION >= PYGIT2_MINVER \
and LIBGIT2_VERSION >= LIBGIT2_MINVER
except AttributeError:
except Exception:
HAS_PYGIT2 = False
HAS_SSHD = bool(salt.utils.path.which('sshd'))

View file

@ -168,24 +168,3 @@ class RootsTest(TestCase, AdaptedConfigurationTestCaseMixin, LoaderModuleMockMix
finally:
if self.test_symlink_list_file_roots:
self.opts['file_roots'] = orig_file_roots
class RootsLimitTraversalTest(TestCase, AdaptedConfigurationTestCaseMixin):
def test_limit_traversal(self):
'''
1) Set up a deep directory structure
2) Enable the configuration option 'fileserver_limit_traversal'
3) Ensure that we can find SLS files in a directory so long as there is
an SLS file in a directory above.
4) Ensure that we cannot find an SLS file in a directory that does not
have an SLS file in a directory above.
'''
file_client_opts = self.get_temp_config('master')
file_client_opts['fileserver_limit_traversal'] = True
ret = salt.fileclient.Client(file_client_opts).list_states('base')
self.assertIn('test_deep.test', ret)
self.assertIn('test_deep.a.test', ret)
self.assertNotIn('test_deep.b.2.test', ret)

View file

@ -19,6 +19,8 @@ from tests.support.mock import (
# Import Salt Libs
from salt.exceptions import SaltInvocationError
import salt.states.test as test
from salt.utils.odict import OrderedDict
from salt.ext import six
@skipIf(NO_MOCK, NO_MOCK_REASON)
@ -310,6 +312,48 @@ class TestTestCase(TestCase, LoaderModuleMockMixin):
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertEqual(test.check_pillar('salt', present='my_pillar'), ret)
def test_check_pillar_string(self):
'''
Test to ensure the check_pillar function
works properly with the 'key_type' checks,
using the string key_type.
'''
ret = {
'name': 'salt',
'changes': {},
'result': True,
'comment': ''
}
pillar_return = 'I am a pillar.'
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertEqual(test.check_pillar('salt', string='my_pillar'), ret)
# With unicode (py2) or str (py3) strings
pillar_return = six.text_type('I am a pillar.')
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertEqual(test.check_pillar('salt', string='my_pillar'), ret)
# With a dict
pillar_return = {'this': 'dictionary'}
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', string='my_pillar')['result'])
# With a list
pillar_return = ['I am a pillar.']
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', string='my_pillar')['result'])
# With a boolean
pillar_return = True
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', string='my_pillar')['result'])
# With an int
pillar_return = 1
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', string='my_pillar')['result'])
def test_check_pillar_dictionary(self):
'''
Test to ensure the check_pillar function
@ -326,3 +370,28 @@ class TestTestCase(TestCase, LoaderModuleMockMixin):
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertEqual(test.check_pillar('salt', dictionary='my_pillar'), ret)
# With an ordered dict
pillar_return = OrderedDict({'this': 'dictionary'})
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertEqual(test.check_pillar('salt', dictionary='my_pillar'), ret)
# With a string
pillar_return = 'I am a pillar.'
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', dictionary='my_pillar')['result'])
# With a list
pillar_return = ['I am a pillar.']
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', dictionary='my_pillar')['result'])
# With a boolean
pillar_return = True
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', dictionary='my_pillar')['result'])
# With an int
pillar_return = 1
pillar_mock = MagicMock(return_value=pillar_return)
with patch.dict(test.__salt__, {'pillar.get': pillar_mock}):
self.assertFalse(test.check_pillar('salt', dictionary='my_pillar')['result'])

View file

@ -314,7 +314,7 @@ class PillarTestCase(TestCase):
'extension_modules': '',
}
pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='dev')
self.assertEqual(pillar.opts['file_roots'],
self.assertEqual(pillar.opts['pillar_roots'],
{'base': ['/srv/pillar/base'], 'dev': ['/srv/pillar/__env__']})
def test_ignored_dynamic_pillarenv(self):
@ -329,7 +329,7 @@ class PillarTestCase(TestCase):
'extension_modules': '',
}
pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='base')
self.assertEqual(pillar.opts['file_roots'], {'base': ['/srv/pillar/base']})
self.assertEqual(pillar.opts['pillar_roots'], {'base': ['/srv/pillar/base']})
def test_malformed_pillar_sls(self):
with patch('salt.pillar.compile_template') as compile_template: