309 lines
12 KiB
Python
309 lines
12 KiB
Python
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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 errno
|
|
import functools
|
|
import json
|
|
import os
|
|
|
|
import mock
|
|
from oslotest import base
|
|
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
|
|
|
|
from glean import cmd
|
|
|
|
|
|
sample_data_path = os.path.join(
|
|
os.path.dirname(os.path.realpath(__file__)), 'fixtures')
|
|
|
|
distros = ['Ubuntu', 'Debian', 'Fedora', 'RedHat', 'CentOS', 'Gentoo',
|
|
'openSUSE', 'networkd']
|
|
styles = ['hp', 'rax', 'rax-iad', 'liberty', 'nokey', 'ovh']
|
|
ips = {'hp': '127.0.1.1',
|
|
'rax': '23.253.229.154',
|
|
'rax-iad': '146.20.110.113',
|
|
'liberty': '23.253.229.154',
|
|
'ovh': '158.69.65.118',
|
|
'nokey': '127.0.1.1'}
|
|
|
|
built_scenarios = []
|
|
for distro in distros:
|
|
for style in styles:
|
|
built_scenarios.append(
|
|
('%s-%s' % (distro, style),
|
|
dict(distro=distro, style=style)))
|
|
|
|
# save these for wrapping
|
|
real_path_exists = os.path.exists
|
|
real_listdir = os.listdir
|
|
|
|
|
|
class TestGlean(base.BaseTestCase):
|
|
|
|
scenarios = built_scenarios
|
|
|
|
def setUp(self):
|
|
super(TestGlean, self).setUp()
|
|
self._resolv_unlinked = False
|
|
self.file_handle_mocks = {}
|
|
|
|
def open_side_effect(self, sample_prefix, *args, **kwargs):
|
|
# incoming file
|
|
path = args[0]
|
|
|
|
# we redirect some files to files in here
|
|
sample_path = os.path.join(sample_data_path, sample_prefix)
|
|
|
|
# Test broken symlink handling -- we want the code to unlink
|
|
# this file and write a new one. Simulate a /etc/resolv.conf
|
|
# that at first returns ELOOP (i.e. broken symlink) when
|
|
# opened, but on the second call opens as usual. We check
|
|
# that there was an os.unlink() performed
|
|
if path.startswith('/etc/resolv.conf'):
|
|
if not self._resolv_unlinked:
|
|
self._resolv_unlinked = True
|
|
raise IOError(errno.ELOOP,
|
|
os.strerror(errno.ELOOP), path)
|
|
|
|
# mock any files in these paths as blank files. Keep track of
|
|
# them in file_handle_mocks{} so we can assert they were
|
|
# called later
|
|
mock_dirs = ('/etc/network', '/etc/sysconfig/network-scripts',
|
|
'/etc/conf.d', '/etc/init.d', '/etc/sysconfig/network',
|
|
'/etc/systemd/network')
|
|
mock_files = ('/etc/resolv.conf', '/etc/hostname', '/etc/hosts',
|
|
'/etc/systemd/resolved.conf', '/bin/systemctl')
|
|
if (path.startswith(mock_dirs) or path in mock_files):
|
|
try:
|
|
mock_handle = self.file_handle_mocks[path]
|
|
except KeyError:
|
|
# note; don't use spec=file here ... it's not py3
|
|
# compatible. It really just limits the allowed
|
|
# mocked functions.
|
|
mock_handle = mock.MagicMock()
|
|
mock_handle.__enter__ = mock.Mock()
|
|
mock_handle.__exit__ = mock.Mock()
|
|
mock_handle.name = path
|
|
# This is a trick to handle open used as a context
|
|
# manager (i.e. with open('foo') as f). It's the
|
|
# returned object that gets called, so we point it
|
|
# back at the underlying mock (see mock.mock_open())
|
|
mock_handle.__enter__.return_value = mock_handle
|
|
mock_handle.read.return_value = ''
|
|
self.file_handle_mocks[path] = mock_handle
|
|
return mock_handle
|
|
|
|
# redirect these files to our samples
|
|
elif path.startswith(('/sys/class/net',
|
|
'/mnt/config')):
|
|
new_args = list(args)
|
|
new_args[0] = os.path.join(sample_path, path[1:])
|
|
return open(*new_args, **kwargs)
|
|
|
|
# otherwise just pass it through
|
|
else:
|
|
return open(*args, **kwargs)
|
|
|
|
def os_listdir_side_effect(self, sample_prefix, path):
|
|
if path.startswith('/'):
|
|
path = path[1:]
|
|
return real_listdir(os.path.join(
|
|
sample_data_path, sample_prefix, path))
|
|
|
|
def os_path_exists_side_effect(self, sample_prefix, path):
|
|
if path.startswith('/mnt/config'):
|
|
path = os.path.join(sample_data_path, sample_prefix, path[1:])
|
|
if path in ('/etc/sysconfig/network-scripts/ifcfg-eth2',
|
|
'/etc/network/interfaces.d/eth2.cfg',
|
|
'/etc/conf.d/net.eth2',
|
|
'/etc/sysconfig/network/ifcfg-eth2',
|
|
'/etc/systemd/network/eth2.network'):
|
|
# Pretend this file exists, we need to test skipping
|
|
# pre-existing config files
|
|
return True
|
|
elif (path.startswith(('/etc/sysconfig/network-scripts/',
|
|
'/etc/sysconfig/network/',
|
|
'/etc/network/interfaces.d/',
|
|
'/etc/conf.d/',
|
|
'/etc/systemd/network/'))):
|
|
# Don't check the host os's network config
|
|
return False
|
|
return real_path_exists(path)
|
|
|
|
def os_system_side_effect(self, distro, command):
|
|
if distro.lower() != 'networkd' and \
|
|
command == 'systemctl is-enabled systemd-resolved':
|
|
return 3
|
|
else:
|
|
return 0
|
|
|
|
@mock.patch('subprocess.call', return_value=0, new_callable=mock.Mock)
|
|
@mock.patch('os.fsync', return_value=0, new_callable=mock.Mock)
|
|
@mock.patch('os.unlink', return_value=0, new_callable=mock.Mock)
|
|
@mock.patch('os.symlink', return_value=0, new_callable=mock.Mock)
|
|
@mock.patch('os.path.exists', new_callable=mock.Mock)
|
|
@mock.patch('os.listdir', new_callable=mock.Mock)
|
|
@mock.patch('os.system', return_value=0, new_callable=mock.Mock)
|
|
@mock.patch('glean.cmd.open', new_callable=mock.Mock)
|
|
def _assert_distro_provider(self, distro, provider, interface,
|
|
mock_open,
|
|
mock_os_system,
|
|
mock_os_listdir,
|
|
mock_os_path_exists,
|
|
mock_os_symlink,
|
|
mock_os_unlink,
|
|
mock_os_fsync,
|
|
mock_call,
|
|
skip_dns=False,
|
|
use_nm=False):
|
|
"""Main test function
|
|
|
|
:param distro: distro to return from "distro.linux_distribution()"
|
|
:param provider: we will look in fixtures/provider for mocked
|
|
out files
|
|
:param interface: --interface argument; None for no argument
|
|
:param skip_dns: --skip-dns argument; False for no argument
|
|
:param use_nm: --use-nm argument; False for no argument
|
|
"""
|
|
|
|
# These functions are watching the path and faking results
|
|
# based on various things
|
|
# XXX : There are several virtual file-systems available, we
|
|
# might like to look into them and just point ourselves at
|
|
# testing file-systems in the future if this becomes more
|
|
# complex.
|
|
mock_os_path_exists.side_effect = functools.partial(
|
|
self.os_path_exists_side_effect, provider)
|
|
mock_os_listdir.side_effect = functools.partial(
|
|
self.os_listdir_side_effect, provider)
|
|
mock_open.side_effect = functools.partial(
|
|
self.open_side_effect, provider)
|
|
# we want os.system to return False for specific commands if
|
|
# running networkd
|
|
mock_os_system.side_effect = functools.partial(
|
|
self.os_system_side_effect, distro)
|
|
|
|
# default args
|
|
argv = ['--hostname']
|
|
|
|
argv.append('--distro=%s' % distro.lower())
|
|
|
|
if interface:
|
|
argv.append('--interface=%s' % interface)
|
|
if skip_dns:
|
|
argv.append('--skip-dns')
|
|
if use_nm:
|
|
argv.append('--use-nm')
|
|
|
|
cmd.main(argv)
|
|
|
|
output_filename = '%s.%s.network.out' % (provider, distro.lower())
|
|
output_path = os.path.join(sample_data_path, 'test', output_filename)
|
|
if not skip_dns:
|
|
if os.path.exists(output_path + '.dns'):
|
|
output_path = output_path + '.dns'
|
|
|
|
# Generate a list of (dest, content) into write_blocks to assert
|
|
write_blocks = []
|
|
lines = []
|
|
with open(output_path) as f:
|
|
for line in f:
|
|
if line == '%NM_CONTROLLED%\n':
|
|
line = 'NM_CONTROLLED=%s\n' % \
|
|
("yes" if use_nm else "no")
|
|
lines.append(line)
|
|
write_dest = None
|
|
write_content = None
|
|
for line in lines:
|
|
if line.startswith('### Write '):
|
|
if write_dest is not None:
|
|
write_blocks.append((write_dest, write_content))
|
|
write_dest = line[len('### Write '):-1]
|
|
write_content = ''
|
|
else:
|
|
write_content += line
|
|
if write_dest is not None:
|
|
write_blocks.append((write_dest, write_content))
|
|
|
|
for dest, content in write_blocks:
|
|
if interface and interface not in dest:
|
|
continue
|
|
self.assertNotIn("eth2", dest)
|
|
# Skip check
|
|
if skip_dns and '/etc/resolv.conf' in dest:
|
|
self.assertNotIn(dest, self.file_handle_mocks)
|
|
continue
|
|
if skip_dns and '/etc/systemd/resolved.conf' in dest:
|
|
self.assertNotIn(dest, self.file_handle_mocks)
|
|
continue
|
|
self.assertIn(dest, self.file_handle_mocks)
|
|
write_handle = self.file_handle_mocks[dest].write
|
|
write_handle.assert_called_once_with(content)
|
|
|
|
if self._resolv_unlinked:
|
|
mock_os_unlink.assert_called_once_with('/etc/resolv.conf')
|
|
|
|
# Check hostname
|
|
meta_data_path = 'mnt/config/openstack/latest/meta_data.json'
|
|
hostname = None
|
|
with open(os.path.join(sample_data_path, provider,
|
|
meta_data_path)) as fh:
|
|
meta_data = json.load(fh)
|
|
hostname = meta_data['name']
|
|
|
|
mock_call.assert_has_calls([mock.call(['hostname', hostname])])
|
|
if distro.lower() is 'gentoo':
|
|
(self.file_handle_mocks['/etc/conf.d/hostname'].write.
|
|
assert_has_calls([mock.call(hostname)]))
|
|
else:
|
|
self.file_handle_mocks['/etc/hostname'].write.assert_has_calls(
|
|
[mock.call(hostname), mock.call('\n')])
|
|
|
|
# Check hosts entry
|
|
hostname_ip = ips[provider]
|
|
calls = [mock.call('%s %s\n' % (hostname_ip, hostname)), ]
|
|
short_hostname = hostname.split('.')[0]
|
|
if hostname != short_hostname:
|
|
calls.append(mock.call('%s %s\n' % (hostname_ip, short_hostname)))
|
|
|
|
self.file_handle_mocks['/etc/hosts'].write.assert_has_calls(
|
|
calls, any_order=True)
|
|
|
|
def test_glean(self):
|
|
with mock.patch('glean.systemlock.Lock'):
|
|
self._assert_distro_provider(self.distro, self.style, None)
|
|
|
|
# In the systemd case, we are a templated unit file
|
|
# (glean@.service) so we get called once for each interface that
|
|
# comes up with "--interface". This simulates that.
|
|
def test_glean_systemd(self):
|
|
with mock.patch('glean.systemlock.Lock'):
|
|
self._assert_distro_provider(self.distro, self.style,
|
|
'eth0', skip_dns=True)
|
|
|
|
def test_glean_systemd_resolved(self):
|
|
with mock.patch('glean.systemlock.Lock'):
|
|
self._assert_distro_provider(self.distro, self.style, 'eth0')
|
|
|
|
def test_glean_skip_dns(self):
|
|
with mock.patch('glean.systemlock.Lock'):
|
|
self._assert_distro_provider(
|
|
self.distro, self.style, None, skip_dns=True)
|
|
|
|
def test_glean_nm(self):
|
|
with mock.patch('glean.systemlock.Lock'):
|
|
self._assert_distro_provider(
|
|
self.distro, self.style, 'eth0', use_nm=True)
|