From e2ad5ec8f43cc9753644254fe37332cd2174a577 Mon Sep 17 00:00:00 2001 From: rahm Date: Tue, 13 Sep 2016 17:45:37 -0500 Subject: [PATCH 1/4] Issue #538 - Adding bash utility --- f5/bigip/tm/util/Bash.py | 65 ++++++++++++++++++++++++++++ f5/bigip/tm/util/__init__.py | 2 + f5/bigip/tm/util/test/test_bash.py | 47 ++++++++++++++++++++ test/functional/tm/util/test_bash.py | 59 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 f5/bigip/tm/util/Bash.py create mode 100644 f5/bigip/tm/util/test/test_bash.py create mode 100644 test/functional/tm/util/test_bash.py diff --git a/f5/bigip/tm/util/Bash.py b/f5/bigip/tm/util/Bash.py new file mode 100644 index 000000000..8aa746a9c --- /dev/null +++ b/f5/bigip/tm/util/Bash.py @@ -0,0 +1,65 @@ +# 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) + 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..5e52b97fa --- /dev/null +++ b/test/functional/tm/util/test_bash.py @@ -0,0 +1,59 @@ + +# 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.utils.util_exceptions import UtilError +from icontrol.session import iControlUnexpectedHTTPError + + +def test_E_bash(mgmt_root): + + # 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__ + + # UtilError should be raised with bad bash command option + with pytest.raises(UtilError) as err: + mgmt_root.tm.util.unix_ls.exec_cmd('run', + utilCmdArgs='-9 hello') + assert 'invalid option' in err.response.text + + # UtilError should be raised if command isn't found + with pytest.raises(UtilError) as err: + mgmt_root.tm.util.unix_ls.exec_cmd('run', + utilCmdArgs='-c hello') + assert 'command not found' in err.response.text + + # UtilError should be raised if no command option and a string + with pytest.raises(UtilError) as err: + mgmt_root.tm.util.unix_ls.exec_cmd('run', + utilCmdArgs='hello') + assert 'No such file or directory' in err.response.text + + # clean up test file + mgmt_root.tm.util.unix_rm.exec_cmd('run', utilCmdArgs='/var/tmp/test.txt') + + # test that unmatched quotes errors out + with pytest.raises(iControlUnexpectedHTTPError) as err: + mgmt_root.tm.util.unix_ls.exec_cmd('run', + utilCmdArgs='-c "df -k') + assert err.response.status_code == 400 + assert 'quotes are not balanced' in err.response.text From 83a5d327453c6556da403a8067c5185a516d3d9f Mon Sep 17 00:00:00 2001 From: rahm Date: Thu, 15 Sep 2016 11:47:34 -0500 Subject: [PATCH 2/4] Issue #538 - Restricted utilCmdArgs submissions to "-c" bash argument --- f5/bigip/tm/util/Bash.py | 5 +++++ test/functional/tm/util/test_bash.py | 19 +++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/f5/bigip/tm/util/Bash.py b/f5/bigip/tm/util/Bash.py index 8aa746a9c..64327a020 100644 --- a/f5/bigip/tm/util/Bash.py +++ b/f5/bigip/tm/util/Bash.py @@ -51,6 +51,11 @@ def _exec_cmd(self, command, **kwargs): 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) diff --git a/test/functional/tm/util/test_bash.py b/test/functional/tm/util/test_bash.py index 5e52b97fa..b5eaa73b8 100644 --- a/test/functional/tm/util/test_bash.py +++ b/test/functional/tm/util/test_bash.py @@ -30,30 +30,25 @@ def test_E_bash(mgmt_root): # commandResult should not be present if this was successful assert 'commandResult' not in bash1.__dict__ - # UtilError should be raised with bad bash command option + # UtilError should be raised if -c is not specified with pytest.raises(UtilError) as err: - mgmt_root.tm.util.unix_ls.exec_cmd('run', + mgmt_root.tm.util.bash.exec_cmd('run', utilCmdArgs='-9 hello') - assert 'invalid option' in err.response.text + 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.unix_ls.exec_cmd('run', + mgmt_root.tm.util.bash.exec_cmd('run', utilCmdArgs='-c hello') assert 'command not found' in err.response.text - # UtilError should be raised if no command option and a string - with pytest.raises(UtilError) as err: - mgmt_root.tm.util.unix_ls.exec_cmd('run', - utilCmdArgs='hello') - assert 'No such file or directory' in err.response.text - # clean up test file mgmt_root.tm.util.unix_rm.exec_cmd('run', utilCmdArgs='/var/tmp/test.txt') - # test that unmatched quotes errors out + # iControlUnexpectedHTTPError should be raised if quotes don't match with pytest.raises(iControlUnexpectedHTTPError) as err: - mgmt_root.tm.util.unix_ls.exec_cmd('run', + 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 From b661d938b592588717a69d998f53ce8328e36c21 Mon Sep 17 00:00:00 2001 From: rahm Date: Thu, 15 Sep 2016 12:28:36 -0500 Subject: [PATCH 3/4] Issue #538 - fixed flake8 errors --- test/functional/tm/util/test_bash.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/tm/util/test_bash.py b/test/functional/tm/util/test_bash.py index b5eaa73b8..a03e1ab77 100644 --- a/test/functional/tm/util/test_bash.py +++ b/test/functional/tm/util/test_bash.py @@ -33,14 +33,14 @@ def test_E_bash(mgmt_root): # 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') + 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') + utilCmdArgs='-c hello') assert 'command not found' in err.response.text # clean up test file @@ -49,6 +49,6 @@ def test_E_bash(mgmt_root): # 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') + utilCmdArgs='-c "df -k') assert err.response.status_code == 400 assert 'quotes are not balanced' in err.response.text From 5c10bc97e0d9b1586f5f6eee929b3ec87cb969c9 Mon Sep 17 00:00:00 2001 From: rahm Date: Thu, 15 Sep 2016 13:58:44 -0500 Subject: [PATCH 4/4] Issue #538 - Added 2 test cases for test coverage --- test/functional/tm/util/test_bash.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/functional/tm/util/test_bash.py b/test/functional/tm/util/test_bash.py index a03e1ab77..55318a44f 100644 --- a/test/functional/tm/util/test_bash.py +++ b/test/functional/tm/util/test_bash.py @@ -16,12 +16,17 @@ 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', @@ -30,6 +35,13 @@ def test_E_bash(mgmt_root): # 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',