Add url access verification that will setup network

Added command that will perform setup and teardown of
required networking configuration.

Network configuration will perform next things:

- set interface up if required
- create vlan tagged interface and set it up
- add required ipv4 settings for interface
- add default route

After verification teardown will be performed.
Teardown is best effort based - e.g we should not fail whole command
if we cant fully execute teardown.

Change-Id: I910c15c2b39a917eb8428bb69271b5dde364b639
Partial-Bug: 1439686
This commit is contained in:
Dmitry Shulyak 2015-05-15 11:53:56 +03:00
parent fac8f1af6b
commit 23659819ae
7 changed files with 385 additions and 3 deletions

View File

@ -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'
],
},
)

View File

@ -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)

View File

@ -15,3 +15,7 @@
class UrlNotAvailable(Exception):
pass
class CommandFailed(Exception):
pass

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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)