Create the Node object.

This adds the Node object and tests,
and updates other unit tests to use the new object.

implements bp:ironic-object-model

Change-Id: Id09343f401ed01b89533dca16c31262ec8e3f732
This commit is contained in:
Devananda van der Veen 2013-06-06 07:43:29 -07:00
parent 59d5bea14a
commit 8634d749a1
5 changed files with 242 additions and 22 deletions

View File

@ -26,6 +26,7 @@ from ironic.common import exception
from ironic.common import utils
from ironic.db import api
from ironic.db.sqlalchemy import models
from ironic.objects import node
from ironic.openstack.common.db.sqlalchemy import session as db_session
from ironic.openstack.common import log
from ironic.openstack.common import uuidutils
@ -81,15 +82,19 @@ class Connection(api.Connection):
def __init__(self):
pass
@node.objectify
def get_nodes(self, columns):
pass
@node.objectify
def get_associated_nodes(self):
pass
@node.objectify
def get_unassociated_nodes(self):
pass
@node.objectify
def reserve_nodes(self, tag, nodes):
# Ensure consistent sort order so we don't run into deadlocks.
nodes.sort()
@ -143,12 +148,14 @@ class Connection(api.Connection):
if ref['reservation'] is not None:
raise exception.NodeLocked(node=node)
@node.objectify
def create_node(self, values):
node = models.Node()
node.update(values)
node.save()
return node
@node.objectify
def get_node(self, node):
query = model_query(models.Node)
query = add_uuid_filter(query, node)
@ -160,6 +167,7 @@ class Connection(api.Connection):
return result
@node.objectify
def get_node_by_instance(self, instance):
query = model_query(models.Node)
if uuidutils.is_uuid_like(instance):
@ -184,6 +192,7 @@ class Connection(api.Connection):
if count != 1:
raise exception.NodeNotFound(node=node)
@node.objectify
def update_node(self, node, values):
session = get_session()
with session.begin():

103
ironic/objects/node.py Normal file
View File

@ -0,0 +1,103 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
#
#
# 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 ironic.db import api as db_api
from ironic.objects import base
from ironic.objects import utils
def objectify(fn):
"""Decorator to convert database results into Node objects."""
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
try:
return Node._from_db_object(Node(), result)
except TypeError:
# TODO(deva): handle lists of objects better
# once support for those lands and is imported.
return [Node._from_db_object(Node(), obj) for obj in result]
return wrapper
class Node(base.IronicObject):
dbapi = db_api.get_instance()
fields = {
'id': int,
'uuid': utils.str_or_none,
'chassis_id': utils.int_or_none,
'instance_uuid': utils.str_or_none,
# NOTE(deva): should driver_info be a nested_object_or_none,
# or does this bind the driver API too tightly?
'driver': utils.str_or_none,
'driver_info': utils.str_or_none,
'properties': utils.str_or_none,
'reservation': utils.str_or_none,
'task_state': utils.str_or_none,
'task_start': utils.datetime_or_none,
'extra': utils.str_or_none,
}
@staticmethod
def _from_db_object(node, db_node):
"""Converts a database entity to a formal object."""
for field in node.fields:
node[field] = db_node[field]
node.obj_reset_changes()
return node
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a node based on uuid and return a Node object.
:param uuid: the uuid of a node.
:returns: a :class:`Node` object.
"""
# TODO(deva): enable getting ports for this node
db_node = cls.dbapi.get_node(uuid)
return Node._from_db_object(cls(), db_node)
@base.remotable
def save(self, context):
"""Save updates to this Node.
Column-wise updates will be made based on the result of
self.what_changed(). If expected_task_state is provided,
it will be checked against the in-database copy of the
node before updates are made.
:param context: Security context
"""
updates = {}
changes = self.obj_what_changed()
for field in changes:
updates[field] = self[field]
self.dbapi.update_node(self.uuid, updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context):
current = self.__class__.get_by_uuid(context, uuid=self.uuid)
for field in self.fields:
if (hasattr(self, base.get_attrname(field)) and
self[field] != current[field]):
self[field] = current[field]

View File

@ -68,20 +68,22 @@ properties = json.dumps(
def get_test_node(**kw):
node = models.Node()
node.id = kw.get('id', 123)
node.uuid = kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
node.task_state = kw.get('task_state', 'NOSTATE')
node.instance_uuid = kw.get('instance_uuid',
'8227348d-5f1d-4488-aad1-7c92b2d42504')
node.driver = kw.get('driver', 'fake')
node.driver_info = kw.get('driver_info', fake_info)
node.properties = kw.get('properties', properties)
node.extra = kw.get('extra', '{}')
node = {
'id': kw.get('id', 123),
'uuid': kw.get('uuid', '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'),
'chassis_id': 5,
'task_start': None,
'task_state': kw.get('task_state', 'NOSTATE'),
'instance_uuid': kw.get('instance_uuid',
'8227348d-5f1d-4488-aad1-7c92b2d42504'),
'driver': kw.get('driver', 'fake'),
'driver_info': kw.get('driver_info', fake_info),
'properties': kw.get('properties', properties),
'reservation': None,
'extra': kw.get('extra', '{}'),
'updated_at': None,
'created_at': None,
}
return node

View File

@ -197,7 +197,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
ipmi._power_on(self.info).AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.driver.power.set_power_state(
task, self.node, states.POWER_ON)
self.mox.VerifyAll()
@ -210,7 +210,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
ipmi._power_off(self.info).AndReturn(states.POWER_OFF)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.driver.power.set_power_state(
task, self.node, states.POWER_OFF)
self.mox.VerifyAll()
@ -224,7 +224,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
ipmi._power_on(self.info).AndReturn(states.ERROR)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.assertRaises(exception.PowerStateFailure,
self.driver.power.set_power_state,
task,
@ -233,7 +233,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
self.mox.VerifyAll()
def test_set_power_invalid_state(self):
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.assertRaises(exception.IronicException,
self.driver.power.set_power_state,
task,
@ -247,12 +247,12 @@ class IPMIDriverTestCase(db_base.DbTestCase):
AndReturn([None, None])
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.driver.power._set_boot_device(task, self.node, 'pxe')
self.mox.VerifyAll()
def test_set_boot_device_bad_device(self):
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.assertRaises(exception.InvalidParameterValue,
self.driver.power._set_boot_device,
task,
@ -267,7 +267,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
ipmi._power_on(self.info).AndReturn(states.POWER_ON)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.driver.power.reboot(task, self.node)
self.mox.VerifyAll()
@ -280,7 +280,7 @@ class IPMIDriverTestCase(db_base.DbTestCase):
ipmi._power_on(self.info).AndReturn(states.ERROR)
self.mox.ReplayAll()
with task_manager.acquire([self.node.uuid]) as task:
with task_manager.acquire([self.node['uuid']]) as task:
self.assertRaises(exception.PowerStateFailure,
self.driver.power.reboot,
task,

View File

@ -0,0 +1,106 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
#
#
# 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 ironic.common import context
from ironic.db import api as db_api
from ironic.db.sqlalchemy import models
from ironic.objects import node
from ironic.tests.db import base
from ironic.tests.db import utils
class TestNodeObject(base.DbTestCase):
def setUp(self):
super(TestNodeObject, self).setUp()
self.fake_node = utils.get_test_node()
self.dbapi = db_api.get_instance()
def test_load(self):
ctxt = context.get_admin_context()
uuid = self.fake_node['uuid']
self.mox.StubOutWithMock(self.dbapi, 'get_node')
self.dbapi.get_node(uuid).AndReturn(self.fake_node)
self.mox.ReplayAll()
node.Node.get_by_uuid(ctxt, uuid)
self.mox.VerifyAll()
# TODO(deva): add tests for load-on-demand info, eg. ports,
# once Port objects are created
def test_save(self):
ctxt = context.get_admin_context()
uuid = self.fake_node['uuid']
self.mox.StubOutWithMock(self.dbapi, 'get_node')
self.mox.StubOutWithMock(self.dbapi, 'update_node')
self.dbapi.get_node(uuid).AndReturn(self.fake_node)
self.dbapi.update_node(uuid, {'properties': "new property"})
self.mox.ReplayAll()
n = node.Node.get_by_uuid(ctxt, uuid)
n.properties = "new property"
n.save()
self.mox.VerifyAll()
def test_refresh(self):
ctxt = context.get_admin_context()
uuid = self.fake_node['uuid']
self.mox.StubOutWithMock(self.dbapi, 'get_node')
self.dbapi.get_node(uuid).AndReturn(
dict(self.fake_node, properties="first"))
self.dbapi.get_node(uuid).AndReturn(
dict(self.fake_node, properties="second"))
self.mox.ReplayAll()
n = node.Node.get_by_uuid(ctxt, uuid)
self.assertEqual(n.properties, "first")
n.refresh()
self.assertEqual(n.properties, "second")
self.mox.VerifyAll()
def test_objectify(self):
def _get_db_node():
n = models.Node()
n.update(self.fake_node)
return n
@node.objectify
def _convert_db_node():
return _get_db_node()
self.assertIsInstance(_get_db_node(), models.Node)
self.assertIsInstance(_convert_db_node(), node.Node)
def test_objectify_many(self):
def _get_db_nodes():
nodes = []
for i in xrange(5):
n = models.Node()
n.update(self.fake_node)
nodes.append(n)
return nodes
@node.objectify
def _convert_db_nodes():
return _get_db_nodes()
for n in _get_db_nodes():
self.assertIsInstance(n, models.Node)
for n in _convert_db_nodes():
self.assertIsInstance(n, node.Node)