Added template rendering to shell_command component (#2268)
* Added template rendering to `shell_command` component * Security upgrades to template rendering in shell_command. * Added new unit tests for shell_command templates. Better failure when template is invalid in shell_commandpull/2326/head
parent
b646accf87
commit
2882f05f2c
|
@ -9,6 +9,8 @@ import subprocess
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'shell_command'
|
||||
|
@ -30,14 +32,38 @@ def setup(hass, config):
|
|||
|
||||
def service_handler(call):
|
||||
"""Execute a shell command service."""
|
||||
cmd = conf[call.service]
|
||||
cmd, shell = _parse_command(hass, cmd, call.data)
|
||||
if cmd is None:
|
||||
return
|
||||
try:
|
||||
subprocess.call(conf[call.service], shell=True,
|
||||
subprocess.call(cmd, shell=shell,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.exception('Error running command')
|
||||
_LOGGER.exception('Error running command: %s', cmd)
|
||||
|
||||
for name in conf.keys():
|
||||
hass.services.register(DOMAIN, name, service_handler,
|
||||
schema=SHELL_COMMAND_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def _parse_command(hass, cmd, variables):
|
||||
"""Parse command and fill in any template arguments if necessary."""
|
||||
cmds = cmd.split()
|
||||
prog = cmds[0]
|
||||
args = ' '.join(cmds[1:])
|
||||
try:
|
||||
rendered_args = template.render(hass, args, variables=variables)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.exception('Error rendering command template: %s', ex)
|
||||
return None, None
|
||||
if rendered_args == args:
|
||||
# no template used. default behavior
|
||||
shell = True
|
||||
else:
|
||||
# template used. Must break into list and use shell=False for security
|
||||
cmd = [prog] + rendered_args.split()
|
||||
shell = False
|
||||
return cmd, shell
|
||||
|
|
|
@ -51,6 +51,30 @@ class TestShellCommand(unittest.TestCase):
|
|||
}
|
||||
})
|
||||
|
||||
def test_template_render_no_template(self):
|
||||
"""Ensure shell_commands without templates get rendered properly."""
|
||||
cmd, shell = shell_command._parse_command(self.hass, 'ls /bin', {})
|
||||
self.assertTrue(shell)
|
||||
self.assertEqual(cmd, 'ls /bin')
|
||||
|
||||
def test_template_render(self):
|
||||
"""Ensure shell_commands with templates get rendered properly."""
|
||||
self.hass.states.set('sensor.test_state', 'Works')
|
||||
cmd, shell = shell_command._parse_command(
|
||||
self.hass,
|
||||
'ls /bin {{ states.sensor.test_state.state }}', {}
|
||||
)
|
||||
self.assertFalse(shell, False)
|
||||
self.assertEqual(cmd[-1], 'Works')
|
||||
|
||||
def test_invalid_template_fails(self):
|
||||
"""Test that shell_commands with invalid templates fail."""
|
||||
cmd, _shell = shell_command._parse_command(
|
||||
self.hass,
|
||||
'ls /bin {{ states. .test_state.state }}', {}
|
||||
)
|
||||
self.assertEqual(cmd, None)
|
||||
|
||||
@patch('homeassistant.components.shell_command.subprocess.call',
|
||||
side_effect=SubprocessError)
|
||||
@patch('homeassistant.components.shell_command._LOGGER.error')
|
||||
|
|
Loading…
Reference in New Issue