a228912827
This patch is part of work splitting inspector api and worker. The rpc server is wrapped into oslo.service, and launched from api process. Now api and worker belongs to separate thread, functional tests use the fake messaging driver, while devstack jobs use rabbitmq. Change-Id: Ie03d16f9d115c3561723463dea6a57d2a763bcc6 Story: #2001842 Task: #23109
795 lines
31 KiB
Python
795 lines
31 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 eventlet
|
|
|
|
import contextlib # noqa
|
|
import copy
|
|
import datetime
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
|
|
import fixtures
|
|
import mock
|
|
from oslo_config import cfg
|
|
from oslo_config import fixture as config_fixture
|
|
from oslo_utils import timeutils
|
|
from oslo_utils import uuidutils
|
|
import pytz
|
|
import requests
|
|
import six
|
|
from six.moves import urllib
|
|
|
|
from ironic_inspector.cmd import all as inspector_cmd
|
|
from ironic_inspector.common import ironic as ir_utils
|
|
from ironic_inspector.common import swift
|
|
from ironic_inspector import db
|
|
from ironic_inspector import dbsync
|
|
from ironic_inspector import introspection_state as istate
|
|
from ironic_inspector import main
|
|
from ironic_inspector import node_cache
|
|
from ironic_inspector import rules
|
|
from ironic_inspector.test import base
|
|
from ironic_inspector.test.unit import test_rules
|
|
|
|
eventlet.monkey_patch()
|
|
|
|
CONF = """
|
|
[ironic]
|
|
auth_type=none
|
|
endpoint_override=http://url
|
|
[pxe_filter]
|
|
driver = noop
|
|
[DEFAULT]
|
|
debug = True
|
|
introspection_delay = 0
|
|
auth_strategy=noauth
|
|
transport_url=fake://
|
|
[database]
|
|
connection = sqlite:///%(db_file)s
|
|
[processing]
|
|
processing_hooks=$default_processing_hooks,lldp_basic
|
|
"""
|
|
|
|
|
|
DEFAULT_SLEEP = 2
|
|
TEST_CONF_FILE = None
|
|
|
|
|
|
def get_test_conf_file():
|
|
global TEST_CONF_FILE
|
|
if not TEST_CONF_FILE:
|
|
d = tempfile.mkdtemp()
|
|
TEST_CONF_FILE = os.path.join(d, 'test.conf')
|
|
db_file = os.path.join(d, 'test.db')
|
|
with open(TEST_CONF_FILE, 'wb') as fp:
|
|
content = CONF % {'db_file': db_file}
|
|
fp.write(content.encode('utf-8'))
|
|
return TEST_CONF_FILE
|
|
|
|
|
|
def get_error(response):
|
|
return response.json()['error']['message']
|
|
|
|
|
|
def _query_string(*field_names):
|
|
def outer(func):
|
|
@six.wraps(func)
|
|
def inner(*args, **kwargs):
|
|
queries = []
|
|
for field_name in field_names:
|
|
field = kwargs.pop(field_name, None)
|
|
if field is not None:
|
|
queries.append('%s=%s' % (field_name, field))
|
|
|
|
query_string = '&'.join(queries)
|
|
if query_string:
|
|
query_string = '?' + query_string
|
|
return func(*args, query_string=query_string, **kwargs)
|
|
return inner
|
|
return outer
|
|
|
|
|
|
class Base(base.NodeTest):
|
|
ROOT_URL = 'http://127.0.0.1:5050'
|
|
IS_FUNCTIONAL = True
|
|
|
|
def setUp(self):
|
|
super(Base, self).setUp()
|
|
rules.delete_all()
|
|
|
|
self.cli_fixture = self.useFixture(
|
|
fixtures.MockPatchObject(ir_utils, 'get_client'))
|
|
self.cli = self.cli_fixture.mock.return_value
|
|
self.cli.node.get.return_value = self.node
|
|
self.cli.node.update.return_value = self.node
|
|
self.cli.node.list.return_value = [self.node]
|
|
|
|
self.patch = [
|
|
{'op': 'add', 'path': '/properties/cpus', 'value': '4'},
|
|
{'path': '/properties/cpu_arch', 'value': 'x86_64', 'op': 'add'},
|
|
{'op': 'add', 'path': '/properties/memory_mb', 'value': '12288'},
|
|
{'path': '/properties/local_gb', 'value': '999', 'op': 'add'}
|
|
]
|
|
self.patch_root_hints = [
|
|
{'op': 'add', 'path': '/properties/cpus', 'value': '4'},
|
|
{'path': '/properties/cpu_arch', 'value': 'x86_64', 'op': 'add'},
|
|
{'op': 'add', 'path': '/properties/memory_mb', 'value': '12288'},
|
|
{'path': '/properties/local_gb', 'value': '19', 'op': 'add'}
|
|
]
|
|
|
|
self.node.power_state = 'power off'
|
|
|
|
self.cfg = self.useFixture(config_fixture.Config())
|
|
conf_file = get_test_conf_file()
|
|
self.cfg.set_config_files([conf_file])
|
|
|
|
# FIXME(milan) FakeListener.poll calls time.sleep() which leads to
|
|
# busy polling with no sleep at all, effectively blocking the whole
|
|
# process by consuming all CPU cycles in a single thread. MonkeyPatch
|
|
# with eventlet.sleep seems to help this.
|
|
self.useFixture(fixtures.MonkeyPatch(
|
|
'oslo_messaging._drivers.impl_fake.time.sleep', eventlet.sleep))
|
|
|
|
def tearDown(self):
|
|
super(Base, self).tearDown()
|
|
node_cache._delete_node(self.uuid)
|
|
|
|
def call(self, method, endpoint, data=None, expect_error=None,
|
|
api_version=None):
|
|
if data is not None:
|
|
data = json.dumps(data)
|
|
endpoint = self.ROOT_URL + endpoint
|
|
headers = {'X-Auth-Token': 'token'}
|
|
if api_version:
|
|
headers[main._VERSION_HEADER] = '%d.%d' % api_version
|
|
res = getattr(requests, method.lower())(endpoint, data=data,
|
|
headers=headers)
|
|
if expect_error:
|
|
self.assertEqual(expect_error, res.status_code)
|
|
else:
|
|
if res.status_code >= 400:
|
|
msg = ('%(meth)s %(url)s failed with code %(code)s: %(msg)s' %
|
|
{'meth': method.upper(), 'url': endpoint,
|
|
'code': res.status_code, 'msg': get_error(res)})
|
|
raise AssertionError(msg)
|
|
return res
|
|
|
|
def call_introspect(self, uuid, manage_boot=True, **kwargs):
|
|
endpoint = '/v1/introspection/%s' % uuid
|
|
if manage_boot is not None:
|
|
endpoint = '%s?manage_boot=%s' % (endpoint, manage_boot)
|
|
return self.call('post', endpoint)
|
|
|
|
def call_get_status(self, uuid, **kwargs):
|
|
return self.call('get', '/v1/introspection/%s' % uuid, **kwargs).json()
|
|
|
|
@_query_string('marker', 'limit')
|
|
def call_get_statuses(self, query_string='', **kwargs):
|
|
path = '/v1/introspection'
|
|
return self.call('get', path + query_string, **kwargs).json()
|
|
|
|
def call_abort_introspect(self, uuid, **kwargs):
|
|
return self.call('post', '/v1/introspection/%s/abort' % uuid, **kwargs)
|
|
|
|
def call_reapply(self, uuid, **kwargs):
|
|
return self.call('post', '/v1/introspection/%s/data/unprocessed' %
|
|
uuid, **kwargs)
|
|
|
|
def call_continue(self, data, **kwargs):
|
|
return self.call('post', '/v1/continue', data=data, **kwargs).json()
|
|
|
|
def call_add_rule(self, data, **kwargs):
|
|
return self.call('post', '/v1/rules', data=data, **kwargs).json()
|
|
|
|
def call_list_rules(self, **kwargs):
|
|
return self.call('get', '/v1/rules', **kwargs).json()['rules']
|
|
|
|
def call_delete_rules(self, **kwargs):
|
|
self.call('delete', '/v1/rules', **kwargs)
|
|
|
|
def call_delete_rule(self, uuid, **kwargs):
|
|
self.call('delete', '/v1/rules/' + uuid, **kwargs)
|
|
|
|
def call_get_rule(self, uuid, **kwargs):
|
|
return self.call('get', '/v1/rules/' + uuid, **kwargs).json()
|
|
|
|
def _fake_status(self, finished=mock.ANY, state=mock.ANY, error=mock.ANY,
|
|
started_at=mock.ANY, finished_at=mock.ANY,
|
|
links=mock.ANY):
|
|
return {'uuid': self.uuid, 'finished': finished, 'error': error,
|
|
'state': state, 'finished_at': finished_at,
|
|
'started_at': started_at,
|
|
'links': [{u'href': u'%s/v1/introspection/%s' % (self.ROOT_URL,
|
|
self.uuid),
|
|
u'rel': u'self'}]}
|
|
|
|
def check_status(self, status, finished, state, error=None):
|
|
self.assertEqual(
|
|
self._fake_status(finished=finished,
|
|
state=state,
|
|
finished_at=finished and mock.ANY or None,
|
|
error=error),
|
|
status
|
|
)
|
|
curr_time = datetime.datetime.fromtimestamp(
|
|
time.time(), tz=pytz.timezone(time.tzname[0]))
|
|
started_at = timeutils.parse_isotime(status['started_at'])
|
|
self.assertLess(started_at, curr_time)
|
|
if finished:
|
|
finished_at = timeutils.parse_isotime(status['finished_at'])
|
|
self.assertLess(started_at, finished_at)
|
|
self.assertLess(finished_at, curr_time)
|
|
else:
|
|
self.assertIsNone(status['finished_at'])
|
|
|
|
def db_row(self):
|
|
"""return database row matching self.uuid."""
|
|
return db.model_query(db.Node).get(self.uuid)
|
|
|
|
|
|
class Test(Base):
|
|
def test_bmc(self):
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
|
|
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
|
self.cli.port.create.assert_called_once_with(
|
|
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
|
|
pxe_enabled=True)
|
|
self.assertTrue(self.cli.node.set_boot_device.called)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
def test_port_creation_update_and_deletion(self):
|
|
cfg.CONF.set_override('add_ports', 'active', 'processing')
|
|
cfg.CONF.set_override('keep_ports', 'added', 'processing')
|
|
|
|
uuid_to_delete = uuidutils.generate_uuid()
|
|
uuid_to_update = uuidutils.generate_uuid()
|
|
# Two ports already exist: one with incorrect pxe_enabled, the other
|
|
# should be deleted.
|
|
self.cli.node.list_ports.return_value = [
|
|
mock.Mock(address=self.macs[1], uuid=uuid_to_update,
|
|
node_uuid=self.uuid, extra={}, pxe_enabled=True),
|
|
mock.Mock(address='foobar', uuid=uuid_to_delete,
|
|
node_uuid=self.uuid, extra={}, pxe_enabled=True),
|
|
]
|
|
# Two more ports are created, one with client_id. Make sure the
|
|
# returned object has the same properties as requested in create().
|
|
self.cli.port.create.side_effect = mock.Mock
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
|
|
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
|
calls = [
|
|
mock.call(node_uuid=self.uuid, address=self.macs[0],
|
|
extra={}, pxe_enabled=True),
|
|
mock.call(node_uuid=self.uuid, address=self.macs[2],
|
|
extra={'client-id': self.client_id}, pxe_enabled=False),
|
|
]
|
|
self.cli.port.create.assert_has_calls(calls, any_order=True)
|
|
self.cli.port.delete.assert_called_once_with(uuid_to_delete)
|
|
self.cli.port.update.assert_called_once_with(
|
|
uuid_to_update,
|
|
[{'op': 'replace', 'path': '/pxe_enabled', 'value': False}])
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
def test_introspection_statuses(self):
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
# NOTE(zhenguo): only test finished=False here, as we don't know
|
|
# other nodes status in this thread.
|
|
statuses = self.call_get_statuses().get('introspection')
|
|
self.assertIn(self._fake_status(finished=False), statuses)
|
|
|
|
# check we've got 1 status with a limit of 1
|
|
statuses = self.call_get_statuses(limit=1).get('introspection')
|
|
self.assertEqual(1, len(statuses))
|
|
|
|
all_statuses = self.call_get_statuses().get('introspection')
|
|
marker_statuses = self.call_get_statuses(
|
|
marker=self.uuid, limit=1).get('introspection')
|
|
marker_index = all_statuses.index(self.call_get_status(self.uuid))
|
|
# marker is the last row on previous page
|
|
self.assertEqual(all_statuses[marker_index+1:marker_index+2],
|
|
marker_statuses)
|
|
|
|
self.call_continue(self.data)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
# fetch all statuses and db nodes to assert pagination
|
|
statuses = self.call_get_statuses().get('introspection')
|
|
nodes = db.model_query(db.Node).order_by(
|
|
db.Node.started_at.desc()).all()
|
|
|
|
# assert ordering
|
|
self.assertEqual([node.uuid for node in nodes],
|
|
[status_.get('uuid') for status_ in statuses])
|
|
|
|
# assert pagination
|
|
half = len(nodes) // 2
|
|
marker = nodes[half].uuid
|
|
statuses = self.call_get_statuses(marker=marker).get('introspection')
|
|
self.assertEqual([node.uuid for node in nodes[half + 1:]],
|
|
[status_.get('uuid') for status_ in statuses])
|
|
|
|
# assert status links work
|
|
self.assertEqual([self.call_get_status(status_.get('uuid'))
|
|
for status_ in statuses],
|
|
[self.call('GET', urllib.parse.urlparse(
|
|
status_.get('links')[0].get('href')).path).json()
|
|
for status_ in statuses])
|
|
|
|
def test_manage_boot(self):
|
|
self.call_introspect(self.uuid, manage_boot=False)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.assertFalse(self.cli.node.set_power_state.called)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
|
|
self.assertFalse(self.cli.node.set_boot_device.called)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
def test_rules_api(self):
|
|
res = self.call_list_rules()
|
|
self.assertEqual([], res)
|
|
|
|
rule = {
|
|
'conditions': [
|
|
{'op': 'eq', 'field': 'memory_mb', 'value': 1024},
|
|
],
|
|
'actions': [{'action': 'fail', 'message': 'boom'}],
|
|
'description': 'Cool actions'
|
|
}
|
|
|
|
res = self.call_add_rule(rule)
|
|
self.assertTrue(res['uuid'])
|
|
rule['uuid'] = res['uuid']
|
|
rule['links'] = res['links']
|
|
rule['conditions'] = [
|
|
test_rules.BaseTest.condition_defaults(rule['conditions'][0]),
|
|
]
|
|
self.assertEqual(rule, res)
|
|
|
|
res = self.call('get', rule['links'][0]['href']).json()
|
|
self.assertEqual(rule, res)
|
|
|
|
res = self.call_list_rules()
|
|
self.assertEqual(rule['links'], res[0].pop('links'))
|
|
self.assertEqual([{'uuid': rule['uuid'],
|
|
'description': 'Cool actions'}],
|
|
res)
|
|
|
|
res = self.call_get_rule(rule['uuid'])
|
|
self.assertEqual(rule, res)
|
|
|
|
self.call_delete_rule(rule['uuid'])
|
|
res = self.call_list_rules()
|
|
self.assertEqual([], res)
|
|
|
|
links = rule.pop('links')
|
|
del rule['uuid']
|
|
for _ in range(3):
|
|
self.call_add_rule(rule)
|
|
|
|
res = self.call_list_rules()
|
|
self.assertEqual(3, len(res))
|
|
|
|
self.call_delete_rules()
|
|
res = self.call_list_rules()
|
|
self.assertEqual([], res)
|
|
|
|
self.call('get', links[0]['href'], expect_error=404)
|
|
self.call('delete', links[0]['href'], expect_error=404)
|
|
|
|
def test_introspection_rules(self):
|
|
self.node.extra['bar'] = 'foo'
|
|
rules = [
|
|
{
|
|
'conditions': [
|
|
{'field': 'memory_mb', 'op': 'eq', 'value': 12288},
|
|
{'field': 'local_gb', 'op': 'gt', 'value': 998},
|
|
{'field': 'local_gb', 'op': 'lt', 'value': 1000},
|
|
{'field': 'local_gb', 'op': 'matches', 'value': '[0-9]+'},
|
|
{'field': 'cpu_arch', 'op': 'contains', 'value': '[0-9]+'},
|
|
{'field': 'root_disk.wwn', 'op': 'is-empty'},
|
|
{'field': 'inventory.interfaces[*].ipv4_address',
|
|
'op': 'contains', 'value': r'127\.0\.0\.1',
|
|
'invert': True, 'multiple': 'all'},
|
|
{'field': 'i.do.not.exist', 'op': 'is-empty'},
|
|
],
|
|
'actions': [
|
|
{'action': 'set-attribute', 'path': '/extra/foo',
|
|
'value': 'bar'}
|
|
]
|
|
},
|
|
{
|
|
'conditions': [
|
|
{'field': 'memory_mb', 'op': 'ge', 'value': 100500},
|
|
],
|
|
'actions': [
|
|
{'action': 'set-attribute', 'path': '/extra/bar',
|
|
'value': 'foo'},
|
|
{'action': 'fail', 'message': 'boom'}
|
|
]
|
|
}
|
|
]
|
|
for rule in rules:
|
|
self.call_add_rule(rule)
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.call_continue(self.data)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_any_call(
|
|
self.uuid,
|
|
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
|
|
|
def test_conditions_scheme_actions_path(self):
|
|
rules = [
|
|
{
|
|
'conditions': [
|
|
{'field': 'node://properties.local_gb', 'op': 'eq',
|
|
'value': 40},
|
|
{'field': 'node://driver_info.ipmi_address', 'op': 'eq',
|
|
'value': self.bmc_address},
|
|
],
|
|
'actions': [
|
|
{'action': 'set-attribute', 'path': '/extra/foo',
|
|
'value': 'bar'}
|
|
]
|
|
},
|
|
{
|
|
'conditions': [
|
|
{'field': 'data://inventory.cpu.count', 'op': 'eq',
|
|
'value': self.data['inventory']['cpu']['count']},
|
|
],
|
|
'actions': [
|
|
{'action': 'set-attribute',
|
|
'path': '/driver_info/ipmi_address',
|
|
'value': '{data[inventory][bmc_address]}'}
|
|
]
|
|
}
|
|
]
|
|
for rule in rules:
|
|
self.call_add_rule(rule)
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.call_continue(self.data)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_any_call(
|
|
self.uuid,
|
|
[{'op': 'add', 'path': '/extra/foo', 'value': 'bar'}])
|
|
|
|
self.cli.node.update.assert_any_call(
|
|
self.uuid,
|
|
[{'op': 'add', 'path': '/driver_info/ipmi_address',
|
|
'value': self.data['inventory']['bmc_address']}])
|
|
|
|
def test_root_device_hints(self):
|
|
self.node.properties['root_device'] = {'size': 20}
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.assertCalledWithPatch(self.patch_root_hints, self.cli.node.update)
|
|
self.cli.port.create.assert_called_once_with(
|
|
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
|
|
pxe_enabled=True)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
def test_abort_introspection(self):
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_abort_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.assertEqual(202, res.status_code)
|
|
status = self.call_get_status(self.uuid)
|
|
self.assertTrue(status['finished'])
|
|
self.assertEqual('Canceled by operator', status['error'])
|
|
|
|
# Note(mkovacik): we're checking just this doesn't pass OK as
|
|
# there might be either a race condition (hard to test) that
|
|
# yields a 'Node already finished.' or an attribute-based
|
|
# look-up error from some pre-processing hooks because
|
|
# node_info.finished() deletes the look-up attributes only
|
|
# after releasing the node lock
|
|
self.call('post', '/v1/continue', self.data, expect_error=400)
|
|
|
|
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
|
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
|
def test_stored_data_processing(self, get_mock, store_mock):
|
|
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
|
|
|
# ramdisk data copy
|
|
# please mind the data is changed during processing
|
|
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
|
get_mock.return_value = ramdisk_data
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
inspect_started_at = timeutils.parse_isotime(status['started_at'])
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
res = self.call_reapply(self.uuid)
|
|
self.assertEqual(202, res.status_code)
|
|
self.assertEqual('', res.text)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
# checks the started_at updated in DB is correct
|
|
reapply_started_at = timeutils.parse_isotime(status['started_at'])
|
|
self.assertLess(inspect_started_at, reapply_started_at)
|
|
|
|
# reapply request data
|
|
get_mock.assert_called_once_with(self.uuid,
|
|
suffix='UNPROCESSED')
|
|
|
|
# store ramdisk data, store processing result data, store
|
|
# reapply processing result data; the ordering isn't
|
|
# guaranteed as store ramdisk data runs in a background
|
|
# thread; hower, last call has to always be reapply processing
|
|
# result data
|
|
store_ramdisk_call = mock.call(mock.ANY, self.uuid,
|
|
suffix='UNPROCESSED')
|
|
store_processing_call = mock.call(mock.ANY, self.uuid,
|
|
suffix=None)
|
|
self.assertEqual(3, len(store_mock.call_args_list))
|
|
self.assertIn(store_ramdisk_call,
|
|
store_mock.call_args_list[0:2])
|
|
self.assertIn(store_processing_call,
|
|
store_mock.call_args_list[0:2])
|
|
self.assertEqual(store_processing_call,
|
|
store_mock.call_args_list[2])
|
|
|
|
# second reapply call
|
|
get_mock.return_value = ramdisk_data
|
|
res = self.call_reapply(self.uuid)
|
|
self.assertEqual(202, res.status_code)
|
|
self.assertEqual('', res.text)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
# reapply saves the result
|
|
self.assertEqual(4, len(store_mock.call_args_list))
|
|
self.assertEqual(store_processing_call,
|
|
store_mock.call_args_list[-1])
|
|
|
|
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
|
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
|
def test_edge_state_transitions(self, get_mock, store_mock):
|
|
"""Assert state transitions work as expected in edge conditions."""
|
|
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
|
|
|
# ramdisk data copy
|
|
# please mind the data is changed during processing
|
|
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
|
get_mock.return_value = ramdisk_data
|
|
|
|
# multiple introspect calls
|
|
self.call_introspect(self.uuid)
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
# an error -start-> starting state transition is possible
|
|
self.call_abort_introspect(self.uuid)
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
# double abort works
|
|
self.call_abort_introspect(self.uuid)
|
|
status = self.call_get_status(self.uuid)
|
|
error = status['error']
|
|
self.check_status(status, finished=True, state=istate.States.error,
|
|
error=error)
|
|
self.call_abort_introspect(self.uuid)
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.error,
|
|
error=error)
|
|
|
|
# preventing stale data race condition
|
|
# waiting -> processing is a strict state transition
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
row = self.db_row()
|
|
row.state = istate.States.processing
|
|
with db.ensure_transaction() as session:
|
|
row.save(session)
|
|
self.call_continue(self.data, expect_error=400)
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.error,
|
|
error=mock.ANY)
|
|
self.assertIn('no defined transition', status['error'])
|
|
# multiple reapply calls
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.call_continue(self.data)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.call_reapply(self.uuid)
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished,
|
|
error=None)
|
|
self.call_reapply(self.uuid)
|
|
# assert an finished -reapply-> reapplying -> finished state transition
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished,
|
|
error=None)
|
|
|
|
def test_without_root_disk(self):
|
|
del self.data['root_disk']
|
|
self.inventory['disks'] = []
|
|
self.patch[-1] = {'path': '/properties/local_gb',
|
|
'value': '0', 'op': 'add'}
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
self.cli.node.update.assert_called_with(self.uuid, mock.ANY)
|
|
self.assertCalledWithPatch(self.patch, self.cli.node.update)
|
|
self.cli.port.create.assert_called_once_with(
|
|
node_uuid=self.uuid, extra={}, address='11:22:33:44:55:66',
|
|
pxe_enabled=True)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
@mock.patch.object(swift, 'store_introspection_data', autospec=True)
|
|
@mock.patch.object(swift, 'get_introspection_data', autospec=True)
|
|
def test_lldp_plugin(self, get_mock, store_mock):
|
|
cfg.CONF.set_override('store_data', 'swift', 'processing')
|
|
|
|
ramdisk_data = json.dumps(copy.deepcopy(self.data))
|
|
get_mock.return_value = ramdisk_data
|
|
|
|
self.call_introspect(self.uuid)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
self.cli.node.set_power_state.assert_called_once_with(self.uuid,
|
|
'reboot')
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=False, state=istate.States.waiting)
|
|
|
|
res = self.call_continue(self.data)
|
|
self.assertEqual({'uuid': self.uuid}, res)
|
|
eventlet.greenthread.sleep(DEFAULT_SLEEP)
|
|
|
|
status = self.call_get_status(self.uuid)
|
|
self.check_status(status, finished=True, state=istate.States.finished)
|
|
|
|
# Verify that the lldp_processed data is written to swift
|
|
# as expected by the lldp plugin
|
|
updated_data = store_mock.call_args[0][0]
|
|
lldp_out = updated_data['all_interfaces']['eth1']
|
|
|
|
expected_chassis_id = "11:22:33:aa:bb:cc"
|
|
expected_port_id = "734"
|
|
self.assertEqual(expected_chassis_id,
|
|
lldp_out['lldp_processed']['switch_chassis_id'])
|
|
self.assertEqual(expected_port_id,
|
|
lldp_out['lldp_processed']['switch_port_id'])
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def mocked_server():
|
|
conf_file = get_test_conf_file()
|
|
dbsync.main(args=['--config-file', conf_file, 'upgrade'])
|
|
|
|
cfg.CONF.reset()
|
|
cfg.CONF.unregister_opt(dbsync.command_opt)
|
|
|
|
eventlet.greenthread.spawn_n(inspector_cmd.main,
|
|
args=['--config-file', conf_file])
|
|
eventlet.greenthread.sleep(1)
|
|
# Wait for service to start up to 30 seconds
|
|
for i in range(10):
|
|
try:
|
|
requests.get('http://127.0.0.1:5050/v1')
|
|
except requests.ConnectionError:
|
|
if i == 9:
|
|
raise
|
|
print('Service did not start yet')
|
|
eventlet.greenthread.sleep(3)
|
|
else:
|
|
break
|
|
# start testing
|
|
yield
|
|
# Make sure all processes finished executing
|
|
eventlet.greenthread.sleep(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
with mocked_server():
|
|
unittest.main(verbosity=2)
|