Skip to content

Commit 4b674ec

Browse files
author
Todd DeLuca
committed
Merge branch 'filter-output' of https://github.com/mmabey/python-vagrant into mmabey-filter-output
Conflicts: tests/test_vagrant.py
2 parents 9e0121f + 9f3b70d commit 4b674ec

File tree

2 files changed

+154
-7
lines changed

2 files changed

+154
-7
lines changed

tests/test_vagrant.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,41 @@ 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_output_filter():
549+
"""
550+
Test output filters with good input.
551+
"""
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)
559+
560+
assert matches['string_pat'] == TEST_BOX_NAME
561+
562+
assert matches['compiled_pat'] is not None # Usually '2222', but may vary depending on the test env
563+
564+
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
580+
581+
547582
def test_make_file_cm():
548583
filename = os.path.join(TD, 'test.log')
549584
if os.path.exists(filename):

vagrant/__init__.py

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ 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, output_filter=None):
304304
'''
305305
Launch the Vagrant box.
306306
vm_name=None: name of VM.
@@ -323,16 +323,22 @@ def up(self, no_provision=False, provider=None, vm_name=None,
323323
no_provision_arg = '--no-provision' if no_provision else None
324324
provision_arg = None if provision is None else '--provision' if provision else '--no-provision'
325325

326-
self._call_vagrant_command(['up', vm_name, no_provision_arg,
327-
provision_arg, provider_arg,
328-
prov_with_arg, providers_arg])
326+
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)
330+
else:
331+
self._call_vagrant_command(args)
332+
329333
try:
330334
self.conf(vm_name=vm_name) # cache configuration
331335
except subprocess.CalledProcessError:
332336
# in multi-VM environments, up() can be used to start all VMs,
333337
# however vm_name is required for conf() or ssh_config().
334338
pass
335339

340+
return filter_results
341+
336342
def provision(self, vm_name=None, provision_with=None):
337343
'''
338344
Runs the provisioners defined in the Vagrantfile.
@@ -345,7 +351,8 @@ 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+
output_filter=None):
349356
'''
350357
Quoting from Vagrant docs:
351358
> The equivalent of running a halt followed by an up.
@@ -362,8 +369,15 @@ def reload(self, vm_name=None, provision=None, provision_with=None):
362369
prov_with_arg = None if provision_with is None else '--provision-with'
363370
providers_arg = None if provision_with is None else ','.join(provision_with)
364371
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])
372+
373+
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)
377+
else:
378+
self._call_vagrant_command(args)
379+
380+
return filter_results
367381

368382
def suspend(self, vm_name=None):
369383
'''
@@ -954,6 +968,104 @@ def _run_vagrant_command(self, args):
954968
return compat.decode(subprocess.check_output(command, cwd=self.root,
955969
env=self.env, stderr=err_fh))
956970

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.
982+
983+
: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
990+
"""
991+
assert isinstance(output_filter, dict)
992+
py3 = sys.version_info > (3, 0)
993+
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+
1018+
# Make subprocess command
1019+
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:
1023+
sp_args = dict(args=command, cwd=self.root, env=self.env,
1024+
stdout=subprocess.PIPE, stderr=err_fh, bufsize=1)
1025+
1026+
# Parse command output for the specified filters. Method used depends on version of Python.
1027+
# See http://stackoverflow.com/questions/2715847/python-read-streaming-input-from-subprocess-communicate#17698359
1028+
if not py3: # Python 2.x
1029+
p = subprocess.Popen(**sp_args)
1030+
with p.stdout:
1031+
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)
1047+
p.wait()
1048+
else: # Python 3.0+
1049+
with subprocess.Popen(**sp_args) as p:
1050+
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
1068+
9571069

9581070
class SandboxVagrant(Vagrant):
9591071
'''

0 commit comments

Comments
 (0)