Deprecate network_checker directory
Change-Id: I549f6c177a4e1fd459666410954accea9c30ae3c Related-Bug: #1506896
This commit is contained in:
parent
73a7dd0757
commit
ff842dd813
|
@ -40,15 +40,6 @@ maintainers:
|
|||
email: ikalnitsky@mirantis.com
|
||||
IRC: ikalnitsky
|
||||
|
||||
- network_checker/:
|
||||
- name: Dmytro Shuliak
|
||||
email: dshulyak@mirantis.com
|
||||
IRC: dshulyak
|
||||
|
||||
- name: Lukasz Oles
|
||||
email: loles@mirantis.com
|
||||
IRC: salmon_
|
||||
|
||||
- nailgun/:
|
||||
- name: Aleksandr Kislitskii
|
||||
email: akislitsky@mirantis.com
|
||||
|
|
|
@ -15,11 +15,5 @@ Depends: ohai (<< 7),
|
|||
ruby-cstruct,
|
||||
ruby-json,
|
||||
${misc:Depends}
|
||||
Description: <insert up to 60 chars description>
|
||||
<insert long description, indented with spaces>
|
||||
|
||||
Package: nailgun-net-check
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}, python-pypcap, vlan, python-scapy, cliff-tablib, python-stevedore, python-daemonize, python-yaml, tcpdump, python-requests, python-six
|
||||
Description: NailGun client net-check
|
||||
Description: Fencing agent
|
||||
.
|
||||
|
|
|
@ -7,12 +7,9 @@ topdir=$(shell pwd)
|
|||
dh $@ --with python2
|
||||
|
||||
override_dh_auto_install:
|
||||
cd network_checker && python setup.py install -O0 --single-version-externally-managed --install-layout=deb --root=$(topdir)/debian/nailgun-net-check
|
||||
dh_auto_install
|
||||
override_dh_auto_build:
|
||||
dh_clean
|
||||
cd network_checker && python setup.py build
|
||||
dh_auto_build
|
||||
override_dh_auto_clean:
|
||||
cd network_checker && python setup.py clean
|
||||
dh_auto_clean
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
FROM phusion/baseimage
|
||||
ENV ARCH amd64
|
||||
ENV DIST trusty
|
||||
RUN echo 'deb http://fuel-repository.mirantis.com/fwm/5.0/ubuntu trusty main' >> /etc/apt/sources.list
|
||||
RUN apt-get -q update
|
||||
RUN apt-get -y --force-yes install cliff-tablib python-pyparsing python-pypcap scapy python-pip wget openssh-server
|
||||
RUN pip install pytest mock
|
||||
RUN sudo locale-gen en_US.UTF-8
|
||||
|
||||
RUN mkdir -p /app
|
|
@ -1 +0,0 @@
|
|||
recursive-include network_checker *
|
|
@ -1,25 +0,0 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.box = "precise64"
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.customize ["modifyvm", :id, "--memory", 348]
|
||||
end
|
||||
|
||||
config.vm.define :develop do |config|
|
||||
config.vm.network :private_network, ip: '10.100.0.0', type: "dhcp"
|
||||
config.vm.network :private_network, ip: '10.200.0.0', type: 'dhcp'
|
||||
config.vm.provision :shell, :inline => "echo 'deb http://fuel-repository.mirantis.com/fwm/5.0/ubuntu trusty main' >> /etc/apt/sources.list"
|
||||
config.vm.provision :shell, :inline => "sudo apt-get update"
|
||||
config.vm.provision :shell, :inline => "sudo apt-get -y --force-yes install cliff-tablib python-pyparsing python-pypcap scapy python-pip vde2"
|
||||
config.vm.provision :shell, :inline => "sudo pip install pytest mock"
|
||||
config.vm.provision :shell, :inline => "cd /vagrant && sudo python setup.py develop"
|
||||
end
|
||||
|
||||
end
|
|
@ -1,44 +0,0 @@
|
|||
# Copyright 2014 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 os
|
||||
|
||||
|
||||
PIDFILE = '/tmp/vde_network_checker'
|
||||
IFACES = ['tap11', 'tap12']
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--vde", action='store_true', default=False,
|
||||
help="Use vde switch for network verification.")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getoption('vde'):
|
||||
base = 'vde_switch -p {pidfile} -d'.format(pidfile=PIDFILE)
|
||||
command = [base]
|
||||
taps = ['-tap {tap}'.format(tap=tap) for tap in IFACES]
|
||||
full_command = command + taps
|
||||
os.system(' '.join(full_command))
|
||||
for tap in IFACES:
|
||||
os.system('ifconfig {tap} up'.format(tap=tap))
|
||||
os.environ['NET_CHECK_IFACE_1'] = IFACES[0]
|
||||
os.environ['NET_CHECK_IFACE_2'] = IFACES[1]
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if os.path.exists(PIDFILE):
|
||||
with open(PIDFILE) as f:
|
||||
pid = f.read().strip()
|
||||
os.kill(int(pid), 15)
|
|
@ -1,35 +0,0 @@
|
|||
nailgun (8.0.0-1) trusty; urgency=low
|
||||
|
||||
* Change version to 8.0.0
|
||||
|
||||
-- Vladimir Kozhukalov <vkozhukalov@mirantis.com> Mon, 03 Sep 2015 11:12:00 +0300
|
||||
|
||||
nailgun (7.0.0-1) trusty; urgency=low
|
||||
|
||||
* Update version to 7.0.0
|
||||
|
||||
-- Aleksandra Fedorova <afedorova@mirantis.com> Mon, 08 Jun 2015 18:15:00 +0300
|
||||
|
||||
nailgun (6.1.0-1) trusty; urgency=low
|
||||
|
||||
* Update version to 6.1.0
|
||||
|
||||
-- Matthew Mosesohn <mmosesohn@mirantis.com> Wed, 22 Apr 2015 14:44:00 +0300
|
||||
|
||||
nailgun (6.0.0-1) trusty; urgency=low
|
||||
|
||||
* Update code from upstream
|
||||
|
||||
-- Igor Kalnitsky <ikalnitsky@mirantis.com> Wed, 26 Nov 2014 19:49:00 +0200
|
||||
|
||||
nailgun (0.1.0-ubuntu1) precise; urgency=low
|
||||
|
||||
* Update code from upstream
|
||||
|
||||
-- OSCI Jenkins <dburmistrov@mirantis.com> Wed, 03 Sep 2014 15:17:07 +0400
|
||||
|
||||
nailgun (0.1.0-1ubuntu0) precise; urgency=low
|
||||
|
||||
* Add fencing agent script as a separated task
|
||||
|
||||
-- Bogdan Dobrelya <bdobrelia@mirantis.com> Thu, 23 Jan 2014 18:55:00 +0400
|
|
@ -1 +0,0 @@
|
|||
8
|
|
@ -1,14 +0,0 @@
|
|||
Source: network-checker
|
||||
Section: unknown
|
||||
Priority: net
|
||||
Maintainer: Mirantis Product <product@mirantis.com>
|
||||
Build-Depends: debhelper (>= 8.0.0), python-setuptools
|
||||
X-Python-Version: 2.6, 2.7
|
||||
Standards-Version: 3.9.2
|
||||
Homepage: mirantis.com
|
||||
|
||||
Package: nailgun-net-check
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}, python-pypcap, vlan, python-scapy, cliff-tablib, python-stevedore, python-daemonize, python-yaml, tcpdump, python-requests, python-six
|
||||
Description: Nailgun network connectivity check tool
|
||||
.
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
DH_VERBOSE=1
|
||||
|
||||
topdir=$(shell pwd)
|
||||
|
||||
%:
|
||||
dh $@ --with python2
|
||||
|
||||
override_dh_auto_install:
|
||||
python setup.py install -O0 --single-version-externally-managed --install-layout=deb --root=$(topdir)/debian/nailgun-net-check
|
||||
dh_auto_install
|
||||
override_dh_auto_build:
|
||||
dh_clean
|
||||
python setup.py build
|
||||
dh_auto_build
|
||||
override_dh_auto_clean:
|
||||
python setup.py clean
|
||||
dh_auto_clean
|
|
@ -1 +0,0 @@
|
|||
3.0 (quilt)
|
|
@ -1,13 +0,0 @@
|
|||
# Copyright 2013 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.
|
|
@ -1,152 +0,0 @@
|
|||
# Copyright 2013 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 itertools
|
||||
import logging
|
||||
import time
|
||||
|
||||
from scapy import config as scapy_config
|
||||
|
||||
scapy_config.use_pcap = True
|
||||
|
||||
logging.getLogger('scapy.runtime').setLevel(logging.CRITICAL)
|
||||
|
||||
from dhcp_checker import utils
|
||||
import pcap
|
||||
from scapy import all as scapy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_dhcp_discover_message(iface):
|
||||
|
||||
dhcp_options = [("message-type", "discover"),
|
||||
("param_req_list", utils.format_options(
|
||||
[1, 2, 3, 4, 5, 6,
|
||||
11, 12, 13, 15, 16, 17, 18, 22, 23,
|
||||
28, 40, 41, 42, 43, 50, 51, 54, 58, 59, 60, 66, 67])),
|
||||
"end"]
|
||||
|
||||
fam, hw = scapy.get_if_raw_hwaddr(iface)
|
||||
|
||||
dhcp_discover = (
|
||||
scapy.Ether(src=hw, dst="ff:ff:ff:ff:ff:ff") /
|
||||
scapy.IP(src="0.0.0.0", dst="255.255.255.255") /
|
||||
scapy.UDP(sport=68, dport=67) /
|
||||
scapy.BOOTP(chaddr=hw) /
|
||||
scapy.DHCP(options=dhcp_options))
|
||||
|
||||
return dhcp_discover
|
||||
|
||||
|
||||
@utils.single_format
|
||||
def check_dhcp_on_eth(iface, timeout):
|
||||
"""Check if there is roque dhcp server in network on given iface
|
||||
|
||||
@iface - name of the ethernet interface
|
||||
@timeout - scapy timeout for waiting on response
|
||||
>>> check_dhcp_on_eth('eth1')
|
||||
"""
|
||||
scapy.conf.iface = iface
|
||||
scapy.conf.checkIPaddr = False
|
||||
dhcp_discover = _get_dhcp_discover_message(iface)
|
||||
ans, unans = scapy.srp(dhcp_discover, multi=True,
|
||||
nofilter=1, timeout=timeout, verbose=0)
|
||||
|
||||
return ans
|
||||
|
||||
|
||||
def check_dhcp(ifaces, timeout=5, repeat=2):
|
||||
"""Given list of ifaces. Process them in separate processes
|
||||
|
||||
@ifaces - lsit of ifaces
|
||||
@timeout - timeout for scapy to wait for response
|
||||
@repeat - number of packets sended
|
||||
>>> check_dhcp(['eth1', 'eth2'])
|
||||
"""
|
||||
config = {}
|
||||
for iface in ifaces:
|
||||
config[iface] = ()
|
||||
return check_dhcp_with_vlans(config, timeout=timeout, repeat=repeat)
|
||||
|
||||
|
||||
def send_dhcp_discover(iface):
|
||||
dhcp_discover = _get_dhcp_discover_message(iface)
|
||||
scapy.sendp(dhcp_discover, iface=iface, verbose=0)
|
||||
|
||||
|
||||
def make_listeners(ifaces):
|
||||
listeners = []
|
||||
for iface in ifaces:
|
||||
try:
|
||||
listener = pcap.pcap(iface)
|
||||
mac_filter = utils.create_mac_filter(iface)
|
||||
# catch the answers only for this iface
|
||||
listener.setfilter('((dst port 68) and {0})'.format(mac_filter))
|
||||
listeners.append(listener)
|
||||
except Exception:
|
||||
LOG.warning(
|
||||
'Spawning listener for {iface} failed.'.format(iface=iface))
|
||||
return listeners
|
||||
|
||||
|
||||
@utils.filter_duplicated_results
|
||||
def check_dhcp_with_vlans(config, timeout=5, repeat=2):
|
||||
"""Provide config of {iface: [vlans..]} pairs
|
||||
|
||||
@config - {'eth0': (100, 101), 'eth1': (100, 102)}
|
||||
"""
|
||||
# vifaces - list of pairs ('eth0', ['eth0.100', 'eth0.101'])
|
||||
with utils.VlansContext(config) as vifaces:
|
||||
ifaces, vlans = zip(*vifaces)
|
||||
listeners = make_listeners(ifaces)
|
||||
|
||||
for _ in xrange(repeat):
|
||||
for i in utils.filtered_ifaces(itertools.chain(ifaces, *vlans)):
|
||||
send_dhcp_discover(i)
|
||||
time.sleep(timeout)
|
||||
|
||||
for l in listeners:
|
||||
for pkt in l.readpkts():
|
||||
yield utils.format_answer(scapy.Ether(pkt[1]), l.name)
|
||||
|
||||
|
||||
@utils.single_format
|
||||
def check_dhcp_request(iface, server, range_start, range_end, timeout=5):
|
||||
"""Provide interface, server endpoint and pool of ip adresses
|
||||
|
||||
Should be used after offer received
|
||||
>>> check_dhcp_request('eth1','10.10.0.5','10.10.0.10','10.10.0.15')
|
||||
"""
|
||||
|
||||
scapy.conf.iface = iface
|
||||
|
||||
scapy.conf.checkIPaddr = False
|
||||
|
||||
fam, hw = scapy.get_if_raw_hwaddr(iface)
|
||||
|
||||
ip_address = next(utils.pick_ip(range_start, range_end))
|
||||
|
||||
# note lxc dhcp server does not respond to unicast
|
||||
dhcp_request = (scapy.Ether(src=hw, dst="ff:ff:ff:ff:ff:ff") /
|
||||
scapy.IP(src="0.0.0.0", dst="255.255.255.255") /
|
||||
scapy.UDP(sport=68, dport=67) /
|
||||
scapy.BOOTP(chaddr=hw) /
|
||||
scapy.DHCP(options=[("message-type", "request"),
|
||||
("server_id", server),
|
||||
("requested_addr", ip_address),
|
||||
"end"]))
|
||||
ans, unans = scapy.srp(dhcp_request, nofilter=1, multi=True,
|
||||
timeout=timeout, verbose=0)
|
||||
return ans
|
|
@ -1,52 +0,0 @@
|
|||
# Copyright 2013 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 logging
|
||||
import os
|
||||
import sys
|
||||
# fixed in cmd2 >=0.6.6
|
||||
os.environ['EDITOR'] = '/usr/bin/nano'
|
||||
|
||||
from cliff.commandmanager import CommandManager
|
||||
|
||||
from fuel_network_checker import base_app
|
||||
|
||||
|
||||
class DhcpApp(base_app.BaseApp):
|
||||
DEFAULT_VERBOSE_LEVEL = 0
|
||||
LOG_FILENAME = '/var/log/dhcp_checker.log'
|
||||
|
||||
def __init__(self):
|
||||
super(DhcpApp, self).__init__(
|
||||
description='Dhcp check application',
|
||||
version='0.1',
|
||||
command_manager=CommandManager('dhcp.check'),
|
||||
)
|
||||
|
||||
def configure_logging(self):
|
||||
super(DhcpApp, self).configure_logging()
|
||||
|
||||
# set scapy logger level only to ERROR
|
||||
# due to a lot of spam
|
||||
runtime_logger = logging.getLogger('scapy.runtime')
|
||||
runtime_logger.setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
myapp = DhcpApp()
|
||||
return myapp.run(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
|
@ -1,111 +0,0 @@
|
|||
# Copyright 2013 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 json
|
||||
import logging
|
||||
|
||||
from cliff import command
|
||||
from cliff import lister
|
||||
from dhcp_checker import api
|
||||
from dhcp_checker import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseCommand(command.Command):
|
||||
"""Base command for all app"""
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BaseCommand, self).get_parser(prog_name)
|
||||
parser.add_argument('--timeout', default=5, type=int,
|
||||
help="Provide timeout for each network request")
|
||||
parser.add_argument('--repeat', default=2, type=int,
|
||||
help="Provide number of repeats for request")
|
||||
return parser
|
||||
|
||||
|
||||
class ListDhcpServers(lister.Lister, BaseCommand):
|
||||
"""Show list of dhcp servers on ethernet interfaces"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListDhcpServers, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--ifaces', metavar='I', nargs='+',
|
||||
help='If no eth provided - will run against all except lo')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('Starting dhcp discover for {0}'.format(parsed_args.ifaces))
|
||||
res = list(api.check_dhcp(
|
||||
parsed_args.ifaces,
|
||||
timeout=parsed_args.timeout,
|
||||
repeat=parsed_args.repeat))
|
||||
# NOTE(dshulyak) unfortunately cliff doesnt allow to configure
|
||||
# PrettyTable output, see link:
|
||||
# https://github.com/dhellmann/cliff/blob/master/
|
||||
# cliff/formatters/table.py#L34
|
||||
# and in case i want always print empty table if nothing found
|
||||
# it is not possible by configuration
|
||||
if not res:
|
||||
res = [{}]
|
||||
return (utils.DHCP_OFFER_COLUMNS,
|
||||
[utils.get_item_properties(item, utils.DHCP_OFFER_COLUMNS)
|
||||
for item in res])
|
||||
|
||||
|
||||
class ListDhcpAssignment(lister.Lister, BaseCommand):
|
||||
"""Make dhcp request to servers and receive acknowledgement messages"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListDhcpAssignment, self).get_parser(prog_name)
|
||||
parser.add_argument('iface',
|
||||
help='Ethernet interface name')
|
||||
parser.add_argument('endpoint',
|
||||
help='Endpoint of server or multicast group')
|
||||
parser.add_argument('--range_start', dest='range_start',
|
||||
help='Start of the range')
|
||||
parser.add_argument('--range_end', dest='range_end', default=None,
|
||||
help='Start of the range')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
res = iter(api.check_dhcp_request(
|
||||
parsed_args.iface,
|
||||
parsed_args.endpoint,
|
||||
parsed_args.range_start,
|
||||
parsed_args.range_end, timeout=parsed_args.timeout))
|
||||
first = res.next()
|
||||
columns = first.keys()
|
||||
return columns, [first.values()] + [item.values() for item in res]
|
||||
|
||||
|
||||
class DhcpWithVlansCheck(lister.Lister, BaseCommand):
|
||||
"""Provide iface with list of vlans to check
|
||||
|
||||
If no vlans created - they will be. After creation they won't be deleted.
|
||||
"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DhcpWithVlansCheck, self).get_parser(prog_name)
|
||||
parser.add_argument('config',
|
||||
help='Ethernet interface name')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
res = api.check_dhcp_with_vlans(json.loads(parsed_args.config),
|
||||
timeout=parsed_args.timeout,
|
||||
repeat=parsed_args.repeat)
|
||||
first = res.next()
|
||||
columns = first.keys()
|
||||
return columns, [first.values()] + [item.values() for item in res]
|
|
@ -1,13 +0,0 @@
|
|||
# Copyright 2013 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.
|
|
@ -1,119 +0,0 @@
|
|||
# Copyright 2013 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.
|
||||
"""
|
||||
For this tests you need
|
||||
vagrant up develop dhcp1 dhcp2
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from dhcp_checker import api
|
||||
from dhcp_checker import utils
|
||||
|
||||
|
||||
class TestDhcpServers(unittest.TestCase):
|
||||
|
||||
def test_dhcp_server_on_eth1(self):
|
||||
"""Test verifies dhcp server on eth1 iface"""
|
||||
response = api.check_dhcp_on_eth('eth1', 2)
|
||||
self.assertEqual(len(response), 1)
|
||||
# we need to guarantee that received answer has server_ip
|
||||
# but dont want to check its real address
|
||||
self.assertTrue(response[0]['server_ip'])
|
||||
|
||||
def test_dhcp_server_on_eth2(self):
|
||||
"""Test verifies dhcp server on eth2 iface"""
|
||||
response = api.check_dhcp_on_eth('eth2', 2)
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertTrue(response[0]['server_ip'])
|
||||
|
||||
|
||||
class TestDhcpUtils(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.iface_down = 'eth1'
|
||||
utils.command_util('ifconfig', self.iface_down, 'down')
|
||||
|
||||
def test_check_network_up(self):
|
||||
"""Verify that true would be returned on test network up"""
|
||||
result = utils.check_network_up('eth0')
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_check_network_down(self):
|
||||
"""Verify that false would be returned on test network down"""
|
||||
self.assertFalse(utils.check_network_up(self.iface_down))
|
||||
|
||||
def tearDown(self):
|
||||
utils.command_util('ifconfig', self.iface_down, 'up')
|
||||
|
||||
|
||||
class TestDhcpWithNetworkDown(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.iface_up = 'eth0'
|
||||
self.iface_down = 'eth2'
|
||||
utils.command_util('ifconfig', self.iface_down, 'down')
|
||||
|
||||
def test_dhcp_server_on_eth2_down(self):
|
||||
"""iface should be ifuped in case it's down and rolledback after"""
|
||||
manager = utils.IfaceState(self.iface_down)
|
||||
with manager as iface:
|
||||
response = api.check_dhcp_on_eth(iface, 2)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertTrue(response[0]['server_ip'])
|
||||
self.assertEqual(manager.pre_iface_state, 'DOWN')
|
||||
self.assertEqual(manager.iface_state, 'UP')
|
||||
self.assertEqual(manager.post_iface_state, 'DOWN')
|
||||
|
||||
def test_dhcp_server_on_eth0_up(self):
|
||||
"""Test verifies that if iface is up, it won't be touched"""
|
||||
manager = utils.IfaceState(self.iface_up)
|
||||
with manager as iface:
|
||||
response = api.check_dhcp_on_eth(iface, 2)
|
||||
|
||||
self.assertEqual(len(response), 1)
|
||||
self.assertTrue(response[0]['server_ip'])
|
||||
self.assertEqual(manager.pre_iface_state, 'UP')
|
||||
self.assertEqual(manager.iface_state, 'UP')
|
||||
self.assertEqual(manager.post_iface_state, 'UP')
|
||||
|
||||
def test_dhcp_server_on_nonexistent_iface(self):
|
||||
|
||||
def test_check():
|
||||
manager = utils.IfaceState('eth10')
|
||||
with manager as iface:
|
||||
api.check_dhcp_on_eth(iface, 2)
|
||||
self.assertRaises(EnvironmentError, test_check)
|
||||
|
||||
def tearDown(self):
|
||||
utils.command_util('ifconfig', self.iface_down, 'up')
|
||||
|
||||
|
||||
class TestMainFunctions(unittest.TestCase):
|
||||
|
||||
def test_with_vlans(self):
|
||||
config = {'eth1': (103, 105),
|
||||
'eth2': range(106, 120)}
|
||||
result = api.check_dhcp_with_vlans(config)
|
||||
self.assertEqual(len(list(result)), 2)
|
||||
|
||||
def test_with_duplicated_with_repeat(self):
|
||||
ifaces = ['eth1', 'eth2']
|
||||
result = api.check_dhcp(ifaces, repeat=3)
|
||||
self.assertEqual(len(list(result)), 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,13 +0,0 @@
|
|||
# Copyright 2013 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.
|
Binary file not shown.
|
@ -1,100 +0,0 @@
|
|||
# Copyright 2013 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 os
|
||||
import unittest
|
||||
|
||||
from mock import patch
|
||||
from scapy import all as scapy
|
||||
|
||||
from dhcp_checker import api
|
||||
|
||||
dhcp_options = [("message-type", "offer"), "end"]
|
||||
|
||||
request = (
|
||||
scapy.Ether(),
|
||||
scapy.Ether(src="", dst="ff:ff:ff:ff:ff:ff") /
|
||||
scapy.IP(src="0.0.0.0", dst="255.255.255.255") /
|
||||
scapy.UDP(sport=68, dport=67) /
|
||||
scapy.BOOTP(chaddr="") /
|
||||
scapy.DHCP(options=dhcp_options)
|
||||
)
|
||||
|
||||
expected_response = {
|
||||
'dport': 67,
|
||||
'gateway': '172.18.194.2',
|
||||
'iface': 'eth1',
|
||||
'mac': '00:15:17:ee:0a:a8',
|
||||
'message': 'offer',
|
||||
'server_id': '172.18.208.44',
|
||||
'server_ip': '172.18.194.2',
|
||||
'yiaddr': '172.18.194.35'
|
||||
}
|
||||
|
||||
|
||||
class TestDhcpApi(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
directory_path = os.path.dirname(__file__)
|
||||
self.scapy_data = list(scapy.rdpcap(os.path.join(directory_path,
|
||||
'dhcp.pcap')))
|
||||
self.dhcp_response = self.scapy_data[1:]
|
||||
|
||||
@patch('dhcp_checker.api.scapy.srp')
|
||||
@patch('dhcp_checker.api.scapy.get_if_raw_hwaddr')
|
||||
def test_check_dhcp_on_eth(self, raw_hwaddr, srp_mock):
|
||||
raw_hwaddr.return_value = ('111', '222')
|
||||
srp_mock.return_value = ([self.dhcp_response], [])
|
||||
response = api.check_dhcp_on_eth('eth1', timeout=5)
|
||||
self.assertEqual([expected_response], response)
|
||||
|
||||
@patch('dhcp_checker.api.scapy.srp')
|
||||
@patch('dhcp_checker.api.scapy.get_if_raw_hwaddr')
|
||||
def test_check_dhcp_on_eth_empty_response(self, raw_hwaddr, srp_mock):
|
||||
raw_hwaddr.return_value = ('111', '222')
|
||||
srp_mock.return_value = ([], [])
|
||||
response = api.check_dhcp_on_eth('eth1', timeout=5)
|
||||
self.assertEqual([], response)
|
||||
|
||||
@patch('dhcp_checker.api.send_dhcp_discover')
|
||||
@patch('dhcp_checker.api.make_listeners')
|
||||
def test_check_dhcp_with_multiple_ifaces(
|
||||
self, make_listeners, send_discover):
|
||||
api.check_dhcp(['eth1', 'eth2'], repeat=1)
|
||||
make_listeners.assert_called_once_with(('eth2', 'eth1'))
|
||||
self.assertEqual(send_discover.call_count, 2)
|
||||
|
||||
@patch('dhcp_checker.api.send_dhcp_discover')
|
||||
@patch('dhcp_checker.api.make_listeners')
|
||||
def test_check_dhcp_with_vlans(self, make_listeners, send_discover):
|
||||
config_sample = {
|
||||
'eth0': (100, 101),
|
||||
'eth1': (100, 102)
|
||||
}
|
||||
api.check_dhcp_with_vlans(config_sample, timeout=1)
|
||||
make_listeners.assert_called_once_with(('eth1', 'eth0'))
|
||||
self.assertEqual(send_discover.call_count, 6)
|
||||
|
||||
@patch('dhcp_checker.api.time.sleep')
|
||||
@patch('dhcp_checker.api.send_dhcp_discover')
|
||||
@patch('dhcp_checker.api.make_listeners')
|
||||
def test_check_dhcp_with_vlans_repeat_2(self, make_listeners,
|
||||
send_discover, sleep_mock):
|
||||
config_sample = {
|
||||
'eth0': (),
|
||||
}
|
||||
api.check_dhcp_with_vlans(config_sample, timeout=1, repeat=3)
|
||||
self.assertEqual(sleep_mock.call_count, 3)
|
||||
make_listeners.assert_called_once_with(('eth0',))
|
||||
self.assertEqual(send_discover.call_count, 3)
|
|
@ -1,61 +0,0 @@
|
|||
# Copyright 2013 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 json
|
||||
from mock import patch
|
||||
import unittest
|
||||
|
||||
from dhcp_checker import cli
|
||||
|
||||
expected_response = {
|
||||
'dport': 67,
|
||||
'gateway': '172.18.194.2',
|
||||
'iface': 'eth1',
|
||||
'mac': '00:15:17:ee:0a:a8',
|
||||
'message': 'offer',
|
||||
'server_id': '172.18.208.44',
|
||||
'server_ip': '172.18.194.2',
|
||||
'yiaddr': '172.18.194.35'
|
||||
}
|
||||
|
||||
|
||||
@patch('dhcp_checker.commands.api')
|
||||
class TestCommandsInterface(unittest.TestCase):
|
||||
|
||||
def test_list_dhcp_servers(self, api):
|
||||
api.check_dhcp.return_value = iter([expected_response])
|
||||
command = cli.main(['discover', '--ifaces', 'eth0', 'eth1',
|
||||
'--format', 'json'])
|
||||
self.assertEqual(command, 0)
|
||||
api.check_dhcp.assert_called_once_with(['eth0', 'eth1'],
|
||||
repeat=2, timeout=5)
|
||||
|
||||
def test_list_dhcp_assignment(self, api):
|
||||
api.check_dhcp_request.return_value = iter([expected_response])
|
||||
command = cli.main(['request', 'eth1', '10.20.0.2',
|
||||
'--range_start', '10.20.0.10',
|
||||
'--range_end', '10.20.0.20'])
|
||||
self.assertEqual(command, 0)
|
||||
api.check_dhcp_request.assert_called_once_with(
|
||||
'eth1', '10.20.0.2', '10.20.0.10', '10.20.0.20', timeout=5
|
||||
)
|
||||
|
||||
def test_list_dhcp_vlans_info(self, api):
|
||||
config_sample = {'eth1': ['100', '101'],
|
||||
'eth2': range(103, 110)}
|
||||
api.check_dhcp_with_vlans.return_value = iter([expected_response])
|
||||
command = cli.main(['vlans', json.dumps(config_sample)])
|
||||
self.assertEqual(command, 0)
|
||||
api.check_dhcp_with_vlans.assert_called_once_with(
|
||||
config_sample, repeat=2, timeout=5)
|
|
@ -1,176 +0,0 @@
|
|||
# Copyright 2013 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 mock import call
|
||||
from mock import patch
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from scapy import all as scapy
|
||||
|
||||
from dhcp_checker import utils
|
||||
|
||||
IP_LINK_SHOW_UP = (
|
||||
"2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>"
|
||||
"mtu 1500 qdisc pfifo_fast state UP qlen 1000"
|
||||
"link/ether 08:60:6e:6f:70:09 brd ff:ff:ff:ff:ff:ff"
|
||||
)
|
||||
|
||||
IP_LINK_SHOW_DOES_NOT_EXIST = 'Device "eth2" does not exist.'
|
||||
|
||||
expected_response = {
|
||||
'dport': 67,
|
||||
'gateway': '172.18.194.2',
|
||||
'iface': 'eth1',
|
||||
'mac': '00:15:17:ee:0a:a8',
|
||||
'message': 'offer',
|
||||
'server_id': '172.18.208.44',
|
||||
'server_ip': '172.18.194.2',
|
||||
'yiaddr': '172.18.194.35'
|
||||
}
|
||||
|
||||
|
||||
class TestDhcpUtils(unittest.TestCase):
|
||||
|
||||
def test_command_utils_helper(self):
|
||||
command = utils.command_util('echo', 'hello')
|
||||
self.assertEqual(command.stdout.read(), 'hello\n')
|
||||
|
||||
@patch('dhcp_checker.utils.command_util')
|
||||
def test_check_iface_state_up(self, command_util):
|
||||
command_util().stdout.read.return_value = IP_LINK_SHOW_UP
|
||||
self.assertEqual(utils._iface_state('eth0'), 'UP')
|
||||
|
||||
@patch('dhcp_checker.utils.command_util')
|
||||
def test_check_network_up(self, command_util):
|
||||
command_util().stdout.read.return_value = IP_LINK_SHOW_UP
|
||||
self.assertTrue(utils.check_network_up('eth0'))
|
||||
|
||||
@patch('dhcp_checker.utils.command_util')
|
||||
def test_check_iface_doesnot_exist(self, command_util):
|
||||
command_util().stderr.read.return_value = IP_LINK_SHOW_DOES_NOT_EXIST
|
||||
self.assertFalse(utils.check_iface_exist('eth2'))
|
||||
|
||||
@patch('dhcp_checker.utils.command_util')
|
||||
def test_check_iface_exist(self, command_util):
|
||||
command_util().stderr.read.return_value = ''
|
||||
self.assertTrue(utils.check_iface_exist('eth0'))
|
||||
|
||||
def test_filter_duplicated_results(self):
|
||||
test_data = [{'first': 'value'}, {'first': 'value'}]
|
||||
|
||||
@utils.filter_duplicated_results
|
||||
def test_func(values):
|
||||
return values
|
||||
|
||||
self.assertEqual(list(test_func(test_data)), [{'first': 'value'}])
|
||||
|
||||
def test_filter_duplicated_results_diff(self):
|
||||
test_data = [{'first': 'value'}, {'second': 'value'}]
|
||||
|
||||
@utils.filter_duplicated_results
|
||||
def test_func(values):
|
||||
return values
|
||||
|
||||
self.assertEqual(list(test_func(test_data)), test_data)
|
||||
|
||||
|
||||
class TestDhcpFormat(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
directory_path = os.path.dirname(__file__)
|
||||
self.scapy_data = list(scapy.rdpcap(os.path.join(directory_path,
|
||||
'dhcp.pcap')))
|
||||
self.dhcp_response = self.scapy_data[1:]
|
||||
|
||||
def test_single_format_decorator(self):
|
||||
"""Test modifying scapy response objects
|
||||
|
||||
Test verifies that single_format decorator contains logic to modify
|
||||
scapy response object into dict with predefined fields
|
||||
"""
|
||||
|
||||
@utils.single_format
|
||||
def tobe_decorated(iface, timeout=5):
|
||||
return [self.dhcp_response]
|
||||
|
||||
self.assertEqual(tobe_decorated('eth1'), [expected_response])
|
||||
|
||||
def test_order_preserver(self):
|
||||
example = {'first': 'first', 'second': 'second'}
|
||||
columns = ['second', 'first']
|
||||
items = utils.get_item_properties(example, columns)
|
||||
self.assertEqual(columns, items)
|
||||
|
||||
|
||||
class TestMultiprocMap(unittest.TestCase):
|
||||
"""Check if multiproc_map decorator works the same for different arg types
|
||||
|
||||
Test verifies that working with function decorated by multiproc_map
|
||||
will work indifirently either args passed as tuple, or *args
|
||||
"""
|
||||
|
||||
def test_multiproc_map_first_tuple(self):
|
||||
|
||||
@utils.multiproc_map
|
||||
def test_multiproc(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
rargs, rkwargs = test_multiproc(('h', 'e'))
|
||||
self.assertEqual(rargs, ('h', 'e'))
|
||||
self.assertEqual(rkwargs, {})
|
||||
|
||||
def test_multiproc_mapn_normal(self):
|
||||
|
||||
@utils.multiproc_map
|
||||
def test_multiproc(*args, **kwargs):
|
||||
return args, kwargs
|
||||
|
||||
rargs, rkwargs = test_multiproc('h', 'e')
|
||||
self.assertEqual(rargs, ('h', 'e'))
|
||||
self.assertEqual(rkwargs, {})
|
||||
|
||||
|
||||
@patch('dhcp_checker.utils._iface_state')
|
||||
@patch('dhcp_checker.utils.command_util')
|
||||
class TestIfaceStateHelper(unittest.TestCase):
|
||||
|
||||
def test_iface_is_up(self, command, iface_state):
|
||||
iface_value = iter(('UP',) * 3)
|
||||
iface_state.side_effect = lambda *args, **kwargs: next(iface_value)
|
||||
with utils.IfaceState('eth1') as iface:
|
||||
self.assertEqual(iface, 'eth1')
|
||||
self.assertEqual(iface_state.call_count, 2)
|
||||
self.assertEqual(command.call_count, 0)
|
||||
|
||||
def test_iface_is_down(self, command, iface_state):
|
||||
iface_value = iter(('DOWN', 'UP', 'DOWN'))
|
||||
iface_state.side_effect = lambda *args, **kwargs: next(iface_value)
|
||||
with utils.IfaceState('eth1') as iface:
|
||||
self.assertEqual(iface, 'eth1')
|
||||
self.assertEqual(iface_state.call_count, 3)
|
||||
self.assertEqual(command.call_count, 2)
|
||||
self.assertEqual(command.call_args_list,
|
||||
[call('ifconfig', 'eth1', 'up'),
|
||||
call('ifconfig', 'eth1', 'down')])
|
||||
|
||||
def test_iface_cant_ifup(self, command, iface_state):
|
||||
iface_value = iter(('DOWN',) * 10)
|
||||
iface_state.side_effect = lambda *args, **kwargs: next(iface_value)
|
||||
|
||||
def test_raises():
|
||||
with utils.IfaceState('eth1', retry=4) as iface:
|
||||
self.assertEqual(iface, 'eth1')
|
||||
self.assertRaises(EnvironmentError, test_raises)
|
||||
self.assertEqual(command.call_count, 4)
|
|
@ -1,236 +0,0 @@
|
|||
# Copyright 2013 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 functools
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from scapy import all as scapy
|
||||
|
||||
|
||||
DHCP_OFFER_COLUMNS = ('iface', 'mac', 'server_ip', 'server_id', 'gateway',
|
||||
'dport', 'message', 'yiaddr')
|
||||
|
||||
|
||||
def command_util(*command):
|
||||
"""object with stderr and stdout"""
|
||||
return subprocess.Popen(command, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
def _check_vconfig():
|
||||
"""Check vconfig installed or not"""
|
||||
return not command_util('which', 'vconfig').stderr.read()
|
||||
|
||||
|
||||
def _iface_state(iface):
|
||||
"""For a given iface return it's state
|
||||
|
||||
returns UP, DOWN, UNKNOWN
|
||||
"""
|
||||
state = command_util('ip', 'link', 'show', iface).stdout.read()
|
||||
search_result = re.search(r'.*<(?P<state>.*)>.*', state)
|
||||
if search_result:
|
||||
state_list = search_result.groupdict().get('state', [])
|
||||
if 'UP' in state_list:
|
||||
return 'UP'
|
||||
else:
|
||||
return 'DOWN'
|
||||
return 'UNKNOWN'
|
||||
|
||||
|
||||
def check_network_up(iface):
|
||||
return _iface_state(iface) == 'UP'
|
||||
|
||||
|
||||
def check_iface_exist(iface):
|
||||
"""Check provided interface exists"""
|
||||
return not command_util("ip", "link", "show", iface).stderr.read()
|
||||
|
||||
|
||||
def filtered_ifaces(ifaces):
|
||||
for iface in ifaces:
|
||||
if not check_iface_exist(iface):
|
||||
sys.stderr.write('Iface {0} does not exist.'.format(iface))
|
||||
else:
|
||||
if not check_network_up(iface):
|
||||
sys.stderr.write('Network for iface {0} is down.'.format(
|
||||
iface))
|
||||
else:
|
||||
yield iface
|
||||
|
||||
|
||||
def pick_ip(range_start, range_end):
|
||||
"""Given start_range, end_range generate list of ips
|
||||
|
||||
>>> next(pick_ip('192.168.1.10','192.168.1.13'))
|
||||
'192.168.1.10'
|
||||
"""
|
||||
split_address = lambda ip_address: \
|
||||
[int(item) for item in ip_address.split('.')]
|
||||
range_start = split_address(range_start)
|
||||
range_end = split_address(range_end)
|
||||
i = 0
|
||||
# ipv4 subnet cant be longer that 4 items
|
||||
while i < 4:
|
||||
# 255 - end of subnet
|
||||
if not range_start[i] == range_end[i] and range_start[i] < 255:
|
||||
yield '.'.join([str(item) for item in range_start])
|
||||
range_start[i] += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
|
||||
def get_item_properties(item, columns):
|
||||
"""Get specified in columns properties, with preserved order.
|
||||
|
||||
Required for correct cli table generation
|
||||
|
||||
:param item: dict
|
||||
:param columns: list with arbitrary keys
|
||||
"""
|
||||
properties = []
|
||||
for key in columns:
|
||||
properties.append(item.get(key, ''))
|
||||
return properties
|
||||
|
||||
|
||||
def format_options(options):
|
||||
"""Util for serializing dhcp options
|
||||
|
||||
@options = [1,2,3]
|
||||
>>> format_options([1, 2, 3])
|
||||
'\x01\x02\x03'
|
||||
"""
|
||||
return "".join((chr(item) for item in options))
|
||||
|
||||
|
||||
def _dhcp_options(dhcp_options):
|
||||
"""Dhcp options returned by scapy is not in usable format
|
||||
|
||||
[('message-type', 2), ('server_id', '192.168.0.5'),
|
||||
('name_server', '192.168.0.1', '192.168.0.2'), 'end']
|
||||
"""
|
||||
for option in dhcp_options:
|
||||
if isinstance(option, (tuple, list)):
|
||||
header = option[0]
|
||||
if len(option[1:]) > 1:
|
||||
yield (header, option)
|
||||
else:
|
||||
yield (header, option[1])
|
||||
|
||||
|
||||
def format_answer(ans, iface):
|
||||
dhcp_options = dict(_dhcp_options(ans[scapy.DHCP].options))
|
||||
results = (
|
||||
iface, ans[scapy.Ether].src, ans[scapy.IP].src,
|
||||
dhcp_options['server_id'], ans[scapy.BOOTP].giaddr,
|
||||
ans[scapy.UDP].sport,
|
||||
scapy.DHCPTypes[dhcp_options['message-type']],
|
||||
ans[scapy.BOOTP].yiaddr)
|
||||
return dict(zip(DHCP_OFFER_COLUMNS, results))
|
||||
|
||||
|
||||
def single_format(func):
|
||||
"""Manage format of dhcp response"""
|
||||
@functools.wraps(func)
|
||||
def formatter(*args, **kwargs):
|
||||
iface = args[0]
|
||||
ans = func(*args, **kwargs)
|
||||
# scapy stores all sequence of requests
|
||||
# so ans[0][1] would be response to first request
|
||||
return [format_answer(response[1], iface) for response in ans]
|
||||
return formatter
|
||||
|
||||
|
||||
def multiproc_map(func):
|
||||
# multiproc map could not work with format *args
|
||||
@functools.wraps(func)
|
||||
def workaround(*args, **kwargs):
|
||||
args = args[0] if isinstance(args[0], (tuple, list)) else args
|
||||
return func(*args, **kwargs)
|
||||
return workaround
|
||||
|
||||
|
||||
def filter_duplicated_results(func):
|
||||
# due to network infra on broadcast multiple duplicated results
|
||||
# returned. This helper filter them out
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
resp = func(*args, **kwargs)
|
||||
return (dict(t) for t in set([tuple(d.items()) for d in resp]))
|
||||
return wrapper
|
||||
|
||||
|
||||
class VlansContext(object):
|
||||
"""Contains all logic to manage vlans"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize VlansContext
|
||||
|
||||
@config - list or tuple of (iface, vlan) pairs
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def __enter__(self):
|
||||
for iface, vlans in self.config.iteritems():
|
||||
vifaces = []
|
||||
for vlan in vlans:
|
||||
if vlan > 0:
|
||||
vifaces.append('{0}.{1}'.format(iface, vlan))
|
||||
yield str(iface), vifaces
|
||||
|
||||
def __exit__(self, type, value, trace):
|
||||
pass
|
||||
|
||||
|
||||
class IfaceState(object):
|
||||
"""Context manager to control state of iface while dhcp checker runs"""
|
||||
|
||||
def __init__(self, iface, rollback=True, retry=3):
|
||||
self.rollback = rollback
|
||||
self.retry = retry
|
||||
self.iface = iface
|
||||
self.pre_iface_state = _iface_state(iface)
|
||||
self.iface_state = self.pre_iface_state
|
||||
self.post_iface_state = ''
|
||||
|
||||
def iface_up(self):
|
||||
while self.retry and self.iface_state != 'UP':
|
||||
command_util('ifconfig', self.iface, 'up')
|
||||
self.iface_state = _iface_state(self.iface)
|
||||
self.retry -= 1
|
||||
if self.iface_state != 'UP':
|
||||
raise EnvironmentError(
|
||||
'Tried my best to ifup iface {0}.'.format(self.iface))
|
||||
|
||||
def __enter__(self):
|
||||
self.iface_up()
|
||||
return self.iface
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.pre_iface_state != 'UP' and self.rollback:
|
||||
command_util('ifconfig', self.iface, 'down')
|
||||
self.post_iface_state = _iface_state(self.iface)
|
||||
|
||||
|
||||
def create_mac_filter(iface):
|
||||
"""tcpdump can not catch all 6 octets so it is splitted
|
||||
|
||||
See http://blog.jasonantman.com/2010/04/dhcp-debugging-and-handy-tcpdump-filters # noqa
|
||||
"""
|
||||
mac = scapy.get_if_hwaddr(iface).split(':')
|
||||
filter1 = '(udp[36:2] = 0x{0})'.format(''.join(mac[:2]))
|
||||
filter2 = '(udp[38:4] = 0x{0})'.format(''.join(mac[2:]))
|
||||
return '{0} and {1}'.format(filter1, filter2)
|
|
@ -1,43 +0,0 @@
|
|||
# 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 logging
|
||||
from logging import handlers
|
||||
|
||||
from cliff.app import App
|
||||
|
||||
|
||||
class BaseApp(App):
|
||||
DEFAULT_VERBOSE_LEVEL = 0
|
||||
LOG_FILENAME = '' # This needs to be redefined in child class
|
||||
|
||||
def configure_logging(self):
|
||||
super(BaseApp, self).configure_logging()
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s (%(module)s) %(message)s',
|
||||
"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.ERROR)
|
||||
stream_handler.setFormatter(formatter)
|
||||
|
||||
file_handler = handlers.TimedRotatingFileHandler(
|
||||
self.LOG_FILENAME)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(stream_handler)
|
||||
logger.addHandler(file_handler)
|
|
@ -1,62 +0,0 @@
|
|||
# Copyright 2014 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 stevedore import driver
|
||||
|
||||
from network_checker import config
|
||||
from network_checker import daemon
|
||||
from network_checker import xmlrpc
|
||||
|
||||
|
||||
class Api(object):
|
||||
|
||||
namespace = 'network_checker'
|
||||
|
||||
def __init__(self, verification, **kwargs):
|
||||
self.verification = verification
|
||||
self.server_config = config.get_config()[verification]
|
||||
self.verification_config = dict(self.server_config['defaults'],
|
||||
**kwargs)
|
||||
|
||||
def serve(self):
|
||||
daemon.cleanup(self.server_config)
|
||||
self.manager = driver.DriverManager(
|
||||
self.namespace,
|
||||
self.verification,
|
||||
invoke_on_load=True,
|
||||
invoke_kwds=self.verification_config)
|
||||
self.driver = self.manager.driver
|
||||
rpc_server = xmlrpc.get_server(self.server_config)
|
||||
# TODO(dshulyak) verification api should know what methods to serve
|
||||
rpc_server.register_function(self.driver.listen, 'listen')
|
||||
rpc_server.register_function(self.driver.send, 'send')
|
||||
rpc_server.register_function(self.driver.get_info, 'get_info')
|
||||
rpc_server.register_function(self.driver.test, 'test')
|
||||
return daemon.run_server(rpc_server, self.server_config)
|
||||
|
||||
def listen(self):
|
||||
return xmlrpc.get_client(self.server_config).listen()
|
||||
|
||||
def send(self):
|
||||
return xmlrpc.get_client(self.server_config).send()
|
||||
|
||||
def info(self):
|
||||
return xmlrpc.get_client(self.server_config).get_info()
|
||||
|
||||
def clean(self):
|
||||
return daemon.cleanup(self.server_config)
|
||||
|
||||
def test(self):
|
||||
return xmlrpc.get_client(self.server_config).test()
|
|
@ -1,62 +0,0 @@
|
|||
# Copyright 2014 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.
|
||||
"""Fuel network checker
|
||||
Available verifications: multicast
|
||||
|
||||
Example:
|
||||
fuel-netcheck multicast serve listen send info clean
|
||||
|
||||
Multicast config options:
|
||||
group: 225.0.0.250
|
||||
port: 13310
|
||||
ttl: 5
|
||||
uid: '1001'
|
||||
repeat: 3
|
||||
iface: eth0
|
||||
timeout: 10
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import textwrap
|
||||
|
||||
from network_checker import api
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=textwrap.dedent(__doc__),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument(
|
||||
'verification',
|
||||
help="Type of verification that should be started.")
|
||||
parser.add_argument(
|
||||
'actions', nargs='+',
|
||||
help="List of actions to perform.")
|
||||
# TODO(dshulyak) Add posibility to provide args like regular shell args
|
||||
parser.add_argument(
|
||||
'-c', '--config', default='{}',
|
||||
help="User defined configuration in json format.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
user_data = json.loads(args.config)
|
||||
api_instance = api.Api(args.verification, **user_data)
|
||||
# Print only last response if user requires multiple sequantial actions
|
||||
for action in args.actions:
|
||||
result = getattr(api_instance, action)()
|
||||
print(json.dumps(result))
|
|
@ -1,30 +0,0 @@
|
|||
# Copyright 2014 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 os
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def _get_config_path():
|
||||
if os.path.exists('/etc/network_checker/config.yaml'):
|
||||
return '/etc/network_checker/config.yaml'
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'config.yaml')
|
||||
|
||||
|
||||
def get_config():
|
||||
with open(_get_config_path()) as conf:
|
||||
return yaml.load(conf.read())
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
multicast:
|
||||
app: multicast
|
||||
pidfile: /var/run/multicast.pid
|
||||
unix: /var/run/multicast.sock
|
||||
defaults:
|
||||
group: 225.0.0.250
|
||||
port: 13310
|
||||
ttl: 5
|
||||
uid: '1001'
|
||||
repeat: 3
|
||||
iface: eth0
|
||||
timeout: 10
|
|
@ -1,51 +0,0 @@
|
|||
# Copyright 2014 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 logging
|
||||
import os
|
||||
|
||||
import daemonize
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_server(server, config):
|
||||
daemon = daemonize.Daemonize(
|
||||
app=config['app'],
|
||||
pid=config['pidfile'],
|
||||
action=server.serve_forever,
|
||||
# keep open stdin, stdout, stderr and socket file
|
||||
keep_fds=[0, 1, 2, server.fileno()])
|
||||
try:
|
||||
daemon.start()
|
||||
# this is required to do some stuff after server is daemonized
|
||||
except SystemExit as e:
|
||||
if e.code is 0:
|
||||
return True
|
||||
raise
|
||||
|
||||
|
||||
def cleanup(config):
|
||||
if os.path.exists(config['unix']):
|
||||
os.unlink(config['unix'])
|
||||
if os.path.exists(config['pidfile']):
|
||||
with open(config['pidfile'], 'r') as f:
|
||||
pid = f.read().strip('\n')
|
||||
try:
|
||||
os.kill(int(pid), 9)
|
||||
except OSError:
|
||||
# it is ok if proc already stopped
|
||||
pass
|
||||
os.unlink(config['pidfile'])
|
||||
return True
|
|
@ -1,93 +0,0 @@
|
|||
# Copyright 2014 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 logging
|
||||
import socket
|
||||
import struct
|
||||
|
||||
import pcap
|
||||
import scapy.all as scapy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# it is not defined in every python version
|
||||
SO_BINDTODEVICE = 25
|
||||
|
||||
|
||||
class MulticastChecker(object):
|
||||
|
||||
def __init__(self, group='225.0.0.250', port='13100',
|
||||
uid='999', iface='eth0',
|
||||
ttl=1, repeat=1, timeout=3):
|
||||
self.group = group
|
||||
self.port = int(port)
|
||||
self.ttl = ttl
|
||||
self.uid = uid
|
||||
self.repeat = repeat
|
||||
self.timeout = timeout
|
||||
self.receiver = None
|
||||
self.messages = []
|
||||
self.iface = iface
|
||||
|
||||
def send(self):
|
||||
ttl_data = struct.pack('@i', self.ttl)
|
||||
_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL,
|
||||
ttl_data)
|
||||
_socket.setsockopt(
|
||||
socket.SOL_SOCKET,
|
||||
SO_BINDTODEVICE, self.iface)
|
||||
|
||||
for _ in xrange(self.repeat):
|
||||
_socket.sendto(self.uid, (self.group, self.port))
|
||||
return {'group': self.group,
|
||||
'port': self.port,
|
||||
'iface': self.iface,
|
||||
'uid': self.uid}
|
||||
|
||||
def listen(self):
|
||||
self._register_group()
|
||||
self._start_listeners()
|
||||
return {'group': self.group,
|
||||
'port': self.port,
|
||||
'iface': self.iface,
|
||||
'uid': self.uid}
|
||||
|
||||
def _register_group(self):
|
||||
self.receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.receiver.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.receiver.setsockopt(
|
||||
socket.SOL_SOCKET,
|
||||
SO_BINDTODEVICE, self.iface)
|
||||
self.receiver.bind(('', self.port))
|
||||
group_packed = socket.inet_aton(self.group)
|
||||
group_data = struct.pack('4sL', group_packed, socket.INADDR_ANY)
|
||||
self.receiver.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
|
||||
group_data)
|
||||
|
||||
def _start_listeners(self):
|
||||
self.listener = pcap.pcap(self.iface)
|
||||
udp_filter = 'udp and dst port {0}'.format(self.port)
|
||||
self.listener.setfilter(udp_filter)
|
||||
|
||||
def get_info(self):
|
||||
for sock, pack in self.listener.readpkts():
|
||||
pack = scapy.Ether(pack)
|
||||
data, _ = pack[scapy.UDP].extract_padding(pack[scapy.UDP].load)
|
||||
self.messages.append(data.decode())
|
||||
self.receiver.close()
|
||||
return list(set(self.messages))
|
||||
|
||||
def test(self):
|
||||
return {'test': 'test'}
|
|
@ -1,13 +0,0 @@
|
|||
# Copyright 2014 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.
|
|
@ -1,706 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 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.
|
||||
|
||||
#
|
||||
# Generate and send Ethernet packets to specified interfaces.
|
||||
# Collect data from interfaces.
|
||||
# Analyse dumps for packets with special cookie in UDP payload.
|
||||
#
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import logging.handlers
|
||||
|
||||
|
||||
from scapy import config as scapy_config
|
||||
|
||||
scapy_config.logLevel = 40
|
||||
scapy_config.use_pcap = True
|
||||
import scapy.all as scapy
|
||||
from scapy import error
|
||||
|
||||
from scapy.utils import PcapReader
|
||||
|
||||
from network_checker.net_check.utils import signal_timeout
|
||||
|
||||
|
||||
class ActorFabric(object):
|
||||
@classmethod
|
||||
def getInstance(cls, config):
|
||||
if config.get('action') not in ('listen', 'generate'):
|
||||
raise Exception(
|
||||
'Wrong config, you need define '
|
||||
'valid action instead of {0}'.format(config.get('action')))
|
||||
if config['action'] in ('listen',):
|
||||
return Listener(config)
|
||||
elif config['action'] in ('generate',):
|
||||
return Sender(config)
|
||||
|
||||
|
||||
class ActorException(Exception):
|
||||
def __init__(self, logger, message='', level='error'):
|
||||
getattr(logger, level, logger.error)(message)
|
||||
super(ActorException, self).__init__(message)
|
||||
|
||||
|
||||
class Actor(object):
|
||||
def __init__(self, config=None):
|
||||
self.config = {
|
||||
'src_mac': None,
|
||||
'src': '198.18.1.1',
|
||||
'dst': '198.18.1.2',
|
||||
'sport': 31337,
|
||||
'dport': 31337,
|
||||
'cookie': "Nailgun:",
|
||||
'pcap_dir': "/var/run/pcap_dir/",
|
||||
'duration': 5,
|
||||
'repeat': 1,
|
||||
'collect_timeout': 300,
|
||||
}
|
||||
if config:
|
||||
self.config.update(config)
|
||||
|
||||
self.logger.debug("Running with config: %s", json.dumps(self.config))
|
||||
self.iface_down_after = {}
|
||||
self.viface_remove_after = {}
|
||||
|
||||
def _define_logger(self, filename=None,
|
||||
appname='netprobe', level=logging.DEBUG):
|
||||
logger = logging.getLogger(appname)
|
||||
logger.setLevel(level)
|
||||
|
||||
syslog_formatter = logging.Formatter(
|
||||
'{appname}: %(message)s'.format(appname=appname)
|
||||
)
|
||||
syslog_handler = logging.handlers.SysLogHandler('/dev/log')
|
||||
syslog_handler.setFormatter(syslog_formatter)
|
||||
logger.addHandler(syslog_handler)
|
||||
|
||||
# A syslog handler should be always. But a file handler is the option.
|
||||
# If you don't want it you can keep 'filename' variable as None to skip
|
||||
# this handler.
|
||||
if filename:
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(name)s %(message)s'
|
||||
)
|
||||
|
||||
file_handler = logging.FileHandler(filename)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
def _execute(self, command, expected_exit_codes=(0,)):
|
||||
self.logger.debug("Running command: %s" % " ".join(command))
|
||||
env = os.environ
|
||||
env["PATH"] = "/bin:/usr/bin:/sbin:/usr/sbin"
|
||||
p = subprocess.Popen(command, shell=False,
|
||||
env=env, stdout=subprocess.PIPE)
|
||||
output, _ = p.communicate()
|
||||
if p.returncode not in expected_exit_codes:
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Command exited with error: %s: %s" % (" ".join(command),
|
||||
p.returncode)
|
||||
)
|
||||
return output.split('\n')
|
||||
|
||||
def _viface_by_iface_vid(self, iface, vid):
|
||||
return (self._try_viface_create(iface, vid) or "%s.%d" % (iface, vid))
|
||||
|
||||
def _iface_name(self, iface, vid=None):
|
||||
if vid:
|
||||
return self._viface_by_iface_vid(iface, vid)
|
||||
return iface
|
||||
|
||||
def _look_for_link(self, iface, vid=None):
|
||||
viface = None
|
||||
if vid:
|
||||
viface = self._viface_by_iface_vid(iface, vid)
|
||||
|
||||
command = ['ip', 'link']
|
||||
r = re.compile(ur"(\d+?):\s+((?P<viface>[^:@]+)@)?(?P<iface>[^:]+?):"
|
||||
".+?(?P<state>UP|DOWN|UNKNOWN).*$")
|
||||
for line in self._execute(command):
|
||||
m = r.search(line)
|
||||
if m:
|
||||
md = m.groupdict()
|
||||
if (iface == md.get('iface') and
|
||||
viface == md.get('viface') and md.get('state')):
|
||||
return (iface, viface, md.get('state'))
|
||||
# If we are here we aren't able to say if iface with vid is up
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Cannot find interface %s with vid=%s" % (iface, vid)
|
||||
)
|
||||
|
||||
def _try_iface_up(self, iface, vid=None):
|
||||
if vid and not self._try_viface_create(iface, vid):
|
||||
# if viface does not exist we raise exception
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Vlan %s on interface %s does not exist" % (str(vid), iface)
|
||||
)
|
||||
|
||||
self.logger.debug("Checking if interface %s with vid %s is up",
|
||||
iface, str(vid))
|
||||
_, _, state = self._look_for_link(iface, vid)
|
||||
return (state == 'UP')
|
||||
|
||||
def _iface_up(self, iface, vid=None):
|
||||
"""Brings interface with vid up"""
|
||||
if vid and not self._try_viface_create(iface, vid):
|
||||
# if viface does not exist we raise exception
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Vlan %s on interface %s does not exist" % (str(vid), iface)
|
||||
)
|
||||
|
||||
set_iface = self._iface_name(iface, vid)
|
||||
|
||||
self.logger.debug("Brining interface %s with vid %s up",
|
||||
set_iface, str(vid))
|
||||
self._execute([
|
||||
"ip",
|
||||
"link", "set",
|
||||
"dev", set_iface,
|
||||
"up"])
|
||||
|
||||
def _ensure_iface_up(self, iface, vid=None):
|
||||
"""Ensures interface is with vid up."""
|
||||
if not self._try_iface_up(iface, vid):
|
||||
# if iface is not up we try to bring it up
|
||||
self._iface_up(iface, vid)
|
||||
if self._try_iface_up(iface, vid):
|
||||
# if iface was down and we have brought it up
|
||||
# we should mark it to be brought down after probing
|
||||
self.iface_down_after[self._iface_name(iface, vid)] = True
|
||||
else:
|
||||
# if viface is still down we raise exception
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Can not bring interface %s with vid %s up" % (iface,
|
||||
str(vid))
|
||||
)
|
||||
|
||||
def _ensure_iface_down(self, iface, vid=None):
|
||||
set_iface = self._iface_name(iface, vid)
|
||||
if self.iface_down_after.get(set_iface, False):
|
||||
# if iface with vid have been marked to be brought down
|
||||
# after probing we try to bring it down
|
||||
self.logger.debug("Brining down interface %s with vid %s",
|
||||
iface, str(vid))
|
||||
self._execute([
|
||||
"ip",
|
||||
"link", "set",
|
||||
"dev", set_iface,
|
||||
"down"])
|
||||
self.iface_down_after.pop(set_iface)
|
||||
|
||||
def _try_viface_create(self, iface, vid):
|
||||
"""Tries to find vlan interface on iface with VLAN_ID=vid and return it
|
||||
|
||||
:returns: name of vlan interface if it exists or None
|
||||
"""
|
||||
self.logger.debug("Checking if vlan %s on interface %s exists",
|
||||
str(vid), iface)
|
||||
with open("/proc/net/vlan/config", "r") as f:
|
||||
for line in f:
|
||||
m = re.search(ur'(.+?)\s+\|\s+(.+?)\s+\|\s+(.+?)\s*$', line)
|
||||
if m and m.group(2) == str(vid) and m.group(3) == iface:
|
||||
return m.group(1)
|
||||
|
||||
def _viface_create(self, iface, vid):
|
||||
"""Creates VLAN interface with VLAN_ID=vid on interface iface
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
self.logger.debug("Creating vlan %s on interface %s", str(vid), iface)
|
||||
self._execute([
|
||||
"ip",
|
||||
"link", "add",
|
||||
"link", iface,
|
||||
"name", self._viface_by_iface_vid(iface, vid),
|
||||
"type", "vlan",
|
||||
"id", str(vid)])
|
||||
|
||||
def _ensure_viface_create(self, iface, vid):
|
||||
"""Ensures that vlan interface exists.
|
||||
|
||||
If it does not already
|
||||
exist, then we need it to be created. It also marks newly created
|
||||
vlan interface to remove it after probing procedure.
|
||||
"""
|
||||
if not self._try_viface_create(iface, vid):
|
||||
# if viface does not exist we try to create it
|
||||
self._viface_create(iface, vid)
|
||||
if self._try_viface_create(iface, vid):
|
||||
# if viface had not existed and have been created
|
||||
# we mark it to be removed after probing procedure
|
||||
self.viface_remove_after[
|
||||
self._viface_by_iface_vid(iface, vid)
|
||||
] = True
|
||||
else:
|
||||
# if viface had not existed and still does not
|
||||
# we raise exception
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Can not create vlan %d on interface %s" % (vid, iface)
|
||||
)
|
||||
|
||||
def _ensure_viface_remove(self, iface, vid):
|
||||
viface = self._viface_by_iface_vid(iface, vid)
|
||||
if self.viface_remove_after.get(viface, False):
|
||||
# if viface have been marked to be removed after probing
|
||||
# we try to remove it
|
||||
self.logger.debug("Removing vlan %s on interface %s",
|
||||
str(vid), iface)
|
||||
self._execute([
|
||||
"ip",
|
||||
"link", "del",
|
||||
"dev", viface])
|
||||
self.viface_remove_after.pop(viface)
|
||||
|
||||
def _parse_vlan_list(self, vlan_string):
|
||||
self.logger.debug("Parsing vlan list: %s", vlan_string)
|
||||
validate = lambda x: (x >= 0) and (x < 4095)
|
||||
chunks = vlan_string.split(",")
|
||||
vlan_list = []
|
||||
for chunk in chunks:
|
||||
delim = chunk.find("-")
|
||||
try:
|
||||
if delim > 0:
|
||||
left = int(chunk[:delim])
|
||||
right = int(chunk[delim + 1:])
|
||||
if validate(left) and validate(right):
|
||||
vlan_list.extend(xrange(left, right + 1))
|
||||
else:
|
||||
raise ValueError
|
||||
else:
|
||||
vlan = int(chunk)
|
||||
if validate(vlan):
|
||||
vlan_list.append(vlan)
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ActorException(self.logger, "Incorrect vlan: %s" % chunk)
|
||||
self.logger.debug("Parsed vlans: %s", str(vlan_list))
|
||||
return vlan_list
|
||||
|
||||
def _ensure_viface_create_and_up(self, iface, vid):
|
||||
self._ensure_viface_create(iface, vid)
|
||||
self._ensure_iface_up(iface, vid)
|
||||
|
||||
def _ensure_viface_down_and_remove(self, iface, vid):
|
||||
self._ensure_iface_down(iface, vid)
|
||||
self._ensure_viface_remove(iface, vid)
|
||||
|
||||
def _iface_vlan_iterator(self):
|
||||
for iface, vlan_list in self.config['interfaces'].iteritems():
|
||||
# Variables iface and vlan_list are getted from decoded JSON
|
||||
# and json.dump convert all string data to Python unicode string.
|
||||
# We use these variables in logging messages later.
|
||||
# CentOS 6.4 uses Python 2.6 and logging module 0.5.0.5 which has
|
||||
# a bug with converting unicode strings to message in
|
||||
# SysLogHandler. So we need to convert all unicode to plain
|
||||
# strings to avoid syslog message corruption.
|
||||
for vlan in self._parse_vlan_list(str(vlan_list)):
|
||||
yield (str(iface), vlan)
|
||||
|
||||
def _iface_iterator(self):
|
||||
for iface in self.config['interfaces']:
|
||||
yield iface
|
||||
|
||||
def _log_ifaces(self, prefix="Current interfaces"):
|
||||
self.logger.debug("%s: ", prefix)
|
||||
for line in self._execute(['ip', 'address']):
|
||||
self.logger.debug(line.rstrip())
|
||||
|
||||
|
||||
class Sender(Actor):
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.logger = self._define_logger('/var/log/netprobe_sender.log',
|
||||
'netprobe_sender')
|
||||
super(Sender, self).__init__(config)
|
||||
self.logger.info("=== Starting Sender ===")
|
||||
self._log_ifaces("Interfaces just before sending probing packages")
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
self.logger.error("An internal error occured: %s\n%s", str(e),
|
||||
traceback.format_exc())
|
||||
|
||||
def _get_iface_mac(self, iface):
|
||||
path = '/sys/class/net/{iface}/address'.format(iface=iface)
|
||||
with open(path, 'r') as address:
|
||||
return address.read().strip('\n')
|
||||
|
||||
def _run(self):
|
||||
for iface, vlan in self._iface_vlan_iterator():
|
||||
self._ensure_iface_up(iface)
|
||||
self._send_packets()
|
||||
self._log_ifaces("Interfaces just after sending probing packages")
|
||||
for iface in self._iface_iterator():
|
||||
self._ensure_iface_down(iface)
|
||||
self._log_ifaces("Interfaces just after ensuring them down in sender")
|
||||
self.logger.info("=== Sender Finished ===")
|
||||
|
||||
def _send_packets(self):
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time <= self.config['duration']:
|
||||
for iface, vlan in self._iface_vlan_iterator():
|
||||
self.logger.debug("Sending packets: iface=%s vlan=%s",
|
||||
iface, str(vlan))
|
||||
for _ in xrange(self.config['repeat']):
|
||||
self._sendp(iface, vlan)
|
||||
|
||||
def _sendp(self, iface, vlan):
|
||||
try:
|
||||
data = str(''.join((self.config['cookie'], iface, ' ',
|
||||
self.config['uid'])))
|
||||
|
||||
p = scapy.Ether(src=self._get_iface_mac(iface),
|
||||
dst="ff:ff:ff:ff:ff:ff")
|
||||
if vlan > 0:
|
||||
p = p / scapy.Dot1Q(vlan=vlan)
|
||||
p = p / scapy.IP(src=self.config['src'], dst=self.config['dst'])
|
||||
p = p / scapy.UDP(sport=self.config['sport'],
|
||||
dport=self.config['dport']) / data
|
||||
scapy.sendp(p, iface=iface)
|
||||
except socket.error as e:
|
||||
self.logger.error("Socket error: %s, %s", e, iface)
|
||||
|
||||
|
||||
class Listener(Actor):
|
||||
def __init__(self, config=None):
|
||||
self.logger = self._define_logger('/var/log/netprobe_listener.log',
|
||||
'netprobe_listener')
|
||||
super(Listener, self).__init__(config)
|
||||
self.logger.info("=== Starting Listener ===")
|
||||
self._log_ifaces("Interfaces just before starting listerning "
|
||||
"for probing packages")
|
||||
|
||||
self.pidfile = self.addpid('/var/run/net_probe')
|
||||
|
||||
self.neighbours = {}
|
||||
self._define_pcap_dir()
|
||||
|
||||
def addpid(self, piddir):
|
||||
pid = os.getpid()
|
||||
if not os.path.exists(piddir):
|
||||
os.mkdir(piddir)
|
||||
pidfile = os.path.join(piddir, str(pid))
|
||||
with open(pidfile, 'w') as fo:
|
||||
fo.write('')
|
||||
return pidfile
|
||||
|
||||
def _define_pcap_dir(self):
|
||||
if os.path.exists(self.config['pcap_dir']):
|
||||
shutil.rmtree(self.config['pcap_dir'])
|
||||
os.mkdir(self.config['pcap_dir'])
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
self.logger.error("An internal error occured: %s\n%s", str(e),
|
||||
traceback.format_exc())
|
||||
|
||||
def _run(self):
|
||||
sniffers = set()
|
||||
listeners = []
|
||||
|
||||
for iface in self._iface_iterator():
|
||||
self._ensure_iface_up(iface)
|
||||
if iface not in sniffers:
|
||||
listeners.append(self.get_probe_frames(iface))
|
||||
listeners.append(self.get_probe_frames(iface, vlan=True))
|
||||
sniffers.add(iface)
|
||||
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect((self.config.get('ready_address', 'locahost'),
|
||||
self.config.get('ready_port', 31338)))
|
||||
except socket.error as e:
|
||||
self.logger.error("Socket error: %s", e)
|
||||
else:
|
||||
self.logger.debug("Listener threads have been launched. "
|
||||
"Reporting READY.")
|
||||
msg = "READY"
|
||||
total_sent = 0
|
||||
while total_sent < len(msg):
|
||||
sent = s.send(msg[total_sent:])
|
||||
if sent == 0:
|
||||
raise ActorException(
|
||||
self.logger,
|
||||
"Socket broken. Cannot send %s status." % msg
|
||||
)
|
||||
total_sent += sent
|
||||
s.shutdown(socket.SHUT_RDWR)
|
||||
s.close()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.logger.debug("Interruption signal catched")
|
||||
|
||||
with signal_timeout(self.config['collect_timeout'], raise_exc=False):
|
||||
for listener in listeners:
|
||||
# terminate and flush pipes
|
||||
listener.terminate()
|
||||
out, err = listener.communicate()
|
||||
|
||||
if err and listener.returncode:
|
||||
self.logger.error('Listerner: %s', err)
|
||||
elif err:
|
||||
self.logger.warning('Listener: %s', err)
|
||||
|
||||
self.logger.debug('Start reading dumped information.')
|
||||
self.read_packets()
|
||||
self._log_ifaces("Interfaces just before ensuring interfaces down")
|
||||
|
||||
for iface in self._iface_iterator():
|
||||
self._ensure_iface_down(iface)
|
||||
self._log_ifaces(
|
||||
"Interfaces just after ensuring them down in listener")
|
||||
|
||||
with open(self.config['dump_file'], 'w') as fo:
|
||||
fo.write(json.dumps(self.neighbours))
|
||||
os.unlink(self.pidfile)
|
||||
self.logger.info("=== Listener Finished ===")
|
||||
|
||||
def read_packets(self):
|
||||
for iface in self._iface_iterator():
|
||||
filenames = ['{0}.pcap'.format(iface),
|
||||
'vlan_{0}.pcap'.format(iface)]
|
||||
for filename in filenames:
|
||||
self.read_pcap_file(iface, filename)
|
||||
|
||||
def read_pcap_file(self, iface, filename):
|
||||
try:
|
||||
pcap_file = os.path.join(self.config['pcap_dir'], filename)
|
||||
for pkt in PcapReader(pcap_file):
|
||||
self.fprn(pkt, iface)
|
||||
except (IOError, error.Scapy_Exception) as exc:
|
||||
self.logger.exception(
|
||||
'Cant read pcap file %s, err: %s', pcap_file, str(exc))
|
||||
|
||||
def fprn(self, p, iface):
|
||||
|
||||
if scapy.Dot1Q in p:
|
||||
vlan = p[scapy.Dot1Q].vlan
|
||||
else:
|
||||
vlan = 0
|
||||
|
||||
self.logger.debug("Catched packet: vlan=%s len=%s payload=%s",
|
||||
str(vlan), p[scapy.UDP].len, p[scapy.UDP].payload)
|
||||
received_msg, _ = p[scapy.UDP].extract_padding(p[scapy.UDP].load)
|
||||
decoded_msg = received_msg.decode()
|
||||
riface, uid = decoded_msg[len(self.config["cookie"]):].split(' ', 1)
|
||||
|
||||
self.neighbours[iface].setdefault(vlan, {})
|
||||
|
||||
if riface not in self.neighbours[iface][vlan].setdefault(uid, []):
|
||||
self.neighbours[iface][vlan][uid].append(riface)
|
||||
|
||||
def get_probe_frames(self, iface, vlan=False):
|
||||
if iface not in self.neighbours:
|
||||
self.neighbours[iface] = {}
|
||||
filter_string = 'udp and dst port {0}'.format(self.config['dport'])
|
||||
filename = '{0}.pcap'.format(iface)
|
||||
if vlan:
|
||||
filter_string = '{0} {1}'.format('vlan and', filter_string)
|
||||
filename = '{0}_{1}'.format('vlan', filename)
|
||||
pcap_file = os.path.join(self.config['pcap_dir'], filename)
|
||||
|
||||
return subprocess.Popen(
|
||||
['tcpdump', '-i', iface, '-w', pcap_file, filter_string],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
# -------------- main ---------------
|
||||
|
||||
|
||||
def define_parser():
|
||||
config_examples = """
|
||||
|
||||
Config file examples:
|
||||
|
||||
Capture frames config file example is:
|
||||
{"action": "listen", "interfaces": {"eth0": "1-4094"},
|
||||
"dump_file": "/var/tmp/net-probe-dump-eth0"}
|
||||
|
||||
Simple frame generation config file example is:
|
||||
{"action": "generate", "uid": "aaa-bb-cccccc",
|
||||
"interfaces": { "eth0": "1-4094"}}
|
||||
|
||||
Full frame generation config file example is:
|
||||
{ "action": "generate",
|
||||
"uid": "aaa-bb-cccccc", "cookie": "Some cookie",
|
||||
"src_mac": "11:22:33:44:55:66",
|
||||
"src": "10.0.0.1", "dst": "10.255.255.255",
|
||||
"sport": 4056, "dport": 4057,
|
||||
"interfaces": {
|
||||
"eth0": "10, 15, 20, 201-210, 301-310, 1000-2000",
|
||||
"eth1": "1-4094"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(epilog=config_examples)
|
||||
parser.add_argument(
|
||||
'-c', '--config', dest='config', action='store', type=str,
|
||||
help='config file', default=None
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def define_subparsers(parser):
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="action", help='actions'
|
||||
)
|
||||
listen_parser = subparsers.add_parser(
|
||||
'listen', help='listen for probe packets'
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-i', '--interface', dest='interface', action='store', type=str,
|
||||
help='interface to listen on', required=True
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-v', '--vlans', dest='vlan_list', action='store', type=str,
|
||||
help='vlan list to send tagged packets ("100,200-300")', required=True
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-k', '--cookie', dest='cookie', action='store', type=str,
|
||||
help='cookie string to insert into probe packets payload',
|
||||
default='Nailgun:'
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-o', '--file', dest='dump_file', action='store', type=str,
|
||||
help='file to dump captured packets', default=None
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-a', '--address', dest='ready_address', action='store', type=str,
|
||||
help='address to report listener ready state', default='localhost'
|
||||
)
|
||||
listen_parser.add_argument(
|
||||
'-p', '--port', dest='ready_port', action='store', type=int,
|
||||
help='port to report listener ready state', default=31338
|
||||
)
|
||||
generate_parser = subparsers.add_parser(
|
||||
'generate', help='generate and send probe packets'
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-i', '--interface', dest='interface', action='store', type=str,
|
||||
help='interface to send packets from', required=True
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-v', '--vlans', dest='vlan_list', action='store', type=str,
|
||||
help='vlan list to send tagged packets ("100,200-300")', required=True
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-k', '--cookie', dest='cookie', action='store', type=str,
|
||||
help='cookie string to insert into probe packets payload',
|
||||
default='Nailgun:'
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-u', '--uid', dest='uid', action='store', type=str,
|
||||
help='uid to insert into probe packets payload', default='1'
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-d', '--duration', dest='duration', type=int, default=5,
|
||||
help='Amount of time to generate network packets. In seconds',
|
||||
)
|
||||
generate_parser.add_argument(
|
||||
'-r', '--repeat', dest='repeat', type=int, default=1,
|
||||
help='Amount of packets sended in one iteration.',
|
||||
)
|
||||
|
||||
|
||||
def term_handler(signum, sigframe):
|
||||
sys.exit()
|
||||
|
||||
|
||||
def main():
|
||||
signal.signal(signal.SIGTERM, term_handler)
|
||||
|
||||
parser = define_parser()
|
||||
params, other_params = parser.parse_known_args()
|
||||
|
||||
config = {}
|
||||
if params.config:
|
||||
# if config file is set then we discard all other
|
||||
# command line parameters
|
||||
try:
|
||||
if params.config == '-':
|
||||
fo = sys.stdin
|
||||
else:
|
||||
fo = open(params.config, 'r')
|
||||
config = json.load(fo)
|
||||
fo.close()
|
||||
except IOError:
|
||||
print("Can not read config file %s" % params.config)
|
||||
exit(1)
|
||||
except ValueError as e:
|
||||
print("Can not parse config file: %s" % str(e))
|
||||
exit(1)
|
||||
|
||||
else:
|
||||
define_subparsers(parser)
|
||||
params, other_params = parser.parse_known_args()
|
||||
|
||||
if params.action == 'listen':
|
||||
config['action'] = 'listen'
|
||||
config['interfaces'] = {}
|
||||
config['interfaces'][params.interface] = params.vlan_list
|
||||
config['cookie'] = params.cookie
|
||||
config['ready_address'] = params.ready_address
|
||||
config['ready_port'] = params.ready_port
|
||||
if params.dump_file:
|
||||
config['dump_file'] = params.dump_file
|
||||
else:
|
||||
config['dump_file'] = "/var/tmp/net-probe-dump-%s" %\
|
||||
config['interface']
|
||||
|
||||
elif params.action == 'generate':
|
||||
config['action'] = 'generate'
|
||||
config['interfaces'] = {}
|
||||
config['interfaces'][params.interface] = params.vlan_list
|
||||
config['uid'] = params.uid
|
||||
config['cookie'] = params.cookie
|
||||
config['duration'] = params.duration
|
||||
config['repeat'] = params.repeat
|
||||
|
||||
actor = ActorFabric.getInstance(config)
|
||||
actor.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,53 +0,0 @@
|
|||
# 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 functools import partial
|
||||
import logging
|
||||
import signal
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeoutException(KeyboardInterrupt):
|
||||
"""Exception should be raised if timeout is exceeded."""
|
||||
|
||||
|
||||
def timeout_handler(timeout, signum, frame):
|
||||
raise TimeoutException("Timeout {0} seconds exceeded".format(timeout))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def signal_timeout(timeout, raise_exc=True):
|
||||
"""Timeout handling using signals
|
||||
|
||||
:param timeout: timeout in seconds, integer
|
||||
:param raise_exc: bool to control suppressing of exception
|
||||
"""
|
||||
handler = partial(timeout_handler, timeout)
|
||||
|
||||
signal.signal(signal.SIGALRM, handler)
|
||||
signal.alarm(timeout)
|
||||
|
||||
try:
|
||||
yield
|
||||
except TimeoutException as exc:
|
||||
if raise_exc:
|
||||
raise
|
||||
else:
|
||||
log.warning(str(exc))
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
|
@ -1,13 +0,0 @@
|
|||
# Copyright 2014 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.
|
|
@ -1,27 +0,0 @@
|
|||
# Copyright 2014 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.
|
||||
|
||||
|
||||
class SimpleChecker(object):
|
||||
|
||||
name = 'simple'
|
||||
|
||||
def send(self):
|
||||
return 'ready'
|
||||
|
||||
def listen(self):
|
||||
return 'ready'
|
||||
|
||||
def get_info(self):
|
||||
return {}
|
|
@ -1,83 +0,0 @@
|
|||
# Copyright 2014 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 json
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from network_checker.multicast import api as multicast_api
|
||||
|
||||
|
||||
class TestMulticastVerification(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.config_node_112 = {"uid": "112", "group": "225.0.0.250",
|
||||
"port": 8890}
|
||||
self.config_node_113 = {"uid": "113", "group": "225.0.0.250",
|
||||
"port": 8890}
|
||||
self.mchecker_node_112 = multicast_api.MulticastChecker(
|
||||
**self.config_node_112)
|
||||
self.mchecker_node_113 = multicast_api.MulticastChecker(
|
||||
**self.config_node_113)
|
||||
|
||||
def test_multicast_verification(self):
|
||||
self.mchecker_node_112.listen()
|
||||
self.mchecker_node_113.listen()
|
||||
self.mchecker_node_112.send()
|
||||
self.mchecker_node_113.send()
|
||||
|
||||
info_node_112 = self.mchecker_node_112.get_info()
|
||||
info_node_113 = self.mchecker_node_113.get_info()
|
||||
|
||||
self.assertEqual(info_node_112, [u"113", u"112"])
|
||||
self.assertEqual(info_node_113, [u"113", u"112"])
|
||||
|
||||
|
||||
class TestSystemMulticastVerification(unittest.TestCase):
|
||||
|
||||
def shell_helper(self, args):
|
||||
proc = subprocess.Popen(args, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out, err = proc.communicate()
|
||||
return json.loads(out)
|
||||
|
||||
def test_multicast_verification_with_detach(self):
|
||||
init_args = ['fuel-netcheck', 'multicast', 'serve', 'listen']
|
||||
listen_data = self.shell_helper(init_args)
|
||||
self.assertIn('uid', listen_data)
|
||||
|
||||
args = ['fuel-netcheck', 'multicast', 'send', 'info']
|
||||
info = self.shell_helper(args)
|
||||
self.assertEqual([listen_data['uid']], info)
|
||||
|
||||
cleanup_args = ['fuel-netcheck', 'multicast', 'clean']
|
||||
clean = self.shell_helper(cleanup_args)
|
||||
self.assertTrue(clean)
|
||||
|
||||
def test_mutlicast_with_config(self):
|
||||
config = {"uid": "112", "group": "225.0.0.250",
|
||||
"port": 8890, "iface": "eth0"}
|
||||
config_json = json.dumps(config)
|
||||
|
||||
init_args = ["fuel-netcheck", "multicast", "serve",
|
||||
"--config", config_json]
|
||||
self.shell_helper(init_args)
|
||||
|
||||
args = ['fuel-netcheck', 'multicast', 'listen', 'send', 'info']
|
||||
info = self.shell_helper(args)
|
||||
self.assertEqual([config['uid']], info)
|
||||
|
||||
cleanup_args = ['fuel-netcheck', 'multicast', 'clean']
|
||||
clean = self.shell_helper(cleanup_args)
|
||||
self.assertTrue(clean)
|
|
@ -1,190 +0,0 @@
|
|||
# Copyright 2014 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 json
|
||||
import multiprocessing
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import pcap
|
||||
from scapy import all as scapy
|
||||
|
||||
from network_checker.net_check import api
|
||||
|
||||
|
||||
class BaseListenerTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self, config=None):
|
||||
self.iface = os.environ.get('NET_CHECK_IFACE_1', 'eth1')
|
||||
default_config = {
|
||||
"src": "1.0.0.0", "ready_port": None,
|
||||
"ready_address": "localhost", "dst": "1.0.0.0",
|
||||
"interfaces": {self.iface: "0,100,101,102,103,104,105,106,107"},
|
||||
"action": "listen",
|
||||
"cookie": "Nailgun:", "dport": 31337, "sport": 31337,
|
||||
"src_mac": None, "dump_file": "/var/tmp/net-probe-dump"
|
||||
}
|
||||
self.config = config or default_config
|
||||
self.start_socket()
|
||||
listener = api.Listener(self.config)
|
||||
self.listener = multiprocessing.Process(target=listener.run)
|
||||
self.listener.start()
|
||||
|
||||
connection, address = self.ready_socket.accept()
|
||||
request = connection.recv(1024)
|
||||
self.assertEqual('READY', request.decode())
|
||||
connection.close()
|
||||
self.ready_socket.shutdown(socket.SHUT_RDWR)
|
||||
self.ready_socket.close()
|
||||
|
||||
self.send_packets()
|
||||
os.kill(self.listener.pid, signal.SIGINT)
|
||||
self.listener.join()
|
||||
|
||||
def start_socket(self):
|
||||
self.ready_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.ready_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.ready_socket.bind((self.config['ready_address'], 0))
|
||||
self.config['ready_port'] = self.ready_socket.getsockname()[1]
|
||||
self.ready_socket.listen(1)
|
||||
self.ready_socket.settimeout(5)
|
||||
|
||||
def send_packets(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
self.ready_socket.close()
|
||||
self.listener.terminate()
|
||||
if os.path.exists(self.config['dump_file']):
|
||||
os.unlink(self.config['dump_file'])
|
||||
|
||||
|
||||
class TestCaseListenerPcap(BaseListenerTestCase):
|
||||
|
||||
def send_packets(self):
|
||||
for vlan in self.config['interfaces'][self.iface].split(','):
|
||||
p = self.get_packet(vlan)
|
||||
for i in xrange(5):
|
||||
scapy.sendp(p, iface=self.iface)
|
||||
|
||||
def get_packet(self, vlan):
|
||||
normal_data = 'Nailgun:{iface} 1'.format(iface=self.iface)
|
||||
p = scapy.Ether(src='64:0b:36:0e:0a:b7',
|
||||
dst="ff:ff:ff:ff:ff:ff")
|
||||
if int(vlan) > 0:
|
||||
p = p / scapy.Dot1Q(vlan=int(vlan))
|
||||
message_len = len(normal_data) + 8
|
||||
p = p / scapy.IP(src=self.config['src'], dst=self.config['dst'])
|
||||
p = p / scapy.UDP(sport=self.config['sport'],
|
||||
dport=self.config['dport'],
|
||||
len=message_len) / normal_data
|
||||
return p
|
||||
|
||||
def test_listener_pcap_file(self):
|
||||
|
||||
with open(self.config['dump_file'], 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
|
||||
self.assertEqual(data, {self.iface: {
|
||||
u'0': {u'1': [self.iface]},
|
||||
u'102': {u'1': [self.iface]},
|
||||
u'103': {u'1': [self.iface]},
|
||||
u'100': {u'1': [self.iface]},
|
||||
u'101': {u'1': [self.iface]},
|
||||
u'106': {u'1': [self.iface]},
|
||||
u'107': {u'1': [self.iface]},
|
||||
u'104': {u'1': [self.iface]},
|
||||
u'105': {u'1': [self.iface]}}})
|
||||
|
||||
|
||||
class TestCaseListenerCorruptedData(BaseListenerTestCase):
|
||||
|
||||
def send_packets(self):
|
||||
normal_data = 'Nailgun:{iface} 2'.format(iface=self.iface)
|
||||
corrupted_data = normal_data + '7h 7\00\00\00'
|
||||
message_len = len(normal_data) + 8
|
||||
p = scapy.Ether(src=self.config['src_mac'],
|
||||
dst="ff:ff:ff:ff:ff:ff")
|
||||
p = p / scapy.IP(src=self.config['src'], dst=self.config['dst'])
|
||||
p = p / scapy.UDP(sport=self.config['sport'],
|
||||
dport=self.config['dport'],
|
||||
len=message_len) / corrupted_data
|
||||
for i in xrange(5):
|
||||
scapy.sendp(p, iface=self.iface)
|
||||
|
||||
def test_listener_corrupted_data(self):
|
||||
|
||||
with open(self.config['dump_file'], 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
|
||||
self.assertEqual(data, {self.iface: {u'0': {u'2': [self.iface]}}})
|
||||
|
||||
|
||||
class TestNetCheckSender(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.iface = os.environ.get('NET_CHECK_IFACE_1', 'eth1')
|
||||
self.config = {
|
||||
"src": "1.0.0.0", "ready_port": 31338,
|
||||
"ready_address": "localhost", "dst": "1.0.0.0",
|
||||
"interfaces": {self.iface: "0,100,101,102,106,107,108"},
|
||||
"action": "listen",
|
||||
"cookie": "Nailgun:", "dport": 31337, "sport": 31337,
|
||||
"src_mac": None,
|
||||
"dump_file": "/var/tmp/net-probe-dump",
|
||||
"uid": "2"
|
||||
}
|
||||
|
||||
def start_pcap_listener(self):
|
||||
self.pcap_listener = pcap.pcap(self.iface)
|
||||
self.vlan_pcap_listener = pcap.pcap(self.iface)
|
||||
filter_string = 'udp and dst port {0}'.format(self.config['dport'])
|
||||
self.vlan_pcap_listener.setfilter('vlan and {0}'.format(filter_string))
|
||||
self.pcap_listener.setfilter(filter_string)
|
||||
|
||||
def start_sender(self):
|
||||
sender = api.Sender(self.config)
|
||||
self.sender = multiprocessing.Process(target=sender.run)
|
||||
self.sender.start()
|
||||
|
||||
@property
|
||||
def pcap_packets(self):
|
||||
for pkt in self.pcap_listener.readpkts():
|
||||
yield pkt
|
||||
for pkt in self.vlan_pcap_listener.readpkts():
|
||||
yield pkt
|
||||
|
||||
@property
|
||||
def received_vlans(self):
|
||||
results = set()
|
||||
for pkt in self.pcap_packets:
|
||||
ether = scapy.Ether(pkt[1])
|
||||
if scapy.Dot1Q in ether:
|
||||
vlan = str(ether[scapy.Dot1Q].vlan)
|
||||
else:
|
||||
vlan = '0'
|
||||
results.update([vlan])
|
||||
return results
|
||||
|
||||
def test_sender(self):
|
||||
self.start_pcap_listener()
|
||||
self.start_sender()
|
||||
time.sleep(3)
|
||||
self.sender.join()
|
||||
|
||||
expected_vlans = set(self.config['interfaces'][self.iface].split(','))
|
||||
self.assertEqual(expected_vlans, self.received_vlans)
|
|
@ -1,68 +0,0 @@
|
|||
# Copyright 2014 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 httplib
|
||||
import SimpleXMLRPCServer
|
||||
import socket
|
||||
import xmlrpclib
|
||||
|
||||
|
||||
class UnixStreamHTTPConnection(httplib.HTTPConnection):
|
||||
|
||||
def connect(self):
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.connect(self.host)
|
||||
|
||||
def getreply(self):
|
||||
response = self.getresponse()
|
||||
self.file = response.fp
|
||||
return response.status, response.reason, response.msg
|
||||
|
||||
def getfile(self):
|
||||
return self.file
|
||||
|
||||
|
||||
class UnixStreamTransport(xmlrpclib.Transport, object):
|
||||
|
||||
def __init__(self, socket_path):
|
||||
self.socket_path = socket_path
|
||||
super(UnixStreamTransport, self).__init__()
|
||||
|
||||
def make_connection(self, host):
|
||||
return UnixStreamHTTPConnection(self.socket_path)
|
||||
|
||||
|
||||
class UnixStreamHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
|
||||
|
||||
# if True leads to calling TCP_NODELAY on AF_UNIX socket
|
||||
# which results in Errno 95
|
||||
disable_nagle_algorithm = False
|
||||
|
||||
|
||||
class UnixXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
|
||||
|
||||
address_family = socket.AF_UNIX
|
||||
|
||||
|
||||
def get_client(config):
|
||||
return xmlrpclib.Server(
|
||||
'http://arg_unused',
|
||||
transport=UnixStreamTransport(config['unix']))
|
||||
|
||||
|
||||
def get_server(config):
|
||||
return UnixXMLRPCServer(
|
||||
config['unix'],
|
||||
requestHandler=UnixStreamHandler,
|
||||
logRequests=False)
|
|
@ -1,10 +0,0 @@
|
|||
argparse
|
||||
cliff-tablib
|
||||
scapy==2.2.0-dev
|
||||
pypcap==1.1.1
|
||||
stevedore
|
||||
daemonize
|
||||
pyyaml
|
||||
requests
|
||||
netifaces
|
||||
six
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
tox -v
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Copyright 2014 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 setuptools
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name="nailgun-net-check",
|
||||
version='8.0.0',
|
||||
author="Mirantis Inc",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: Apache 2.0",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Software Development :: Testing"
|
||||
],
|
||||
include_package_data=True,
|
||||
packages=setuptools.find_packages(),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'net_probe.py = network_checker.net_check.api:main',
|
||||
'fuel-netcheck = network_checker.cli:main',
|
||||
'dhcpcheck = dhcp_checker.cli:main',
|
||||
'urlaccesscheck = url_access_checker.cli:main',
|
||||
],
|
||||
'dhcp.check': [
|
||||
'discover = dhcp_checker.commands:ListDhcpServers',
|
||||
'request = dhcp_checker.commands:ListDhcpAssignment',
|
||||
'vlans = dhcp_checker.commands:DhcpWithVlansCheck'
|
||||
],
|
||||
'network_checker': [
|
||||
'multicast = network_checker.multicast.api:MulticastChecker',
|
||||
'simple = network_checker.tests.simple:SimpleChecker'
|
||||
],
|
||||
'urlaccesscheck': [
|
||||
'check = url_access_checker.commands:CheckUrls',
|
||||
'with_setup = url_access_checker.commands:CheckUrlsWithSetup'
|
||||
],
|
||||
},
|
||||
)
|
|
@ -1,41 +0,0 @@
|
|||
%define name nailgun-net-check
|
||||
%{!?version: %define version 8.0.0}
|
||||
%{!?release: %define release 1}
|
||||
|
||||
Name: %{name}
|
||||
Summary: Network checking package for CentOS6.x
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
License: GPLv2
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
URL: http://github.com/Mirantis
|
||||
Requires: vconfig
|
||||
Requires: scapy
|
||||
Requires: python-argparse
|
||||
Requires: python-pypcap
|
||||
Requires: python-cliff-tablib
|
||||
Requires: python-stevedore
|
||||
Requires: python-daemonize
|
||||
Requires: python-yaml
|
||||
Requires: tcpdump
|
||||
Requires: python-requests
|
||||
Requires: python-netifaces
|
||||
|
||||
%prep
|
||||
%setup -cq -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
cd %{_builddir}/%{name}-%{version}/network_checker && python setup.py build
|
||||
|
||||
%install
|
||||
cd %{_builddir}/%{name}-%{version}/network_checker && python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=%{_builddir}/%{name}-%{version}/network_checker/INSTALLED_FILES
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
%description
|
||||
This is a network tool that helps to verify networks connectivity
|
||||
between hosts in network.
|
||||
|
||||
%files -f %{_builddir}/%{name}-%{version}/network_checker/INSTALLED_FILES
|
||||
%defattr(-,root,root)
|
|
@ -1,6 +0,0 @@
|
|||
-r requirements.txt
|
||||
hacking==0.7
|
||||
mock==1.0.1
|
||||
pytest
|
||||
unittest2
|
||||
requests-mock
|
|
@ -1,6 +0,0 @@
|
|||
# Command line options here
|
||||
DHCRELAYARGS=""
|
||||
# DHCPv4 only
|
||||
INTERFACES="eth1"
|
||||
# DHCPv4 only
|
||||
DHCPSERVERS="10.10.0.8"
|
|
@ -1,12 +0,0 @@
|
|||
default-lease-time 600;
|
||||
max-lease-time 7200;
|
||||
option subnet-mask 255.255.255.0;
|
||||
option broadcast-address 192.168.0.255;
|
||||
option routers 192.168.0.254;
|
||||
option domain-name-servers 192.168.0.1, 192.168.0.2;
|
||||
option domain-name "mydomain.org";
|
||||
|
||||
subnet 192.168.0.0 netmask 255.255.255.0 {
|
||||
range 192.168.0.10 192.168.0.100;
|
||||
range 192.168.0.150 192.168.0.200;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
default-lease-time 600;
|
||||
max-lease-time 7200;
|
||||
option subnet-mask 255.255.255.0;
|
||||
option broadcast-address 10.10.0.255;
|
||||
option routers 10.10.0.254;
|
||||
option domain-name-servers 10.10.0.1;
|
||||
option domain-name "mydomain1.org";
|
||||
option dhcp-server-identifier 10.10.0.8;
|
||||
|
||||
subnet 10.10.0.0 netmask 255.255.255.0 {
|
||||
range 10.10.0.10 10.10.0.100;
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
[tox]
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
envlist = py26,py27,pep8
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install --allow-external -U {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = py.test {toxinidir}/url_access_checker/tests
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
||||
[testenv:pep8]
|
||||
deps = hacking==0.10
|
||||
usedevelop = False
|
||||
commands =
|
||||
flake8 {posargs:.}
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs:}
|
||||
|
||||
[testenv:devenv]
|
||||
envdir = devenv
|
||||
usedevelop = True
|
||||
|
||||
[flake8]
|
||||
ignore = H234,H302,H802
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools,__init__.py,docs
|
||||
show-pep8 = True
|
||||
show-source = True
|
||||
count = True
|
||||
|
||||
[hacking]
|
||||
import_exceptions = testtools.matchers
|
|
@ -1,112 +0,0 @@
|
|||
# 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 json
|
||||
import os
|
||||
import socket
|
||||
|
||||
import requests
|
||||
from six.moves import urllib
|
||||
|
||||
import url_access_checker.errors as errors
|
||||
|
||||
|
||||
def check_urls(urls, proxies=None, timeout=60):
|
||||
"""Checks a set of urls to see if they are valid
|
||||
|
||||
Url is valid if
|
||||
- it returns 200 upon requesting it with http (if it doesn't specify
|
||||
protocol as "file")
|
||||
- it is an existing file or directory (if the protocol used in url is
|
||||
"file")
|
||||
|
||||
Arguments:
|
||||
urls -- an iterable containing urls for testing
|
||||
proxies -- proxy servers to use for the request
|
||||
timeout -- the max time to wait for a response, default 60 seconds
|
||||
"""
|
||||
responses = map(lambda u: _get_response_tuple(
|
||||
u, proxies=proxies, timeout=timeout), urls)
|
||||
failed_responses = filter(lambda x: x[0], responses)
|
||||
|
||||
if failed_responses:
|
||||
raise errors.UrlNotAvailable(json.dumps(
|
||||
{'failed_urls': map(lambda r: r[1], failed_responses)}))
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _get_response_tuple(url, proxies=None, timeout=60):
|
||||
"""Return a tuple which contains a result of url test
|
||||
|
||||
Arguments:
|
||||
url -- a string containing url for testing, can be local file
|
||||
proxies -- proxy servers to use for the request
|
||||
timeout -- the max time to wait for a response, default 60 seconds
|
||||
|
||||
Result tuple content:
|
||||
result[0] -- boolean value, True if the url is deemed failed
|
||||
result[1] -- unchange url argument
|
||||
"""
|
||||
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.scheme == 'file':
|
||||
return _get_file_existence_tuple(url)
|
||||
elif parsed.scheme in ['http', 'https']:
|
||||
return _get_http_response_tuple(url, proxies, timeout)
|
||||
elif parsed.scheme == 'ftp':
|
||||
return _get_ftp_response_tuple(url, timeout)
|
||||
else:
|
||||
raise errors.InvalidProtocol(url)
|
||||
|
||||
|
||||
def _get_file_existence_tuple(url):
|
||||
path = url[len('file://'):]
|
||||
return (not os.path.exists(path), url)
|
||||
|
||||
|
||||
def _get_http_response_tuple(url, proxies=None, timeout=60):
|
||||
try:
|
||||
# requests seems to correctly handle various corner cases:
|
||||
# proxies=None or proxies={} mean 'use the default' rather than
|
||||
# "don't use proxy". To force a direct connection one should pass
|
||||
# proxies={'http': None}.
|
||||
# Setting the timeout for requests.get(...) sets max request time. We
|
||||
# want to set a value to prevent this process from hanging as the
|
||||
# default timeout is None which can lead to bad things when processes
|
||||
# never exit. LP#1478138
|
||||
response = requests.get(url, proxies=proxies, timeout=timeout)
|
||||
return (response.status_code != 200, url)
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.HTTPError,
|
||||
requests.exceptions.ProxyError,
|
||||
ValueError,
|
||||
socket.timeout):
|
||||
return (True, url)
|
||||
|
||||
|
||||
def _get_ftp_response_tuple(url, timeout=60):
|
||||
"""Return a tuple which contains a result of ftp url test
|
||||
|
||||
It will try to open ftp url as anonymous user and return (True, url) if
|
||||
any errors occur, or return (False, url) otherwise.
|
||||
"""
|
||||
try:
|
||||
# NOTE(mkwiek): requests don't have tested ftp adapter yet, so
|
||||
# lower level urllib2 is used here
|
||||
urllib.request.urlopen(url, timeout=timeout)
|
||||
return (False, url)
|
||||
except urllib.error.URLError:
|
||||
return (True, url)
|
|
@ -1,42 +0,0 @@
|
|||
# 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 os
|
||||
import sys
|
||||
# fixed in cmd2 >=0.6.6
|
||||
os.environ['EDITOR'] = '/usr/bin/nano'
|
||||
|
||||
from cliff.commandmanager import CommandManager
|
||||
|
||||
from fuel_network_checker import base_app
|
||||
|
||||
|
||||
class UrlAccessCheckApp(base_app.BaseApp):
|
||||
LOG_FILENAME = '/var/log/url_access_checker.log'
|
||||
|
||||
def __init__(self):
|
||||
super(UrlAccessCheckApp, self).__init__(
|
||||
description='Url access check application',
|
||||
version='0.1',
|
||||
command_manager=CommandManager('urlaccesscheck'),
|
||||
)
|
||||
|
||||
|
||||
def main(argv=sys.argv[1:]):
|
||||
myapp = UrlAccessCheckApp()
|
||||
return myapp.run(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
|
@ -1,63 +0,0 @@
|
|||
# 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 logging
|
||||
import sys
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class CheckUrls(command.Command):
|
||||
"""Check if it is possible to retrieve urls."""
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CheckUrls, self).get_parser(prog_name)
|
||||
parser.add_argument('urls', type=str, nargs='+',
|
||||
help='List of urls to check')
|
||||
parser.add_argument('--timeout', type=int, default=60,
|
||||
help='Max time to wait for response, Default: 60')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.info('Starting url access check for {0}'.format(parsed_args.urls))
|
||||
try:
|
||||
api.check_urls(parsed_args.urls, timeout=parsed_args.timeout)
|
||||
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)
|
|
@ -1,25 +0,0 @@
|
|||
# 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.
|
||||
|
||||
|
||||
class UrlNotAvailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CommandFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidProtocol(Exception):
|
||||
pass
|
|
@ -1,207 +0,0 @@
|
|||
# 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')
|
|
@ -1,68 +0,0 @@
|
|||
# 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 unittest2
|
||||
|
||||
import mock
|
||||
import requests_mock
|
||||
|
||||
from url_access_checker import api
|
||||
from url_access_checker import errors
|
||||
|
||||
|
||||
class TestApi(unittest2.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.urls = ['http://url{0}'.format(i) for i in range(10)]
|
||||
self.paths = ['file:///tmp/test_api{0}'.format(i) for i in range(10)]
|
||||
self.ftps = ['ftp://url{0}'.format(i) for i in range(10)]
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_check_urls(self, req_mocker):
|
||||
for url in self.urls:
|
||||
req_mocker.get(url, status_code=200)
|
||||
|
||||
check_result = api.check_urls(self.urls)
|
||||
|
||||
self.assertTrue(check_result)
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_check_urls_fail(self, req_mocker):
|
||||
for url in self.urls:
|
||||
req_mocker.get(url, status_code=404)
|
||||
|
||||
with self.assertRaises(errors.UrlNotAvailable):
|
||||
api.check_urls(self.urls)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
def test_check_paths(self, mock_exists):
|
||||
mock_exists.return_value = True
|
||||
check_result = api.check_urls(self.paths)
|
||||
|
||||
self.assertTrue(check_result)
|
||||
|
||||
@mock.patch('os.path.exists')
|
||||
def test_check_paths_fail(self, mock_exists):
|
||||
mock_exists.return_value = False
|
||||
with self.assertRaises(errors.UrlNotAvailable):
|
||||
api.check_urls(self.paths)
|
||||
|
||||
@mock.patch('urllib2.urlopen')
|
||||
def test_check_ftp(self, _):
|
||||
check_result = api.check_urls(self.ftps, timeout=5)
|
||||
self.assertTrue(check_result)
|
||||
|
||||
def test_check_ftp_fail(self):
|
||||
with self.assertRaises(errors.UrlNotAvailable):
|
||||
api.check_urls(self.paths)
|
|
@ -1,47 +0,0 @@
|
|||
# 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
|
||||
|
||||
import mock
|
||||
|
||||
from url_access_checker import cli
|
||||
|
||||
|
||||
class TestUrlCheckerCommands(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.urls = ['http://url{0}'.format(i) for i in range(10)]
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_check_urls_success(self, get_mock):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.status_code = 200
|
||||
get_mock.return_value = response_mock
|
||||
|
||||
exit_code = cli.main(['check'] + self.urls)
|
||||
self.assertEqual(exit_code, 0)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_check_urls_fail(self, get_mock):
|
||||
response_mock = mock.Mock()
|
||||
response_mock.status_code = 404
|
||||
get_mock.return_value = response_mock
|
||||
|
||||
exit_code = cli.main(['check'] + self.urls)
|
||||
self.assertEqual(exit_code, 1)
|
||||
|
||||
def test_check_urls_fail_on_requests_error(self):
|
||||
exit_code = cli.main(['check'] + self.urls)
|
||||
self.assertEqual(exit_code, 1)
|
|
@ -1,114 +0,0 @@
|
|||
# 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)
|
|
@ -1,32 +0,0 @@
|
|||
# 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
|
|
@ -399,7 +399,6 @@ function run_extensions_tests {
|
|||
function run_flake8 {
|
||||
local result=0
|
||||
run_flake8_subproject nailgun && \
|
||||
run_flake8_subproject network_checker && \
|
||||
run_flake8_subproject fuel_upgrade_system/fuel_upgrade && \
|
||||
run_flake8_subproject fuel_upgrade_system/fuel_package_updates && \
|
||||
return $result
|
||||
|
|
|
@ -116,12 +116,10 @@ cd %{_builddir}/%{name}-%{version}/nailgun && %{_builddir}/%{name}-%{version}/na
|
|||
[ -n %{_builddir} ] && rm -rf %{_builddir}/%{name}-%{version}/nailgun/static
|
||||
mv %{_builddir}/%{name}-%{version}/nailgun/compressed_static %{_builddir}/%{name}-%{version}/nailgun/static
|
||||
cd %{_builddir}/%{name}-%{version}/nailgun && python setup.py build
|
||||
cd %{_builddir}/%{name}-%{version}/network_checker && python setup.py build
|
||||
cd %{_builddir}/%{name}-%{version}/fuel_upgrade_system/fuel_package_updates && python setup.py build
|
||||
|
||||
%install
|
||||
cd %{_builddir}/%{name}-%{version}/nailgun && python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=%{_builddir}/%{name}-%{version}/nailgun/INSTALLED_FILES
|
||||
cd %{_builddir}/%{name}-%{version}/network_checker && python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=%{_builddir}/%{name}-%{version}/network_checker/INSTALLED_FILES
|
||||
cd %{_builddir}/%{name}-%{version}/fuel_upgrade_system/fuel_package_updates && python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=%{_builddir}/%{name}-%{version}/fuel_upgrade_system/fuel_package_updates/INSTALLED_FILES
|
||||
mkdir -p %{buildroot}/opt/nailgun/bin
|
||||
mkdir -p %{buildroot}/etc/cron.d
|
||||
|
@ -155,35 +153,6 @@ This package currently installs just a single file openstack.yaml
|
|||
%{_datadir}/fuel-openstack-metadata/*
|
||||
%{_sysconfdir}/fuel_openstack_version
|
||||
|
||||
%package -n nailgun-net-check
|
||||
|
||||
Summary: Network checking package for CentOS6.x
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
License: GPLv2
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
URL: http://github.com/Mirantis
|
||||
Requires: vconfig
|
||||
Requires: scapy
|
||||
Requires: python-argparse
|
||||
Requires: python-pypcap
|
||||
Requires: python-cliff-tablib
|
||||
Requires: python-stevedore
|
||||
Requires: python-daemonize
|
||||
Requires: python-yaml
|
||||
Requires: tcpdump
|
||||
Requires: python-requests
|
||||
Requires: python-netifaces
|
||||
|
||||
|
||||
%description -n nailgun-net-check
|
||||
This is a network tool that helps to verify networks connectivity
|
||||
between hosts in network.
|
||||
|
||||
%files -n nailgun-net-check -f %{_builddir}/%{name}-%{version}/network_checker/INSTALLED_FILES
|
||||
%defattr(-,root,root)
|
||||
|
||||
|
||||
%package -n fencing-agent
|
||||
Summary: Fencing agent
|
||||
Version: %{version}
|
||||
|
|
Loading…
Reference in New Issue