diff --git a/os_xenapi/tests/utils/test_iptable.py b/os_xenapi/tests/utils/test_iptable.py new file mode 100644 index 0000000..60ddddf --- /dev/null +++ b/os_xenapi/tests/utils/test_iptable.py @@ -0,0 +1,309 @@ +# 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 +from os_xenapi.client import exception +from os_xenapi.client.i18n import _ +from os_xenapi.tests import base +from os_xenapi.utils import common_function +from os_xenapi.utils import himn +from os_xenapi.utils import iptables +from os_xenapi.utils import sshclient + + +class fake_client_exception(exception.OsXenApiException): + msg_fmt = _("Failed to connect to server") + + +class XenapiIptableTestCase(base.TestCase): + @mock.patch.object(iptables, 'configure_himn_forwards') + @mock.patch.object(iptables, 'configure_dom0_iptables') + def test_config_iptables(self, mock_conf_dom0, mock_himn_forwards): + client = mock.Mock() + client.ip = 'fake_ip' + + iptables.config_iptables(client, 'fake_interface') + mock_himn_forwards.assert_called_once_with('fake_interface', 'fake_ip') + mock_conf_dom0.assert_called_once_with(client) + + @mock.patch.object(iptables, 'configure_himn_forwards') + @mock.patch.object(iptables, 'configure_dom0_iptables') + def test_config_iptables_without_forwards(self, mock_conf_dom0, + mock_himn_forwards): + client = mock.Mock() + client.ip = 'fake_ip' + + iptables.config_iptables(client, None) + mock_himn_forwards.assert_not_called() + mock_conf_dom0.assert_called_once_with(client) + + def test_configure_dom0_iptables(self): + client = mock.Mock() + client.ssh.side_effect = [sshclient.SshExecCmdFailure( + command="fake_cmd", + stdout="fake_out", + stderr="fake_err"), + None, + None, + sshclient.SshExecCmdFailure( + command="fake_cmd", + stdout="fake_out", + stderr="fake_err"), + None, + sshclient.SshExecCmdFailure( + command="fake_cmd", + stdout="fake_out", + stderr="fake_err"), + None, + None] + xs_chain = 'XenServer-Neutron-INPUT' + expect_call1 = mock.call('iptables -t filter -L %s' % xs_chain) + expect_call2 = mock.call('iptables -t filter --new %s' % xs_chain) + expect_call3 = mock.call('iptables -t filter -I INPUT -j %s' + % xs_chain) + expect_call4 = mock.call('iptables -t filter -C %s -p tcp -m ' + 'tcp --dport 6640 -j ACCEPT' % xs_chain) + expect_call5 = mock.call('iptables -t filter -I %s -p tcp -m ' + 'tcp --dport 6640 -j ACCEPT' % xs_chain) + expect_call6 = mock.call('iptables -t filter -C %s -p udp -m ' + 'multiport --dport 4789 -j ACCEPT' % xs_chain) + expect_call7 = mock.call('iptables -t filter -I %s -p udp -m ' + 'multiport --dport 4789 -j ACCEPT' % xs_chain) + expect_call8 = mock.call("service iptables save") + expect_calls = [expect_call1, expect_call2, expect_call3, expect_call4, + expect_call5, expect_call6, expect_call7, expect_call8] + iptables.configure_dom0_iptables(client) + client.ssh.assert_has_calls(expect_calls) + + @mock.patch.object(himn, 'get_local_himn_eth') + @mock.patch.object(common_function, 'execute') + def test_configure_himn_forwards(self, mock_execute, mock_get_eth): + mock_get_eth.return_value = 'fake_eth' + fack_end_point = ['br-storage', 'br-mgmt'] + mock_execute.side_effect = [None, + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + exception.ExecuteCommandFailed('fake_cmd'), + None, + None, + None, + None] + + expect_call1 = mock.call( + 'sed', + '-i', 's/.*net\.ipv4\.ip_forward.*=.*/net.ipv4.ip_forward=1/g', + '/etc/sysctl.conf') + expect_call2 = mock.call('sysctl', 'net.ipv4.ip_forward=1') + + expect_call3 = mock.call('iptables', '-t', 'nat', '-C', 'POSTROUTING', + '-o', fack_end_point[0], '-j', 'MASQUERADE') + expect_call4 = mock.call('iptables', '-t', 'nat', '-I', 'POSTROUTING', + '-o', fack_end_point[0], '-j', 'MASQUERADE') + + expect_call5 = mock.call('iptables', '-t', 'nat', '-C', 'POSTROUTING', + '-o', fack_end_point[1], '-j', 'MASQUERADE') + expect_call6 = mock.call('iptables', '-t', 'nat', '-I', 'POSTROUTING', + '-o', fack_end_point[1], '-j', 'MASQUERADE') + + expect_call7 = mock.call('iptables', '-t', 'filter', '-C', 'FORWARD', + '-i', fack_end_point[0], '-o', 'fake_eth', + '-m', 'state', '--state', + 'RELATED,ESTABLISHED', '-j', 'ACCEPT') + expect_call8 = mock.call('iptables', '-t', 'filter', '-I', 'FORWARD', + '-i', fack_end_point[0], '-o', 'fake_eth', + '-m', 'state', '--state', + 'RELATED,ESTABLISHED', '-j', 'ACCEPT') + + expect_call9 = mock.call('iptables', '-t', 'filter', '-C', 'FORWARD', + '-i', fack_end_point[1], '-o', 'fake_eth', + '-m', 'state', '--state', + 'RELATED,ESTABLISHED', '-j', 'ACCEPT') + expect_call10 = mock.call('iptables', '-t', 'filter', '-I', 'FORWARD', + '-i', fack_end_point[1], '-o', 'fake_eth', + '-m', 'state', '--state', + 'RELATED,ESTABLISHED', '-j', 'ACCEPT') + + expect_call11 = mock.call('iptables', '-t', 'filter', '-C', 'FORWARD', + '-i', 'fake_eth', '-o', fack_end_point[0], + '-j', 'ACCEPT') + expect_call12 = mock.call('iptables', '-t', 'filter', '-I', 'FORWARD', + '-i', 'fake_eth', '-o', fack_end_point[0], + '-j', 'ACCEPT') + + expect_call13 = mock.call('iptables', '-t', 'filter', '-C', 'FORWARD', + '-i', 'fake_eth', '-o', fack_end_point[1], + '-j', 'ACCEPT') + expect_call14 = mock.call('iptables', '-t', 'filter', '-I', 'FORWARD', + '-i', 'fake_eth', '-o', fack_end_point[1], + '-j', 'ACCEPT') + + expect_call15 = mock.call('iptables', '-t', 'filter', '-C', 'INPUT', + '-i', 'fake_eth', '-j', 'ACCEPT') + expect_call16 = mock.call('iptables', '-t', 'filter', '-I', 'INPUT', + '-i', 'fake_eth', '-j', 'ACCEPT') + + expect_call17 = mock.call('iptables', '-t', 'filter', '-S', 'FORWARD') + expect_call18 = mock.call('iptables', '-t', 'nat', '-S', 'POSTROUTING') + + expect_calls = [expect_call1, expect_call2, + expect_call3, expect_call4, + expect_call7, expect_call8, + expect_call11, expect_call12, + expect_call5, expect_call6, + expect_call9, expect_call10, + expect_call13, expect_call14, + expect_call15, expect_call16, + expect_call17, expect_call18] + iptables.configure_himn_forwards(fack_end_point, 'fake_dom0_himn_ip') + mock_get_eth.assert_called_once_with('fake_dom0_himn_ip') + mock_execute.assert_has_calls(expect_calls) + + @mock.patch.object(himn, 'get_local_himn_eth') + @mock.patch.object(common_function, 'execute') + def test_configure_himn_forwards_no_eth_exc(self, mock_execute, + mock_get_eth): + mock_get_eth.return_value = None + self.assertRaises(exception.NoNetworkInterfaceInSameSegment, + iptables.configure_himn_forwards, + 'fake_end_point', 'fake_dom0_himn_ip') + + @mock.patch.object(common_function, 'execute') + def test_execute_local_iptables_cmd(self, mock_execute): + fake_rule_spec = 'fake_rule' + mock_execute.return_value = 'success' + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + fake_rule_spec) + self.assertTrue(execute_result) + mock_execute.assert_called_once_with('iptables', '-t', 'fake_table', + 'fake_action', 'fake_chain', + fake_rule_spec) + + @mock.patch.object(common_function, 'execute') + def test_execute_local_iptables_cmd_failed(self, mock_execute): + fake_rule_spec = 'fake_rule' + mock_execute.side_effect = [exception.ExecuteCommandFailed('fake_cmd')] + + self.assertRaises(exception.ExecuteCommandFailed, + iptables.execute_iptables_cmd, + 'fake_table', 'fake_action', + 'fake_chain', fake_rule_spec) + + mock_execute.assert_called_once_with('iptables', '-t', 'fake_table', + 'fake_action', 'fake_chain', + fake_rule_spec) + + @mock.patch.object(common_function, 'execute') + def test_execute_local_iptables_cmd_expect_failed(self, mock_execute): + fake_rule_spec = 'fake_rule' + mock_execute.side_effect = [exception.ExecuteCommandFailed('fake_cmd')] + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + fake_rule_spec, + None, + True) + self.assertFalse(execute_result) + mock_execute.assert_called_once_with('iptables', '-t', 'fake_table', + 'fake_action', 'fake_chain', + fake_rule_spec) + + @mock.patch.object(common_function, 'execute') + def test_execute_local_iptables_cmd_no_rule_spec(self, mock_execute): + mock_execute.return_value = 'success' + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + None) + self.assertTrue(execute_result) + mock_execute.assert_called_once_with('iptables', '-t', 'fake_table', + 'fake_action', 'fake_chain') + + def test_execute_remote_iptables_cmd(self): + fake_client = mock.Mock() + fake_rule_spec = 'fake_rule' + fake_client.ssh.return_value = 'success' + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + fake_rule_spec, + fake_client) + self.assertTrue(execute_result) + fake_client.ssh.assert_called_once_with('iptables -t fake_table ' + + 'fake_action fake_chain ' + + fake_rule_spec) + + def test_execute_remote_iptables_cmd_failed(self): + fake_client = mock.Mock() + fake_rule_spec = 'fake_rule' + fake_client.ssh.side_effect = [sshclient.SshExecCmdFailure( + command="fake_cmd", + stdout="fake_out", + stderr="fake_err")] + + self.assertRaises(sshclient.SshExecCmdFailure, + iptables.execute_iptables_cmd, + 'fake_table', 'fake_action', + 'fake_chain', fake_rule_spec, + fake_client) + + fake_client.ssh.assert_called_once_with('iptables -t fake_table ' + + 'fake_action fake_chain ' + + fake_rule_spec) + + def test_execute_remote_iptables_cmd_expect_failed(self): + fake_client = mock.Mock() + fake_rule_spec = 'fake_rule' + fake_client.ssh.side_effect = [sshclient.SshExecCmdFailure( + command="fake_cmd", + stdout="fake_out", + stderr="fake_err")] + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + fake_rule_spec, + fake_client, + True) + self.assertFalse(execute_result) + fake_client.ssh.assert_called_once_with('iptables -t fake_table ' + + 'fake_action fake_chain ' + + fake_rule_spec) + + def test_execute_remote_iptables_cmd_no_rule_spec(self): + fake_client = mock.Mock() + fake_client.ssh.return_value = 'success' + + execute_result = iptables.execute_iptables_cmd('fake_table', + 'fake_action', + 'fake_chain', + None, + fake_client) + self.assertTrue(execute_result) + fake_client.ssh.assert_called_once_with("iptables -t fake_table " + "fake_action fake_chain") diff --git a/os_xenapi/utils/common_function.py b/os_xenapi/utils/common_function.py index 42ba255..c023261 100644 --- a/os_xenapi/utils/common_function.py +++ b/os_xenapi/utils/common_function.py @@ -62,7 +62,7 @@ def detailed_execute(*cmd, **kwargs): proc.returncode) else: LOG.warn('proc.returncode: %s', proc.returncode) - raise exception(err) + raise exception.ExecuteCommandFailed(cmd) return proc.returncode, out, err diff --git a/os_xenapi/utils/iptables.py b/os_xenapi/utils/iptables.py new file mode 100644 index 0000000..16c0825 --- /dev/null +++ b/os_xenapi/utils/iptables.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# Copyright 2017 Citrix Systems +# +# 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. +"""iptable utils + +It contains the utilities relative to iptable settings.""" + +import sys + +from os_xenapi.client import exception +from os_xenapi.utils import common_function +from os_xenapi.utils import himn +from os_xenapi.utils import sshclient + + +OVS_NATIVE_TCP_PORT = '6640' +VXLAN_UDP_PORT = '4789' + + +def exit_with_error(err_msg): + sys.stderr.write(err_msg) + sys.exit(1) + + +def configure_dom0_iptables(client): + xs_chain = 'XenServer-Neutron-INPUT' + # Check XenServer specific chain, create if not exist + if not execute_iptables_cmd('filter', '-L', xs_chain, client=client, + expect_exception=True): + execute_iptables_cmd('filter', '--new', xs_chain, client=client) + rule_spec = ('-j %s' % xs_chain) + execute_iptables_cmd('filter', '-I', 'INPUT', rule_spec, client) + + # Check XenServer rule for ovs native mode, create if not exist + rule_spec = ('-p tcp -m tcp --dport %s -j ACCEPT' + % OVS_NATIVE_TCP_PORT) + ensure_iptables('filter', xs_chain, rule_spec, client) + + # Check XenServer rule for vxlan, create if not exist + rule_spec = ('-p udp -m multiport --dport %s -j ACCEPT' + % VXLAN_UDP_PORT) + ensure_iptables('filter', xs_chain, rule_spec, client) + + # Persist iptables rules + client.ssh('service iptables save') + + +def configure_himn_forwards(forwarding_interfaces, dom0_himn_ip): + # enable forward + # make change to be persistent + common_function.execute( + 'sed', '-i', 's/.*net\.ipv4\.ip_forward.*=.*/net.ipv4.ip_forward=1/g', + '/etc/sysctl.conf') + # make it to take effective now. + common_function.execute('sysctl', 'net.ipv4.ip_forward=1') + eth = himn.get_local_himn_eth(dom0_himn_ip) + if not eth: + raise exception.NoNetworkInterfaceInSameSegment(dom0_himn_ip) + for interface in forwarding_interfaces: + # allow traffic from HIMN and forward traffic + rule_spec = '-o ' + interface + ' -j MASQUERADE' + ensure_iptables('nat', 'POSTROUTING', rule_spec) + + rule_spec = '-i ' + interface + ' -o ' + eth + ' -m state ' + \ + '--state RELATED,ESTABLISHED -j ACCEPT' + ensure_iptables('filter', 'FORWARD', rule_spec) + + rule_spec = '-i ' + eth + ' -o ' + interface + ' -j ACCEPT' + ensure_iptables('filter', 'FORWARD', rule_spec) + + rule_spec = '-i ' + eth + ' -j ACCEPT' + ensure_iptables('filter', 'INPUT', rule_spec) + execute_iptables_cmd('filter', '-S', 'FORWARD') + execute_iptables_cmd('nat', '-S', 'POSTROUTING') + + +def ensure_iptables(table, chain, rule_spec, client=None): + if not execute_iptables_cmd(table, '-C', chain, rule_spec, client, True): + execute_iptables_cmd(table, '-I', chain, rule_spec, client) + + +def execute_iptables_cmd(table, action, chain, rule_spec=None, client=None, + expect_exception=False): + """This function is used to run iptables command. + + Users could run command to configure iptables for remote and local hosts. + If the user want to configure remote host, the session client is needed, or + the command would be run on local host. + + :param table: table you want you configure. + :param rule_spec: rule spec you want to apply. + :param client: session client to remote host you want to configure. + :param expect_exception: When you just want to do a rule check, set this + flag to 'True'. Then the reture value would be 'Ture' or 'False'. + :param forwarding_interfaces: network interface list which user want to + forward HIMN packages. + """ + if client: + if not rule_spec: + rule_spec = '' + command = ('iptables -t %(table)s %(action)s %(chain)s %(rule_spec)s' + % {'table': table, 'action': action, + 'chain': chain, 'rule_spec': rule_spec}) + command = command.strip() + try: + client.ssh(command) + except sshclient.SshExecCmdFailure: + if expect_exception: + return False + else: + raise + else: + if rule_spec: + rule_spec = rule_spec.split() + else: + rule_spec = [] + command = ['iptables', '-t', table, action, chain] + rule_spec + try: + common_function.execute(*command) + except exception.ExecuteCommandFailed: + if expect_exception: + return False + else: + raise + return True + + +def config_iptables(client, forwarding_interfaces=None): + """This function is used to configure iptables on a XenServer compute node. + + :param client: session client with Dom0 + :param forwarding_interfaces: network interface list which user want to + forward HIMN packages. + """ + if forwarding_interfaces: + configure_himn_forwards(forwarding_interfaces, client.ip) + configure_dom0_iptables(client) + + +if __name__ == '__main__': + if len(sys.argv) != 5: + exit_with_error("Wrong parameters input.") + dom0_himn_ip, user_name, password, forwarding_interfaces = sys.argv[1:] + forwarding_interfaces = forwarding_interfaces.split() + try: + client = sshclient.SSHClient(dom0_himn_ip, user_name, password) + except Exception: + exit_with_error("Create connection failed, ip: %(dom0_himn_ip)s," + " user_name: %(user_name)s" % + {'dom0_himn_ip': dom0_himn_ip, 'user_name': user_name}) + config_iptables(client, forwarding_interfaces)