Fix issues with using the database

* Fix issue with 'database locked' in tests
* Fix issue with database thread safety

Change-Id: Ieffbc42a7767d0790e57461d564fccf68c0274db
This commit is contained in:
Dmitry Tantsur 2014-11-27 11:48:30 +01:00
parent d4c5f5050a
commit 7894c3d3e7
3 changed files with 78 additions and 61 deletions

View File

@ -32,7 +32,8 @@
; Whether to authenticate with Keystone on discovery initialization endpoint.
; Note that discovery postback endpoint is never authenticated.
;authenticate = true
; SQLite3 database to store nodes under discovery, defaults to in-memory.
; SQLite3 database to store nodes under discovery, defaults to a temporary
; file. Do not use :memory: here, it won't work.
;database =
; Debug mode enabled/disabled.

View File

@ -13,8 +13,11 @@
"""Cache for nodes currently under discovery."""
import atexit
import logging
import os
import sqlite3
import tempfile
import time
from ironic_discoverd import conf
@ -22,7 +25,7 @@ from ironic_discoverd import utils
LOG = logging.getLogger("discoverd")
_DB = None
_DB_NAME = None
_SCHEMA = """
create table if not exists nodes
(uuid text primary key, started_at real);
@ -36,16 +39,25 @@ create table if not exists attributes
def init():
"""Initialize the database."""
global _DB
conn = conf.get('discoverd', 'database') or ':memory:'
_DB = sqlite3.connect(conn)
_DB.executescript(_SCHEMA)
global _DB_NAME
_DB_NAME = conf.get('discoverd', 'database').strip()
if not _DB_NAME:
# We can't use in-memory, so we create a temporary file
fd, _DB_NAME = tempfile.mkstemp(prefix='discoverd-')
os.close(fd)
def cleanup():
if os.path.exists(_DB_NAME):
os.unlink(_DB_NAME)
atexit.register(cleanup)
sqlite3.connect(_DB_NAME).executescript(_SCHEMA)
def _db():
if _DB is None:
if _DB_NAME is None:
init()
return _DB
return sqlite3.connect(_DB_NAME)
def add_node(uuid, **attributes):
@ -58,9 +70,9 @@ def add_node(uuid, **attributes):
:param attributes: attributes known about this node (like macs, BMC etc)
"""
drop_node(uuid)
with _db():
_db().execute("insert into nodes(uuid, started_at) "
"values(?, ?)", (uuid, time.time()))
with _db() as db:
db.execute("insert into nodes(uuid, started_at) "
"values(?, ?)", (uuid, time.time()))
for (name, value) in attributes.items():
if not value:
continue
@ -68,9 +80,9 @@ def add_node(uuid, **attributes):
value = [value]
try:
_db().executemany("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
[(name, v, uuid) for v in value])
db.executemany("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
[(name, v, uuid) for v in value])
except sqlite3.IntegrityError:
raise utils.DiscoveryFailed(
'Some or all of %(name)s\'s %(value)s are already '
@ -86,9 +98,9 @@ def macs_on_discovery():
def drop_node(uuid):
"""Forget information about node with given uuid."""
with _db():
_db().execute("delete from nodes where uuid=?", (uuid,))
_db().execute("delete from attributes where uuid=?", (uuid,))
with _db() as db:
db.execute("delete from nodes where uuid=?", (uuid,))
db.execute("delete from attributes where uuid=?", (uuid,))
def pop_node(**attributes):
@ -101,6 +113,7 @@ def pop_node(**attributes):
"""
# NOTE(dtantsur): sorting is not required, but gives us predictability
found = set()
db = _db()
for (name, value) in sorted(attributes.items()):
if not value:
LOG.warning('Empty value for attribute %s', name)
@ -109,9 +122,9 @@ def pop_node(**attributes):
value = [value]
LOG.debug('Trying to use %s %s for discovery' % (name, value))
rows = _db().execute('select distinct uuid from attributes where ' +
' OR '.join('name=? AND value=?' for _ in value),
sum(([name, v] for v in value), [])).fetchall()
rows = db.execute('select distinct uuid from attributes where ' +
' OR '.join('name=? AND value=?' for _ in value),
sum(([name, v] for v in value), [])).fetchall()
if rows:
found.update(item[0] for item in rows)

View File

@ -14,6 +14,7 @@
import eventlet
eventlet.monkey_patch(thread=False)
import os
import time
import unittest
@ -30,18 +31,22 @@ from ironic_discoverd import node_cache
from ironic_discoverd import utils
def init_conf():
conf.init_conf()
conf.CONF.add_section('discoverd')
conf.CONF.set('discoverd', 'database', '')
node_cache._DB = None
class BaseTest(unittest.TestCase):
def setUp(self):
super(BaseTest, self).setUp()
conf.init_conf()
conf.CONF.add_section('discoverd')
conf.CONF.set('discoverd', 'database', '')
node_cache._DB_NAME = None
self.db = node_cache._db()
self.addCleanup(lambda: os.unlink(node_cache._DB_NAME))
# FIXME(dtantsur): this test suite is far from being complete
@patch.object(firewall, 'update_filters', autospec=True)
@patch.object(node_cache, 'pop_node', autospec=True)
@patch.object(utils, 'get_client', autospec=True)
class TestProcess(unittest.TestCase):
class TestProcess(BaseTest):
def setUp(self):
self.node = Mock(driver_info={'ipmi_address': '1.2.3.4'},
properties={'cpu_arch': 'i386', 'local_gb': 40},
@ -68,7 +73,6 @@ class TestProcess(unittest.TestCase):
}
}
self.macs = ['11:22:33:44:55:66', 'broken', '', '66:55:44:33:22:11']
init_conf()
def _do_test(self, client_mock, pop_mock, filters_mock):
cli = client_mock.return_value
@ -135,7 +139,7 @@ class TestProcess(unittest.TestCase):
@patch.object(firewall, 'update_filters', autospec=True)
@patch.object(node_cache, 'add_node', autospec=True)
@patch.object(utils, 'get_client', autospec=True)
class TestDiscover(unittest.TestCase):
class TestDiscover(BaseTest):
def setUp(self):
super(TestDiscover, self).setUp()
self.node1 = Mock(driver='pxe_ssh',
@ -159,7 +163,6 @@ class TestDiscover(unittest.TestCase):
instance_uuid=None,
power_state='power off',
extra={'on_discovery': True})
init_conf()
@patch.object(time, 'time', lambda: 42.0)
def test_ok(self, client_mock, add_mock, filters_mock, spawn_mock):
@ -311,9 +314,9 @@ class TestDiscover(unittest.TestCase):
self.assertFalse(add_mock.called)
class TestApi(unittest.TestCase):
class TestApi(BaseTest):
def setUp(self):
init_conf()
super(TestApi, self).setUp()
main.app.config['TESTING'] = True
self.app = main.app.test_client()
@ -394,11 +397,7 @@ class TestClient(unittest.TestCase):
@patch.object(eventlet.greenthread, 'sleep', autospec=True)
@patch.object(utils, 'get_client')
class TestCheckIronicAvailable(unittest.TestCase):
def setUp(self):
super(TestCheckIronicAvailable, self).setUp()
init_conf()
class TestCheckIronicAvailable(BaseTest):
def test_ok(self, client_mock, sleep_mock):
utils.check_ironic_available()
client_mock.return_value.driver.list.assert_called_once_with()
@ -421,22 +420,22 @@ class TestCheckIronicAvailable(unittest.TestCase):
self.assertEqual(attempts, sleep_mock.call_count)
class TestNodeCache(unittest.TestCase):
class TestNodeCache(BaseTest):
def setUp(self):
super(TestNodeCache, self).setUp()
init_conf()
self.db = node_cache._db()
self.node = Mock(driver_info={'ipmi_address': '1.2.3.4'},
uuid='uuid')
self.macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
def test_add_node(self):
# Ensure previous node information is cleared
self.db.execute("insert into nodes(uuid) values(?)", (self.node.uuid,))
self.db.execute("insert into nodes(uuid) values('uuid2')")
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', self.node.uuid))
with self.db:
self.db.execute("insert into nodes(uuid) values(?)",
(self.node.uuid,))
self.db.execute("insert into nodes(uuid) values('uuid2')")
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', self.node.uuid))
node_cache.add_node(self.node.uuid, mac=self.macs,
bmc_address='1.2.3.4', foo=None)
@ -454,21 +453,25 @@ class TestNodeCache(unittest.TestCase):
res)
def test_add_node_duplicate_mac(self):
self.db.execute("insert into nodes(uuid) values(?)", ('another-uuid',))
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', 'another-uuid'))
with self.db:
self.db.execute("insert into nodes(uuid) values(?)",
('another-uuid',))
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', 'another-uuid'))
self.assertRaises(utils.DiscoveryFailed,
node_cache.add_node,
self.node.uuid, mac=['11:22:11:22:11:22'])
def test_drop_node(self):
self.db.execute("insert into nodes(uuid) values(?)", (self.node.uuid,))
self.db.execute("insert into nodes(uuid) values('uuid2')")
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', self.node.uuid))
with self.db:
self.db.execute("insert into nodes(uuid) values(?)",
(self.node.uuid,))
self.db.execute("insert into nodes(uuid) values('uuid2')")
self.db.execute("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
('mac', '11:22:11:22:11:22', self.node.uuid))
node_cache.drop_node(self.node.uuid)
@ -478,20 +481,20 @@ class TestNodeCache(unittest.TestCase):
"select * from attributes").fetchall())
def test_macs_on_discovery(self):
self.db.execute("insert into nodes(uuid) values(?)", (self.node.uuid,))
self.db.executemany("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
[('mac', '11:22:11:22:11:22', self.node.uuid),
('mac', '22:11:22:11:22:11', self.node.uuid)])
with self.db:
self.db.execute("insert into nodes(uuid) values(?)",
(self.node.uuid,))
self.db.executemany("insert into attributes(name, value, uuid) "
"values(?, ?, ?)",
[('mac', '11:22:11:22:11:22', self.node.uuid),
('mac', '22:11:22:11:22:11', self.node.uuid)])
self.assertEqual({'11:22:11:22:11:22', '22:11:22:11:22:11'},
node_cache.macs_on_discovery())
class TestNodeCachePop(unittest.TestCase):
class TestNodeCachePop(BaseTest):
def setUp(self):
super(TestNodeCachePop, self).setUp()
init_conf()
self.db = node_cache._db()
self.uuid = 'uuid'
self.macs = ['11:22:33:44:55:66', '66:55:44:33:22:11']
self.macs2 = ['00:00:00:00:00:00']