Skip to content

Commit 83b26f9

Browse files
author
Todd DeLuca
committed
Merge branch 'mmabey-filter-output'
2 parents 9e0121f + 202ded5 commit 83b26f9

File tree

3 files changed

+104
-21
lines changed

3 files changed

+104
-21
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
This document lists the changes (and individuals who contributed to those
55
changes) for each release of python-vagrant.
66

7-
## To be released
7+
## 0.5.15
88

99
- Pull Request #54: Create ssh() method to run shell commands in a VM
1010
Authors: Parker Thompson (https://github.com/mothran) and Todd DeLuca
1111
(https://github.com/todddeluca)
12+
- Pull Request #56: Return generator for `up` and `reload` output lines to
13+
avoid having entire output in memory.
14+
Authors: mmabey (https://github.com/mmabey) and Todd DeLuca
15+
(https://github.com/todddeluca)
16+
1217

1318
## 0.5.14
1419

tests/test_vagrant.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import sys
2424
import tempfile
2525
import time
26-
from nose.tools import eq_, ok_, with_setup
26+
from nose.tools import eq_, ok_, with_setup, assert_raises
2727

2828
import vagrant
2929
from vagrant import compat
@@ -544,6 +544,34 @@ def test_ssh_command_multivm():
544544
assert output.strip() == 'I like your hat'
545545

546546

547+
@with_setup(make_setup_vm(), teardown_vm)
548+
def test_streaming_output():
549+
"""
550+
Test streaming output of up or reload.
551+
"""
552+
test_string = 'Waiting for machine to boot.'
553+
v = vagrant.Vagrant(TD)
554+
555+
with assert_raises(subprocess.CalledProcessError):
556+
v.up(vm_name='incorrect-name')
557+
558+
streaming_up = False
559+
for line in v.up(stream_output=True):
560+
print('output line:', line)
561+
if test_string in line:
562+
streaming_up = True
563+
564+
assert streaming_up
565+
566+
streaming_reload = False
567+
for line in v.reload(stream_output=True):
568+
print('output line:', line)
569+
if test_string in line:
570+
streaming_reload = True
571+
572+
assert streaming_reload
573+
574+
547575
def test_make_file_cm():
548576
filename = os.path.join(TD, 'test.log')
549577
if os.path.exists(filename):

vagrant/__init__.py

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
# python package version
2828
# should match r"^__version__ = '(?P<version>[^']+)'$" for setup.py
29-
__version__ = '0.5.14'
29+
__version__ = '0.5.15'
3030

3131

3232
log = logging.getLogger(__name__)
@@ -300,17 +300,24 @@ def init(self, box_name=None, box_url=None):
300300
self._call_vagrant_command(['init', box_name, box_url])
301301

302302
def up(self, no_provision=False, provider=None, vm_name=None,
303-
provision=None, provision_with=None):
303+
provision=None, provision_with=None, stream_output=False):
304304
'''
305-
Launch the Vagrant box.
305+
Invoke `vagrant up` to start a box or boxes, possibly streaming the
306+
command output.
306307
vm_name=None: name of VM.
307308
provision_with: optional list of provisioners to enable.
308309
provider: Back the machine with a specific provider
309310
no_provision: if True, disable provisioning. Same as 'provision=False'.
310311
provision: optional boolean. Enable or disable provisioning. Default
311312
behavior is to use the underlying vagrant default.
313+
stream_output: if True, return a generator that yields each line of the
314+
output of running the command. Consume the generator or the
315+
subprocess might hang. if False, None is returned and the command
316+
is run to completion without streaming the output. Defaults to
317+
False.
312318
Note: If provision and no_provision are not None, no_provision will be
313319
ignored.
320+
returns: None or a generator yielding lines of output.
314321
'''
315322
provider_arg = '--provider=%s' % provider if provider else None
316323
prov_with_arg = None if provision_with is None else '--provision-with'
@@ -323,15 +330,14 @@ def up(self, no_provision=False, provider=None, vm_name=None,
323330
no_provision_arg = '--no-provision' if no_provision else None
324331
provision_arg = None if provision is None else '--provision' if provision else '--no-provision'
325332

326-
self._call_vagrant_command(['up', vm_name, no_provision_arg,
327-
provision_arg, provider_arg,
328-
prov_with_arg, providers_arg])
329-
try:
330-
self.conf(vm_name=vm_name) # cache configuration
331-
except subprocess.CalledProcessError:
332-
# in multi-VM environments, up() can be used to start all VMs,
333-
# however vm_name is required for conf() or ssh_config().
334-
pass
333+
args = ['up', vm_name, no_provision_arg, provision_arg, provider_arg, prov_with_arg, providers_arg]
334+
if stream_output:
335+
generator = self._stream_vagrant_command(args)
336+
else:
337+
self._call_vagrant_command(args)
338+
339+
self._cached_conf[vm_name] = None # remove cached configuration
340+
return generator if stream_output else None
335341

336342
def provision(self, vm_name=None, provision_with=None):
337343
'''
@@ -345,25 +351,40 @@ def provision(self, vm_name=None, provision_with=None):
345351
self._call_vagrant_command(['provision', vm_name, prov_with_arg,
346352
providers_arg])
347353

348-
def reload(self, vm_name=None, provision=None, provision_with=None):
354+
def reload(self, vm_name=None, provision=None, provision_with=None,
355+
stream_output=False):
349356
'''
350357
Quoting from Vagrant docs:
351358
> The equivalent of running a halt followed by an up.
352-
353-
> This command is usually required for changes made in the Vagrantfile to take effect. After making any modifications to the Vagrantfile, a reload should be called.
354-
355-
> The configured provisioners will not run again, by default. You can force the provisioners to re-run by specifying the --provision flag.
359+
> This command is usually required for changes made in the Vagrantfile
360+
to take effect. After making any modifications to the Vagrantfile, a
361+
reload should be called.
362+
> The configured provisioners will not run again, by default. You can
363+
force the provisioners to re-run by specifying the --provision flag.
356364
357365
provision: optional boolean. Enable or disable provisioning. Default
358366
behavior is to use the underlying vagrant default.
359367
provision_with: optional list of provisioners to enable.
360368
e.g. ['shell', 'chef_solo']
369+
stream_output: if True, return a generator that yields each line of the
370+
output of running the command. Consume the generator or the
371+
subprocess might hang. if False, None is returned and the command
372+
is run to completion without streaming the output. Defaults to
373+
False.
374+
returns: None or a generator yielding lines of output.
361375
'''
362376
prov_with_arg = None if provision_with is None else '--provision-with'
363377
providers_arg = None if provision_with is None else ','.join(provision_with)
364378
provision_arg = None if provision is None else '--provision' if provision else '--no-provision'
365-
self._call_vagrant_command(['reload', vm_name, provision_arg,
366-
prov_with_arg, providers_arg])
379+
380+
args = ['reload', vm_name, provision_arg, prov_with_arg, providers_arg]
381+
if stream_output:
382+
generator = self._stream_vagrant_command(args)
383+
else:
384+
self._call_vagrant_command(args)
385+
386+
self._cached_conf[vm_name] = None # remove cached configuration
387+
return generator if stream_output else None
367388

368389
def suspend(self, vm_name=None):
369390
'''
@@ -954,6 +975,35 @@ def _run_vagrant_command(self, args):
954975
return compat.decode(subprocess.check_output(command, cwd=self.root,
955976
env=self.env, stderr=err_fh))
956977

978+
def _stream_vagrant_command(self, args):
979+
"""
980+
Execute a vagrant command, returning a generator of the output lines.
981+
Caller should consume the entire generator to avoid the hanging the
982+
subprocess.
983+
984+
:param args: Arguments for the Vagrant command.
985+
:return: generator that yields each line of the command stdout.
986+
:rtype: generator iterator
987+
"""
988+
py3 = sys.version_info > (3, 0)
989+
990+
# Make subprocess command
991+
command = self._make_vagrant_command(args)
992+
with self.err_cm() as err_fh:
993+
sp_args = dict(args=command, cwd=self.root, env=self.env,
994+
stdout=subprocess.PIPE, stderr=err_fh, bufsize=1)
995+
996+
# Iterate over output lines.
997+
# See http://stackoverflow.com/questions/2715847/python-read-streaming-input-from-subprocess-communicate#17698359
998+
p = subprocess.Popen(**sp_args)
999+
with p.stdout:
1000+
for line in iter(p.stdout.readline, b''):
1001+
yield compat.decode(line) # if PY3 decode bytestrings
1002+
p.wait()
1003+
# Raise CalledProcessError for consistency with _call_vagrant_command
1004+
if p.returncode != 0:
1005+
raise subprocess.CalledProcessError(p.returncode, command)
1006+
9571007

9581008
class SandboxVagrant(Vagrant):
9591009
'''

0 commit comments

Comments
 (0)