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

This commit is contained in:
Mickey Malone 2013-10-08 05:54:08 -05:00
commit 06f12b0664
16 changed files with 363 additions and 67 deletions

View File

@ -119,10 +119,10 @@
# to reconnect immediately, if the socket is disconnected (for example if
# the master processes are restarted). In large setups this will have all
# minions reconnect immediately which might flood the master (the ZeroMQ-default
# is usually a 100ms delay). To prevent this, these three recon_* settings
# is usually a 100ms delay). To prevent this, these three recon_* settings
# can be used.
#
# recon_default: the interval in milliseconds that the socket should wait before
# recon_default: the interval in milliseconds that the socket should wait before
# trying to reconnect to the master (100ms = 1 second)
#
# recon_max: the maximum time a socket should wait. each interval the time to wait
@ -136,14 +136,14 @@
# reconnect 5: value from previous interval * 2
# reconnect x: if value >= recon_max, it starts again with recon_default
#
# recon_randomize: generate a random wait time on minion start. The wait time will
# be a random value between recon_default and recon_default +
# recon_max. Having all minions reconnect with the same recon_default
# and recon_max value kind of defeats the purpose of being able to
# change these settings. If all minions have the same values and your
# setup is quite large (several thousand minions), they will still
# recon_randomize: generate a random wait time on minion start. The wait time will
# be a random value between recon_default and recon_default +
# recon_max. Having all minions reconnect with the same recon_default
# and recon_max value kind of defeats the purpose of being able to
# change these settings. If all minions have the same values and your
# setup is quite large (several thousand minions), they will still
# flood the master. The desired behaviour is to have timeframe within
# all minions try to reconnect.
# all minions try to reconnect.
# Example on how to use these settings:
# The goal: have all minions reconnect within a 60 second timeframe on a disconnect
@ -155,9 +155,9 @@
#
# Each minion will have a randomized reconnect value between 'recon_default'
# and 'recon_default + recon_max', which in this example means between 1000ms
# 60000ms (or between 1 and 60 seconds). The generated random-value will be
# doubled after each attempt to reconnect. Lets say the generated random
# value is 11 seconds (or 11000ms).
# 60000ms (or between 1 and 60 seconds). The generated random-value will be
# doubled after each attempt to reconnect. Lets say the generated random
# value is 11 seconds (or 11000ms).
#
# reconnect 1: wait 11 seconds
# reconnect 2: wait 22 seconds
@ -238,6 +238,13 @@
# Enable Cython modules searching and loading. (Default: False)
#cython_enable: False
#
#
#
# Specify a max size (in bytes) for modules on import
# this feature is currently only supported on *nix OSs and requires psutil
# modules_max_memory: -1
##### State Management Settings #####
###########################################

View File

@ -17,6 +17,15 @@ Environments
directories. Environments can be made to be self-contained or state
trees can be made to bleed through environments.
.. note::
Environments in Salt are very flexible, this section defines how the top
file can be used to define what ststates from what environments are to be
used fro specific minions.
If the intent is to bind minions to specific environments, then the
`environment` option can be set in the minion configuration file.
The environments in the top file corresponds with the environments defined in
the :conf_master:`file_roots` variable. In a simple, single environment setup
you only have the ``base`` environment, and therefore only one state tree. Here

View File

@ -155,6 +155,7 @@ class Authorize(object):
def __init__(self, opts, load, loadauth=None):
self.opts = salt.config.master_config(opts['conf_file'])
self.load = load
self.ckminions = salt.utils.minions.CkMinions(opts)
if loadauth is None:
self.loadauth = LoadAuth(opts)
else:
@ -171,6 +172,112 @@ class Authorize(object):
auth_data.append(getattr(self.loadauth.auth)())
return auth_data
def token(self, adata, load):
'''
Determine if token auth is valid and yield the adata
'''
try:
token = self.loadauth.get_tok(load['token'])
except Exception as exc:
log.error(
'Exception occurred when generating auth token: {0}'.format(
exc
)
)
yield {}
if not token:
log.warning('Authentication failure of type "token" occurred.')
yield {}
for sub_auth in adata:
if token['eauth'] not in adata:
continue
if not ((token['name'] in adata[token['eauth']]) |
('*' in adata[token['eauth']])):
continue
yield {'sub_auth': sub_auth, 'token': token}
yield {}
def eauth(self, adata, load):
'''
Determine if the given eauth is valid and yield the adata
'''
for sub_auth in adata:
if load['eauth'] not in sub_auth:
continue
try:
name = self.loadauth.load_name(load)
if not ((name in sub_auth[load['eauth']]) |
('*' in sub_auth[load['eauth']])):
continue
if not self.loadauth.time_auth(load):
continue
except Exception as exc:
log.error(
'Exception occurred while authenticating: {0}'.format(exc)
)
continue
yield {'sub_auth': sub_auth, 'name': name}
yield {}
def rights_check(self, form, sub_auth, name, load, eauth=None):
'''
Read in the access system to determine if the validated user has
requested rights
'''
if load.get('eauth'):
sub_auth = sub_auth[load['eauth']]
good = self.ckminions.any_check(
form,
sub_auth[name] if name in sub_auth else sub_auth['*'],
load.get('fun', None),
load.get('tgt', None),
load.get('tgt_type', 'glob'))
if not good:
# Accept find_job so the CLI will function cleanly
if load.get('fun', '') != 'saltutil.find_job':
return good
return good
def rights(self, form, load):
'''
Determine what type of authentication is being requested and pass
authorization
'''
adata = self.auth_data()
if load.get('token', False):
good = False
for sub_auth in self.token(adata, load):
if sub_auth:
if self.rights_check(
form,
sub_auth['sub_auth'],
sub_auth['token']['name'],
load,
sub_auth['token']['eauth']):
good = True
if not good:
log.warning(
'Authentication failure of type "token" occurred.'
)
return False
elif load.get('eauth'):
good = False
for sub_auth in self.eauth(adata, load):
if sub_auth:
if self.rights_check(
form,
sub_auth['sub_auth'],
sub_auth['name'],
load,
load['eauth']):
good = True
if not good:
log.warning(
'Authentication failure of type "eauth" occurred.'
)
return False
return good
class Resolver(object):
'''

View File

@ -33,7 +33,7 @@ RSTR = '_edbc7885e4f9aac9b83b35999b68d015148caf467b78fa39c05f669c0ff89878'
# This shim facilitaites remote salt-call operations
# - Explicitly invokes bourne shell for univeral compatibility
#
#
# 1. Identify a suitable python
# 2. Test for remote salt-call and version if present
# 3. Signal to (re)deploy if missing or out of date
@ -89,6 +89,7 @@ EOF\n'''.format(salt.__version__, RSTR)
log = logging.getLogger(__name__)
class SSH(object):
'''
Create an SSH execution system
@ -430,8 +431,8 @@ class Single(object):
def run(self, deploy_attempted=False):
'''
Execute the routine, the routine can be either:
1. Execute a raw shell command
2. Execute a wrapper func
1. Execute a raw shell command
2. Execute a wrapper func
3. Execute a remote Salt command
If a (re)deploy is needed, then retry the operation after a deploy
@ -566,11 +567,11 @@ class Single(object):
"to be root or use sudo:\n {0}"
errors = [
("sudo: no tty present and no askpass program specified",
"sudo expected a password, NOPASSWD required"),
"sudo expected a password, NOPASSWD required"),
("Python too old",
"salt requires python 2.6 or better on target hosts"),
"salt requires python 2.6 or better on target hosts"),
("sudo: sorry, you must have a tty to run sudo",
"sudo is configured with requiretty"),
"sudo is configured with requiretty"),
("Failed to open log file",
perm_error_fmt.format(stderr)),
("Permission denied:.*/salt",
@ -584,7 +585,6 @@ class Single(object):
return error[1]
return None
def sls_seed(self, mods, env='base', test=None, exclude=None, **kwargs):
'''
Create the seed file for a state.sls run

View File

@ -162,6 +162,7 @@ VALID_OPTS = {
'win_repo': str,
'win_repo_mastercachefile': str,
'win_gitrepos': list,
'modules_max_memory': int,
}
# default configurations
@ -248,12 +249,13 @@ DEFAULT_MINION_OPTS = {
'tcp_keepalive_idle': 300,
'tcp_keepalive_cnt': -1,
'tcp_keepalive_intvl': -1,
'modules_max_memory': -1,
}
DEFAULT_MASTER_OPTS = {
'interface': '0.0.0.0',
'publish_port': '4505',
'pub_hwm': 1,
'pub_hwm': 100,
'auth_mode': 1,
'user': 'root',
'worker_threads': 5,

View File

@ -1865,6 +1865,16 @@ class ClearFuncs(object):
self.event.fire_event(eload, tagify(prefix='auth'))
return ret
def cloud(self, clear_load):
'''
Hook into the salt-cloud libs and execute cloud routines
# NOT HOOKED IN YET
'''
authorize = salt.auth.Authorize(self.opts, clear_load, self.loadauth)
if not authorize.rights('cloud', clear_load):
return False
return True
def runner(self, clear_load):
'''
Send a master control function back to the runner system

View File

@ -36,6 +36,20 @@ try:
except ImportError:
pass
HAS_PSUTIL = False
try:
import psutil
HAS_PSUTIL = True
except ImportError:
pass
HAS_RESOURCE = False
try:
import resource
HAS_RESOURCE = True
except ImportError:
pass
# Import salt libs
from salt.exceptions import (
AuthenticationError, CommandExecutionError, CommandNotFoundError,
@ -504,9 +518,31 @@ class Minion(object):
Return the functions and the returners loaded up from the loader
module
'''
# if this is a *nix system AND modules_max_memory is set, lets enforce
# a memory limit on module imports
# this feature ONLY works on *nix like OSs (resource module doesn't work on windows)
modules_max_memory = False
if self.opts.get('modules_max_memory', -1) > 0 and HAS_PSUTIL and HAS_RESOURCE:
log.debug('modules_max_memory set, enforcing a maximum of {0}'.format(self.opts['modules_max_memory']))
modules_max_memory = True
old_mem_limit = resource.getrlimit(resource.RLIMIT_AS)
rss, vms = psutil.Process(os.getpid()).get_memory_info()
mem_limit = rss + vms + self.opts['modules_max_memory']
resource.setrlimit(resource.RLIMIT_AS, (mem_limit, mem_limit))
elif self.opts.get('modules_max_memory', -1) > 0:
if not HAS_PSUTIL:
log.error('Unable to enforce modules_max_memory because psutil is missing')
if not HAS_RESOURCE:
log.error('Unable to enforce modules_max_memory because resource is missing')
self.opts['grains'] = salt.loader.grains(self.opts)
functions = salt.loader.minion_mods(self.opts)
returners = salt.loader.returners(self.opts, functions)
# we're done, reset the limits!
if modules_max_memory is True:
resource.setrlimit(resource.RLIMIT_AS, old_mem_limit)
return functions, returners
def _fire_master(self, data=None, tag=None, events=None, pretag=None):

View File

@ -553,7 +553,7 @@ def sed(path,
escape_all=False,
negate_match=False):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :py:func:`~salt.modules.file.replace` instead.
Make a simple edit to a file
@ -582,7 +582,7 @@ def sed(path,
negate_match : False
Negate the search command (``!``)
.. versionadded:: 0.17
.. versionadded:: 0.17.0
Forward slashes and single quotes will be escaped automatically in the
``before`` and ``after`` patterns.
@ -627,7 +627,7 @@ def sed_contains(path,
limit='',
flags='g'):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :func:`search` instead.
Return True if the file at ``path`` contains ``text``. Utilizes sed to
@ -673,7 +673,7 @@ def psed(path,
escape_all=False,
multi=False):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :py:func:`~salt.modules.file.replace` instead.
Make a simple edit to a file (pure Python version)
@ -785,7 +785,7 @@ def uncomment(path,
char='#',
backup='.bak'):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :py:func:`~salt.modules.file.replace` instead.
Uncomment specified commented lines in a file
@ -824,7 +824,7 @@ def comment(path,
char='#',
backup='.bak'):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :py:func:`~salt.modules.file.replace` instead.
Comment out specified lines in a file
@ -904,7 +904,7 @@ def replace(path,
'''
Replace occurances of a pattern in a file
.. versionadded:: 0.17
.. versionadded:: 0.17.0
This is a pure Python implementation that wraps Python's :py:func:`~re.sub`.
@ -997,7 +997,7 @@ def search(path,
'''
Search for occurances of a pattern in a file
.. versionadded:: 0.17
.. versionadded:: 0.17.0
Params are identical to :py:func:`~salt.modules.file.replace`.
@ -1057,7 +1057,7 @@ def patch(originalfile, patchfile, options='', dry_run=False):
def contains(path, text):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :func:`search` instead.
Return ``True`` if the file at ``path`` contains ``text``
@ -1084,7 +1084,7 @@ def contains(path, text):
def contains_regex(path, regex, lchar=''):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :func:`search` instead.
Return True if the given regular expression matches on any line in the text
@ -1116,7 +1116,7 @@ def contains_regex(path, regex, lchar=''):
def contains_regex_multiline(path, regex):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :func:`search` instead.
Return True if the given regular expression matches anything in the text
@ -1146,7 +1146,7 @@ def contains_regex_multiline(path, regex):
def contains_glob(path, glob):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :func:`search` instead.
Return True if the given glob matches a string in the named file
@ -1186,7 +1186,23 @@ def append(path, *args):
'''
# Largely inspired by Fabric's contrib.files.append()
with salt.utils.fopen(path, "a") as ofile:
with salt.utils.fopen(path, "r+") as ofile:
# Make sure we have a newline at the end of the file
try:
ofile.seek(-1, os.SEEK_END)
except IOError as exc:
if exc.errno == errno.EINVAL:
# Empty file, simply append lines at the beginning of the file
pass
else:
raise
else:
if ofile.read(1) != '\n':
ofile.seek(0, os.SEEK_END)
ofile.write('\n')
else:
ofile.seek(0, os.SEEK_END)
# Append lines
for line in args:
ofile.write('{0}\n'.format(line))

View File

@ -23,20 +23,25 @@ def _parse_return_code_powershell(string):
'''
return from the input string the return code of the powershell command
'''
regex = re.search(r'ReturnValue\s*: (\d*)', string)
if not regex:
return False
else:
return int(regex.group(1))
def _psrdp(cmd):
'''
Create a Win32_TerminalServiceSetting WMI Object as $RDP and execute the command cmd
returns the STDOUT of the command
Create a Win32_TerminalServiceSetting WMI Object as $RDP and execute the
command cmd returns the STDOUT of the command
'''
rdp = '$RDP = Get-WmiObject -Class Win32_TerminalServiceSetting -Namespace root\\CIMV2\\TerminalServices -Computer . -Authentication 6 -ErrorAction Stop'
return __salt__['cmd.run']('{0} ; {1}'.format(rdp, cmd), shell='powershell')
rdp = ('$RDP = Get-WmiObject -Class Win32_TerminalServiceSetting '
'-Namespace root\\CIMV2\\TerminalServices -Computer . '
'-Authentication 6 -ErrorAction Stop')
return __salt__['cmd.run']('{0} ; {1}'.format(rdp, cmd),
shell='powershell')
def enable():
'''
@ -49,7 +54,8 @@ def enable():
salt '*' rdp.enable
'''
return _parse_return_code_powershell(_psrdp('$RDP.SetAllowTsConnections(1,1)')) == 0
return _parse_return_code_powershell(
_psrdp('$RDP.SetAllowTsConnections(1,1)')) == 0
def disable():
@ -63,7 +69,8 @@ def disable():
salt '*' rdp.disable
'''
return _parse_return_code_powershell(_psrdp('$RDP.SetAllowTsConnections(0,1)')) == 0
return _parse_return_code_powershell(
_psrdp('$RDP.SetAllowTsConnections(0,1)')) == 0
def status():

View File

@ -233,11 +233,6 @@ def highstate(test=None, queue=False, **kwargs):
if conflict:
__context__['retcode'] = 1
return conflict
if not _check_pillar(kwargs):
__context__['retcode'] = 5
err = ['Pillar failed to render with the following messages:']
err += __pillar__['_errors']
return err
opts = copy.copy(__opts__)
if salt.utils.test_mode(test=test, **kwargs):
@ -256,7 +251,8 @@ def highstate(test=None, queue=False, **kwargs):
ret = st_.call_highstate(
exclude=kwargs.get('exclude', []),
cache=kwargs.get('cache', None),
cache_name=kwargs.get('cache_name', 'highstate')
cache_name=kwargs.get('cache_name', 'highstate'),
force=kwargs.get('force', False)
)
finally:
st_.pop_active()

View File

@ -498,9 +498,8 @@ class State(object):
if 'grains' not in opts:
opts['grains'] = salt.loader.grains(opts)
self.opts = opts
self.opts['pillar'] = self.__gather_pillar()
if pillar and isinstance(pillar, dict):
self.opts['pillar'].update(pillar)
self._pillar_override = pillar
self.opts['pillar'] = self._gather_pillar()
self.state_con = {}
self.load_modules()
self.active = set()
@ -509,7 +508,7 @@ class State(object):
self.__run_num = 0
self.jid = jid
def __gather_pillar(self):
def _gather_pillar(self):
'''
Whenever a state run starts, gather the pillar data fresh
'''
@ -518,7 +517,10 @@ class State(object):
self.opts['grains'],
self.opts['id']
)
return pillar.compile_pillar()
ret = pillar.compile_pillar()
if self._pillar_override and isinstance(self._pillar_override, dict):
ret.update(self._pillar_override)
return ret
def _mod_init(self, low):
'''
@ -1524,7 +1526,8 @@ class State(object):
self.pre[tag] = self.call(low)
else:
running[tag] = self.call(low)
self.event(running[tag])
if tag in running:
self.event(running[tag])
return running
def call_high(self, high):
@ -1912,6 +1915,7 @@ class BaseHighState(object):
syncd = self.state.functions['saltutil.sync_all'](list(matches))
if syncd['grains']:
self.opts['grains'] = salt.loader.grains(self.opts)
self.state.opts['pillar'] = self.state._gather_pillar()
self.state.module_refresh()
def render_state(self, sls, env, mods, matches):
@ -2238,7 +2242,19 @@ class BaseHighState(object):
'Error when rendering state with contents: {0}'.format(state)
)
def call_highstate(self, exclude=None, cache=None, cache_name='highstate'):
def _check_pillar(self, force=False):
'''
Check the pillar for errors, refuse to run the state if there are
errors in the pillar and return the pillar errors
'''
if force:
return True
if '_errors' in self.state.opts['pillar']:
return False
return True
def call_highstate(self, exclude=None, cache=None, cache_name='highstate',
force=False):
'''
Run the sequence to execute the salt highstate for this minion
'''
@ -2276,15 +2292,19 @@ class BaseHighState(object):
ret[tag_name]['comment'] = msg
return ret
self.load_dynamic(matches)
high, errors = self.render_highstate(matches)
if exclude:
if isinstance(exclude, str):
exclude = exclude.split(',')
if '__exclude__' in high:
high['__exclude__'].extend(exclude)
else:
high['__exclude__'] = exclude
err += errors
if not self._check_pillar(force):
err += ['Pillar failed to render with the following messages:']
err += self.state.opts['pillar']['_errors']
else:
high, errors = self.render_highstate(matches)
if exclude:
if isinstance(exclude, str):
exclude = exclude.split(',')
if '__exclude__' in high:
high['__exclude__'].extend(exclude)
else:
high['__exclude__'] = exclude
err += errors
if err:
return err
if not high:

View File

@ -999,7 +999,7 @@ def managed(name,
file of any kind. Ignores hashes and does not use a templating engine.
contents_pillar
.. versionadded:: 0.17
.. versionadded:: 0.17.0
Operates like ``contents``, but draws from a value stored in pillar,
using the pillar path syntax used in :mod:`pillar.get
@ -1739,7 +1739,7 @@ def replace(name,
'''
Maintain an edit in a file
.. versionadded:: 0.17
.. versionadded:: 0.17.0
Params are identical to :py:func:`~salt.modules.file.replace`.
@ -1779,7 +1779,7 @@ def sed(name,
flags='g',
negate_match=False):
'''
.. deprecated:: 0.17
.. deprecated:: 0.17.0
Use :py:func:`~salt.states.file.replace` instead.
Maintain a simple edit to a file
@ -1808,7 +1808,7 @@ def sed(name,
negate_match : False
Negate the search command (``!``)
.. versionadded:: 0.17
.. versionadded:: 0.17.0
Usage::

View File

@ -18,6 +18,7 @@ import yaml
# Import salt libs
import salt
import salt.fileclient
from salt.utils.odict import OrderedDict
log = logging.getLogger(__name__)
@ -27,6 +28,15 @@ __all__ = [
]
# To dump OrderedDict objects as regular dicts. Used by the yaml
# template filter.
class OrderedDictDumper(yaml.Dumper):
pass
yaml.add_representer(OrderedDict,
yaml.representer.SafeRepresenter.represent_dict,
Dumper=OrderedDictDumper)
class SaltCacheLoader(BaseLoader):
'''
A special jinja Template Loader for salt.
@ -223,7 +233,8 @@ class SerializerExtension(Extension, object):
return Markup(json.dumps(value, sort_keys=True).strip())
def format_yaml(self, value):
return Markup(yaml.dump(value, default_flow_style=True).strip())
return Markup(yaml.dump(value, default_flow_style=True,
Dumper=OrderedDictDumper).strip())
def load_yaml(self, value):
if isinstance(value, TemplateModule):

View File

@ -393,6 +393,21 @@ class CkMinions(object):
log.error('Invalid regular expression: {0}'.format(regex))
return all(vals)
def any_auth(self, form, auth_list, fun, tgt=None, tgt_type='glob'):
'''
Read in the form and determine which auth check routine to execute
'''
if form == 'publish':
return self.auth_check(
auth_list,
fun,
tgt,
tgt_type)
return self.spec_check(
auth_list,
fun,
form)
def auth_check(self, auth_list, funs, tgt, tgt_type='glob'):
'''
Returns a bool which defines if the requested function is authorized.
@ -490,3 +505,34 @@ class CkMinions(object):
if self.match_check(regex, fun):
return True
return False
def spec_check(self, auth_list, fun, form):
'''
Check special API permissions
'''
comps = fun.split('.')
if len(comps) != 2:
return False
mod = comps[0]
fun = comps[1]
for ind in auth_list:
if isinstance(ind, str):
if ind.startswith('@') and ind[1:] == mod:
return True
if ind == '@{0}'.format(form):
return True
if ind == '@{0}s'.format(form):
return True
elif isinstance(ind, dict):
if len(ind) != 1:
continue
valid = ind.keys()[0]
if valid.startswith('@') and valid[1:] == mod:
if isinstance(ind[valid], str):
if self.match_check(ind[valid], fun):
return True
elif isinstance(ind[valid], list):
for regex in ind[valid]:
if self.match_check(regex, fun):
return True
return False

View File

@ -1599,6 +1599,10 @@ class SaltSSHOptionParser(OptionParser, ConfigDirMixIn, MergeConfigMixIn,
'initial deployment of keys very fast and easy')
def _mixin_after_parsed(self):
if not self.args:
self.print_help()
self.exit(1)
if self.options.list:
if ',' in self.args[0]:
self.config['tgt'] = self.args[0].split(',')

View File

@ -121,6 +121,31 @@ class FileModuleTestCase(TestCase):
newfile.read()
)
def test_append_newline_at_eof(self):
'''
Check that file.append works consistently on files with and without
newlines at end of file.
'''
# File ending with a newline
with tempfile.NamedTemporaryFile() as tfile:
tfile.write('foo\n')
tfile.flush()
filemod.append(tfile.name, 'bar')
with open(tfile.name) as tfile2:
self.assertEqual(tfile2.read(), 'foo\nbar\n')
# File not ending with a newline
with tempfile.NamedTemporaryFile() as tfile:
tfile.write('foo')
tfile.flush()
filemod.append(tfile.name, 'bar')
with open(tfile.name) as tfile2:
self.assertEqual(tfile2.read(), 'foo\nbar\n')
# A newline should not be added in empty files
with tempfile.NamedTemporaryFile() as tfile:
filemod.append(tfile.name, 'bar')
with open(tfile.name) as tfile2:
self.assertEqual(tfile2.read(), 'bar\n')
if __name__ == '__main__':
from integration import run_tests