ironic-inspector/ironic_inspector_ramdisk/test/test_discover.py
Dmitry Tantsur d6404d2f99 Rename discoverd -> inspector
As agreed on the summit I'm renaming the python modules
and doing some adjustments:
* This is a breaking change, so version is bumped to 2.0.0
* Used this chance to split conf options over proper sections
* RELEASES.rst is gone; it's too hard to keep it up-to-date;
  anyway git does better job at doing history
* Dropped deprecated option ports_for_inactive_interfaces
* Dropped old /v1/discover endpoint and associated client call
* No longer set on_discovery and newly_discovered in Node.extra
  (deprecated since 1.0.0, superseded by the get status API)
* Default firewall chain name is "ironic-inspector" and
  is configurable

Notes:
* Some links will be updated after real move.
* Stable branches will probably use the old name.
* Some usage of discovery word is left in context of
  "discovered data"
* DIB element will probably be deprecated, so leaving it
  alone for now.
* Some usages of word "discovery" in the README will be updated
  later to make this patch a bit smaller
* Ramdisk code will be moved to IPA, so not touching it too much

Change-Id: I59f1f5bfb1248ab69973dab845aa028df493054e
2015-05-29 09:06:56 +02:00

403 lines
14 KiB
Python

# 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 base64
import collections
import io
import os
import shutil
import subprocess
import tarfile
import tempfile
import unittest
try:
# mock library is buggy under Python 3.4, but we have a stdlib one
from unittest import mock
except ImportError:
import mock
import netifaces
import requests
from ironic_inspector_ramdisk import discover
def get_fake_args():
return mock.Mock(callback_url='url', daemonize_on_failure=True,
benchmark=None)
FAKE_ARGS = get_fake_args()
class TestCommands(unittest.TestCase):
@mock.patch.object(discover.LOG, 'warn', autospec=True)
@mock.patch.object(subprocess, 'Popen', autospec=True)
def test_try_call(self, mock_popen, mock_warn):
mock_popen.return_value.communicate.return_value = ('out', 'err')
mock_popen.return_value.returncode = 0
discover.try_call('ls', '-l')
mock_popen.assert_called_once_with(('ls', '-l'),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
self.assertFalse(mock_warn.called)
@mock.patch.object(discover.LOG, 'warn', autospec=True)
@mock.patch.object(subprocess, 'Popen', autospec=True)
def test_try_call_fails(self, mock_popen, mock_warn):
mock_popen.return_value.communicate.return_value = ('out', 'err')
mock_popen.return_value.returncode = 42
discover.try_call('ls', '-l')
mock_popen.assert_called_once_with(('ls', '-l'),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
mock_warn.assert_called_once_with(mock.ANY, ('ls', '-l'), 42, 'err')
@mock.patch.object(discover.LOG, 'warn', autospec=True)
def test_try_call_os_error(self, mock_warn):
discover.try_call('I don\'t exist!', '-l')
mock_warn.assert_called_once_with(mock.ANY, ('I don\'t exist!', '-l'),
mock.ANY)
@mock.patch.object(discover.LOG, 'warn', autospec=True)
def test_try_shell(self, mock_warn):
res = discover.try_shell('echo Hello; echo World')
self.assertEqual(b'Hello\nWorld', res)
self.assertFalse(mock_warn.called)
@mock.patch.object(discover.LOG, 'warn', autospec=True)
def test_try_shell_fails(self, mock_warn):
res = discover.try_shell('exit 1')
self.assertIsNone(res)
self.assertTrue(mock_warn.called)
@mock.patch.object(discover.LOG, 'warn', autospec=True)
def test_try_shell_no_strip(self, mock_warn):
res = discover.try_shell('echo Hello; echo World',
strip=False)
self.assertEqual(b'Hello\nWorld\n', res)
self.assertFalse(mock_warn.called)
class TestFailures(unittest.TestCase):
def test(self):
f = discover.AccumulatedFailure()
self.assertFalse(f)
self.assertIsNone(f.get_error())
f.add('foo')
f.add('%s', 'bar')
f.add(RuntimeError('baz'))
exp = ('The following errors were encountered during '
'hardware discovery:\n* foo\n* bar\n* baz')
self.assertEqual(exp, f.get_error())
self.assertTrue(f)
class BaseDiscoverTest(unittest.TestCase):
def setUp(self):
super(BaseDiscoverTest, self).setUp()
self.failures = discover.AccumulatedFailure()
self.data = {}
@mock.patch.object(discover, 'try_shell', autospec=True)
class TestDiscoverBasicProperties(BaseDiscoverTest):
def test(self, mock_shell):
mock_shell.return_value = '1.2.3.4'
discover.discover_basic_properties(
self.data, mock.Mock(bootif='boot:if'))
self.assertEqual({'ipmi_address': '1.2.3.4',
'boot_interface': 'boot:if'},
self.data)
@mock.patch.object(netifaces, 'ifaddresses', autospec=True)
@mock.patch.object(netifaces, 'interfaces', autospec=True)
class TestDiscoverNetworkInterfaces(BaseDiscoverTest):
def _call(self):
discover.discover_network_interfaces(self.data, self.failures)
def test_nothing(self, mock_ifaces, mock_ifaddr):
mock_ifaces.return_value = ['lo']
self._call()
mock_ifaces.assert_called_once_with()
self.assertFalse(mock_ifaddr.called)
self.assertIn('no network interfaces', self.failures.get_error())
self.assertEqual({'interfaces': {}}, self.data)
def test_ok(self, mock_ifaces, mock_ifaddr):
interfaces = [
{
netifaces.AF_LINK: [{'addr': '11:22:33:44:55:66'}],
netifaces.AF_INET: [{'addr': '1.2.3.4'}],
},
{
netifaces.AF_LINK: [{'addr': '11:22:33:44:55:44'}],
netifaces.AF_INET: [{'addr': '1.2.3.2'}],
},
]
mock_ifaces.return_value = ['lo', 'em1', 'em2']
mock_ifaddr.side_effect = iter(interfaces)
self._call()
mock_ifaddr.assert_any_call('em1')
mock_ifaddr.assert_any_call('em2')
self.assertEqual(2, mock_ifaddr.call_count)
self.assertEqual({'em1': {'mac': '11:22:33:44:55:66',
'ip': '1.2.3.4'},
'em2': {'mac': '11:22:33:44:55:44',
'ip': '1.2.3.2'}},
self.data['interfaces'])
self.assertFalse(self.failures)
def test_missing(self, mock_ifaces, mock_ifaddr):
interfaces = [
{
netifaces.AF_INET: [{'addr': '1.2.3.4'}],
},
{
netifaces.AF_LINK: [],
netifaces.AF_INET: [{'addr': '1.2.3.4'}],
},
{
netifaces.AF_LINK: [{'addr': '11:22:33:44:55:66'}],
netifaces.AF_INET: [],
},
{
netifaces.AF_LINK: [{'addr': '11:22:33:44:55:44'}],
},
]
mock_ifaces.return_value = ['lo', 'br0', 'br1', 'em1', 'em2']
mock_ifaddr.side_effect = iter(interfaces)
self._call()
self.assertEqual(4, mock_ifaddr.call_count)
self.assertEqual({'em1': {'mac': '11:22:33:44:55:66', 'ip': None},
'em2': {'mac': '11:22:33:44:55:44', 'ip': None}},
self.data['interfaces'])
self.assertFalse(self.failures)
@mock.patch.object(discover, 'try_shell', autospec=True)
class TestDiscoverSchedulingProperties(BaseDiscoverTest):
def test_ok(self, mock_shell):
mock_shell.side_effect = iter(('2', 'x86_64', '5368709120',
'1024\n1024\nno\n2048\n'))
discover.discover_scheduling_properties(self.data, self.failures)
self.assertFalse(self.failures)
self.assertEqual({'cpus': 2, 'cpu_arch': 'x86_64', 'local_gb': 4,
'memory_mb': 4096}, self.data)
def test_no_ram(self, mock_shell):
mock_shell.side_effect = iter(('2', 'x86_64', '5368709120', None))
discover.discover_scheduling_properties(self.data, self.failures)
self.assertIn('failed to get RAM', self.failures.get_error())
self.assertEqual({'cpus': 2, 'cpu_arch': 'x86_64', 'local_gb': 4,
'memory_mb': None}, self.data)
def test_local_gb_too_small(self, mock_shell):
mock_shell.side_effect = iter(('2', 'x86_64', '42',
'1024\n1024\nno\n2048\n'))
discover.discover_scheduling_properties(self.data, self.failures)
self.assertIn('local_gb is less than 1 GiB', self.failures.get_error())
self.assertEqual({'cpus': 2, 'cpu_arch': 'x86_64', 'local_gb': None,
'memory_mb': 4096}, self.data)
@mock.patch.object(discover, 'try_call')
class TestDiscoverAdditionalProperties(BaseDiscoverTest):
def test_ok(self, mock_call):
mock_call.return_value = '["prop1", "prop2"]'
discover.discover_additional_properties(
FAKE_ARGS, self.data, self.failures)
self.assertFalse(self.failures)
mock_call.assert_called_once_with('hardware-detect')
self.assertEqual(['prop1', 'prop2'], self.data['data'])
def test_failure(self, mock_call):
mock_call.return_value = None
discover.discover_additional_properties(
FAKE_ARGS, self.data, self.failures)
self.assertIn('unable to get extended hardware properties',
self.failures.get_error())
self.assertNotIn('data', self.data)
def test_not_json(self, mock_call):
mock_call.return_value = 'foo?'
discover.discover_additional_properties(
FAKE_ARGS, self.data, self.failures)
self.assertIn('unable to get extended hardware properties',
self.failures.get_error())
self.assertNotIn('data', self.data)
@mock.patch.object(discover, 'try_shell')
class TestDiscoverBlockDevices(BaseDiscoverTest):
def test_ok(self, mock_shell):
mock_shell.return_value = 'QM00005\nQM00006'
discover.discover_block_devices(self.data)
self.assertEqual({'serials': ['QM00005', 'QM00006']},
self.data['block_devices'])
def test_failure(self, mock_shell):
mock_shell.return_value = None
discover.discover_block_devices(self.data)
self.assertNotIn('block_devices', self.data)
@mock.patch.object(requests, 'post', autospec=True)
class TestCallDiscoverd(unittest.TestCase):
def test_ok(self, mock_post):
failures = discover.AccumulatedFailure()
data = collections.OrderedDict(data=42)
mock_post.return_value.status_code = 200
discover.call_inspector(FAKE_ARGS, data, failures)
mock_post.assert_called_once_with('url',
data='{"data": 42, "error": null}')
def test_send_failure(self, mock_post):
failures = mock.Mock(spec=discover.AccumulatedFailure)
failures.get_error.return_value = "boom"
data = collections.OrderedDict(data=42)
mock_post.return_value.status_code = 200
discover.call_inspector(FAKE_ARGS, data, failures)
mock_post.assert_called_once_with('url',
data='{"data": 42, "error": "boom"}')
def test_inspector_error(self, mock_post):
failures = discover.AccumulatedFailure()
data = collections.OrderedDict(data=42)
mock_post.return_value.status_code = 400
discover.call_inspector(FAKE_ARGS, data, failures)
mock_post.assert_called_once_with('url',
data='{"data": 42, "error": null}')
mock_post.return_value.raise_for_status.assert_called_once_with()
@mock.patch.object(discover, 'try_shell')
class TestCollectLogs(unittest.TestCase):
def _fake_journal_write(self, shell):
file_name = shell.rsplit(' ', 1)[1].strip("'")
with open(file_name, 'wb') as fp:
fp.write(b'journal contents')
return ""
def setUp(self):
super(TestCollectLogs, self).setUp()
temp_dir = tempfile.mkdtemp()
self.addCleanup(lambda: shutil.rmtree(temp_dir))
self.files = [os.path.join(temp_dir, fname)
for fname in ('main', 'log_1', 'log_2')]
for fname in self.files[:2]:
with open(fname, 'wb') as fp:
fp.write(fname.encode())
self.fake_args = get_fake_args()
self.fake_args.log_file = self.files[0]
self.fake_args.system_log_file = self.files[1:]
def test(self, mock_shell):
mock_shell.side_effect = self._fake_journal_write
res = discover.collect_logs(self.fake_args)
res = io.BytesIO(base64.b64decode(res))
with tarfile.open(fileobj=res) as tar:
members = list(sorted((m.name, m.size) for m in tar))
self.assertEqual(
[('journal', 16)] +
list(sorted((name[1:], len(name)) for name in self.files[:2])),
members)
def test_no_journal(self, mock_shell):
mock_shell.return_value = None
res = discover.collect_logs(self.fake_args)
res = io.BytesIO(base64.b64decode(res))
with tarfile.open(fileobj=res) as tar:
members = list(sorted((m.name, m.size) for m in tar))
self.assertEqual(
list(sorted((name[1:], len(name)) for name in self.files[:2])),
members)
@mock.patch.object(discover, 'try_call', autospec=True)
class TestSetupIpmiCredentials(unittest.TestCase):
def setUp(self):
super(TestSetupIpmiCredentials, self).setUp()
self.resp = {'ipmi_username': 'user',
'ipmi_password': 'pwd'}
def test_ok(self, mock_call):
mock_call.return_value = ""
discover.setup_ipmi_credentials(self.resp)
mock_call.assert_any_call('ipmitool', 'user', 'set', 'name',
'2', 'user')
mock_call.assert_any_call('ipmitool', 'user', 'set', 'password',
'2', 'pwd')
mock_call.assert_any_call('ipmitool', 'user', 'enable', '2')
mock_call.assert_any_call('ipmitool', 'channel', 'setaccess', '1', '2',
'link=on', 'ipmi=on', 'callin=on',
'privilege=4')
def test_user_failed(self, mock_call):
mock_call.return_value = None
self.assertRaises(RuntimeError, discover.setup_ipmi_credentials,
self.resp)
mock_call.assert_called_once_with('ipmitool', 'user', 'set', 'name',
'2', 'user')
def test_password_failed(self, mock_call):
mock_call.side_effect = iter(("", None))
self.assertRaises(RuntimeError, discover.setup_ipmi_credentials,
self.resp)
mock_call.assert_any_call('ipmitool', 'user', 'set', 'name',
'2', 'user')
mock_call.assert_any_call('ipmitool', 'user', 'set', 'password',
'2', 'pwd')