Skip to content

Commit

Permalink
Rename NOSTATE to AVAILABLE
Browse files Browse the repository at this point in the history
This patch adds the new provisioning state AVAILABLE, and bumps the API
microversion to 1.1. This version exposes the renaming of the previous
provision_state="None" to provision_state="available" for nodes which
are available for use.

A database migration is added which updates all DB records'
provision_state from NOSTATE to AVAILABLE.

Since database migrations and code changes may be rolled out at
different times, the conductor can deploy to a node in either
NOSTATE or AVAILABLE states.

OperatorImpact:
    This change should be rolled out to production services, and the
    conductor service should be restarted, *before* the database
    migration is applied. Ironic will then begin "translating" existing
    node states to the new AVAILABLE state automatically when it touches
    them. If the DB migration is run much later, it may not actually
    update any records (and that is OK).

DocImpact:
    This change updates the Node provision_state value which represents
    a node that is available for provisioning. It is changed from
    "None" to "available", but this change is only realized when the
    X-OpenStack-Ironic-API-Version header is >= 1.1

*** NOTE ***
Nova interprets the provision_state of Nodes to determine which should
be exposed to the scheduler and counted as available resources. Up to
the Juno release, Nova looked for the "NOSTATE" state to indicate this,
represented as provision_state=None.
After commit Idbd36b230cf997bed7a86c3f56cf9c70995028b2 landed in Nova,
both the old "None" and new "available" states are interpreted in this
way. As such, Nova may continue to use Juno Ironic, which did not
support microversions, or may begin using the 1.1 version.

Implements: blueprint new-ironic-state-machine

Change-Id: I5e6f6ee5877d475136ce2ebad4a9333b424dc96b
  • Loading branch information
AevaOnline committed Jan 30, 2015
1 parent 4159532 commit e7958de
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 53 deletions.
2 changes: 1 addition & 1 deletion ironic/api/controllers/v1/__init__.py
Expand Up @@ -38,7 +38,7 @@


MIN_VER = 0
MAX_VER = 0
MAX_VER = 1


class MediaType(base.APIBase):
Expand Down
9 changes: 9 additions & 0 deletions ironic/api/controllers/v1/node.py
Expand Up @@ -53,6 +53,13 @@
_VENDOR_METHODS = {}


def assert_juno_provision_state_name(obj):
# if requested version is < 1.1, convert AVAILABLE to the old NOSTATE
if (pecan.request.version.minor < 1 and
obj.provision_state == ir_states.AVAILABLE):
obj.provision_state = ir_states.NOSTATE


class NodePatchType(types.JsonPatchType):

@staticmethod
Expand Down Expand Up @@ -240,6 +247,7 @@ def convert(rpc_node):
states = NodeStates()
for attr in attr_list:
setattr(states, attr, getattr(rpc_node, attr))
assert_juno_provision_state_name(states)
return states

@classmethod
Expand Down Expand Up @@ -520,6 +528,7 @@ def _convert_with_links(node, url, expand=True):
@classmethod
def convert_with_links(cls, rpc_node, expand=True):
node = Node(**rpc_node.as_dict())
assert_juno_provision_state_name(node)
return cls._convert_with_links(node, pecan.request.host_url,
expand)

Expand Down
5 changes: 2 additions & 3 deletions ironic/common/fsm.py
Expand Up @@ -144,9 +144,8 @@ def process_event(self, event):
if (self._target_state is not None and
self._target_state == replacement.name):
self._target_state = None
# set target if there is a new one
if (self._target_state is None and
self._states[replacement.name]['target'] is not None):
# if new state has a different target, update the target
if self._states[replacement.name]['target'] is not None:
self._target_state = self._states[replacement.name]['target']

def is_valid_event(self, event):
Expand Down
26 changes: 17 additions & 9 deletions ironic/common/states.py
Expand Up @@ -40,7 +40,14 @@
NOSTATE = None
""" No state information.
Default for the power and provision state of newly created nodes.
This state is used with power_state to represent a lack of knowledge of
power state, and in target_*_state fields when there is no target.
"""

AVAILABLE = 'available'
""" Node is available for use and scheduling.
This state is replacing the NOSTATE state used prior to Kilo.
"""

ACTIVE = 'active'
Expand Down Expand Up @@ -78,8 +85,10 @@
DELETED = 'deleted'
""" Node tear down was successful.
This is mainly a target provision state used during node tear down. A
successful tear down leaves the node with a `provision_state` of NOSTATE.
In Juno, target_provision_state was set to this value during node tear down.
In Kilo, this will be a transitory value of provision_state, and never
represented in target_provision_state.
"""

ERROR = 'error'
Expand Down Expand Up @@ -131,7 +140,7 @@ def on_enter(new_state, event):
machine = fsm.FSM()

# Add stable states
machine.add_state(NOSTATE, **watchers)
machine.add_state(AVAILABLE, **watchers)
machine.add_state(ACTIVE, **watchers)
machine.add_state(ERROR, **watchers)

Expand All @@ -145,11 +154,10 @@ def on_enter(new_state, event):
# Add delete* states
# NOTE(deva): Juno shows a target_provision_state of DELETED
# this is changed in Kilo to AVAILABLE
# TODO(deva): change NOSTATE to AVAILABLE here
machine.add_state(DELETING, target=NOSTATE, **watchers)
machine.add_state(DELETING, target=AVAILABLE, **watchers)

# From NOSTATE, a deployment may be started
machine.add_transition(NOSTATE, DEPLOYING, 'deploy')
# From AVAILABLE, a deployment may be started
machine.add_transition(AVAILABLE, DEPLOYING, 'deploy')

# A deployment may fail
machine.add_transition(DEPLOYING, DEPLOYFAIL, 'fail')
Expand Down Expand Up @@ -185,7 +193,7 @@ def on_enter(new_state, event):
machine.add_transition(DEPLOYFAIL, DELETING, 'delete')

# A delete may complete
machine.add_transition(DELETING, NOSTATE, 'done')
machine.add_transition(DELETING, AVAILABLE, 'done')

# This state can also transition to error
machine.add_transition(DELETING, ERROR, 'error')
Expand Down
6 changes: 0 additions & 6 deletions ironic/conductor/manager.py
Expand Up @@ -1480,12 +1480,6 @@ def do_node_tear_down(task):
LOG.info(_LI('Successfully unprovisioned node %(node)s with '
'instance %(instance)s.'),
{'node': node.uuid, 'instance': node.instance_uuid})
# NOTE(deva): Currently, NOSTATE is represented as None
# However, FSM class treats a target_state of None as
# the lack of a target state -- not a target of NOSTATE
# Thus, until we migrate to an explicit AVAILABLE state
# we need to clear the target_state here manually.
node.target_provision_state = None
finally:
# NOTE(deva): there is no need to unset conductor_affinity
# because it is a reference to the most recent conductor which
Expand Down
7 changes: 7 additions & 0 deletions ironic/conductor/task_manager.py
Expand Up @@ -201,6 +201,13 @@ def reserve_node():
self.ports = objects.Port.list_by_node_id(context, self.node.id)
self.driver = driver_factory.get_driver(driver_name or
self.node.driver)

# NOTE(deva): this handles the Juno-era NOSTATE state
# and should be deleted after Kilo is released
if self.node.provision_state is states.NOSTATE:
self.node.provision_state = states.AVAILABLE
self.node.save()

self.fsm.initialize(self.node.provision_state)

except Exception:
Expand Down
4 changes: 2 additions & 2 deletions ironic/db/api.py
Expand Up @@ -135,8 +135,8 @@ def create_node(self, values):
{
'uuid': utils.generate_uuid(),
'instance_uuid': None,
'power_state': states.NOSTATE,
'provision_state': states.NOSTATE,
'power_state': states.POWER_OFF,
'provision_state': states.AVAILABLE,
'driver': 'pxe_ipmitool',
'driver_info': { ... },
'properties': { ... },
Expand Down
@@ -0,0 +1,52 @@
# 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.

"""replace NOSTATE with AVAILABLE
Revision ID: 5674c57409b9
Revises: 242cc6a923b3
Create Date: 2015-01-14 16:55:44.718196
"""

# revision identifiers, used by Alembic.
revision = '5674c57409b9'
down_revision = '242cc6a923b3'

from alembic import op
from sqlalchemy import String
from sqlalchemy.sql import table, column

node = table('nodes',
column('uuid', String(36)),
column('provision_state', String(15)))


# NOTE(deva): We must represent the states as static strings in this migration
# file, rather than import ironic.common.states, because that file may change
# in the future. This migration script must still be able to be run with
# future versions of the code and still produce the same results.
AVAILABLE = 'available'


def upgrade():
op.execute(
node.update().where(
node.c.provision_state == None).values(
{'provision_state': op.inline_literal(AVAILABLE)}))


def downgrade():
op.execute(
node.update().where(
node.c.provision_state == op.inline_literal(AVAILABLE)).values(
{'provision_state': None}))
9 changes: 5 additions & 4 deletions ironic/db/sqlalchemy/api.py
Expand Up @@ -253,12 +253,13 @@ def release_node(self, tag, node_id):

def create_node(self, values):
# ensure defaults are present for new nodes
if not values.get('uuid'):
if 'uuid' not in values:
values['uuid'] = utils.generate_uuid()
if not values.get('power_state'):
if 'power_state' not in values:
values['power_state'] = states.NOSTATE
if not values.get('provision_state'):
values['provision_state'] = states.NOSTATE
if 'provision_state' not in values:
# TODO(deva): change this to ENROLL
values['provision_state'] = states.AVAILABLE

node = models.Node()
node.update(values)
Expand Down
33 changes: 23 additions & 10 deletions ironic/tests/api/v1/test_nodes.py
Expand Up @@ -25,15 +25,16 @@
from testtools.matchers import HasLength
from wsme import types as wtypes

from ironic.api.controllers import base as api_base
from ironic.api.controllers.v1 import node as api_node
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common import states
from ironic.common import utils
from ironic.conductor import rpcapi
from ironic import objects
from ironic.tests.api import base as api_base
from ironic.tests.api import utils as apiutils
from ironic.tests.api import base as test_api_base
from ironic.tests.api import utils as test_api_utils
from ironic.tests import base
from ironic.tests.db import utils as dbutils
from ironic.tests.objects import utils as obj_utils
Expand All @@ -42,7 +43,7 @@
# NOTE(lucasagomes): When creating a node via API (POST)
# we have to use chassis_uuid
def post_get_test_node(**kw):
node = apiutils.node_post_data(**kw)
node = test_api_utils.node_post_data(**kw)
chassis = dbutils.get_test_chassis()
node['chassis_id'] = None
node['chassis_uuid'] = kw.get('chassis_uuid', chassis['uuid'])
Expand All @@ -52,13 +53,13 @@ def post_get_test_node(**kw):
class TestNodeObject(base.TestCase):

def test_node_init(self):
node_dict = apiutils.node_post_data(chassis_id=None)
node_dict = test_api_utils.node_post_data(chassis_id=None)
del node_dict['instance_uuid']
node = api_node.Node(**node_dict)
self.assertEqual(wtypes.Unset, node.instance_uuid)


class TestListNodes(api_base.FunctionalTest):
class TestListNodes(test_api_base.FunctionalTest):

def setUp(self):
super(TestListNodes, self).setUp()
Expand Down Expand Up @@ -151,6 +152,18 @@ def test_detail_against_single(self):
expect_errors=True)
self.assertEqual(404, response.status_int)

def test_mask_available_state(self):
node = obj_utils.create_test_node(self.context,
provision_state=states.AVAILABLE)

data = self.get_json('/nodes/%s' % node['uuid'],
headers={api_base.Version.string: "1.0"})
self.assertEqual(states.NOSTATE, data['provision_state'])

data = self.get_json('/nodes/%s' % node['uuid'],
headers={api_base.Version.string: "1.1"})
self.assertEqual(states.AVAILABLE, data['provision_state'])

def test_many(self):
nodes = []
for id in range(5):
Expand Down Expand Up @@ -486,7 +499,7 @@ def test_get_supported_boot_devices_iface_not_supported(self, mock_gsbd):
mock_gsbd.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')


class TestPatch(api_base.FunctionalTest):
class TestPatch(test_api_base.FunctionalTest):

def setUp(self):
super(TestPatch, self).setUp()
Expand Down Expand Up @@ -779,7 +792,7 @@ def test_replace_provision_updated_at(self):
self.assertTrue(response.json['error_message'])


class TestPost(api_base.FunctionalTest):
class TestPost(test_api_base.FunctionalTest):

def setUp(self):
super(TestPost, self).setUp()
Expand Down Expand Up @@ -927,7 +940,7 @@ def test_vendor_passthru_without_method(self):

def test_post_ports_subresource(self):
node = obj_utils.create_test_node(self.context)
pdict = apiutils.port_post_data(node_id=None)
pdict = test_api_utils.port_post_data(node_id=None)
pdict['node_uuid'] = node.uuid
response = self.post_json('/nodes/ports', pdict,
expect_errors=True)
Expand Down Expand Up @@ -1012,7 +1025,7 @@ def test_vendor_passthru_methods(self, get_methods_mock):
self.assertFalse(get_methods_mock.called)


class TestDelete(api_base.FunctionalTest):
class TestDelete(test_api_base.FunctionalTest):

def setUp(self):
super(TestDelete, self).setUp()
Expand Down Expand Up @@ -1073,7 +1086,7 @@ def test_delete_node_maintenance_mode(self, mock_update, mock_get):
topic='test-topic')


class TestPut(api_base.FunctionalTest):
class TestPut(test_api_base.FunctionalTest):

def setUp(self):
super(TestPut, self).setUp()
Expand Down

0 comments on commit e7958de

Please sign in to comment.