mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
Merge branch 'develop' into kernelpkg-remove
This commit is contained in:
commit
a063ddbaad
@ -21,7 +21,7 @@ Or you may specify a map which includes all VMs to perform the action on:
|
||||
|
||||
$ salt-cloud -a reboot -m /path/to/mapfile
|
||||
|
||||
The following is a list of actions currently supported by salt-cloud:
|
||||
The following is an example list of actions currently supported by ``salt-cloud``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
@ -36,5 +36,5 @@ The following is a list of actions currently supported by salt-cloud:
|
||||
- start
|
||||
- stop
|
||||
|
||||
Another useful reference for viewing more salt-cloud actions is the
|
||||
:ref:Salt Cloud Feature Matrix <salt-cloud-feature-matrix>
|
||||
Another useful reference for viewing more ``salt-cloud`` actions is the
|
||||
:ref:`Salt Cloud Feature Matrix <salt-cloud-feature-matrix>`.
|
||||
|
@ -26,5 +26,5 @@ gathering information about instances on a provider basis:
|
||||
$ salt-cloud -f list_nodes_full linode
|
||||
$ salt-cloud -f list_nodes_select linode
|
||||
|
||||
Another useful reference for viewing salt-cloud functions is the
|
||||
Another useful reference for viewing ``salt-cloud`` functions is the
|
||||
:ref:`Salt Cloud Feature Matrix <salt-cloud-feature-matrix>`.
|
||||
|
@ -3,3 +3,13 @@ Salt 2016.11.7 Release Notes
|
||||
============================
|
||||
|
||||
Version 2016.11.7 is a bugfix release for :ref:`2016.11.0 <release-2016-11-0>`.
|
||||
|
||||
Changes for v2016.11.6..v2016.11.7
|
||||
----------------------------------
|
||||
|
||||
Security Fix
|
||||
============
|
||||
|
||||
CVE-2017-12791 Maliciously crafted minion IDs can cause unwanted directory traversals on the Salt-master
|
||||
|
||||
Correct a flaw in minion id validation which could allow certain minions to authenticate to a master despite not having the correct credentials. To exploit the vulnerability, an attacker must create a salt-minion with an ID containing characters that will cause a directory traversal. Credit for discovering the security flaw goes to: Vernhk@qq.com
|
||||
|
@ -4,6 +4,13 @@ Salt 2017.7.1 Release Notes
|
||||
|
||||
Version 2017.7.1 is a bugfix release for :ref:`2017.7.0 <release-2017-7-0>`.
|
||||
|
||||
Security Fix
|
||||
============
|
||||
|
||||
CVE-2017-12791 Maliciously crafted minion IDs can cause unwanted directory traversals on the Salt-master
|
||||
|
||||
Correct a flaw in minion id validation which could allow certain minions to authenticate to a master despite not having the correct credentials. To exploit the vulnerability, an attacker must create a salt-minion with an ID containing characters that will cause a directory traversal. Credit for discovering the security flaw goes to: Vernhk@qq.com
|
||||
|
||||
Changes for v2017.7.0..v2017.7.1
|
||||
--------------------------------
|
||||
|
||||
|
@ -383,8 +383,8 @@ Section -Post
|
||||
nsExec::Exec "nssm.exe set salt-minion Description Salt Minion from saltstack.com"
|
||||
nsExec::Exec "nssm.exe set salt-minion Start SERVICE_AUTO_START"
|
||||
nsExec::Exec "nssm.exe set salt-minion AppNoConsole 1"
|
||||
|
||||
RMDir /R "$INSTDIR\var\cache\salt" ; removing cache from old version
|
||||
nsExec::Exec "nssm.exe set salt-minion AppStopMethodConsole 24000"
|
||||
nsExec::Exec "nssm.exe set salt-minion AppStopMethodWindow 2000"
|
||||
|
||||
Call updateMinionConfig
|
||||
|
||||
|
7
salt/cache/redis_cache.py
vendored
7
salt/cache/redis_cache.py
vendored
@ -162,7 +162,7 @@ from salt.exceptions import SaltCacheError
|
||||
|
||||
__virtualname__ = 'redis'
|
||||
__func_alias__ = {
|
||||
'list_': 'list'
|
||||
'ls': 'list'
|
||||
}
|
||||
|
||||
log = logging.getLogger(__file__)
|
||||
@ -196,6 +196,9 @@ def __virtual__():
|
||||
# helper functions -- will not be exported
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def init_kwargs(kwargs):
|
||||
return {}
|
||||
|
||||
|
||||
def _get_redis_cache_opts():
|
||||
'''
|
||||
@ -475,7 +478,7 @@ def flush(bank, key=None):
|
||||
return True
|
||||
|
||||
|
||||
def list_(bank):
|
||||
def ls(bank):
|
||||
'''
|
||||
Lists entries stored in the specified bank.
|
||||
'''
|
||||
|
@ -445,6 +445,7 @@ class SyncClientMixin(object):
|
||||
_use_fnmatch = True
|
||||
else:
|
||||
target_mod = arg + u'.' if not arg.endswith(u'.') else arg
|
||||
_use_fnmatch = False
|
||||
if _use_fnmatch:
|
||||
docs = [(fun, self.functions[fun].__doc__)
|
||||
for fun in fnmatch.filter(self.functions, target_mod)]
|
||||
|
@ -728,12 +728,18 @@ def request_instance(vm_=None, call=None):
|
||||
|
||||
else:
|
||||
pool = floating_ip_conf.get('pool', 'public')
|
||||
for fl_ip, opts in six.iteritems(conn.floating_ip_list()):
|
||||
if opts['fixed_ip'] is None and opts['pool'] == pool:
|
||||
floating_ip = fl_ip
|
||||
break
|
||||
if floating_ip is None:
|
||||
try:
|
||||
floating_ip = conn.floating_ip_create(pool)['ip']
|
||||
except Exception:
|
||||
log.info('A new IP address was unable to be allocated. '
|
||||
'An IP address will be pulled from the already allocated list, '
|
||||
'This will cause a race condition when building in parallel.')
|
||||
for fl_ip, opts in six.iteritems(conn.floating_ip_list()):
|
||||
if opts['fixed_ip'] is None and opts['pool'] == pool:
|
||||
floating_ip = fl_ip
|
||||
break
|
||||
if floating_ip is None:
|
||||
log.error('No IP addresses available to allocate for this server: {0}'.format(vm_['name']))
|
||||
|
||||
def __query_node_data(vm_):
|
||||
try:
|
||||
|
@ -1663,7 +1663,8 @@ DEFAULT_PROXY_MINION_OPTS = {
|
||||
'log_file': os.path.join(salt.syspaths.LOGS_DIR, 'proxy'),
|
||||
'add_proxymodule_to_opts': False,
|
||||
'proxy_merge_grains_in_module': True,
|
||||
'append_minionid_config_dirs': ['cachedir', 'pidfile', 'default_include'],
|
||||
'extension_modules': os.path.join(salt.syspaths.CACHE_DIR, 'proxy', 'extmods'),
|
||||
'append_minionid_config_dirs': ['cachedir', 'pidfile', 'default_include', 'extension_modules'],
|
||||
'default_include': 'proxy.d/*.conf',
|
||||
|
||||
# By default, proxies will preserve the connection.
|
||||
|
@ -640,6 +640,13 @@ class Client(object):
|
||||
|
||||
def on_header(hdr):
|
||||
if write_body[1] is not False and write_body[2] is None:
|
||||
if not hdr.strip() and 'Content-Type' not in write_body[1]:
|
||||
# We've reached the end of the headers and not yet
|
||||
# found the Content-Type. Reset the values we're
|
||||
# tracking so that we properly follow the redirect.
|
||||
write_body[0] = None
|
||||
write_body[1] = False
|
||||
return
|
||||
# Try to find out what content type encoding is used if
|
||||
# this is a text file
|
||||
write_body[1].parse_line(hdr) # pylint: disable=no-member
|
||||
|
@ -84,13 +84,13 @@ else:
|
||||
# We list un-supported functions here. These will be removed from the loaded.
|
||||
# TODO: remove the need for this cross-module code. Maybe use NotImplemented
|
||||
LIBCLOUD_FUNCS_NOT_SUPPORTED = (
|
||||
'parallels.avail_sizes',
|
||||
'parallels.avail_locations',
|
||||
'proxmox.avail_sizes',
|
||||
'rackspace.reboot',
|
||||
'openstack.list_locations',
|
||||
'rackspace.list_locations'
|
||||
)
|
||||
u'parallels.avail_sizes',
|
||||
u'parallels.avail_locations',
|
||||
u'proxmox.avail_sizes',
|
||||
u'rackspace.reboot',
|
||||
u'openstack.list_locations',
|
||||
u'rackspace.list_locations'
|
||||
)
|
||||
|
||||
# Will be set to pyximport module at runtime if cython is enabled in config.
|
||||
pyximport = None
|
||||
|
@ -1275,7 +1275,7 @@ class Minion(MinionBase):
|
||||
ret = yield channel.send(load, timeout=timeout)
|
||||
raise tornado.gen.Return(ret)
|
||||
|
||||
def _fire_master(self, data=None, tag=None, events=None, pretag=None, timeout=60, sync=True):
|
||||
def _fire_master(self, data=None, tag=None, events=None, pretag=None, timeout=60, sync=True, timeout_handler=None):
|
||||
'''
|
||||
Fire an event on the master, or drop message if unable to send.
|
||||
'''
|
||||
@ -1294,10 +1294,6 @@ class Minion(MinionBase):
|
||||
else:
|
||||
return
|
||||
|
||||
def timeout_handler(*_):
|
||||
log.info(u'fire_master failed: master could not be contacted. Request timed out.')
|
||||
return True
|
||||
|
||||
if sync:
|
||||
try:
|
||||
self._send_req_sync(load, timeout)
|
||||
@ -1308,6 +1304,12 @@ class Minion(MinionBase):
|
||||
log.info(u'fire_master failed: %s', traceback.format_exc())
|
||||
return False
|
||||
else:
|
||||
if timeout_handler is None:
|
||||
def handle_timeout(*_):
|
||||
log.info(u'fire_master failed: master could not be contacted. Request timed out.')
|
||||
return True
|
||||
timeout_handler = handle_timeout
|
||||
|
||||
with tornado.stack_context.ExceptionStackContext(timeout_handler):
|
||||
self._send_req_async(load, timeout, callback=lambda f: None) # pylint: disable=unexpected-keyword-arg
|
||||
return True
|
||||
@ -2027,8 +2029,9 @@ class Minion(MinionBase):
|
||||
elif tag.startswith(u'_minion_mine'):
|
||||
self._mine_send(tag, data)
|
||||
elif tag.startswith(u'fire_master'):
|
||||
log.debug(u'Forwarding master event tag=%s', data[u'tag'])
|
||||
self._fire_master(data[u'data'], data[u'tag'], data[u'events'], data[u'pretag'])
|
||||
if self.connected:
|
||||
log.debug(u'Forwarding master event tag=%s', data[u'tag'])
|
||||
self._fire_master(data[u'data'], data[u'tag'], data[u'events'], data[u'pretag'])
|
||||
elif tag.startswith(master_event(type=u'disconnected')) or tag.startswith(master_event(type=u'failback')):
|
||||
# if the master disconnect event is for a different master, raise an exception
|
||||
if tag.startswith(master_event(type=u'disconnected')) and data[u'master'] != self.opts[u'master']:
|
||||
@ -2249,13 +2252,15 @@ class Minion(MinionBase):
|
||||
if ping_interval > 0 and self.connected:
|
||||
def ping_master():
|
||||
try:
|
||||
if not self._fire_master(u'ping', u'minion_ping'):
|
||||
def ping_timeout_handler(*_):
|
||||
if not self.opts.get(u'auth_safemode', True):
|
||||
log.error(u'** Master Ping failed. Attempting to restart minion**')
|
||||
delay = self.opts.get(u'random_reauth_delay', 5)
|
||||
log.info(u'delaying random_reauth_delay %ss', delay)
|
||||
# regular sys.exit raises an exception -- which isn't sufficient in a thread
|
||||
os._exit(salt.defaults.exitcodes.SALT_KEEPALIVE)
|
||||
|
||||
self._fire_master('ping', 'minion_ping', sync=False, timeout_handler=ping_timeout_handler)
|
||||
except Exception:
|
||||
log.warning(u'Attempt to ping master failed.', exc_on_loglevel=logging.DEBUG)
|
||||
self.periodic_callbacks[u'ping'] = tornado.ioloop.PeriodicCallback(ping_master, ping_interval * 1000, io_loop=self.io_loop)
|
||||
@ -2270,7 +2275,7 @@ class Minion(MinionBase):
|
||||
except Exception:
|
||||
log.critical(u'The beacon errored: ', exc_info=True)
|
||||
if beacons and self.connected:
|
||||
self._fire_master(events=beacons)
|
||||
self._fire_master(events=beacons, sync=False)
|
||||
|
||||
self.periodic_callbacks[u'beacons'] = tornado.ioloop.PeriodicCallback(handle_beacons, loop_interval * 1000, io_loop=self.io_loop)
|
||||
|
||||
|
@ -2699,11 +2699,11 @@ def request_vpc_peering_connection(requester_vpc_id=None, requester_vpc_name=Non
|
||||
Name tag of the requesting VPC. Exclusive with requester_vpc_id.
|
||||
|
||||
peer_vpc_id
|
||||
ID of the VPC tp crete VPC peering connection with. This can be a VPC in
|
||||
ID of the VPC to create VPC peering connection with. This can be a VPC in
|
||||
another account. Exclusive with peer_vpc_name.
|
||||
|
||||
peer_vpc_name
|
||||
Name tag of the VPC tp crete VPC peering connection with. This can only
|
||||
Name tag of the VPC to create VPC peering connection with. This can only
|
||||
be a VPC in the same account, else resolving it into a vpc ID will almost
|
||||
certainly fail. Exclusive with peer_vpc_id.
|
||||
|
||||
|
@ -3848,7 +3848,6 @@ def save(name,
|
||||
if os.path.exists(path) and not overwrite:
|
||||
raise CommandExecutionError('{0} already exists'.format(path))
|
||||
|
||||
compression = kwargs.get('compression')
|
||||
if compression is None:
|
||||
if path.endswith('.tar.gz') or path.endswith('.tgz'):
|
||||
compression = 'gzip'
|
||||
@ -3954,7 +3953,7 @@ def save(name,
|
||||
ret['Size_Human'] = _size_fmt(ret['Size'])
|
||||
|
||||
# Process push
|
||||
if kwargs.get(push, False):
|
||||
if kwargs.get('push', False):
|
||||
ret['Push'] = __salt__['cp.push'](path)
|
||||
|
||||
return ret
|
||||
|
@ -152,7 +152,8 @@ Optional small program to encrypt data without needing salt modules.
|
||||
from __future__ import absolute_import
|
||||
import base64
|
||||
import os
|
||||
import salt.utils
|
||||
import salt.utils.files
|
||||
import salt.utils.platform
|
||||
import salt.utils.win_functions
|
||||
import salt.utils.win_dacl
|
||||
import salt.syspaths
|
||||
@ -203,7 +204,7 @@ def _get_sk(**kwargs):
|
||||
key = config['sk']
|
||||
sk_file = config['sk_file']
|
||||
if not key and sk_file:
|
||||
with salt.utils.fopen(sk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'rb') as keyf:
|
||||
key = str(keyf.read()).rstrip('\n')
|
||||
if key is None:
|
||||
raise Exception('no key or sk_file found')
|
||||
@ -218,7 +219,7 @@ def _get_pk(**kwargs):
|
||||
pubkey = config['pk']
|
||||
pk_file = config['pk_file']
|
||||
if not pubkey and pk_file:
|
||||
with salt.utils.fopen(pk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'rb') as keyf:
|
||||
pubkey = str(keyf.read()).rstrip('\n')
|
||||
if pubkey is None:
|
||||
raise Exception('no pubkey or pk_file found')
|
||||
@ -256,9 +257,9 @@ def keygen(sk_file=None, pk_file=None):
|
||||
if sk_file and pk_file is None:
|
||||
if not os.path.isfile(sk_file):
|
||||
kp = libnacl.public.SecretKey()
|
||||
with salt.utils.fopen(sk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.sk))
|
||||
if salt.utils.is_windows():
|
||||
if salt.utils.platform.is_windows():
|
||||
cur_user = salt.utils.win_functions.get_current_user()
|
||||
salt.utils.win_dacl.set_owner(sk_file, cur_user)
|
||||
salt.utils.win_dacl.set_permissions(sk_file, cur_user, 'full_control', 'grant', reset_perms=True, protected=True)
|
||||
@ -277,25 +278,25 @@ def keygen(sk_file=None, pk_file=None):
|
||||
|
||||
if os.path.isfile(sk_file) and not os.path.isfile(pk_file):
|
||||
# generate pk using the sk
|
||||
with salt.utils.fopen(sk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'rb') as keyf:
|
||||
sk = str(keyf.read()).rstrip('\n')
|
||||
sk = base64.b64decode(sk)
|
||||
kp = libnacl.public.SecretKey(sk)
|
||||
with salt.utils.fopen(pk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.pk))
|
||||
return 'saved pk_file: {0}'.format(pk_file)
|
||||
|
||||
kp = libnacl.public.SecretKey()
|
||||
with salt.utils.fopen(sk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.sk))
|
||||
if salt.utils.is_windows():
|
||||
if salt.utils.platform.is_windows():
|
||||
cur_user = salt.utils.win_functions.get_current_user()
|
||||
salt.utils.win_dacl.set_owner(sk_file, cur_user)
|
||||
salt.utils.win_dacl.set_permissions(sk_file, cur_user, 'full_control', 'grant', reset_perms=True, protected=True)
|
||||
else:
|
||||
# chmod 0600 file
|
||||
os.chmod(sk_file, 1536)
|
||||
with salt.utils.fopen(pk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.pk))
|
||||
return 'saved sk_file:{0} pk_file: {1}'.format(sk_file, pk_file)
|
||||
|
||||
@ -335,13 +336,13 @@ def enc_file(name, out=None, **kwargs):
|
||||
data = __salt__['cp.get_file_str'](name)
|
||||
except Exception as e:
|
||||
# likly using salt-run so fallback to local filesystem
|
||||
with salt.utils.fopen(name, 'rb') as f:
|
||||
with salt.utils.files.fopen(name, 'rb') as f:
|
||||
data = f.read()
|
||||
d = enc(data, **kwargs)
|
||||
if out:
|
||||
if os.path.isfile(out):
|
||||
raise Exception('file:{0} already exist.'.format(out))
|
||||
with salt.utils.fopen(out, 'wb') as f:
|
||||
with salt.utils.files.fopen(out, 'wb') as f:
|
||||
f.write(d)
|
||||
return 'Wrote: {0}'.format(out)
|
||||
return d
|
||||
@ -382,13 +383,13 @@ def dec_file(name, out=None, **kwargs):
|
||||
data = __salt__['cp.get_file_str'](name)
|
||||
except Exception as e:
|
||||
# likly using salt-run so fallback to local filesystem
|
||||
with salt.utils.fopen(name, 'rb') as f:
|
||||
with salt.utils.files.fopen(name, 'rb') as f:
|
||||
data = f.read()
|
||||
d = dec(data, **kwargs)
|
||||
if out:
|
||||
if os.path.isfile(out):
|
||||
raise Exception('file:{0} already exist.'.format(out))
|
||||
with salt.utils.fopen(out, 'wb') as f:
|
||||
with salt.utils.files.fopen(out, 'wb') as f:
|
||||
f.write(d)
|
||||
return 'Wrote: {0}'.format(out)
|
||||
return d
|
||||
|
@ -80,7 +80,8 @@ for service_dir in VALID_SERVICE_DIRS:
|
||||
AVAIL_SVR_DIRS = []
|
||||
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = 'service'
|
||||
__virtualname__ = 'runit'
|
||||
__virtual_aliases__ = ('runit',)
|
||||
|
||||
|
||||
def __virtual__():
|
||||
@ -91,8 +92,12 @@ def __virtual__():
|
||||
if __grains__.get('init') == 'runit':
|
||||
if __grains__['os'] == 'Void':
|
||||
add_svc_avail_path('/etc/sv')
|
||||
global __virtualname__
|
||||
__virtualname__ = 'service'
|
||||
return __virtualname__
|
||||
return False
|
||||
if salt.utils.which('sv'):
|
||||
return __virtualname__
|
||||
return (False, 'Runit not available. Please install sv')
|
||||
|
||||
|
||||
def _service_path(name):
|
||||
|
@ -1285,6 +1285,18 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
|
||||
#Compute msiexec string
|
||||
use_msiexec, msiexec = _get_msiexec(pkginfo[version_num].get('msiexec', False))
|
||||
|
||||
# Build cmd and arguments
|
||||
# cmd and arguments must be seperated for use with the task scheduler
|
||||
if use_msiexec:
|
||||
cmd = msiexec
|
||||
arguments = ['/i', cached_pkg]
|
||||
if pkginfo['version_num'].get('allusers', True):
|
||||
arguments.append('ALLUSERS="1"')
|
||||
arguments.extend(salt.utils.shlex_split(install_flags))
|
||||
else:
|
||||
cmd = cached_pkg
|
||||
arguments = salt.utils.shlex_split(install_flags)
|
||||
|
||||
# Install the software
|
||||
# Check Use Scheduler Option
|
||||
if pkginfo[version_num].get('use_scheduler', False):
|
||||
@ -1313,21 +1325,43 @@ def install(name=None, refresh=False, pkgs=None, **kwargs):
|
||||
start_time='01:00',
|
||||
ac_only=False,
|
||||
stop_if_on_batteries=False)
|
||||
|
||||
# Run Scheduled Task
|
||||
if not __salt__['task.run_wait'](name='update-salt-software'):
|
||||
log.error('Failed to install {0}'.format(pkg_name))
|
||||
log.error('Scheduled Task failed to run')
|
||||
ret[pkg_name] = {'install status': 'failed'}
|
||||
else:
|
||||
# Build the install command
|
||||
cmd = []
|
||||
if use_msiexec:
|
||||
cmd.extend([msiexec, '/i', cached_pkg])
|
||||
if pkginfo[version_num].get('allusers', True):
|
||||
cmd.append('ALLUSERS="1"')
|
||||
# Special handling for installing salt
|
||||
if pkg_name in ['salt-minion', 'salt-minion-py3']:
|
||||
ret[pkg_name] = {'install status': 'task started'}
|
||||
if not __salt__['task.run'](name='update-salt-software'):
|
||||
log.error('Failed to install {0}'.format(pkg_name))
|
||||
log.error('Scheduled Task failed to run')
|
||||
ret[pkg_name] = {'install status': 'failed'}
|
||||
else:
|
||||
|
||||
# Make sure the task is running, try for 5 secs
|
||||
from time import time
|
||||
t_end = time() + 5
|
||||
while time() < t_end:
|
||||
task_running = __salt__['task.status'](
|
||||
'update-salt-software') == 'Running'
|
||||
if task_running:
|
||||
break
|
||||
|
||||
if not task_running:
|
||||
log.error(
|
||||
'Failed to install {0}'.format(pkg_name))
|
||||
log.error('Scheduled Task failed to run')
|
||||
ret[pkg_name] = {'install status': 'failed'}
|
||||
|
||||
# All other packages run with task scheduler
|
||||
else:
|
||||
cmd.append(cached_pkg)
|
||||
cmd.extend(salt.utils.args.shlex_split(install_flags))
|
||||
if not __salt__['task.run_wait'](name='update-salt-software'):
|
||||
log.error('Failed to install {0}'.format(pkg_name))
|
||||
log.error('Scheduled Task failed to run')
|
||||
ret[pkg_name] = {'install status': 'failed'}
|
||||
else:
|
||||
|
||||
# Combine cmd and arguments
|
||||
cmd = [cmd].extend(arguments)
|
||||
|
||||
# Launch the command
|
||||
result = __salt__['cmd.run_all'](cmd,
|
||||
cache_path,
|
||||
|
@ -302,6 +302,11 @@ def get_community_names():
|
||||
# Windows SNMP service GUI.
|
||||
if isinstance(current_values, list):
|
||||
for current_value in current_values:
|
||||
|
||||
# Ignore error values
|
||||
if not isinstance(current_value, dict):
|
||||
continue
|
||||
|
||||
permissions = str()
|
||||
for permission_name in _PERMISSION_TYPES:
|
||||
if current_value['vdata'] == _PERMISSION_TYPES[permission_name]:
|
||||
|
@ -1260,7 +1260,7 @@ def status(name, location='\\'):
|
||||
task_service = win32com.client.Dispatch("Schedule.Service")
|
||||
task_service.Connect()
|
||||
|
||||
# get the folder to delete the folder from
|
||||
# get the folder where the task is defined
|
||||
task_folder = task_service.GetFolder(location)
|
||||
task = task_folder.GetTask(name)
|
||||
|
||||
|
@ -67,6 +67,17 @@ provider: ``napalm_base``
|
||||
|
||||
.. versionadded:: 2017.7.1
|
||||
|
||||
multiprocessing: ``False``
|
||||
Overrides the :conf_minion:`multiprocessing` option, per proxy minion.
|
||||
The ``multiprocessing`` option must be turned off for SSH-based proxies.
|
||||
However, some NAPALM drivers (e.g. Arista, NX-OS) are not SSH-based.
|
||||
As multiple proxy minions may share the same configuration file,
|
||||
this option permits the configuration of the ``multiprocessing`` option
|
||||
more specifically, for some proxy minions.
|
||||
|
||||
.. versionadded:: 2017.7.2
|
||||
|
||||
|
||||
.. _`NAPALM Read the Docs page`: https://napalm.readthedocs.io/en/latest/#supported-network-operating-systems
|
||||
.. _`optional arguments`: http://napalm.readthedocs.io/en/latest/support/index.html#list-of-supported-optional-arguments
|
||||
|
||||
|
@ -17,16 +17,28 @@ import salt.netapi
|
||||
|
||||
|
||||
def mk_token(**load):
|
||||
'''
|
||||
r'''
|
||||
Create an eauth token using provided credentials
|
||||
|
||||
Non-root users may specify an expiration date -- if allowed via the
|
||||
:conf_master:`token_expire_user_override` setting -- by passing an
|
||||
additional ``token_expire`` param. This overrides the
|
||||
:conf_master:`token_expire` setting of the same name in the Master config
|
||||
and is how long a token should live in seconds.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
salt-run auth.mk_token username=saltdev password=saltdev eauth=auto
|
||||
salt-run auth.mk_token username=saltdev password=saltdev eauth=auto \\
|
||||
|
||||
# Create a token valid for three years.
|
||||
salt-run auth.mk_token username=saltdev password=saltdev eauth=auto \
|
||||
token_expire=94670856
|
||||
|
||||
# Calculate the number of seconds using expr.
|
||||
salt-run auth.mk_token username=saltdev password=saltdev eauth=auto \
|
||||
token_expire=$(expr \( 365 \* 24 \* 60 \* 60 \) \* 3)
|
||||
'''
|
||||
# This will hang if the master daemon is not running.
|
||||
netapi = salt.netapi.NetapiClient(__opts__)
|
||||
|
@ -149,10 +149,14 @@ Optional small program to encrypt data without needing salt modules.
|
||||
|
||||
'''
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import base64
|
||||
import os
|
||||
import salt.utils
|
||||
|
||||
# Import Salt libs
|
||||
import salt.utils.files
|
||||
import salt.utils.platform
|
||||
import salt.utils.win_functions
|
||||
import salt.utils.win_dacl
|
||||
import salt.syspaths
|
||||
@ -203,7 +207,7 @@ def _get_sk(**kwargs):
|
||||
key = config['sk']
|
||||
sk_file = config['sk_file']
|
||||
if not key and sk_file:
|
||||
with salt.utils.fopen(sk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'rb') as keyf:
|
||||
key = str(keyf.read()).rstrip('\n')
|
||||
if key is None:
|
||||
raise Exception('no key or sk_file found')
|
||||
@ -218,7 +222,7 @@ def _get_pk(**kwargs):
|
||||
pubkey = config['pk']
|
||||
pk_file = config['pk_file']
|
||||
if not pubkey and pk_file:
|
||||
with salt.utils.fopen(pk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'rb') as keyf:
|
||||
pubkey = str(keyf.read()).rstrip('\n')
|
||||
if pubkey is None:
|
||||
raise Exception('no pubkey or pk_file found')
|
||||
@ -256,9 +260,9 @@ def keygen(sk_file=None, pk_file=None):
|
||||
if sk_file and pk_file is None:
|
||||
if not os.path.isfile(sk_file):
|
||||
kp = libnacl.public.SecretKey()
|
||||
with salt.utils.fopen(sk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.sk))
|
||||
if salt.utils.is_windows():
|
||||
if salt.utils.platform.is_windows():
|
||||
cur_user = salt.utils.win_functions.get_current_user()
|
||||
salt.utils.win_dacl.set_owner(sk_file, cur_user)
|
||||
salt.utils.win_dacl.set_permissions(sk_file, cur_user, 'full_control', 'grant', reset_perms=True, protected=True)
|
||||
@ -277,25 +281,25 @@ def keygen(sk_file=None, pk_file=None):
|
||||
|
||||
if os.path.isfile(sk_file) and not os.path.isfile(pk_file):
|
||||
# generate pk using the sk
|
||||
with salt.utils.fopen(sk_file, 'rb') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'rb') as keyf:
|
||||
sk = str(keyf.read()).rstrip('\n')
|
||||
sk = base64.b64decode(sk)
|
||||
kp = libnacl.public.SecretKey(sk)
|
||||
with salt.utils.fopen(pk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.pk))
|
||||
return 'saved pk_file: {0}'.format(pk_file)
|
||||
|
||||
kp = libnacl.public.SecretKey()
|
||||
with salt.utils.fopen(sk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(sk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.sk))
|
||||
if salt.utils.is_windows():
|
||||
if salt.utils.platform.is_windows():
|
||||
cur_user = salt.utils.win_functions.get_current_user()
|
||||
salt.utils.win_dacl.set_owner(sk_file, cur_user)
|
||||
salt.utils.win_dacl.set_permissions(sk_file, cur_user, 'full_control', 'grant', reset_perms=True, protected=True)
|
||||
else:
|
||||
# chmod 0600 file
|
||||
os.chmod(sk_file, 1536)
|
||||
with salt.utils.fopen(pk_file, 'w') as keyf:
|
||||
with salt.utils.files.fopen(pk_file, 'w') as keyf:
|
||||
keyf.write(base64.b64encode(kp.pk))
|
||||
return 'saved sk_file:{0} pk_file: {1}'.format(sk_file, pk_file)
|
||||
|
||||
@ -335,13 +339,13 @@ def enc_file(name, out=None, **kwargs):
|
||||
data = __salt__['cp.get_file_str'](name)
|
||||
except Exception as e:
|
||||
# likly using salt-run so fallback to local filesystem
|
||||
with salt.utils.fopen(name, 'rb') as f:
|
||||
with salt.utils.files.fopen(name, 'rb') as f:
|
||||
data = f.read()
|
||||
d = enc(data, **kwargs)
|
||||
if out:
|
||||
if os.path.isfile(out):
|
||||
raise Exception('file:{0} already exist.'.format(out))
|
||||
with salt.utils.fopen(out, 'wb') as f:
|
||||
with salt.utils.files.fopen(out, 'wb') as f:
|
||||
f.write(d)
|
||||
return 'Wrote: {0}'.format(out)
|
||||
return d
|
||||
@ -382,13 +386,13 @@ def dec_file(name, out=None, **kwargs):
|
||||
data = __salt__['cp.get_file_str'](name)
|
||||
except Exception as e:
|
||||
# likly using salt-run so fallback to local filesystem
|
||||
with salt.utils.fopen(name, 'rb') as f:
|
||||
with salt.utils.files.fopen(name, 'rb') as f:
|
||||
data = f.read()
|
||||
d = dec(data, **kwargs)
|
||||
if out:
|
||||
if os.path.isfile(out):
|
||||
raise Exception('file:{0} already exist.'.format(out))
|
||||
with salt.utils.fopen(out, 'wb') as f:
|
||||
with salt.utils.files.fopen(out, 'wb') as f:
|
||||
f.write(d)
|
||||
return 'Wrote: {0}'.format(out)
|
||||
return d
|
||||
|
@ -900,7 +900,7 @@ def mod_watch(name,
|
||||
try:
|
||||
result = func(name, **func_kwargs)
|
||||
except CommandExecutionError as exc:
|
||||
ret['result'] = True
|
||||
ret['result'] = False
|
||||
ret['comment'] = exc.strerror
|
||||
return ret
|
||||
|
||||
|
@ -78,7 +78,7 @@ def recursive_copy(source, dest):
|
||||
(identical to cp -r on a unix machine)
|
||||
'''
|
||||
for root, _, files in os.walk(source):
|
||||
path_from_source = root.replace(source, '').lstrip('/')
|
||||
path_from_source = root.replace(source, '').lstrip(os.sep)
|
||||
target_directory = os.path.join(dest, path_from_source)
|
||||
if not os.path.exists(target_directory):
|
||||
os.makedirs(target_directory)
|
||||
|
@ -1050,17 +1050,17 @@ class GitPython(GitProvider):
|
||||
else:
|
||||
new = True
|
||||
|
||||
try:
|
||||
ssl_verify = self.repo.git.config('--get', 'http.sslVerify')
|
||||
except git.exc.GitCommandError:
|
||||
ssl_verify = ''
|
||||
desired_ssl_verify = str(self.ssl_verify).lower()
|
||||
if ssl_verify != desired_ssl_verify:
|
||||
self.repo.git.config('http.sslVerify', desired_ssl_verify)
|
||||
try:
|
||||
ssl_verify = self.repo.git.config('--get', 'http.sslVerify')
|
||||
except git.exc.GitCommandError:
|
||||
ssl_verify = ''
|
||||
desired_ssl_verify = str(self.ssl_verify).lower()
|
||||
if ssl_verify != desired_ssl_verify:
|
||||
self.repo.git.config('http.sslVerify', desired_ssl_verify)
|
||||
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
|
||||
return new
|
||||
|
||||
@ -1528,18 +1528,18 @@ class Pygit2(GitProvider):
|
||||
else:
|
||||
new = True
|
||||
|
||||
try:
|
||||
ssl_verify = self.repo.config.get_bool('http.sslVerify')
|
||||
except KeyError:
|
||||
ssl_verify = None
|
||||
if ssl_verify != self.ssl_verify:
|
||||
self.repo.config.set_multivar('http.sslVerify',
|
||||
'',
|
||||
str(self.ssl_verify).lower())
|
||||
try:
|
||||
ssl_verify = self.repo.config.get_bool('http.sslVerify')
|
||||
except KeyError:
|
||||
ssl_verify = None
|
||||
if ssl_verify != self.ssl_verify:
|
||||
self.repo.config.set_multivar('http.sslVerify',
|
||||
'',
|
||||
str(self.ssl_verify).lower())
|
||||
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
# Ensure that refspecs for the "origin" remote are set up as configured
|
||||
if hasattr(self, 'refspecs'):
|
||||
self.configure_refspecs()
|
||||
|
||||
return new
|
||||
|
||||
|
@ -243,6 +243,11 @@ def get_device_opts(opts, salt_obj=None):
|
||||
network_device = {}
|
||||
# by default, look in the proxy config details
|
||||
device_dict = opts.get('proxy', {}) or opts.get('napalm', {})
|
||||
if opts.get('proxy') or opts.get('napalm'):
|
||||
opts['multiprocessing'] = device_dict.get('multiprocessing', False)
|
||||
# Most NAPALM drivers are SSH-based, so multiprocessing should default to False.
|
||||
# But the user can be allows to have a different value for the multiprocessing, which will
|
||||
# override the opts.
|
||||
if salt_obj and not device_dict:
|
||||
# get the connection details from the opts
|
||||
device_dict = salt_obj['config.merge']('napalm')
|
||||
|
@ -134,8 +134,10 @@ def get_pidfile(pidfile):
|
||||
'''
|
||||
with salt.utils.files.fopen(pidfile) as pdf:
|
||||
pid = pdf.read()
|
||||
|
||||
return int(pid)
|
||||
if pid:
|
||||
return int(pid)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
def clean_proc(proc, wait_for_kill=10):
|
||||
|
@ -284,6 +284,9 @@ class ReactWrap(object):
|
||||
# Update called function's low data with event user to
|
||||
# segregate events fired by reactor and avoid reaction loops
|
||||
kwargs['__user__'] = self.event_user
|
||||
# Replace ``state`` kwarg which comes from high data compiler.
|
||||
# It breaks some runner functions and seems unnecessary.
|
||||
kwargs['__state__'] = kwargs.pop('state')
|
||||
|
||||
l_fun(*f_call.get('args', ()), **kwargs)
|
||||
except Exception:
|
||||
|
@ -852,6 +852,12 @@ class Schedule(object):
|
||||
|
||||
ret['return'] = self.functions[func](*args, **kwargs)
|
||||
|
||||
# runners do not provide retcode
|
||||
if 'retcode' in self.functions.pack['__context__']:
|
||||
ret['retcode'] = self.functions.pack['__context__']['retcode']
|
||||
|
||||
ret['success'] = True
|
||||
|
||||
data_returner = data.get('returner', None)
|
||||
if data_returner or self.schedule_returner:
|
||||
if 'return_config' in data:
|
||||
@ -868,7 +874,6 @@ class Schedule(object):
|
||||
for returner in OrderedDict.fromkeys(rets):
|
||||
ret_str = '{0}.returner'.format(returner)
|
||||
if ret_str in self.returners:
|
||||
ret['success'] = True
|
||||
self.returners[ret_str](ret)
|
||||
else:
|
||||
log.info(
|
||||
@ -877,11 +882,6 @@ class Schedule(object):
|
||||
)
|
||||
)
|
||||
|
||||
# runners do not provide retcode
|
||||
if 'retcode' in self.functions.pack['__context__']:
|
||||
ret['retcode'] = self.functions.pack['__context__']['retcode']
|
||||
|
||||
ret['success'] = True
|
||||
except Exception:
|
||||
log.exception("Unhandled exception running {0}".format(ret['fun']))
|
||||
# Although catch-all exception handlers are bad, the exception here
|
||||
|
@ -482,12 +482,21 @@ def clean_path(root, path, subdir=False):
|
||||
return ''
|
||||
|
||||
|
||||
def clean_id(id_):
|
||||
'''
|
||||
Returns if the passed id is clean.
|
||||
'''
|
||||
if re.search(r'\.\.\{sep}'.format(sep=os.sep), id_):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def valid_id(opts, id_):
|
||||
'''
|
||||
Returns if the passed id is valid
|
||||
'''
|
||||
try:
|
||||
return bool(clean_path(opts['pki_dir'], id_))
|
||||
return bool(clean_path(opts['pki_dir'], id_)) and clean_id(id_)
|
||||
except (AttributeError, KeyError, TypeError) as e:
|
||||
return False
|
||||
|
||||
|
@ -19,8 +19,9 @@ from tornado.httpclient import HTTPClient
|
||||
|
||||
GEM = 'tidy'
|
||||
GEM_VER = '1.1.2'
|
||||
OLD_GEM = 'thor'
|
||||
OLD_VERSION = '0.17.0'
|
||||
OLD_GEM = 'brass'
|
||||
OLD_VERSION = '1.0.0'
|
||||
NEW_VERSION = '1.2.1'
|
||||
GEM_LIST = [GEM, OLD_GEM]
|
||||
|
||||
|
||||
@ -129,18 +130,18 @@ class GemModuleTest(ModuleCase):
|
||||
|
||||
self.run_function('gem.install', [OLD_GEM], version=OLD_VERSION)
|
||||
gem_list = self.run_function('gem.list', [OLD_GEM])
|
||||
self.assertEqual({'thor': ['0.17.0']}, gem_list)
|
||||
self.assertEqual({OLD_GEM: [OLD_VERSION]}, gem_list)
|
||||
|
||||
self.run_function('gem.update', [OLD_GEM])
|
||||
gem_list = self.run_function('gem.list', [OLD_GEM])
|
||||
self.assertEqual({'thor': ['0.19.4', '0.17.0']}, gem_list)
|
||||
self.assertEqual({OLD_GEM: [NEW_VERSION, OLD_VERSION]}, gem_list)
|
||||
|
||||
self.run_function('gem.uninstall', [OLD_GEM])
|
||||
self.assertFalse(self.run_function('gem.list', [OLD_GEM]))
|
||||
|
||||
def test_udpate_system(self):
|
||||
def test_update_system(self):
|
||||
'''
|
||||
gem.udpate_system
|
||||
gem.update_system
|
||||
'''
|
||||
ret = self.run_function('gem.update_system')
|
||||
self.assertTrue(ret)
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import pwd
|
||||
import grp
|
||||
import random
|
||||
@ -21,6 +22,8 @@ from salt.utils.pycrypto import gen_hash
|
||||
# Import 3rd-party libs
|
||||
from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def gen_password():
|
||||
'''
|
||||
@ -99,6 +102,7 @@ class AuthTest(ShellCase):
|
||||
cmd = ('-a pam "*" test.ping '
|
||||
'--username {0} --password {1}'.format(self.userA, password))
|
||||
resp = self.run_salt(cmd)
|
||||
log.debug('resp = %s', resp)
|
||||
self.assertTrue(
|
||||
'minion:' in resp
|
||||
)
|
||||
|
@ -11,15 +11,11 @@ from tests.support.unit import skipIf, TestCase
|
||||
from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch
|
||||
|
||||
# Import Salt libs
|
||||
import salt.config
|
||||
import salt.loader
|
||||
import salt.utils.boto
|
||||
from salt.ext import six
|
||||
from salt.utils.versions import LooseVersion
|
||||
import salt.states.boto_vpc as boto_vpc
|
||||
|
||||
# Import test suite libs
|
||||
|
||||
# pylint: disable=import-error,unused-import
|
||||
from tests.unit.modules.test_boto_vpc import BotoVpcTestCaseMixin
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
# Import Python libs
|
||||
from __future__ import absolute_import
|
||||
import errno
|
||||
import os
|
||||
|
||||
# Import Salt Testing libs
|
||||
from tests.support.mock import patch, Mock
|
||||
@ -38,7 +39,7 @@ class FileclientTestCase(TestCase):
|
||||
for exists in range(2):
|
||||
with patch('os.makedirs', self._fake_makedir()):
|
||||
with Client(self.opts)._cache_loc('testfile') as c_ref_itr:
|
||||
assert c_ref_itr == '/__test__/files/base/testfile'
|
||||
assert c_ref_itr == os.sep + os.sep.join(['__test__', 'files', 'base', 'testfile'])
|
||||
|
||||
def test_cache_raises_exception_on_non_eexist_ioerror(self):
|
||||
'''
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
# Import python libs
|
||||
from __future__ import absolute_import
|
||||
import textwrap
|
||||
|
||||
# Import Salt Libs
|
||||
from yaml.constructor import ConstructorError
|
||||
@ -36,12 +37,11 @@ class YamlLoaderTestCase(TestCase):
|
||||
'''
|
||||
Test parsing an ordinary path
|
||||
'''
|
||||
|
||||
self.assertEqual(
|
||||
self._render_yaml(b'''
|
||||
p1:
|
||||
- alpha
|
||||
- beta'''),
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1:
|
||||
- alpha
|
||||
- beta''')),
|
||||
{'p1': ['alpha', 'beta']}
|
||||
)
|
||||
|
||||
@ -49,38 +49,37 @@ p1:
|
||||
'''
|
||||
Test YAML anchors
|
||||
'''
|
||||
|
||||
# Simple merge test
|
||||
self.assertEqual(
|
||||
self._render_yaml(b'''
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v2: beta'''),
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v2: beta''')),
|
||||
{'p1': {'v1': 'alpha'}, 'p2': {'v1': 'alpha', 'v2': 'beta'}}
|
||||
)
|
||||
|
||||
# Test that keys/nodes are overwritten
|
||||
self.assertEqual(
|
||||
self._render_yaml(b'''
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v1: new_alpha'''),
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v1: new_alpha''')),
|
||||
{'p1': {'v1': 'alpha'}, 'p2': {'v1': 'new_alpha'}}
|
||||
)
|
||||
|
||||
# Test merging of lists
|
||||
self.assertEqual(
|
||||
self._render_yaml(b'''
|
||||
p1: &p1
|
||||
v1: &v1
|
||||
- t1
|
||||
- t2
|
||||
p2:
|
||||
v2: *v1'''),
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1: &p1
|
||||
v1: &v1
|
||||
- t1
|
||||
- t2
|
||||
p2:
|
||||
v2: *v1''')),
|
||||
{"p2": {"v2": ["t1", "t2"]}, "p1": {"v1": ["t1", "t2"]}}
|
||||
)
|
||||
|
||||
@ -89,15 +88,27 @@ p2:
|
||||
Test that duplicates still throw an error
|
||||
'''
|
||||
with self.assertRaises(ConstructorError):
|
||||
self._render_yaml(b'''
|
||||
p1: alpha
|
||||
p1: beta''')
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1: alpha
|
||||
p1: beta'''))
|
||||
|
||||
with self.assertRaises(ConstructorError):
|
||||
self._render_yaml(b'''
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v2: beta
|
||||
v2: betabeta''')
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
p1: &p1
|
||||
v1: alpha
|
||||
p2:
|
||||
<<: *p1
|
||||
v2: beta
|
||||
v2: betabeta'''))
|
||||
|
||||
def test_yaml_with_unicode_literals(self):
|
||||
'''
|
||||
Test proper loading of unicode literals
|
||||
'''
|
||||
self.assertEqual(
|
||||
self._render_yaml(textwrap.dedent('''\
|
||||
foo:
|
||||
a: Д
|
||||
b: {'a': u'\\u0414'}''')),
|
||||
{'foo': {'a': u'\u0414', 'b': {'a': u'\u0414'}}}
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user