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:
parent
fac8f1af6b
commit
23659819ae
|
@ -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'
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -15,3 +15,7 @@
|
|||
|
||||
class UrlNotAvailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CommandFailed(Exception):
|
||||
pass
|
||||
|
|
|
@ -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')
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue