Skip to content

Commit d6d5d70

Browse files
author
Todd DeLuca
committed
Return generator of output lines
This change removes the regex filtering of output lines and returns a generator that yields the output lines instead.
1 parent 4b674ec commit d6d5d70

File tree

3 files changed

+62
-126
lines changed

3 files changed

+62
-126
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ changes) for each release of python-vagrant.
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: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -545,38 +545,28 @@ def test_ssh_command_multivm():
545545

546546

547547
@with_setup(make_setup_vm(), teardown_vm)
548-
def test_output_filter():
548+
def test_streaming_output():
549549
"""
550-
Test output filters with good input.
550+
Test streaming output of up or reload.
551551
"""
552-
v = vagrant.Vagrant(TD, out_cm=vagrant.stdout_cm)
553-
of = {'string_pat': {'pat': r"Importing base box '(\S+)'",
554-
'group': 1},
555-
'compiled_pat': {'pat': re.compile(r'22 \(guest\) => (\d+) \(host\)'),
556-
'group': 1}
557-
}
558-
matches = v.up(output_filter=of)
552+
test_string = 'Waiting for machine to boot.'
553+
v = vagrant.Vagrant(TD)
559554

560-
assert matches['string_pat'] == TEST_BOX_NAME
555+
streaming_up = False
556+
for line in v.up(stream_output=True):
557+
print('output line:', line)
558+
if test_string in line:
559+
streaming_up = True
561560

562-
assert matches['compiled_pat'] is not None # Usually '2222', but may vary depending on the test env
561+
assert streaming_up
563562

563+
streaming_reload = False
564+
for line in v.reload(stream_output=True):
565+
print('output line:', line)
566+
if test_string in line:
567+
streaming_reload = True
564568

565-
@with_setup(make_setup_vm(), teardown_vm)
566-
def test_output_filter_fail():
567-
"""
568-
Test output filters with bad input.
569-
"""
570-
v = vagrant.Vagrant(TD, out_cm=vagrant.stdout_cm)
571-
of = {'failing_test': {'pat': ['invalid', 'regex', 'pattern'],
572-
'group': 0}
573-
}
574-
try:
575-
v.up(output_filter=of)
576-
except TypeError:
577-
pass
578-
else:
579-
assert False
569+
assert streaming_reload
580570

581571

582572
def test_make_file_cm():

vagrant/__init__.py

Lines changed: 41 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -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, output_filter=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'
@@ -324,20 +331,13 @@ def up(self, no_provision=False, provider=None, vm_name=None,
324331
provision_arg = None if provision is None else '--provision' if provision else '--no-provision'
325332

326333
args = ['up', vm_name, no_provision_arg, provision_arg, provider_arg, prov_with_arg, providers_arg]
327-
filter_results = None
328-
if isinstance(output_filter, dict):
329-
filter_results = self._filter_vagrant_command(args, output_filter)
334+
if stream_output:
335+
generator = self._stream_vagrant_command(args)
330336
else:
331337
self._call_vagrant_command(args)
332338

333-
try:
334-
self.conf(vm_name=vm_name) # cache configuration
335-
except subprocess.CalledProcessError:
336-
# in multi-VM environments, up() can be used to start all VMs,
337-
# however vm_name is required for conf() or ssh_config().
338-
pass
339-
340-
return filter_results
339+
self._cached_conf[vm_name] = None # remove cached configuration
340+
return generator if stream_output else None
341341

342342
def provision(self, vm_name=None, provision_with=None):
343343
'''
@@ -352,32 +352,39 @@ def provision(self, vm_name=None, provision_with=None):
352352
providers_arg])
353353

354354
def reload(self, vm_name=None, provision=None, provision_with=None,
355-
output_filter=None):
355+
stream_output=False):
356356
'''
357357
Quoting from Vagrant docs:
358358
> The equivalent of running a halt followed by an up.
359-
360-
> 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.
361-
362-
> 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.
363364
364365
provision: optional boolean. Enable or disable provisioning. Default
365366
behavior is to use the underlying vagrant default.
366367
provision_with: optional list of provisioners to enable.
367368
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.
368375
'''
369376
prov_with_arg = None if provision_with is None else '--provision-with'
370377
providers_arg = None if provision_with is None else ','.join(provision_with)
371378
provision_arg = None if provision is None else '--provision' if provision else '--no-provision'
372379

373380
args = ['reload', vm_name, provision_arg, prov_with_arg, providers_arg]
374-
filter_results = None
375-
if isinstance(output_filter, dict):
376-
filter_results = self._filter_vagrant_command(args, output_filter)
381+
if stream_output:
382+
generator = self._stream_vagrant_command(args)
377383
else:
378384
self._call_vagrant_command(args)
379385

380-
return filter_results
386+
self._cached_conf[vm_name] = None # remove cached configuration
387+
return generator if stream_output else None
381388

382389
def suspend(self, vm_name=None):
383390
'''
@@ -968,103 +975,37 @@ def _run_vagrant_command(self, args):
968975
return compat.decode(subprocess.check_output(command, cwd=self.root,
969976
env=self.env, stderr=err_fh))
970977

971-
def _filter_vagrant_command(self, args, output_filter):
972-
"""Execute the Vagrant command, return matches to the output filters.
973-
974-
Output filter must have the following form:
975-
{
976-
'filter_name': {'pat': r'regex pattern',
977-
'group': <int group number to return>},
978-
...
979-
}
980-
981-
The `group` key is actually optional, but defaults to 1 when omitted.
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.
982983
983984
:param args: Arguments for the Vagrant command.
984-
:param output_filter: Dictionary of output filters.
985-
:type output_filter: dict
986-
:return: Dictionary of results with the following form:
987-
{'filter_name': 'matching result', ...}. If no match is found, the
988-
value will be None.
989-
:rtype: dict
985+
:return: generator that yields each line of the command stdout.
986+
:rtype: generator iterator
990987
"""
991-
assert isinstance(output_filter, dict)
992988
py3 = sys.version_info > (3, 0)
993989

994-
# Create dictionary that will store the results from the filters
995-
filter_results = dict.fromkeys(output_filter)
996-
997-
for f in output_filter.keys():
998-
# Check the filter values, compile as regular expression objects if necessary
999-
if not isinstance(output_filter[f]['pat'], type(re.compile(''))):
1000-
if py3 and isinstance(output_filter[f]['pat'], str):
1001-
# In Python 3, the output from subprocess will be bytes, so the pattern has to be bytes as well
1002-
# for it to match.
1003-
output_filter[f]['pat'] = output_filter[f]['pat'].encode()
1004-
try:
1005-
output_filter[f]['pat'] = re.compile(output_filter[f]['pat'])
1006-
except TypeError:
1007-
raise TypeError('Output filters must have either a compiled regular expression or a regular '
1008-
'expression string stored in key ["pat"], got: {}'.
1009-
format(type(output_filter[f]['pat'])))
1010-
1011-
# Check that the group is an int, set to 1 if omitted
1012-
if output_filter[f].get('group') is None:
1013-
output_filter[f]['group'] = 1
1014-
elif not isinstance(output_filter[f]['group'], int):
1015-
raise TypeError('For output filters, value for key `group` must be an int, got {}'.
1016-
format(type(output_filter[f]['group'])))
1017-
1018990
# Make subprocess command
1019991
command = self._make_vagrant_command(args)
1020-
1021-
# Don't override the user-specified output and error context managers
1022-
with self.out_cm() as out_fh, self.err_cm() as err_fh:
992+
with self.err_cm() as err_fh:
1023993
sp_args = dict(args=command, cwd=self.root, env=self.env,
1024994
stdout=subprocess.PIPE, stderr=err_fh, bufsize=1)
1025995

1026-
# Parse command output for the specified filters. Method used depends on version of Python.
996+
# Method to iterate over output lines depends on version of Python.
1027997
# See http://stackoverflow.com/questions/2715847/python-read-streaming-input-from-subprocess-communicate#17698359
1028998
if not py3: # Python 2.x
1029999
p = subprocess.Popen(**sp_args)
10301000
with p.stdout:
10311001
for line in iter(p.stdout.readline, b''):
1032-
pop_key = None
1033-
for f in output_filter.keys():
1034-
m = re.search(output_filter[f]['pat'], line)
1035-
if m:
1036-
try:
1037-
filter_results[f] = m.group(output_filter[f]['group'])
1038-
except IndexError:
1039-
# User must have not included parenthases in their pattern
1040-
pass
1041-
# No need to search for this pattern again in future lines
1042-
pop_key = f
1043-
break
1044-
if pop_key:
1045-
output_filter.pop(pop_key)
1046-
out_fh.write(line)
1002+
yield line
10471003
p.wait()
10481004
else: # Python 3.0+
10491005
with subprocess.Popen(**sp_args) as p:
10501006
for line in p.stdout:
1051-
pop_key = None
1052-
for f in output_filter.keys():
1053-
m = re.search(output_filter[f]['pat'], line)
1054-
if m:
1055-
try:
1056-
filter_results[f] = m.group(output_filter[f]['group'])
1057-
except IndexError:
1058-
# User must have not included parenthases in their pattern
1059-
pass
1060-
# No need to search for this pattern again in future lines
1061-
pop_key = f
1062-
break
1063-
if pop_key:
1064-
output_filter.pop(pop_key)
1065-
out_fh.write(line)
1066-
1067-
return filter_results
1007+
yield line
1008+
10681009

10691010

10701011
class SandboxVagrant(Vagrant):

0 commit comments

Comments
 (0)