diff --git a/f5/bigip/tm/util/Bash.py b/f5/bigip/tm/util/Bash.py new file mode 100644 index 000000000..64327a020 --- /dev/null +++ b/f5/bigip/tm/util/Bash.py @@ -0,0 +1,70 @@ +# coding=utf-8 +# +# Copyright 2016 F5 Networks Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +"""BIG-IP® utility module + +REST URI + ``http://localhost/mgmt/tm/util/bash`` + +GUI Path + N/A + +REST Kind + ``tm:util:bash:*`` +""" + +from f5.bigip.mixins import CommandExecutionMixin +from f5.bigip.resource import UnnamedResource +from f5.utils.util_exceptions import UtilError + + +class Bash(UnnamedResource, CommandExecutionMixin): + """BIG-IP® utility command + + .. note:: + + This is an unnamed resource so it has not ~Partition~Name pattern + at the end of its URI. + """ + + def __init__(self, util): + super(Bash, self).__init__(util) + self._meta_data['required_command_parameters'].update(('utilCmdArgs',)) + self._meta_data['required_json_kind'] = 'tm:util:bash:runstate' + self._meta_data['allowed_commands'].append('run') + + def _exec_cmd(self, command, **kwargs): + kwargs['command'] = command + self._check_exclusive_parameters(**kwargs) + requests_params = self._handle_requests_params(kwargs) + self._check_command_parameters(**kwargs) + + if not kwargs['utilCmdArgs'].startswith("-c"): + raise UtilError( + 'Required format is "-c "') + + session = self._meta_data['bigip']._meta_data['icr_session'] + response = session.post( + self._meta_data['uri'], json=kwargs, **requests_params) + self._local_update(response.json()) + + if 'commandResult' in self.__dict__: + if self.commandResult.startswith('/bin/bash'): + raise UtilError('%s' % self.commandResult.split(' ', 1)[1]) + else: + return self + else: + return self diff --git a/f5/bigip/tm/util/__init__.py b/f5/bigip/tm/util/__init__.py index a5cb6436f..1f28202fe 100644 --- a/f5/bigip/tm/util/__init__.py +++ b/f5/bigip/tm/util/__init__.py @@ -28,6 +28,7 @@ """ from f5.bigip.resource import PathElement +from f5.bigip.tm.util.Bash import Bash from f5.bigip.tm.util.Unix_Ls import Unix_Ls from f5.bigip.tm.util.Unix_Mv import Unix_Mv from f5.bigip.tm.util.Unix_Rm import Unix_Rm @@ -37,6 +38,7 @@ class Util(PathElement): def __init__(self, bigip): super(Util, self).__init__(bigip) self._meta_data['allowed_lazy_attributes'] = [ + Bash, Unix_Ls, Unix_Mv, Unix_Rm diff --git a/f5/bigip/tm/util/test/test_bash.py b/f5/bigip/tm/util/test/test_bash.py new file mode 100644 index 000000000..192034493 --- /dev/null +++ b/f5/bigip/tm/util/test/test_bash.py @@ -0,0 +1,47 @@ +# Copyright 2016 F5 Networks Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import mock +import pytest + +from f5.bigip import ManagementRoot +from f5.bigip.tm.util.Bash import Bash + + +@pytest.fixture +def FakeBash(): + fake_sys = mock.MagicMock() + fake_bash = Bash(fake_sys) + return fake_bash + + +@pytest.fixture +def FakeiControl(fakeicontrolsession): + mr = ManagementRoot('host', 'fake_admin', 'fake_admin') + mock_session = mock.MagicMock() + mock_session.post.return_value.json.return_value = {} + mr._meta_data['icr_session'] = mock_session + return mr.tm.util.bash + + +class TestBashCommand(object): + def test_command_bash(self, FakeiControl): + FakeiControl.exec_cmd('run', + utilCmdArgs='-c "df -k"') + session = FakeiControl._meta_data['bigip']._meta_data['icr_session'] + assert session.post.call_args == mock.call( + 'https://host:443/mgmt/tm/util/bash/', + json={'utilCmdArgs': '-c "df -k"', 'command': 'run'} + ) diff --git a/test/functional/tm/util/test_bash.py b/test/functional/tm/util/test_bash.py new file mode 100644 index 000000000..55318a44f --- /dev/null +++ b/test/functional/tm/util/test_bash.py @@ -0,0 +1,66 @@ + +# Copyright 2016 F5 Networks Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import pytest + +from f5.bigip.resource import MissingRequiredCommandParameter +from f5.utils.util_exceptions import UtilError +from icontrol.session import iControlUnexpectedHTTPError + + +def test_E_bash(mgmt_root): + + with pytest.raises(MissingRequiredCommandParameter) as err: + mgmt_root.tm.util.bash.exec_cmd('run') + assert "Missing required params: ['utilCmdArgs']" in err.response.text + + # use bash to create a test file + bash1 = mgmt_root.tm.util.bash.exec_cmd( + 'run', + utilCmdArgs='-c "echo hello >> /var/tmp/test.txt"') + + # commandResult should not be present if this was successful + assert 'commandResult' not in bash1.__dict__ + + bash2 = mgmt_root.tm.util.bash.exec_cmd( + 'run', + utilCmdArgs='-c df -k') + + # commandResult should b present with data from 'df -k' + assert 'commandResult' in bash2.__dict__ + + # UtilError should be raised if -c is not specified + with pytest.raises(UtilError) as err: + mgmt_root.tm.util.bash.exec_cmd('run', + utilCmdArgs='-9 hello') + assert 'Required format is "-c "'\ + in err.response.text + + # UtilError should be raised if command isn't found + with pytest.raises(UtilError) as err: + mgmt_root.tm.util.bash.exec_cmd('run', + utilCmdArgs='-c hello') + assert 'command not found' in err.response.text + + # clean up test file + mgmt_root.tm.util.unix_rm.exec_cmd('run', utilCmdArgs='/var/tmp/test.txt') + + # iControlUnexpectedHTTPError should be raised if quotes don't match + with pytest.raises(iControlUnexpectedHTTPError) as err: + mgmt_root.tm.util.bash.exec_cmd('run', + utilCmdArgs='-c "df -k') + assert err.response.status_code == 400 + assert 'quotes are not balanced' in err.response.text