Merge remote-tracking branch 'upstream/develop' into sam_raet_10

This commit is contained in:
Samuel M Smith 2014-02-24 16:02:11 -07:00
commit f466161fa0
50 changed files with 1355 additions and 279 deletions

View File

@ -184,7 +184,7 @@ dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=__opts__,__salt__,__pillar__,__grains__,__context__,__ret__,__env__,__low__,__lowstate__,__running__,__active_provider_name__
additional-builtins=__opts__,__salt__,__pillar__,__grains__,__context__,__ret__,__env__,__low__,__states__,__lowstate__,__running__,__active_provider_name__
[SIMILARITIES]

View File

@ -142,7 +142,7 @@ indent-string=' '
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=__opts__,__salt__,__pillar__,__grains__,__context__,__ret__,__env__,__low__,__lowstate__,__running__,__active_provider_name__
additional-builtins=__opts__,__salt__,__pillar__,__grains__,__context__,__ret__,__env__,__low__,__states__,__lowstate__,__running__,__active_provider_name__
[IMPORTS]

View File

@ -67,7 +67,7 @@ salt's code.
.. _`report an issue`: https://github.com/saltstack/salt/issues
.. _`Salt's documentation`: http://docs.saltstack.org/en/latest/index.html
.. _`Developing Salt`: http://docs.saltstack.org/en/latest/topics/community.html#developing-salt
.. _`pull request`: http://docs.saltstack.org/en/latest/topics/community.html#sending-a-github-pull-request
.. _`Developing Salt`: http://docs.saltstack.com/topics/hacking.html
.. _`pull request`: http://docs.saltstack.com/topics/hacking.html#sending-a-github-pull-request
.. vim: fenc=utf-8 spell spl=en

View File

@ -117,7 +117,7 @@ copyright = '2014 SaltStack, Inc.'
version = salt.version.__version__
#release = '.'.join(map(str, salt.version.__version_info__))
release = '2014.1'
release = '2014.1.0'
language = 'en'
locale_dirs = [

View File

@ -223,4 +223,5 @@ Full list of builtin execution modules
zcbuildout
zfs
zpool
znc
zypper

View File

@ -4,3 +4,4 @@ salt.modules.img
.. automodule:: salt.modules.img
:members:
:exclude-members: mnt_image

View File

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

View File

@ -15,6 +15,7 @@ Full list of builtin renderer modules
mako
py
pydsl
pyobjects
stateconf
wempy
yaml

View File

@ -0,0 +1,6 @@
========================
salt.renderers.pyobjects
========================
.. automodule:: salt.renderers.pyobjects
:members:

View File

@ -14,7 +14,7 @@ documents. But since the only thing the state system cares about is raw data,
the SLS files can be any structured format that can be dreamed up.
Currently there is support for ``Jinja + YAML``, ``Mako + YAML``,
``Wempy + YAML``, ``Jinja + json`` ``Mako + json`` and ``Wempy + json``.
``Wempy + YAML``, ``Jinja + json``, ``Mako + json`` and ``Wempy + json``.
Renderers can be written to support any template type. This means that the
Salt states could be managed by XML files, HTML files, Puppet files, or any
@ -148,4 +148,3 @@ Here is a simple YAML renderer example:
yaml_data = yaml_data.read()
data = yaml.load(yaml_data)
return data if data else {}

View File

@ -11,21 +11,24 @@ Installation
Quick Install
-------------
Many popular distributions will be able to install the salt minion by executing
the bootstrap script:
On most distributions, you can set up a **Salt Minion** with the bootstrap script:
.. code-block:: bash
wget -O - http://bootstrap.saltstack.org | sudo sh
curl -L http://bootstrap.saltstack.org | sudo sh
Run the following script to install just the Salt Master:
or, to connect immediately to a running Salt Master:
.. code-block:: bash
curl -L http://bootstrap.saltstack.org | sudo sh -s -- -A saltmaster.example.com
To set up a **Salt Master**:
.. code-block:: bash
curl -L http://bootstrap.saltstack.org | sudo sh -s -- -M -N
The script should also make it simple to install a salt master, if desired.
Currently the install script has been tested to work on:
* Ubuntu 10.x/11.x/12.x

View File

@ -106,7 +106,9 @@ same way as in the above example, only without a top-level ``grains:`` key:
Matching Grains in the Top File
===============================
With correctly setup grains on the Minion, the Top file used in Pillar or during Highstate can be made really efficient. Like for example, you could do:
With correctly configured grains on the Minion, the :term:`top file` used in
Pillar or during Highstate can be made very efficient. For example, consider
the following configuration:
.. code-block:: yaml
@ -126,17 +128,28 @@ With correctly setup grains on the Minion, the Top file used in Pillar or during
- match: grain
- lb
For this example to work, you would need the grain ``node_type`` and the correct value to match on. This simple example is nice, but too much of the code is similar. To go one step further, we can place some Jinja template code into the Top file.
For this example to work, you would need to have defined the grain
``node_type`` for the minions you wish to match. This simple example is nice,
but too much of the code is similar. To go one step further, Jinja templating
can be used to simplify the the :term:`top file`.
.. code-block:: yaml
{% set self = grains['node_type'] %}
{% set node_type = salt['grains.get']('node_type', '') %}
{% if node_type %}
'node_type:{{ self }}':
- match: grain
- {{ self }}
{% endif %}
The Jinja code simplified the Top file, and allowed SaltStack to work its magic.
Using Jinja templating, only one match entry needs to be defined.
.. note::
The example above uses the :mod:`grains.get <salt.modules.grains.get>`
function to account for minions which do not have the ``node_type`` grain
set.
.. _writing-grains:

View File

@ -23,9 +23,9 @@ nodegroups. Here's an example nodegroup configuration within
.. note::
The 'L' within group1 is matching a list of minions, while the 'G' in
group2 is matching specific grains. See the
:doc:`compound matchers <compound>` documentation for more details.
The ``L`` within group1 is matching a list of minions, while the ``G`` in
group2 is matching specific grains. See the :doc:`compound matchers
<compound>` documentation for more details.
To match a nodegroup on the CLI, use the ``-N`` command-line option:
@ -33,8 +33,8 @@ To match a nodegroup on the CLI, use the ``-N`` command-line option:
salt -N group1 test.ping
To match in your :term:`top file`, make sure to put ``- match: nodegroup`` on
the line directly following the nodegroup name.
To match a nodegroup in your :term:`top file`, make sure to put ``- match:
nodegroup`` on the line directly following the nodegroup name.
.. code-block:: yaml

View File

@ -27,6 +27,18 @@ available to Salt.
Simple Configuration
====================
.. note::
GitFS requires the Python module ``GitPython``, version 0.3.0 or newer.
If your Master runs Ubuntu 12.04 LTS, you will likely need to install
GitPython using `pip`_.
.. code-block:: bash
# pip install GitPython
.. _`pip`: http://www.pip-installer.org/
To use the gitfs backend only two configuration changes are required on the
master. The ``fileserver_backend`` option needs to be set with a value of
``git``:
@ -205,19 +217,6 @@ In order to configure a ``gitfs_remotes`` repository over SSH transport the
The private key used to connect to the repository must be located in ``~/.ssh/id_rsa``
for the user running the salt-master.
.. note::
GitFS requires the Python module ``GitPython``, version 0.3.0 or newer.
If your Master runs Ubuntu 12.04 LTS, you will likely need to install
GitPython using `pip`_.
.. code-block:: bash
# pip install GitPython
.. _`pip`: http://www.pip-installer.org/
Using Git as an External Pillar Source
======================================

View File

@ -31,7 +31,9 @@ master config file.
3. Distribute the minion keys.
There is no single method to get the keypair to your minion. The difficulty is
finding a distribution method which is secure.
finding a distribution method which is secure. For Amazon EC2 only, an AWS best
practice is to use IAM Roles to pass credentials. (See blog post,
http://blogs.aws.amazon.com/php/post/Tx1F82CR0ANO3ZI/Providing-credentials-to-the-AWS-SDK-for-PHP )
.. admonition:: Security Warning

View File

@ -204,7 +204,7 @@ class SSH(object):
)
pub = '{0}.pub'.format(priv)
with open(pub, 'r') as fp_:
return '{0} root@master'.format(fp_.read().split()[1])
return '{0} rsa root@master'.format(fp_.read().split()[1])
def key_deploy(self, host, ret):
'''
@ -255,7 +255,7 @@ class SSH(object):
self.opts['arg_str'],
host,
**target)
stdout, stderr = single.cmd_block()
stdout, stderr, retcode = single.cmd_block()
try:
data = salt.utils.find_json(stdout)
return {host: data.get('local', data)}
@ -276,25 +276,27 @@ class SSH(object):
host,
**target)
ret = {'id': single.id}
stdout, stderr = single.run()
stdout, stderr, retcode = single.run()
if stdout.startswith('deploy'):
single.deploy()
stdout, stderr = single.run()
stdout, stderr, retcode = single.run()
# This job is done, yield
try:
if not stdout and stderr:
if 'Permission denied' in stderr:
ret['ret'] = 'Permission denied'
else:
ret['ret'] = stderr
else:
data = salt.utils.find_json(stdout)
if len(data) < 2 and 'local' in data:
ret['ret'] = data['local']
else:
ret['ret'] = data
ret['ret'] = {
'stdout': stdout,
'stderr': stderr,
'retcode': retcode,
}
except Exception:
ret['ret'] = stdout
ret['ret'] = {
'stdout': stdout,
'stderr': stderr,
'retcode': retcode,
}
que.put(ret)
def handle_ssh(self):
@ -503,27 +505,27 @@ class Single(object):
If a (re)deploy is needed, then retry the operation after a deploy
attempt
Returns tuple of (stdout, stderr)
Returns tuple of (stdout, stderr, retcode)
'''
stdout, stderr = None, None
stdout = stderr = retcode = None
arg_str = self.arg_str
if self.opts.get('raw_shell'):
if not arg_str.startswith(('"', "'")) and not arg_str.endswith(('"', "'")):
arg_str = "'{0}'".format(arg_str)
stdout, stderr = self.shell.exec_cmd(arg_str)
stdout, stderr, retcode = self.shell.exec_cmd(arg_str)
elif self.fun in self.wfuncs:
stdout, stderr = self.run_wfunc()
stdout, stderr, retcode = self.run_wfunc()
else:
stdout, stderr = self.cmd_block()
stdout, stderr, retcode = self.cmd_block()
if stdout.startswith('deploy') and not deploy_attempted:
self.deploy()
return self.run(deploy_attempted=True)
return stdout, stderr
return stdout, stderr, retcode
def run_wfunc(self):
'''
@ -587,7 +589,7 @@ class Single(object):
self.wfuncs = salt.loader.ssh_wrapper(opts, wrapper)
wrapper.wfuncs = self.wfuncs
ret = json.dumps(self.wfuncs[self.fun](*self.arg))
return ret, ''
return ret, '', None
def cmd(self):
'''
@ -613,8 +615,8 @@ class Single(object):
self.opts['hash_type'],
thin_sum,
self.minion_config)
for stdout, stderr in self.shell.exec_nb_cmd(cmd):
yield stdout, stderr
for stdout, stderr, retcode in self.shell.exec_nb_cmd(cmd):
yield stdout, stderr, retcode
def cmd_block(self, is_retry=False):
'''
@ -641,26 +643,32 @@ class Single(object):
thin_sum,
self.minion_config)
log.debug('Performing shimmed command as follows:\n{0}'.format(cmd))
stdout, stderr = self.shell.exec_cmd(cmd)
stdout, stderr, retcode = self.shell.exec_cmd(cmd)
log.debug('STDOUT {1}\n{0}'.format(stdout, self.target['host']))
log.debug('STDERR {1}\n{0}'.format(stderr, self.target['host']))
log.debug('RETCODE {1}\n{0}'.format(retcode, self.target['host']))
error = self.categorize_shim_errors(stdout, stderr)
error = self.categorize_shim_errors(stdout, stderr, retcode)
if error:
return 'ERROR: {0}'.format(error), stderr
return 'ERROR: {0}'.format(error), stderr, retcode
if RSTR in stdout:
stdout = stdout.split(RSTR)[1].strip()
if stdout.startswith('deploy'):
self.deploy()
stdout, stderr = self.shell.exec_cmd(cmd)
stdout, stderr, retcode = self.shell.exec_cmd(cmd)
if RSTR in stdout:
stdout = stdout.split(RSTR)[1].strip()
return stdout, stderr
return stdout, stderr, retcode
def categorize_shim_errors(self, stdout, stderr, retcode):
# Unused stdout and retcode for now but these may be used to
# categorize errors
_ = stdout
_ = retcode
def categorize_shim_errors(self, stdout, stderr):
perm_error_fmt = 'Permissions problem, target user may need '\
'to be root or use sudo:\n {0}'
if stderr.startswith('Permission denied'):

View File

@ -162,7 +162,7 @@ class Shell(object):
'''
Execute ssh-copy-id to plant the id file on the target
'''
stdout, stderr = self._run_cmd(self._copy_id_str_old())
_, stderr, _ = self._run_cmd(self._copy_id_str_old())
if stderr.startswith('Usage'):
self._run_cmd(self._copy_id_str_new())
@ -208,9 +208,9 @@ class Shell(object):
)
data = proc.communicate()
return data
return data[0], data[1], proc.returncode
except Exception:
return ('local', 'Unknown Error')
return ('local', 'Unknown Error', None)
def _run_nb_cmd(self, cmd):
'''
@ -227,13 +227,14 @@ class Shell(object):
time.sleep(0.1)
out = proc.recv()
err = proc.recv_err()
rcode = proc.returncode
if out is None and err is None:
break
if err:
err = self.get_error(err)
yield out, err
yield out, err, rcode
except Exception:
yield ('', 'Unknown Error')
yield ('', 'Unknown Error', None)
def exec_nb_cmd(self, cmd):
'''
@ -241,6 +242,7 @@ class Shell(object):
'''
r_out = []
r_err = []
rcode = None
cmd = self._cmd_str(cmd)
logmsg = 'Executing non-blocking command: {0}'.format(cmd)
@ -248,13 +250,13 @@ class Shell(object):
logmsg = logmsg.replace(self.passwd, ('*' * len(self.passwd))[:6])
log.debug(logmsg)
for out, err in self._run_nb_cmd(cmd):
for out, err, rcode in self._run_nb_cmd(cmd):
if out is not None:
r_out.append(out)
if err is not None:
r_err.append(err)
yield None, None
yield ''.join(r_out), ''.join(r_err)
yield None, None, None
yield ''.join(r_out), ''.join(r_err), rcode
def exec_cmd(self, cmd):
'''

View File

@ -54,10 +54,10 @@ class FunctionWrapper(object):
''.join(arg_str),
**self.kwargs
)
stdout, stderr = single.cmd_block()
stdout, _, _ = single.cmd_block()
if stdout.startswith('deploy'):
single.deploy()
stdout, stderr = single.cmd_block()
stdout, _, _ = single.cmd_block()
try:
ret = json.loads(stdout, object_hook=salt.utils.decode_dict)
except ValueError:

View File

@ -74,7 +74,7 @@ def sls(mods, saltenv='base', test=None, exclude=None, env=None, **kwargs):
single.shell.send(
trans_tar,
'/tmp/.salt/salt_state.tgz')
stdout, stderr = single.cmd_block()
stdout, stderr, _ = single.cmd_block()
return json.loads(stdout, object_hook=salt.utils.decode_dict)
@ -111,7 +111,7 @@ def low(data):
single.shell.send(
trans_tar,
'/tmp/.salt/salt_state.tgz')
stdout, stderr = single.cmd_block()
stdout, stderr, _ = single.cmd_block()
return json.loads(stdout, object_hook=salt.utils.decode_dict)
@ -145,7 +145,7 @@ def high(data):
single.shell.send(
trans_tar,
'/tmp/.salt/salt_state.tgz')
stdout, stderr = single.cmd_block()
stdout, stderr, _ = single.cmd_block()
return json.loads(stdout, object_hook=salt.utils.decode_dict)
@ -182,7 +182,7 @@ def highstate(test=None, **kwargs):
single.shell.send(
trans_tar,
'/tmp/.salt/salt_state.tgz')
stdout, stderr = single.cmd_block()
stdout, stderr, _ = single.cmd_block()
return json.loads(stdout, object_hook=salt.utils.decode_dict)
@ -223,7 +223,7 @@ def top(topfn, test=None, **kwargs):
single.shell.send(
trans_tar,
'/tmp/.salt/salt_state.tgz')
stdout, stderr = single.cmd_block()
stdout, stderr, _ = single.cmd_block()
return json.loads(stdout, object_hook=salt.utils.decode_dict)

View File

@ -333,7 +333,10 @@ class Cloud(object):
opts = self.opts.copy()
multiprocessing_data = []
for alias, drivers in self.opts['providers'].iteritems():
# Optimize Providers
opts['providers'] = self._optimize_providers(opts['providers'])
for alias, drivers in opts['providers'].iteritems():
for driver, details in drivers.iteritems():
fun = '{0}.{1}'.format(driver, query)
if fun not in self.clouds:
@ -383,7 +386,8 @@ class Cloud(object):
self.__cached_provider_queries[query] = output
return output
def get_running_by_names(self, names, query='list_nodes', cached=False):
def get_running_by_names(self, names, query='list_nodes', cached=False,
profile=None):
if isinstance(names, basestring):
names = [names]
@ -394,6 +398,16 @@ class Cloud(object):
for driver, vms in drivers.iteritems():
if driver not in handled_drivers:
handled_drivers[driver] = alias
# When a profile is specified, only return an instance
# that matches the provider specified in the profile.
# This solves the issues when many providers return the
# same instance. For example there may be one provider for
# each avaliablity zone in amazon in the same region, but
# the search returns the same instance for each provider because
# amazon returns all instances in a region, not avaliabilty zone.
if profile:
if alias not in self.opts['profiles'][profile]['provider'].split(':')[0]:
continue
for vm_name, details in vms.iteritems():
# XXX: The logic bellow can be removed once the aws driver
@ -418,6 +432,43 @@ class Cloud(object):
return matches
def _optimize_providers(self, providers):
'''
Return an optimized mapping of available providers
'''
new_providers = {}
provider_by_driver = {}
for alias, driver in providers.iteritems():
for name, data in driver.iteritems():
if name not in provider_by_driver:
provider_by_driver[name] = {}
provider_by_driver[name][alias] = data
for driver, providers_data in provider_by_driver.iteritems():
fun = '{0}.optimize_providers'.format(driver)
if fun not in self.clouds:
log.debug(
'The {0!r} cloud driver is unable to be optimized.'.format(
driver)
)
for name, prov_data in providers_data.iteritems():
if name not in new_providers:
new_providers[name] = {}
new_providers[name][driver] = prov_data
continue
new_data = self.clouds[fun](providers_data)
if new_data:
for name, prov_data in new_data.iteritems():
if name not in new_providers:
new_providers[name] = {}
new_providers[name][driver] = prov_data
return new_providers
def location_list(self, lookup='all'):
'''
Return a mapping of all location data for available providers

View File

@ -237,7 +237,8 @@ class SaltCloud(parsers.SaltCloudParser):
matching = mapper.delete_map(query='list_nodes')
else:
matching = mapper.get_running_by_names(
self.config.get('names', ())
self.config.get('names', ()),
profile=self.options.profile
)
if not matching:

View File

@ -236,6 +236,45 @@ def _xml_to_dict(xmltree):
return xmldict
def optimize_providers(providers):
'''
Return an optimized list of providers.
We want to reduce the duplication of querying
the same region.
If a provider is using the same credentials for the same region
the same data will be returned for each provider, thus causing
un-wanted duplicate data and API calls to EC2.
'''
tmp_providers = {}
optimized_providers = {}
for name, data in providers.iteritems():
if 'location' not in data:
data['location'] = DEFAULT_LOCATION
if data['location'] not in tmp_providers:
tmp_providers[data['location']] = {}
creds = (data['id'], data['key'])
if creds not in tmp_providers[data['location']]:
tmp_providers[data['location']][creds] = {'name': name,
'data': data,
}
for location, tmp_data in tmp_providers.iteritems():
for creds, data in tmp_data.iteritems():
_id, _key = creds
_name = data['name']
_data = data['data']
if _name not in optimized_providers:
optimized_providers[_name] = _data
return optimized_providers
def query(params=None, setname=None, requesturl=None, location=None,
return_url=False, return_root=False):

View File

@ -1722,6 +1722,8 @@ def get_id(root_dir=None, minion_id=False, cache=True):
with salt.utils.fopen('/etc/hosts') as hfl:
for line in hfl:
names = line.split()
if not names:
continue
ip_ = names.pop(0)
if ip_.startswith('127.'):
for name in names:

View File

@ -237,9 +237,6 @@ class Auth(object):
public key to encrypt the AES key sent back form the master.
'''
payload = {}
key = self.get_keys()
tmp_pub = salt.utils.mkstemp()
key.save_pub_key(tmp_pub)
payload['enc'] = 'clear'
payload['load'] = {}
payload['load']['cmd'] = '_auth'
@ -251,9 +248,8 @@ class Auth(object):
payload['load']['token'] = pub.public_encrypt(self.token, RSA.pkcs1_oaep_padding)
except Exception:
pass
with salt.utils.fopen(tmp_pub, 'r') as fp_:
with salt.utils.fopen(self.pub_path, 'r') as fp_:
payload['load']['pub'] = fp_.read()
os.remove(tmp_pub)
return payload
def decrypt_aes(self, payload, master_pub=True):

View File

@ -28,6 +28,12 @@ class SaltMasterError(SaltException):
'''
class SaltSyndicMasterError(SaltException):
'''
Problem while proxying a request in the syndication master
'''
class MasterExit(SystemExit):
'''
Rise when the master exits

View File

@ -268,15 +268,17 @@ def ssh_wrapper(opts, functions=None):
return load.gen_functions(pack)
def render(opts, functions):
def render(opts, functions, states=None):
'''
Returns the render modules
'''
load = _create_loader(
opts, 'renderers', 'render', ext_type_dirs='render_dirs'
)
pack = {'name': '__salt__',
'value': functions}
pack = [{'name': '__salt__',
'value': functions}]
if states:
pack.append({'name': '__states__', 'value': states})
rend = load.filter_func('render', pack)
if not check_render_pipe_str(opts['renderer'], rend):
err = ('The renderer {0} is unavailable, this error is often because '

View File

@ -411,7 +411,21 @@ class Publisher(multiprocessing.Process):
# SIGUSR1 gracefully so we don't choke and die horribly
try:
package = pull_sock.recv()
pub_sock.send(package)
unpacked_package = salt.payload.unpackage(package)
payload = unpacked_package['payload']
# if you have a specific topic list, use that
if 'topic_lst' in unpacked_package:
for topic in unpacked_package['topic_lst']:
# zmq filters are substring match, hash the topic
# to avoid collisions
htopic = hashlib.sha1(topic).hexdigest()
pub_sock.send(htopic, flags=zmq.SNDMORE)
pub_sock.send(payload)
# otherwise its a broadcast
else:
pub_sock.send('broadcast', flags=zmq.SNDMORE)
pub_sock.send(payload)
except zmq.ZMQError as exc:
if exc.errno == errno.EINTR:
continue
@ -2638,7 +2652,14 @@ class ClearFuncs(object):
os.path.join(self.opts['sock_dir'], 'publish_pull.ipc')
)
pub_sock.connect(pull_uri)
pub_sock.send(self.serial.dumps(payload))
int_payload = {'payload': self.serial.dumps(payload)}
# add some targeting stuff for lists only (for now)
if load['tgt_type'] == 'list':
int_payload['topic_lst'] = load['tgt']
pub_sock.send(self.serial.dumps(int_payload))
return {
'enc': 'clear',
'load': {

View File

@ -56,7 +56,8 @@ except ImportError:
# Import salt libs
from salt.exceptions import (
AuthenticationError, CommandExecutionError, CommandNotFoundError,
SaltInvocationError, SaltReqTimeoutError, SaltClientError, SaltSystemExit
SaltInvocationError, SaltReqTimeoutError, SaltClientError,
SaltSystemExit, SaltSyndicMasterError
)
import salt.client
import salt.crypt
@ -607,6 +608,10 @@ class Minion(MinionBase):
self.grains_cache = self.opts['grains']
# store your hexid to subscribe to zmq, hash since zmq filters are prefix
# matches this way we can avoid collisions
self.hexid = hashlib.sha1(self.opts['id']).hexdigest()
if 'proxy' in self.opts['pillar']:
log.debug('I am {0} and I need to start some proxies for {0}'.format(self.opts['id'],
self.opts['pillar']['proxy']))
@ -1129,7 +1134,8 @@ class Minion(MinionBase):
)
def _setsockopts(self):
self.socket.setsockopt(zmq.SUBSCRIBE, '')
self.socket.setsockopt(zmq.SUBSCRIBE, 'broadcast')
self.socket.setsockopt(zmq.SUBSCRIBE, self.hexid)
self.socket.setsockopt(zmq.IDENTITY, self.opts['id'])
self._set_ipv4only()
self._set_reconnect_ivl_max()
@ -1195,6 +1201,20 @@ class Minion(MinionBase):
).compile_pillar()
self.module_refresh()
def environ_setenv(self, package):
'''
Set the salt-minion main process environment according to
the data contained in the minion event data
'''
tag, data = salt.utils.event.MinionEvent.unpack(package)
environ = data.get('environ', None)
if environ is None:
return False
false_unsets = data.get('false_unsets', False)
clear_all = data.get('clear_all', False)
import salt.modules.environ as mod_environ
return mod_environ.setenv(environ, false_unsets, clear_all)
def clean_die(self, signum, frame):
'''
Python does not handle the SIGTERM cleanly, if it is signaled exit
@ -1313,6 +1333,8 @@ class Minion(MinionBase):
if self.grains_cache != self.opts['grains']:
self.pillar_refresh()
self.grains_cache = self.opts['grains']
elif package.startswith('environ_setenv'):
self.environ_setenv(package)
elif package.startswith('fire_master'):
tag, data = salt.utils.event.MinionEvent.unpack(package)
log.debug('Forwarding master event tag={tag}'.format(tag=data['tag']))
@ -1381,7 +1403,12 @@ class Minion(MinionBase):
def _do_socket_recv(self, socks):
if socks.get(self.socket) == zmq.POLLIN:
payload = self.serial.loads(self.socket.recv(zmq.NOBLOCK))
# topic filtering is done at the zmq level, so we just strip it
recv_str = self.socket.recv(zmq.NOBLOCK)
# if you have a header, then you have another one coming down the pipe
if recv_str in (self.hexid, 'broadcast'):
recv_str = self.socket.recv(zmq.NOBLOCK)
payload = self.serial.loads(recv_str)
log.trace('Handling payload')
self._handle_payload(payload)
@ -1500,6 +1527,8 @@ class Syndic(Minion):
# Share the poller with the event object
self.poller = self.local.event.poller
self.socket = self.context.socket(zmq.SUB)
# no filters for syndication masters, unless we want to maintain a
# list of all connected minions and update the filter
self.socket.setsockopt(zmq.SUBSCRIBE, '')
self.socket.setsockopt(zmq.IDENTITY, self.opts['id'])
if hasattr(zmq, 'RECONNECT_IVL_MAX'):
@ -1577,7 +1606,18 @@ class Syndic(Minion):
def _process_cmd_socket(self):
try:
payload = self.serial.loads(self.socket.recv(zmq.NOBLOCK))
messages = self.socket.recv_multipart(zmq.NOBLOCK)
messages_len = len(messages)
idx = None
if messages_len == 1:
idx = 0
elif messages_len == 2:
idx = 1
else:
raise SaltSyndicMasterError('Syndication master recieved message of invalid len ({0}/2)'.format(messages_len))
payload = self.serial.loads(messages[idx])
except zmq.ZMQError as e:
# Swallow errors for bad wakeups or signals needing processing
if e.errno != errno.EAGAIN and e.errno != errno.EINTR:

View File

@ -418,6 +418,7 @@ def _run(cmd,
except TimedProcTimeoutError as exc:
ret['stdout'] = str(exc)
ret['stderr'] = ''
ret['retcode'] = None
ret['pid'] = proc.process.pid
# ok return code for timeouts?
ret['retcode'] = 1
@ -679,6 +680,8 @@ def run_stdout(cmd,
log.log(lvl, 'stdout: {0}'.format(ret['stdout']))
if ret['stderr']:
log.log(lvl, 'stderr: {0}'.format(ret['stderr']))
if ret['retcode']:
log.log(lvl, 'retcode: {0}'.format(ret['retcode']))
return ret['stdout']
@ -757,6 +760,8 @@ def run_stderr(cmd,
log.log(lvl, 'stdout: {0}'.format(ret['stdout']))
if ret['stderr']:
log.log(lvl, 'stderr: {0}'.format(ret['stderr']))
if ret['retcode']:
log.log(lvl, 'retcode: {0}'.format(ret['retcode']))
return ret['stderr']
@ -835,6 +840,8 @@ def run_all(cmd,
log.log(lvl, 'stdout: {0}'.format(ret['stdout']))
if ret['stderr']:
log.log(lvl, 'stderr: {0}'.format(ret['stderr']))
if ret['retcode']:
log.log(lvl, 'retcode: {0}'.format(ret['retcode']))
return ret

View File

@ -23,7 +23,7 @@ def _encode(string):
def _cron_id(cron):
'''SAFEBELT, Oanly setted if we really have an identifier'''
'''SAFETYBELT, Only set if we really have an identifier'''
cid = None
if cron['identifier']:
cid = cron['identifier']
@ -36,11 +36,11 @@ def _cron_id(cron):
def _cron_matched(cron, cmd, identifier=None):
'''Check if:
- we find a cron with same cmd, old state behavior
- but also be enough smart to remove states changed crons where we do
not removed priorly by a cron.absent by matching on the providen
- but also be smart enough to remove states changed crons where we do
not removed priorly by a cron.absent by matching on the provided
identifier.
We assure retrocompatiblity by only checking on identifier if
and only an identifier was set on the serialized crontab
and only if an identifier was set on the serialized crontab
'''
ret, id_matched = False, None
cid = _cron_id(cron)

View File

@ -421,8 +421,8 @@ def logs(container, *args, **kwargs):
status = base_status.copy()
client = _get_client()
try:
info = client.logs(_get_container_infos(container)['id'])
valid(status, id=container, out=info)
container_logs = client.logs(_get_container_infos(container)['id'])
valid(status, id=container, out=container_logs)
except Exception:
invalid(status, id=container, out=traceback.format_exc())
return status
@ -462,7 +462,7 @@ def commit(container,
client = _get_client()
try:
container = _get_container_infos(container)['id']
info = client.commit(
commit_info = client.commit(
container,
repository=repository,
tag=tag,
@ -471,14 +471,14 @@ def commit(container,
conf=conf)
found = False
for k in ('Id', 'id', 'ID'):
if k in info:
if k in commit_info:
found = True
image_id = info[k]
image_id = commit_info[k]
if not found:
raise Exception('Invalid commit return')
image = _get_image_infos(image_id)['id']
comment = 'Image {0} created from {1}'.format(image, container)
valid(status, id=image, out=info, comment=comment)
valid(status, id=image, out=commit_info, comment=comment)
except Exception:
invalid(status, id=container, out=traceback.format_exc())
return status
@ -500,8 +500,8 @@ def diff(container, *args, **kwargs):
status = base_status.copy()
client = _get_client()
try:
info = client.diff(_get_container_infos(container)['id'])
valid(status, id=container, out=info)
container_diff = client.diff(_get_container_infos(container)['id'])
valid(status, id=container, out=container_diff)
except Exception:
invalid(status, id=container, out=traceback.format_exc())
return status
@ -622,7 +622,7 @@ def create_container(image,
mounted = parts[0]
mountpoints[mountpoint] = {}
binds[mounted] = mountpoint
info = client.create_container(
container_info = client.create_container(
image=image,
command=command,
hostname=hostname,
@ -638,12 +638,12 @@ def create_container(image,
volumes_from=volumes_from,
name=name,
)
container = info['Id']
container = container_info['Id']
callback = valid
comment = 'Container created'
out = {
'info': _get_container_infos(container),
'out': info
'out': container_info
}
return callback(status, id=container, comment=comment, out=out)
except Exception:
@ -664,8 +664,8 @@ def version(*args, **kwargs):
status = base_status.copy()
client = _get_client()
try:
info = client.version()
valid(status, out=info)
docker_version = client.version()
valid(status, out=docker_version)
except Exception:
invalid(status, out=traceback.format_exc())
return status
@ -687,8 +687,8 @@ def info(*args, **kwargs):
status = base_status.copy()
client = _get_client()
try:
info = client.info()
valid(status, out=info)
version_info = client.info()
valid(status, out=version_info)
except Exception:
invalid(status, out=traceback.format_exc())
return status
@ -715,10 +715,10 @@ def port(container, private_port, *args, **kwargs):
status = base_status.copy()
client = _get_client()
try:
info = client.port(
port_info = client.port(
_get_container_infos(container)['id'],
port)
valid(status, id=container, out=info)
valid(status, id=container, out=port_info)
except Exception:
invalid(status, id=container, out=traceback.format_exc())
return status
@ -1251,7 +1251,7 @@ def import_image(src, repo, tag=None, *args, **kwargs):
try:
ret = client.import_image(src, repository=repo, tag=tag)
if ret:
logs, info = _parse_image_multilogs_string(ret, repo)
logs, _info = _parse_image_multilogs_string(ret, repo)
_create_image_assemble_error_status(status, ret, logs)
if status['status'] is not False:
infos = _get_image_infos(logs[0]['status'])

View File

@ -6,14 +6,12 @@ of the current salt process.
# Import python libs
import os
import logging
# Import salt libs
from salt._compat import string_types
from salt.exceptions import SaltException
__func_alias__ = {
'set_': 'set'
}
log = logging.getLogger(__name__)
def __virtual__():
@ -23,51 +21,229 @@ def __virtual__():
return True
def set_(environ):
def setval(key, val, false_unsets=False):
'''
Set the salt process environment variables.
Set a single salt process environment variable. Returns True
on success.
Accepts a dict 'environ'. Each top-level key of the dict
are the names of the environment variables to set.
The value to set must be a string.
key
The environment key to set. Must be a string.
val
The value to set. Must be a string or False. Refer to the
'false_unsets' parameter for behavior when set to False.
false_unsets
If val is False and false_unsets is True, then the key will be
removed from the salt processes environment dict entirely.
If val is False and false_unsets is not True, then the key's
value will be set to an empty string.
Default: False.
CLI Example:
.. code-block:: bash
salt '*' environ.set '{"foo": "bar", "baz": "quux"}'
salt '*' environ.setval foo bar
salt '*' environ.setval baz val=False false_unsets=True
'''
if not isinstance(key, string_types):
log.debug(
'{0}: "key" argument is not a string type: {1!r}'
.format(__name__, key)
)
if val is False:
if false_unsets is True:
try:
os.environ.pop(key, None)
return None
except Exception as exc:
log.error(
'{0}: Exception occurred when unsetting '
'environ key "{1!r}": {2!r}'
.format(__name__, key, exc)
)
return False
else:
val = ''
if isinstance(val, string_types):
try:
os.environ[key] = val
return os.environ[key]
except Exception as exc:
log.error(
'{0}: Exception occurred when setting'
'environ key "{1!r}": {2!r}'
.format(__name__, key, exc)
)
return False
else:
log.debug(
'{0}: "val" argument for key "{1!r}" is not a string '
'or False: {2!r}'
.format(__name__, key, val)
)
return False
def setenv(environ, false_unsets=False, clear_all=False, update_minion=False):
'''
Set multiple salt process environment variables from a dict.
Returns a dict.
environ
Must be a dict. The top-level keys of the dict are the names
of the environment variables to set. Each key's value must be
a string or False. Refer to the 'false_unsets' parameter for
behavior when a value set to False.
false_unsets
If a key's value is False and false_unsets is True, then the
key will be removed from the salt processes environment dict
entirely. If a key's value is Flase and false_unsets is not
True, then the key's value will be set to an empty string.
Default: False
clear_all
USE WITH CAUTION! This option can unset environment variables
needed for salt to function properly.
If clear_all is True, then any environment variables not
defined in the environ dict will be deleted.
Default: False
update_minion
If True, apply these environ changes to the main salt-minion
process. If False, the environ changes will only affect the
current salt subprocess.
Default: False
CLI Example:
.. code-block:: bash
salt '*' environ.setenv '{"foo": "bar", "baz": "quux"}'
salt '*' environ.setenv '{"a": "b", "c": False}' false_unsets=True
'''
ret = {}
if not isinstance(environ, dict):
raise SaltException('The "environ" argument variable must be a dict')
try:
for key, val in environ.items():
if not isinstance(val, string_types):
raise SaltException(
'The value of "environ" keys must be string type'
log.debug(
'{0}: "environ" argument is not a dict: {1!r}'
.format(__name__, environ)
)
os.environ[key] = val
ret[key] = os.environ[key]
except Exception as exc:
raise SaltException(exc)
return False
if clear_all is True:
# Unset any keys not defined in 'environ' dict supplied by user
to_unset = [key for key in os.environ.keys() if key not in environ]
for key in to_unset:
ret[key] = setval(key, False, false_unsets)
for key, val in environ.items():
if isinstance(val, string_types):
ret[key] = setval(key, val)
elif val is False:
ret[key] = setval(key, val, false_unsets)
else:
log.debug(
'{0}: "val" argument for key "{1!r}" is not a string '
'or False: {2!r}'
.format(__name__, key, val)
)
return False
if update_minion is True:
__salt__['event.fire']({'environ': environ,
'false_unsets': false_unsets,
'clear_all': clear_all
},
'environ_setenv')
return ret
def get(keys):
def get(key, default=''):
'''
Get the salt process environment variables.
Get a single salt process environment variable.
key
String used as the key for environment lookup.
default
If the key is not found in the enironment, return this value.
Default: ''
'keys' can be either a string or a list of strings that will
be used as the keys for environment lookup.
CLI Example:
.. code-block:: bash
salt '*' environ.get foo
salt '*' environ.get '[foo, baz]'
salt '*' environ.get baz default=False
'''
if not isinstance(key, string_types):
log.debug(
'{0}: "key" argument is not a string type: {1!r}'
.format(__name__, key)
)
return False
return os.environ.get(key, default)
def has_value(key, value=None):
'''
Determine whether the key exists in the current salt process
environment dictionary. Optionally compare the current value
of the environment against the supplied value string.
key
Must be a string. Used as key for environment lookup.
value:
Optional. If key exists in the environment, compare the
current value with this value. Return True if they are equal.
CLI Example:
.. code-block:: bash
salt '*' environ.has_value foo
'''
if not isinstance(key, string_types):
log.debug(
'{0}: "key" argument is not a string type: {1!r}'
.format(__name__, key)
)
return False
try:
cur_val = os.environ[key]
if value is not None:
if cur_val == value:
return True
else:
return False
except KeyError:
return False
return True
def item(keys, default=''):
'''
Get one or more salt process environment variables.
Returns a dict.
keys
Either a string or a list of strings that will be used as the
keys for environment lookup.
default
If the key is not found in the enironment, return this value.
Default: ''
CLI Example:
.. code-block:: bash
salt '*' environ.item foo
salt '*' environ.item '[foo, baz]' default=None
'''
ret = {}
key_list = []
@ -76,15 +252,16 @@ def get(keys):
elif isinstance(keys, list):
key_list = keys
else:
raise SaltException(
'The "keys" argument variable must be string or list.'
log.debug(
'{0}: "keys" argument is not a string or list type: {1!r}'
.format(__name__, keys)
)
for key in key_list:
ret[key] = os.environ[key]
ret[key] = os.environ.get(key, default)
return ret
def get_all():
def items():
'''
Return a dict of the entire environment set for the salt process
@ -92,6 +269,6 @@ def get_all():
.. code-block:: bash
salt '*' environ.get:all
salt '*' environ.items
'''
return dict(os.environ)

View File

@ -84,7 +84,7 @@ def bootstrap(location, size, fmt):
.. code-block:: bash
salt '*' qemu_nbd.bootstrap /srv/salt-images/host.qcow 4096 qcow2
salt '*' img.bootstrap /srv/salt-images/host.qcow 4096 qcow2
'''
location = __salt__['img.make_image'](location, size, fmt)
if not location:

View File

@ -338,7 +338,7 @@ def chgroups(name, groups, append=False):
if isinstance(groups, string_types):
groups = groups.split(',')
bad_groups = any(salt.utils.contains_whitespace(x) for x in groups)
bad_groups = [x for x in groups if salt.utils.contains_whitespace(x)]
if bad_groups:
raise SaltInvocationError(
'Invalid group name(s): {0}'.format(', '.join(bad_groups))

353
salt/modules/macports.py Normal file
View File

@ -0,0 +1,353 @@
# -*- coding: utf-8 -*-
'''
Support for MacPorts under MacOSX
'''
# Import python libs
import copy
import logging
import re
# Import salt libs
import salt.utils
log = logging.getLogger(__name__)
LIST_ACTIVE_ONLY = True
def __virtual__():
'''
Confine this module to Mac OS with MacPorts.
'''
if salt.utils.which('port') and __grains__['os'] == 'MacOS':
return 'pkg'
return False
def _list(query=""):
ret = {}
cmd = 'port list %s' % query
for line in __salt__['cmd.run'](cmd).splitlines():
try:
name, version_num, category = re.split(r"\s+", line.lstrip())[0:3]
version_num = version_num[1:]
except ValueError:
continue
ret[name] = version_num
return ret
def list_pkgs(versions_as_list=False, **kwargs):
'''
List the packages currently installed in a dict::
{'<package_name>': '<version>'}
CLI Example:
.. code-block:: bash
salt '*' pkg.list_pkgs
'''
versions_as_list = salt.utils.is_true(versions_as_list)
# 'removed' not yet implemented or not applicable
if salt.utils.is_true(kwargs.get('removed')):
return {}
if 'pkg.list_pkgs' in __context__:
if versions_as_list:
return __context__['pkg.list_pkgs']
else:
ret = copy.deepcopy(__context__['pkg.list_pkgs'])
__salt__['pkg_resource.stringify'](ret)
return ret
ret = {}
cmd = 'port installed'
for line in __salt__['cmd.run'](cmd).splitlines():
try:
name, version_num, active = re.split(r"\s+", line.lstrip())[0:3]
version_num = version_num[1:]
except ValueError:
continue
if not LIST_ACTIVE_ONLY or active == "(active)":
__salt__['pkg_resource.add_pkg'](ret, name, version_num)
__salt__['pkg_resource.sort_pkglist'](ret)
__context__['pkg.list_pkgs'] = copy.deepcopy(ret)
if not versions_as_list:
__salt__['pkg_resource.stringify'](ret)
return ret
def version(*names, **kwargs):
'''
Returns a string representing the package version or an empty string if not
installed. If more than one package name is specified, a dict of
name/version pairs is returned.
CLI Example:
.. code-block:: bash
salt '*' pkg.version <package name>
salt '*' pkg.version <package1> <package2> <package3>
'''
return __salt__['pkg_resource.version'](*names, **kwargs)
def latest_version(*names, **kwargs):
'''
Return the latest version of the named package available for upgrade or
installation
Options:
refresh
Update ports with ``port selfupdate``
CLI Example:
.. code-block:: bash
salt '*' pkg.latest_version <package name>
salt '*' pkg.latest_version <package1> <package2> <package3>
'''
if salt.utils.is_true(kwargs.get('refresh', True)):
refresh_db()
available = _list(" ".join(names)) or {}
installed = __salt__['pkg.list_pkgs']() or {}
ret = {}
for k, v in available.items():
if not k in installed or salt.utils.compare_versions(ver1=installed[k], oper='<', ver2=v):
ret[k] = v
else:
ret[k] = ""
# Return a string if only one package name passed
if len(names) == 1:
return ret[names[0]]
return ret
# available_version is being deprecated
available_version = latest_version
def remove(name=None, pkgs=None, **kwargs):
'''
Removes packages with ``port uninstall``.
name
The name of the package to be deleted.
Multiple Package Options:
pkgs
A list of packages to delete. Must be passed as a python list. The
``name`` parameter will be ignored if this option is passed.
.. versionadded:: 0.16.0
Returns a dict containing the changes.
CLI Example:
.. code-block:: bash
salt '*' pkg.remove <package name>
salt '*' pkg.remove <package1>,<package2>,<package3>
salt '*' pkg.remove pkgs='["foo", "bar"]'
'''
pkg_params = __salt__['pkg_resource.parse_targets'](name,
pkgs,
**kwargs)[0]
old = list_pkgs()
targets = [x for x in pkg_params if x in old]
if not targets:
return {}
cmd = 'port uninstall {0}'.format(' '.join(targets))
__salt__['cmd.run_all'](cmd)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
return __salt__['pkg_resource.find_changes'](old, new)
def install(name=None, refresh=False, pkgs=None, **kwargs):
'''
Install the passed package(s) with ``port install``
name
The name of the formula to be installed. Note that this parameter is
ignored if "pkgs" is passed.
CLI Example:
.. code-block:: bash
salt '*' pkg.install <package name>
version
Specify a version to pkg to install. Ignored if pkgs is sepcified.
CLI Example:
.. code-block:: bash
salt '*' pkg.install <package name>
salt '*' pkg.install git-core version='1.8.5.5'
variant
Specify a variant to pkg to install. Ignored if pkgs is sepcified.
CLI Example:
.. code-block:: bash
salt '*' pkg.install <package name>
salt '*' pkg.install git-core version='1.8.5.5' variant='+credential_osxkeychain+doc+pcre'
Multiple Package Installation Options:
pkgs
A list of formulas to install. Must be passed as a python list.
CLI Example:
.. code-block:: bash
salt '*' pkg.install pkgs='["foo","bar"]'
salt '*' pkg.install pkgs='["foo@1.2","bar"]'
salt '*' pkg.install pkgs='["foo@1.2+ssl","bar@2.3"]'
Returns a dict containing the new package names and versions::
{'<package>': {'old': '<old-version>',
'new': '<new-version>'}}
CLI Example:
.. code-block:: bash
salt '*' pkg.install 'package package package'
'''
pkg_params, pkg_type = \
__salt__['pkg_resource.parse_targets'](name,
pkgs,
{})
if salt.utils.is_true(refresh):
refresh_db()
# Handle version kwarg for a single package target
if pkgs is None:
version_num = kwargs.get('version')
variant_spec = kwargs.get('variant')
spec = None
if version_num:
spec = (spec or "") + "@" + version_num
if variant_spec:
spec = (spec or "") + variant_spec
pkg_params = {name: spec}
if pkg_params is None or len(pkg_params) == 0:
return {}
formulas_array = []
for pname, pparams in pkg_params.items():
formulas_array.append(pname + (pparams or ""))
formulas = " ".join(formulas_array)
old = list_pkgs()
cmd = 'port install {0}'.format(formulas)
__salt__['cmd.run'](cmd)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
return __salt__['pkg_resource.find_changes'](old, new)
def list_upgrades(refresh=True):
'''
Check whether or not an upgrade is available for all packages
Options:
refresh
Update ports with ``port selfupdate``
CLI Example:
.. code-block:: bash
salt '*' pkg.list_upgrades
'''
if refresh:
refresh_db()
return _list("outdated")
def upgrade_available(pkg, refresh=True):
'''
Check whether or not an upgrade is available for a given package
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade_available <package name>
'''
return pkg in list_upgrades(refresh=refresh)
def refresh_db():
'''
Update ports with ``port selfupdate``
'''
__salt__['cmd.run_all']("port selfupdate")
def upgrade(refresh=True):
'''
Run a full upgrade
Options:
refresh
Update ports with ``port selfupdate``
Return a dict containing the new package names and versions::
{'<package>': {'old': '<old-version>',
'new': '<new-version>'}}
CLI Example:
.. code-block:: bash
salt '*' pkg.upgrade
'''
old = list_pkgs()
for pkg in list_upgrades(refresh=refresh):
__salt__["pkg.install"](pkg)
__context__.pop('pkg.list_pkgs', None)
new = list_pkgs()
return __salt__['pkg_resource.find_changes'](old, new)

View File

@ -222,7 +222,7 @@ def volume_list(search_opts=None, profile=None):
return volume
def volume_show(volume_name, profile=None):
def volume_show(name, profile=None):
'''
Create a block storage volume
@ -241,7 +241,7 @@ def volume_show(volume_name, profile=None):
'''
nt_ks = _auth(profile, service_type='volume')
volumes = volume_list(
search_opts={'display_name': volume_name},
search_opts={'display_name': name},
profile=profile
)
try:
@ -321,37 +321,14 @@ def volume_delete(name, profile=None):
return response
def volume_delete(volume_name, profile=None):
'''
Create a block storage volume
name
Name of the new volume (must be first)
profile
Profile to build on
CLI Example:
.. code-block:: bash
salt '*' nova.delete myblock profile=openstack
'''
nt_ks = _auth(profile, service_type='volume')
volume = volume_show(volume_name, profile)
response = nt_ks.volumes.delete(volume['id'])
return response
def volume_detach(volume_name,
def volume_detach(name,
server_name,
profile=None,
timeout=300):
'''
Attach a block storage volume
volume_name
name
Name of the new volume to attach
server_name
@ -368,7 +345,7 @@ def volume_detach(volume_name,
'''
nt_ks = _auth(profile)
volume = volume_show(volume_name, profile)
volume = volume_show(name, profile)
server = server_by_name(server_name, profile)
response = nt_ks.volumes.delete_server_volume(
server['id'],
@ -383,7 +360,7 @@ def volume_detach(volume_name,
if response['status'] == 'available':
return response
except Exception as exc:
log.debug('Volume is detaching: {0}'.format(volume_name))
log.debug('Volume is detaching: {0}'.format(name))
time.sleep(1)
if time.time() - start > timeout:
log.error('Timed out after {0} seconds '
@ -395,7 +372,7 @@ def volume_detach(volume_name,
)
def volume_attach(volume_name,
def volume_attach(name,
server_name,
device='/dev/xvdb',
profile=None,
@ -403,7 +380,7 @@ def volume_attach(volume_name,
'''
Attach a block storage volume
volume_name
name
Name of the new volume to attach
server_name
@ -425,7 +402,7 @@ def volume_attach(volume_name,
'''
nt_ks = _auth(profile)
volume = volume_show(volume_name, profile)
volume = volume_show(name, profile)
server = server_by_name(server_name, profile)
response = nt_ks.volumes.create_server_volume(
server['id'],
@ -441,7 +418,7 @@ def volume_attach(volume_name,
if response['status'] == 'in-use':
return response
except Exception as exc:
log.debug('Volume is attaching: {0}'.format(volume_name))
log.debug('Volume is attaching: {0}'.format(name))
time.sleep(1)
if time.time() - start > timeout:
log.error('Timed out after {0} seconds '

View File

@ -75,14 +75,11 @@ def read_key(hkey, path, key):
registry = Registry()
hkey2 = getattr(registry, hkey)
# handle = _winreg.OpenKey(hkey2, path)
# value, type = _winreg.QueryValueEx(handle, key)
# return value
try:
handle = _winreg.OpenKey(hkey2, path)
return _winreg.QueryValueEx(handle, key)[0]
except Exception:
return False
return None
def set_key(hkey, path, key, value, vtype='REG_DWORD'):

View File

@ -2,8 +2,15 @@
'''
Connection module for Amazon S3
:configuration: This module is not usable until the following are specified
either in a pillar or in the minion's config file::
:configuration: This module accepts explicit s3 credentials but can also utilize
IAM roles assigned to the instance trough Instance Profiles. Dynamic
credentials are then automatically obtained from AWS API and no further
configuration is necessary. More Information available at::
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
If IAM roles are not used you need to specify them either in a pillar or
in the minion's config file::
s3.keyid: GKTADJGHEIQSXMKKRBJ08H
s3.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs

118
salt/modules/znc.py Normal file
View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
'''
znc - An advanced IRC bouncer
.. versionadded:: Helium
Provides an interace to basic ZNC functionality
'''
# Import python libs
import hashlib
import logging
import os.path
import random
import signal
# Import salt libs
import salt.utils
log = logging.getLogger(__name__)
def __virtual__():
'''
Only load the module if znc is installed
'''
if salt.utils.which('znc'):
return 'znc'
return False
def _makepass(password, hasher='sha256'):
'''
Create a znc compatible hashed password
'''
# Setup the hasher
if hasher == 'sha256':
h = hashlib.sha256(password)
elif hasher == 'md5':
h = hashlib.md5(password)
else:
return NotImplemented
c = "abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
"0123456789!?.,:;/*-+_()"
r = {
'Method': h.name,
'Salt': ''.join(random.choice(c) for x in xrange(20)),
}
# Salt the password hash
h.update(r['Salt'])
r['Hash'] = h.hexdigest()
return r
def buildmod(*modules):
'''
Build module using znc-buildmod
CLI Example:
.. code-block:: bash
salt '*' znc.buildmod module.cpp [...]
'''
# Check if module files are missing
missing = [module for module in modules if not os.path.exists(module)]
if missing:
return 'Error: The file ({0}) does not exist.'.format(', '.join(missing))
cmd = 'znc-buildmod {0}'.format(' '.join(modules))
out = __salt__['cmd.run'](cmd).splitlines()
return out[-1]
def dumpconf():
'''
Wite the active configuration state to config file
CLI Example:
.. code-block:: bash
salt '*' znc.dumpconf
'''
return __salt__['ps.pkill']('znc', signal=signal.SIGUSR1)
def rehashconf():
'''
Rehash the active configuration state from config file
CLI Example:
.. code-block:: bash
salt '*' znc.rehashconf
'''
return __salt__['ps.pkill']('znc', signal=signal.SIGHUP)
def version():
'''
Return server version from znc --version
CLI Example:
.. code-block:: bash
salt '*' znc.version
'''
cmd = 'znc --version'
out = __salt__['cmd.run'](cmd).splitlines()
ret = out[0].split(' - ')
return ret[0]

View File

@ -168,7 +168,6 @@ TODO
import logging
import sys
from salt.loader import states
from salt.utils.pyobjects import StateRegistry, StateFactory, SaltObject
log = logging.getLogger(__name__)
@ -183,6 +182,12 @@ def render(template, saltenv='base', sls='',
_registry = StateRegistry()
if _states is None:
try:
_states = __states__
except NameError:
from salt.loader import states
__opts__['grains'] = __grains__
__opts__['pillar'] = __pillar__
_states = states(__opts__, __salt__)
# build our list of states and functions

57
salt/runners/map.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
'''
General map/reduce style salt-runner for aggregating identical results returned by several different minions.
Aggregated results are sorted by the size of the minion pools returning identical results.
Useful for playing the game: " some of these things are not like the others... " when identifying discrepancies
in a large infrastructure managed by salt.
'''
import salt.client
def hash(*args, **kwargs):
'''
Return the aggregated and sorted results from a salt command submitted by a
salt runner...
CLI Example #1: ( functionally equivalent to "salt-run manage.up" )
salt-run map.hash "*" test.ping
CLI Example #2: ( find an "outlier" minion config file )
salt-run map.hash "*" file.get_hash /etc/salt/minion
'''
import hashlib
tgt = args[0]
cmd = args[1]
client = salt.client.LocalClient(__opts__['conf_file'])
minions = client.cmd(tgt, cmd, args[2:], timeout=__opts__['timeout'])
ret = {}
# hash minion return values as a string
for minion in minions:
h = hashlib.sha256(str(minions[minion])).hexdigest()
if not h in ret:
ret[h] = []
ret[h].append(minion)
for k in sorted(ret, key=lambda k: len(ret[k]), reverse=True):
# return aggregated results, sorted by size of the hash pool
# TODO: use a custom outputter for better display results
print 'minion pool:\n--------'
print ret[k]
print 'size:\n-----'
print ' ' + str(len(ret[k]))
print 'result:\n-------'
print ' ' + str(minions[ret[k][0]])
print '\n'
return ret

View File

@ -596,7 +596,7 @@ class State(object):
)
self.functions[f_key] = funcs[func]
self.states = salt.loader.states(self.opts, self.functions)
self.rend = salt.loader.render(self.opts, self.functions)
self.rend = salt.loader.render(self.opts, self.functions, states=self.states)
def module_refresh(self):
'''
@ -2615,7 +2615,7 @@ class MasterState(State):
# Load the states, but they should not be used in this class apart
# from inspection
self.states = salt.loader.states(self.opts, self.functions)
self.rend = salt.loader.render(self.opts, self.functions)
self.rend = salt.loader.render(self.opts, self.functions, states=self.states)
class MasterHighState(HighState):

View File

@ -24,7 +24,7 @@ parameters used by Salt to define the various timing values for a cron job:
the cron job is for another user, it is necessary to specify that user with
the ``user`` parameter.
In a time, a long ago when making changes to an existing cron job,
A long time ago (before 2014.2), when making changes to an existing cron job,
the name declaration is the parameter used to uniquely identify the job,
so if an existing cron that looks like this:
@ -48,8 +48,9 @@ Is changed to this:
Then the existing cron will be updated, but if the cron command is changed,
then a new cron job will be added to the user's crontab.
The current behavior is still relying on that mecanism, but you can also
The current behavior is still relying on that mechanism, but you can also
specify an identifier to identify your crontabs:
.. versionadded:: 2014.2
.. code-block:: yaml
date > /tmp/crontest:
@ -59,7 +60,8 @@ specify an identifier to identify your crontabs:
- minute: 7
- hour: 2
And, some monthes later, you modify it:
And, some months later, you modify it:
.. versionadded:: 2014.2
.. code-block:: yaml
superscript > /tmp/crontest:
@ -72,7 +74,7 @@ And, some monthes later, you modify it:
The old **date > /tmp/crontest** will be replaced by
**superscript > /tmp/crontest**.
Additionaly, Salt also supports running a cron every ``x minutes`` very similarly to the Unix
Additionally, Salt also supports running a cron every ``x minutes`` very similarly to the Unix
convention of using ``*/5`` to have a job run every five minutes. In Salt, this
looks like:
@ -241,8 +243,8 @@ def present(name,
User comment to be added on line previous the cron job
identifier
Custom defined identifier for tracking the cron line for futur crontab
edits. This defaults to state id
Custom-defined identifier for tracking the cron line for future crontab
edits. This defaults to the state id
'''
name = ' '.join(name.strip().split())
if not identifier:
@ -315,8 +317,8 @@ def absent(name,
the root user
identifier
Custom defined identifier for tracking the cron line for futur crontab
edits. This defaults to state id
Custom-defined identifier for tracking the cron line for future crontab
edits. This defaults to the state id
'''
### NOTE: The keyword arguments in **kwargs are ignored in this state, but
### cannot be removed from the function definition, otherwise the use

View File

@ -10,10 +10,6 @@ import os
# Import salt libs
from salt._compat import string_types
__func_alias__ = {
'set_': 'set'
}
def __virtual__():
'''
@ -22,19 +18,43 @@ def __virtual__():
return True
def set_(name, value):
def setenv(name,
value,
false_unsets=False,
clear_all=False,
update_minion=False):
'''
Set the salt process environment variables.
name
The environment variable key to set if 'value' is a string
The environment key to set. Must be a string.
value
Either a string or dict. When string, it will be the value
set for the environment variable of 'name' above.
set for the environment key of 'name' above.
When a dict, each key/value pair represents an environment
variable to set.
false_unsets
If a key's value is False and false_unsets is True, then the
key will be removed from the salt processes environment dict
entirely. If a key's value is Flase and false_unsets is not
True, then the key's value will be set to an empty string.
Default: False
clear_all
USE WITH CAUTION! This option can unset environment variables
needed for salt to function properly.
If clear_all is True, then any environment variables not
defined in the environ dict will be deleted.
Default: False
update_minion
If True, apply these environ changes to the main salt-minion
process. If False, the environ changes will only affect the
current salt subprocess.
Default: False
CLI Example:
.. code-block:: yaml
@ -43,6 +63,7 @@ def set_(name, value):
environ.set:
- name: foo
- value: bar
- update_minion: True
a_dict_env:
environ.set:
@ -65,12 +86,42 @@ def set_(name, value):
ret['result'] = False
ret['comment'] = 'Environ value must be string or dict'
return ret
if clear_all is True:
# Any keys not in 'environ' dict supplied by user will be unset
to_unset = [key for key in os.environ.keys() if key not in environ]
for key in to_unset:
if false_unsets is not True:
# This key value will change to ''
ret['changes'].update({key: ''})
else:
# We're going to delete the key
ret['changes'].update({key: None})
current_environ = dict(os.environ)
already_set = []
for key, val in environ.items():
if current_environ.get(key, '') == val:
if val is False:
# We unset this key from the environment if
# false_unsets is True. Otherwise we want to set
# the value to ''
if current_environ.get(key, None) is None:
# The key does not exist in environment
if false_unsets is not True:
# This key will be added with value ''
ret['changes'].update({key: ''})
else:
# The key exists.
if false_unsets is not True:
# Check to see if the value will change
if current_environ.get(key, None) != '':
# This key value will change to ''
ret['changes'].update({key: ''})
else:
# We're going to delete the key
ret['changes'].update({key: None})
elif current_environ.get(key, '') == val:
already_set.append(key)
environ.pop(key)
else:
ret['changes'].update({key: val})
@ -82,7 +133,10 @@ def set_(name, value):
ret['comment'] = 'Environ values are already set with the correct values'
return ret
environ_ret = __salt__['environ.set'](environ)
environ_ret = __salt__['environ.setenv'](environ,
false_unsets,
clear_all,
update_minion)
if not environ_ret:
ret['result'] = False
ret['comment'] = 'Failed to set environ variables'

View File

@ -47,7 +47,7 @@ def volume_exists(name, profile=None, **kwargs):
if not ret['result']:
return ret
volume = __salt__['nova.volume_show'](volume_name=name, profile=profile)
volume = __salt__['nova.volume_show'](name=name, profile=profile)
if volume:
ret['comment'] = 'Volume exists: {0}'.format(name)
@ -58,17 +58,16 @@ def volume_exists(name, profile=None, **kwargs):
ret['result'] = None
return ret
try:
response = __salt__['nova.volume_create'](
name=name,
profile=profile,
**kwargs
)
if response:
ret['result'] = True
ret['comment'] = 'Volume {0} was created'.format(name)
ret['changes'] = {'old': None, 'new': response}
return ret
except:
else:
ret['result'] = False
ret['comment'] = 'Volume {0} failed to create.'.format(name)
return ret
@ -87,13 +86,12 @@ def volume_attached(name, server_name, profile=None, **kwargs):
return ret
volume = __salt__['nova.volume_show'](
volume_name=name,
name=name,
profile=profile
)
server = __salt__['nova.server_by_name'](server_name, profile=profile)
if volume and volume['attachments']:
print(volume)
ret['comment'] = ('Volume {name} is already'
'attached: {attachments}').format(**volume)
ret['result'] = True
@ -113,18 +111,17 @@ def volume_attached(name, server_name, profile=None, **kwargs):
ret['result'] = False
return ret
try:
response = __salt__['nova.volume_attach'](
name,
server_name,
profile=profile,
**kwargs
)
if response:
ret['result'] = True
ret['comment'] = 'Volume {0} was created'.format(name)
ret['changes'] = {'old': volume, 'new': response}
return ret
except:
else:
ret['result'] = False
ret['comment'] = 'Volume {0} failed to attach.'.format(name)
return ret

View File

@ -29,6 +29,7 @@ import logging
# Import salt libs
import salt.utils
import salt._compat
log = logging.getLogger(__name__)
@ -54,7 +55,7 @@ def state(
sls=None,
env=None,
test=False,
fail_minions='',
fail_minions=None,
allow_fail=0,
timeout=None):
'''
@ -93,7 +94,7 @@ def state(
fail_minions
An optional list of targeted minions where failure is an option
'''
cmd_kw = {'arg': [], 'ret': ret, 'timeout': timeout}
cmd_kw = {'arg': [], 'kwarg': {}, 'ret': ret, 'timeout': timeout}
ret = {'name': name,
'changes': {},
@ -135,10 +136,12 @@ def state(
ret['comment'] = 'No highstate or sls specified, no execution made'
ret['result'] = False
return ret
if test:
cmd_kw['arg'].append('test={0}'.format(test))
if __env__ != 'base':
cmd_kw['arg'].append('saltenv={0}'.format(__env__))
cmd_kw['kwarg']['test'] = test
cmd_kw['kwarg']['saltenv'] = __env__
if __opts__['test'] is True:
ret['comment'] = (
'State run to be executed on target {0} as test={1}'
@ -151,8 +154,17 @@ def state(
fail = set()
failures = {}
no_change = set()
if isinstance(fail_minions, str):
fail_minions = [fail_minions]
if fail_minions is None:
fail_minions = ()
elif isinstance(fail_minions, salt._compat.string_types):
fail_minions = [minion.strip() for minion in fail_minions.split(',')]
elif not isinstance(fail_minions, list):
ret.setdefault('warnings', []).append(
'\'fail_minions\' needs to be a list or a comma separated '
'string. Ignored.'
)
fail_minions = ()
for minion, mdata in cmd_ret.iteritems():
if mdata['out'] != 'highstate':

View File

@ -3,6 +3,7 @@
Tests to try out packeting. Potentially ephemeral
'''
# pylint: skip-file
# pylint: disable=C0103
from ioflo.base.odicting import odict

View File

@ -427,6 +427,43 @@ def wait_for_port(host, port=22, timeout=900, gateway=None):
)
def wait_for_winexesvc(host, port, username, password, timeout=900, gateway=None):
'''
Wait until winexe connection can be established.
'''
start = time.time()
log.debug(
'Attempting winexe connection to host {0} on port {1}'.format(
host, port
)
)
creds = '-U {0}%{1} //{2}'.format(
username, password, host)
trycount = 0
while True:
trycount += 1
try:
# Shell out to winexe to check %TEMP%
ret_code = win_cmd('winexe {0} "sc query winexesvc"'.format(creds))
if ret_code == 0:
log.debug('winexe connected...')
return True
log.debug('Return code was {0}'.format(ret_code))
time.sleep(1)
except socket.error as exc:
log.debug('Caught exception in wait_for_winexesvc: {0}'.format(exc))
time.sleep(1)
if time.time() - start > timeout:
log.error('winexe connection timed out: {0}'.format(timeout))
return False
log.debug(
'Retrying winexe connection to host {0} on port {1} '
'(try {2})'.format(
host, port, trycount
)
)
def validate_windows_cred(host, username='Administrator', password=None):
'''
Check if the windows credentials are valid
@ -512,7 +549,10 @@ def deploy_windows(host, port=445, timeout=900, username='Administrator',
'''
starttime = time.mktime(time.localtime())
log.debug('Deploying {0} at {1} (Windows)'.format(host, starttime))
if wait_for_port(host=host, port=port, timeout=port_timeout * 60):
if wait_for_port(host=host, port=port, timeout=port_timeout * 60) and \
wait_for_winexesvc(host=host, port=port,
username=username, password=password,
timeout=port_timeout * 60):
log.debug('SMB port {0} on {1} is available'.format(port, host))
newtimeout = timeout - (time.mktime(time.localtime()) - starttime)
log.debug(

View File

@ -2149,17 +2149,14 @@ class SaltSSHOptionParser(OptionParser, ConfigDirMixIn, MergeConfigMixIn,
help=('Don\'t execute a salt routine on the targets, execute a '
'raw shell command')
)
self.add_option(
'--priv',
dest='ssh_priv',
help=('Ssh private key file'))
self.add_option(
'--roster',
dest='roster',
default='',
help=('Define which roster system to use, this defines if a '
'database backend, scanner, or custom roster system is '
'used. Default is the flat file roster.'))
'used. Default is the flat file roster.')
)
self.add_option(
'--roster-file',
dest='roster_file',
@ -2167,7 +2164,8 @@ class SaltSSHOptionParser(OptionParser, ConfigDirMixIn, MergeConfigMixIn,
help=('define an alternative location for the default roster '
'file location. The default roster file is called roster '
'and is found in the same directory as the master config '
'file.'))
'file.')
)
self.add_option(
'--refresh', '--refresh-cache',
dest='refresh_cache',
@ -2176,7 +2174,15 @@ class SaltSSHOptionParser(OptionParser, ConfigDirMixIn, MergeConfigMixIn,
help=('Force a refresh of the master side data cache of the '
'target\'s data. This is needed if a target\'s grains have '
'been changed and the auto refresh timeframe has not been '
'reached.'))
'reached.')
)
self.add_option(
'-v', '--verbose',
default=False,
action='store_true',
help=('Turn on command verbosity, display jid')
)
self.add_option(
'--max-procs',
dest='ssh_max_procs',
@ -2185,35 +2191,50 @@ class SaltSSHOptionParser(OptionParser, ConfigDirMixIn, MergeConfigMixIn,
help='Set the number of concurrent minions to communicate with. '
'This value defines how many processes are opened up at a '
'time to manage connections, the more running processes the '
'faster communication should be, default is %default')
self.add_option(
'-i',
'--ignore-host-keys',
dest='ignore_host_keys',
default=False,
action='store_true',
help='By default ssh host keys are honored and connections will '
'ask for approval')
'faster communication should be, default is %default'
)
self.add_option(
'-v', '--verbose',
default=False,
action='store_true',
help=('Turn on command verbosity, display jid')
)
self.add_option(
auth_group = optparse.OptionGroup(
self, 'Authentication Options',
'Parameters affecting authentication'
)
auth_group.add_option(
'--priv',
dest='ssh_priv',
help='Ssh private key file'
)
auth_group.add_option(
'-i',
'--ignore-host-keys',
dest='ignore_host_keys',
default=False,
action='store_true',
help='By default ssh host keys are honored and connections will '
'ask for approval'
)
auth_group.add_option(
'--passwd',
dest='ssh_passwd',
default='',
help='Set the default password to attempt to use when '
'authenticating')
self.add_option(
'authenticating'
)
auth_group.add_option(
'--key-deploy',
dest='ssh_key_deploy',
default=False,
action='store_true',
help='Set this flag to atempt to deploy the authorized ssh key '
'with all minions. This combined with --passwd can make '
'initial deployment of keys very fast and easy')
'initial deployment of keys very fast and easy'
)
self.add_option_group(auth_group)
def _mixin_after_parsed(self):
if not self.args:

View File

@ -39,7 +39,7 @@ class MacUserTestCase(TestCase):
mock_getgrall = [grp.struct_group(('accessibility', '*', 90, [])),
grp.struct_group(('admin', '*', 80, ['root', 'admin']))]
mock_info_ret = {'shell': '/bin/bash', 'name': 'test', 'gid': 4376,
'groups': ['TEST_GROUP'], 'home': '/var/test',
'groups': ['TEST_GROUP'], 'home': '/Users/foo',
'fullname': 'TEST USER', 'uid': 4376}
def test_osmajor(self):
@ -110,16 +110,14 @@ class MacUserTestCase(TestCase):
'''
Tests if the uid is an int
'''
with self.assertRaises(SaltInvocationError):
mac_user.chuid('foo', 'foo')
self.assertRaises(SaltInvocationError, mac_user.chuid, 'foo', 'foo')
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chuid_user_exists(self):
'''
Tests if the user exists or not
'''
with self.assertRaises(CommandExecutionError):
mac_user.chuid('foo', 4376)
self.assertRaises(CommandExecutionError, mac_user.chuid, 'foo', 4376)
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chuid_same_uid(self):
@ -132,16 +130,14 @@ class MacUserTestCase(TestCase):
'''
Tests if the gid is an int
'''
with self.assertRaises(SaltInvocationError):
mac_user.chgid('foo', 'foo')
self.assertRaises(SaltInvocationError, mac_user.chgid, 'foo', 'foo')
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chgid_user_exists(self):
'''
Tests if the user exists or not
'''
with self.assertRaises(CommandExecutionError):
mac_user.chgid('foo', 4376)
self.assertRaises(CommandExecutionError, mac_user.chgid, 'foo', 4376)
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chgid_same_gid(self):
@ -150,6 +146,62 @@ class MacUserTestCase(TestCase):
'''
self.assertTrue(mac_user.chgid('foo', 4376))
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chshell_user_exists(self):
'''
Tests if the user exists or not
'''
self.assertRaises(CommandExecutionError, mac_user.chshell, 'foo', '/bin/bash')
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chshell_same_shell(self):
'''
Tests if the user's shell is the same as the argument
'''
self.assertTrue(mac_user.chshell('foo', '/bin/bash'))
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chhome_user_exists(self):
'''
Test if the user exists or not
'''
self.assertRaises(CommandExecutionError, mac_user.chhome, 'foo', '/Users/foo')
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chhome_same_home(self):
'''
Tests if the user's home is the same as the argument
'''
self.assertTrue(mac_user.chhome('foo', '/Users/foo'))
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chfullname_user_exists(self):
'''
Tests if the user exists or not
'''
self.assertRaises(CommandExecutionError, mac_user.chfullname, 'test', 'TEST USER')
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chfullname_same_name(self):
'''
Tests if the user's full name is the same as the argument
'''
self.assertTrue(mac_user.chfullname('test', 'TEST USER'))
@patch('salt.modules.mac_user.info', MagicMock(return_value={}))
def test_chgroups_user_exists(self):
'''
Tests if the user exists or not
'''
self.assertRaises(CommandExecutionError, mac_user.chgroups, 'foo', 'wheel, root')
@patch('salt.modules.mac_user.info', MagicMock(return_value=mock_info_ret))
def test_chgroups_bad_groups(self):
'''
Test if there is white space in groups argument
'''
self.assertRaises(SaltInvocationError, mac_user.chgroups, 'test', 'bad group')
@patch('salt.modules.mac_user.list_groups',
MagicMock(return_value=['_TEST_GROUP']))
def test_info(self):