diff --git a/network_checker/setup.py b/network_checker/setup.py index 2d34f418b3..b6fde7d689 100644 --- a/network_checker/setup.py +++ b/network_checker/setup.py @@ -46,7 +46,8 @@ setuptools.setup( 'simple = network_checker.tests.simple:SimpleChecker' ], 'urlaccesscheck': [ - 'check = url_access_checker.commands:CheckUrls' + 'check = url_access_checker.commands:CheckUrls', + 'with_setup = url_access_checker.commands:CheckUrlsWithSetup' ], }, ) diff --git a/network_checker/url_access_checker/commands.py b/network_checker/url_access_checker/commands.py index e6397b8b3d..b7b8602330 100644 --- a/network_checker/url_access_checker/commands.py +++ b/network_checker/url_access_checker/commands.py @@ -19,6 +19,8 @@ from cliff import command import url_access_checker.api as api import url_access_checker.errors as errors +from url_access_checker.network import manage_network + LOG = logging.getLogger(__name__) @@ -38,3 +40,22 @@ class CheckUrls(command.Command): except errors.UrlNotAvailable as e: sys.stdout.write(str(e)) raise e + + +class CheckUrlsWithSetup(CheckUrls): + + def get_parser(self, prog_name): + parser = super(CheckUrlsWithSetup, self).get_parser( + prog_name) + parser.add_argument('-i', type=str, help='Interface', required=True) + parser.add_argument('-a', type=str, help='Addr/Mask pair', + required=True) + parser.add_argument('-g', type=str, required=True, + help='Gateway to be used as default') + parser.add_argument('--vlan', type=int, help='Vlan tag') + return parser + + def take_action(self, pa): + with manage_network(pa.i, pa.a, pa.g, pa.vlan): + return super( + CheckUrlsWithSetup, self).take_action(pa) diff --git a/network_checker/url_access_checker/errors.py b/network_checker/url_access_checker/errors.py index ccc0dc1938..186bb03314 100644 --- a/network_checker/url_access_checker/errors.py +++ b/network_checker/url_access_checker/errors.py @@ -15,3 +15,7 @@ class UrlNotAvailable(Exception): pass + + +class CommandFailed(Exception): + pass diff --git a/network_checker/url_access_checker/network.py b/network_checker/url_access_checker/network.py new file mode 100644 index 0000000000..465fa1faac --- /dev/null +++ b/network_checker/url_access_checker/network.py @@ -0,0 +1,209 @@ +# Copyright 2015 Mirantis, 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. + +from contextlib import contextmanager +from logging import getLogger + +import netifaces + +from url_access_checker.errors import CommandFailed +from url_access_checker.utils import execute + + +logger = getLogger(__name__) + + +def get_default_gateway(): + """Return ipaddress, interface pair for default gateway + """ + gws = netifaces.gateways() + if 'default' in gws: + return gws['default'][netifaces.AF_INET] + return None, None + + +def check_ifaddress_present(iface, addr): + """Check if required ipaddress already assigned to the iface + """ + for ifaddress in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []): + if ifaddress['addr'] in addr: + return True + return False + + +def check_exist(iface): + rc, _, err = execute(['ip', 'link', 'show', iface]) + if rc == 1 and 'does not exist' in err: + return False + elif rc: + msg = 'ip link show {0} failed with {1}'.format(iface, err) + raise CommandFailed(msg) + return True + + +def check_up(iface): + rc, stdout, _ = execute(['ip', 'link', 'show', iface]) + return 'UP' in stdout + + +def log_network_info(stage): + logger.info('Logging networking info at %s', stage) + stdout = execute(['ip', 'a'])[1] + logger.info('ip a: %s', stdout) + stdout = execute(['ip', 'ro'])[1] + logger.info('ip ro: %s', stdout) + + +class Eth(object): + + def __init__(self, iface): + self.iface = iface + self.is_up = None + + def setup(self): + self.is_up = check_up(self.iface) + if self.is_up is False: + rc, out, err = execute(['ip', 'link', 'set', + 'dev', self.iface, 'up']) + if rc: + msg = 'Cannot up interface {0}. Err: {1}'.format( + self.iface, err) + raise CommandFailed(msg) + + def teardown(self): + if self.is_up is False: + execute(['ip', 'link', 'set', 'dev', self.iface, 'down']) + + +class Vlan(Eth): + + def __init__(self, iface, vlan): + self.parent = iface + self.vlan = str(vlan) + self.iface = '{0}.{1}'.format(iface, vlan) + self.is_present = None + self.is_up = None + + def setup(self): + self.is_present = check_exist(self.iface) + if self.is_present is False: + rc, out, err = execute( + ['ip', 'link', 'add', + 'link', self.parent, 'name', + self.iface, 'type', 'vlan', 'id', self.vlan]) + + if rc: + msg = ( + 'Cannot create tagged interface {0}.' + ' With parent {1}. Err: {2}'.format( + self.iface, self.parent, err)) + raise CommandFailed(msg) + super(Vlan, self).setup() + + def teardown(self): + super(Vlan, self).teardown() + if self.is_present is False: + execute(['ip', 'link', 'delete', self.iface]) + + +class IP(object): + + def __init__(self, iface, addr): + self.iface = iface + self.addr = addr + self.is_present = None + + def setup(self): + self.is_present = check_ifaddress_present(self.iface, self.addr) + if self.is_present is False: + rc, out, err = execute(['ip', 'a', 'add', self.addr, + 'dev', self.iface]) + if rc: + msg = 'Cannot add address {0} to {1}. Err: {2}'.format( + self.addr, self.iface, err) + raise CommandFailed(msg) + + def teardown(self): + if self.is_present is False: + execute(['ip', 'a', 'del', self.addr, 'dev', self.iface]) + + +class Route(object): + + def __init__(self, iface, gateway): + self.iface = iface + self.gateway = gateway + self.default_gateway = None + self.df_iface = None + + def setup(self): + self.default_gateway, self.df_iface = get_default_gateway() + + rc = None + if (self.default_gateway, self.df_iface) == (None, None): + rc, out, err = execute( + ['ip', 'ro', 'add', + 'default', 'via', self.gateway, 'dev', self.iface]) + elif ((self.default_gateway, self.df_iface) + != (self.gateway, self.iface)): + rc, out, err = execute( + ['ip', 'ro', 'change', + 'default', 'via', self.gateway, 'dev', self.iface]) + + if rc: + msg = ('Cannot add default gateway {0} on iface {1}.' + ' Err: {2}'.format(self.gateway, self.iface, err)) + raise CommandFailed(msg) + + def teardown(self): + if (self.default_gateway, self.df_iface) == (None, None): + execute(['ip', 'ro', 'del', + 'default', 'via', self.gateway, 'dev', self.iface]) + elif ((self.default_gateway, self.df_iface) + != (self.gateway, self.iface)): + execute(['ip', 'ro', 'change', + 'default', 'via', self.default_gateway, + 'dev', self.df_iface]) + + +@contextmanager +def manage_network(iface, addr, gateway, vlan=None): + + log_network_info('before setup') + + actions = [Eth(iface)] + if vlan: + vlan_action = Vlan(iface, vlan) + actions.append(vlan_action) + iface = vlan_action.iface + actions.append(IP(iface, addr)) + actions.append(Route(iface, gateway)) + executed = [] + + try: + for a in actions: + a.setup() + executed.append(a) + + log_network_info('after setup') + + yield + except Exception: + logger.exception('Unexpected failure.') + raise + finally: + for a in reversed(executed): + a.teardown() + + log_network_info('after teardown') diff --git a/network_checker/url_access_checker/tests/unit/test_with_network_setup.py b/network_checker/url_access_checker/tests/unit/test_with_network_setup.py new file mode 100644 index 0000000000..90bf8e49ed --- /dev/null +++ b/network_checker/url_access_checker/tests/unit/test_with_network_setup.py @@ -0,0 +1,114 @@ +# Copyright 2015 Mirantis, 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 unittest + +from mock import call +from mock import Mock +from mock import patch +import netifaces + +from url_access_checker import cli + + +@patch('url_access_checker.network.execute') +@patch('url_access_checker.network.netifaces.gateways') +@patch('requests.get', Mock(status_code=200)) +@patch('url_access_checker.network.check_up') +@patch('url_access_checker.network.check_exist') +@patch('url_access_checker.network.check_ifaddress_present') +class TestVerificationWithNetworkSetup(unittest.TestCase): + + def assert_by_items(self, expected_items, received_items): + """In case of failure will show difference only for failed item.""" + for expected, executed in zip(expected_items, received_items): + self.assertEqual(expected, executed) + + def test_verification_route(self, mifaddr, mexist, mup, mgat, mexecute): + mexecute.return_value = (0, '', '') + mup.return_value = True + mexist.return_value = True + mifaddr.return_value = False + + default_gw, default_iface = '172.18.0.1', 'eth2' + mgat.return_value = { + 'default': {netifaces.AF_INET: (default_gw, default_iface)}} + + iface = 'eth1' + addr = '10.10.0.2/24' + gw = '10.10.0.1' + + cmd = ['with', 'setup', '-i', iface, + '-a', addr, '-g', gw, 'test.url'] + + cli.main(cmd) + + execute_stack = [ + call(['ip', 'a']), + call(['ip', 'ro']), + call(['ip', 'a', 'add', addr, 'dev', iface]), + call(['ip', 'ro', 'change', 'default', 'via', gw, 'dev', iface]), + call(['ip', 'a']), + call(['ip', 'ro']), + call(['ip', 'ro', 'change', 'default', 'via', default_gw, + 'dev', default_iface]), + call(['ip', 'a', 'del', addr, 'dev', iface]), + call(['ip', 'a']), + call(['ip', 'ro'])] + + self.assert_by_items(mexecute.call_args_list, execute_stack) + + def test_verification_vlan(self, mifaddr, mexist, mup, mgat, mexecute): + mexecute.return_value = (0, '', '') + mup.return_value = False + mexist.return_value = False + mifaddr.return_value = False + + default_gw, default_iface = '172.18.0.1', 'eth2' + mgat.return_value = { + 'default': {netifaces.AF_INET: (default_gw, default_iface)}} + + iface = 'eth1' + addr = '10.10.0.2/24' + gw = '10.10.0.1' + vlan = '101' + tagged_iface = '{0}.{1}'.format(iface, vlan) + + cmd = ['with', 'setup', '-i', iface, + '-a', addr, '-g', gw, '--vlan', vlan, 'test.url'] + + cli.main(cmd) + + execute_stack = [ + call(['ip', 'a']), + call(['ip', 'ro']), + call(['ip', 'link', 'set', 'dev', iface, 'up']), + call(['ip', 'link', 'add', 'link', 'eth1', 'name', + tagged_iface, 'type', 'vlan', 'id', vlan]), + call(['ip', 'link', 'set', 'dev', tagged_iface, 'up']), + call(['ip', 'a', 'add', addr, 'dev', tagged_iface]), + call(['ip', 'ro', 'change', 'default', + 'via', gw, 'dev', tagged_iface]), + call(['ip', 'a']), + call(['ip', 'ro']), + call(['ip', 'ro', 'change', 'default', 'via', + default_gw, 'dev', default_iface]), + call(['ip', 'a', 'del', addr, 'dev', tagged_iface]), + call(['ip', 'link', 'set', 'dev', tagged_iface, 'down']), + call(['ip', 'link', 'delete', tagged_iface]), + call(['ip', 'link', 'set', 'dev', iface, 'down']), + call(['ip', 'a']), + call(['ip', 'ro'])] + + self.assert_by_items(mexecute.call_args_list, execute_stack) diff --git a/network_checker/url_access_checker/utils.py b/network_checker/url_access_checker/utils.py new file mode 100644 index 0000000000..bfdd23a28d --- /dev/null +++ b/network_checker/url_access_checker/utils.py @@ -0,0 +1,32 @@ +# Copyright 2015 Mirantis, 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. + +from logging import getLogger +import subprocess + +logger = getLogger(__name__) + + +def execute(cmd): + logger.debug('Executing command %s', cmd) + command = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = command.communicate() + msg = 'Command {0} executed. RC {1}, stdout {2}, stderr {3}'.format( + cmd, command.returncode, stdout, stderr) + if command.returncode: + logger.error(msg) + else: + logger.debug(msg) + return command.returncode, stdout, stderr diff --git a/specs/nailgun.spec b/specs/nailgun.spec index 033942bc1f..cc2deb138e 100644 --- a/specs/nailgun.spec +++ b/specs/nailgun.spec @@ -61,7 +61,7 @@ Requires: pytz Nailgun package %prep -%setup -cq -n %{name}-%{version} +%setup -cq -n %{name}-%{version} npm install --prefix %{_builddir}/%{name}-%{version}/nailgun/ gulp %build @@ -149,6 +149,7 @@ Requires: python-daemonize Requires: python-yaml Requires: tcpdump Requires: python-requests +Requires: python-netifaces %description -n nailgun-net-check @@ -179,7 +180,7 @@ Requires: openssh-clients Requires: xz %description -n shotgun -Shotgun package. +Shotgun package. %files -n shotgun -f %{_builddir}/%{name}-%{version}/shotgun/INSTALLED_FILES %defattr(-,root,root)