diff options
-rw-r--r-- | ironic/api/controllers/v1/__init__.py | 364 | ||||
-rw-r--r-- | ironic/api/controllers/v1/collection.py | 54 | ||||
-rw-r--r-- | ironic/conductor/manager.py | 13 | ||||
-rw-r--r-- | ironic/conductor/utils.py | 25 | ||||
-rw-r--r-- | ironic/drivers/modules/iscsi_deploy.py | 4 | ||||
-rw-r--r-- | ironic/tests/unit/api/controllers/v1/test_root.py | 125 | ||||
-rw-r--r-- | ironic/tests/unit/api/test_root.py | 9 | ||||
-rw-r--r-- | ironic/tests/unit/conductor/test_manager.py | 16 | ||||
-rw-r--r-- | releasenotes/notes/unrescue-token-ae664a17343e0610.yaml | 5 | ||||
-rw-r--r-- | zuul.d/ironic-jobs.yaml | 1 |
10 files changed, 378 insertions, 238 deletions
diff --git a/ironic/api/controllers/v1/__init__.py b/ironic/api/controllers/v1/__init__.py index b0be184f9..a944dec69 100644 --- a/ironic/api/controllers/v1/__init__.py +++ b/ironic/api/controllers/v1/__init__.py @@ -18,8 +18,9 @@ Version 1 of the Ironic API Specification can be found at doc/source/webapi/v1.rst """ +from http import client as http_client + import pecan -from pecan import rest from webob import exc from ironic import api @@ -39,7 +40,7 @@ from ironic.api.controllers.v1 import utils from ironic.api.controllers.v1 import versions from ironic.api.controllers.v1 import volume from ironic.api.controllers import version -from ironic.api import expose +from ironic.api import method from ironic.common.i18n import _ BASE_VERSION = versions.BASE_VERSION @@ -57,205 +58,161 @@ def max_version(): versions.min_version_string(), versions.max_version_string()) -class MediaType(base.Base): - """A media type representation.""" - - base = str - type = str - - def __init__(self, base, type): - self.base = base - self.type = type - - -class V1(base.Base): - """The representation of the version 1 of the API.""" - - id = str - """The ID of the version, also acts as the release number""" - - media_types = [MediaType] - """An array of supported media types for this version""" - - links = None - """Links that point to a specific URL for this version and documentation""" - - chassis = None - """Links to the chassis resource""" - - nodes = None - """Links to the nodes resource""" - - ports = None - """Links to the ports resource""" - - portgroups = None - """Links to the portgroups resource""" - - drivers = None - """Links to the drivers resource""" - - volume = None - """Links to the volume resource""" - - lookup = None - """Links to the lookup resource""" - - heartbeat = None - """Links to the heartbeat resource""" - - conductors = None - """Links to the conductors resource""" - - allocations = None - """Links to the allocations resource""" - - deploy_templates = None - """Links to the deploy_templates resource""" - - version = None - """Version discovery information.""" - - events = None - """Links to the events resource""" - - @staticmethod - def convert(): - v1 = V1() - v1.id = "v1" - v1.links = [link.make_link('self', api.request.public_url, - 'v1', '', bookmark=True), - link.make_link('describedby', - 'https://docs.openstack.org', - '/ironic/latest/contributor/', - 'webapi.html', - bookmark=True, type='text/html') - ] - v1.media_types = [MediaType('application/json', - 'application/vnd.openstack.ironic.v1+json')] - v1.chassis = [link.make_link('self', api.request.public_url, - 'chassis', ''), - link.make_link('bookmark', - api.request.public_url, - 'chassis', '', - bookmark=True) - ] - v1.nodes = [link.make_link('self', api.request.public_url, - 'nodes', ''), - link.make_link('bookmark', - api.request.public_url, - 'nodes', '', - bookmark=True) - ] - v1.ports = [link.make_link('self', api.request.public_url, - 'ports', ''), - link.make_link('bookmark', - api.request.public_url, - 'ports', '', - bookmark=True) - ] - if utils.allow_portgroups(): - v1.portgroups = [ - link.make_link('self', api.request.public_url, - 'portgroups', ''), - link.make_link('bookmark', api.request.public_url, - 'portgroups', '', bookmark=True) - ] - v1.drivers = [link.make_link('self', api.request.public_url, - 'drivers', ''), - link.make_link('bookmark', - api.request.public_url, - 'drivers', '', - bookmark=True) - ] - if utils.allow_volume(): - v1.volume = [ - link.make_link('self', - api.request.public_url, - 'volume', ''), - link.make_link('bookmark', - api.request.public_url, - 'volume', '', - bookmark=True) - ] - if utils.allow_ramdisk_endpoints(): - v1.lookup = [link.make_link('self', api.request.public_url, - 'lookup', ''), - link.make_link('bookmark', - api.request.public_url, - 'lookup', '', - bookmark=True) - ] - v1.heartbeat = [link.make_link('self', - api.request.public_url, - 'heartbeat', ''), - link.make_link('bookmark', - api.request.public_url, - 'heartbeat', '', - bookmark=True) - ] - if utils.allow_expose_conductors(): - v1.conductors = [link.make_link('self', - api.request.public_url, - 'conductors', ''), - link.make_link('bookmark', - api.request.public_url, - 'conductors', '', - bookmark=True) - ] - if utils.allow_allocations(): - v1.allocations = [link.make_link('self', - api.request.public_url, - 'allocations', ''), - link.make_link('bookmark', - api.request.public_url, - 'allocations', '', - bookmark=True) - ] - if utils.allow_expose_events(): - v1.events = [link.make_link('self', api.request.public_url, - 'events', ''), - link.make_link('bookmark', - api.request.public_url, - 'events', '', - bookmark=True) - ] - if utils.allow_deploy_templates(): - v1.deploy_templates = [ - link.make_link('self', - api.request.public_url, - 'deploy_templates', ''), - link.make_link('bookmark', - api.request.public_url, - 'deploy_templates', '', - bookmark=True) - ] - v1.version = version.default_version() - return v1 - - -class Controller(rest.RestController): +def v1(): + v1 = { + 'id': "v1", + 'links': [ + link.make_link('self', api.request.public_url, + 'v1', '', bookmark=True), + link.make_link('describedby', + 'https://docs.openstack.org', + '/ironic/latest/contributor/', + 'webapi.html', + bookmark=True, type='text/html') + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'chassis': [ + link.make_link('self', api.request.public_url, + 'chassis', ''), + link.make_link('bookmark', + api.request.public_url, + 'chassis', '', + bookmark=True) + ], + 'nodes': [ + link.make_link('self', api.request.public_url, + 'nodes', ''), + link.make_link('bookmark', + api.request.public_url, + 'nodes', '', + bookmark=True) + ], + 'ports': [ + link.make_link('self', api.request.public_url, + 'ports', ''), + link.make_link('bookmark', + api.request.public_url, + 'ports', '', + bookmark=True) + ], + 'drivers': [ + link.make_link('self', api.request.public_url, + 'drivers', ''), + link.make_link('bookmark', + api.request.public_url, + 'drivers', '', + bookmark=True) + ], + 'version': version.default_version() + } + if utils.allow_portgroups(): + v1['portgroups'] = [ + link.make_link('self', api.request.public_url, + 'portgroups', ''), + link.make_link('bookmark', api.request.public_url, + 'portgroups', '', bookmark=True) + ] + if utils.allow_volume(): + v1['volume'] = [ + link.make_link('self', + api.request.public_url, + 'volume', ''), + link.make_link('bookmark', + api.request.public_url, + 'volume', '', + bookmark=True) + ] + if utils.allow_ramdisk_endpoints(): + v1['lookup'] = [ + link.make_link('self', api.request.public_url, + 'lookup', ''), + link.make_link('bookmark', + api.request.public_url, + 'lookup', '', + bookmark=True) + ] + v1['heartbeat'] = [ + link.make_link('self', + api.request.public_url, + 'heartbeat', ''), + link.make_link('bookmark', + api.request.public_url, + 'heartbeat', '', + bookmark=True) + ] + if utils.allow_expose_conductors(): + v1['conductors'] = [ + link.make_link('self', + api.request.public_url, + 'conductors', ''), + link.make_link('bookmark', + api.request.public_url, + 'conductors', '', + bookmark=True) + ] + if utils.allow_allocations(): + v1['allocations'] = [ + link.make_link('self', + api.request.public_url, + 'allocations', ''), + link.make_link('bookmark', + api.request.public_url, + 'allocations', '', + bookmark=True) + ] + if utils.allow_expose_events(): + v1['events'] = [ + link.make_link('self', api.request.public_url, + 'events', ''), + link.make_link('bookmark', + api.request.public_url, + 'events', '', + bookmark=True) + ] + if utils.allow_deploy_templates(): + v1['deploy_templates'] = [ + link.make_link('self', + api.request.public_url, + 'deploy_templates', ''), + link.make_link('bookmark', + api.request.public_url, + 'deploy_templates', '', + bookmark=True) + ] + return v1 + + +class Controller(object): """Version 1 API controller root.""" - nodes = node.NodesController() - ports = port.PortsController() - portgroups = portgroup.PortgroupsController() - chassis = chassis.ChassisController() - drivers = driver.DriversController() - volume = volume.VolumeController() - lookup = ramdisk.LookupController() - heartbeat = ramdisk.HeartbeatController() - conductors = conductor.ConductorsController() - allocations = allocation.AllocationsController() - events = event.EventsController() - deploy_templates = deploy_template.DeployTemplatesController() - - @expose.expose(V1) - def get(self): - # NOTE: The reason why convert() it's being called for every + _subcontroller_map = { + 'nodes': node.NodesController(), + 'ports': port.PortsController(), + 'portgroups': portgroup.PortgroupsController(), + 'chassis': chassis.ChassisController(), + 'drivers': driver.DriversController(), + 'volume': volume.VolumeController(), + 'lookup': ramdisk.LookupController(), + 'heartbeat': ramdisk.HeartbeatController(), + 'conductors': conductor.ConductorsController(), + 'allocations': allocation.AllocationsController(), + 'events': event.EventsController(), + 'deploy_templates': deploy_template.DeployTemplatesController() + } + + @method.expose() + def index(self): + # NOTE: The reason why v1() it's being called for every # request is because we need to get the host url from # the request object to make the links. - return V1.convert() + self._add_version_attributes() + if api.request.method != "GET": + pecan.abort(http_client.METHOD_NOT_ALLOWED) + + return v1() def _check_version(self, version, headers=None): if headers is None: @@ -279,8 +236,7 @@ class Controller(rest.RestController): 'max': versions.max_version_string()}, headers=headers) - @pecan.expose() - def _route(self, args, request=None): + def _add_version_attributes(self): v = base.Version(api.request.headers, versions.min_version_string(), versions.max_version_string()) @@ -295,7 +251,15 @@ class Controller(rest.RestController): api.response.headers[base.Version.string] = str(v) api.request.version = v - return super(Controller, self)._route(args, request) + @pecan.expose() + def _lookup(self, primary_key, *remainder): + self._add_version_attributes() + + controller = self._subcontroller_map.get(primary_key) + if not controller: + pecan.abort(http_client.NOT_FOUND) + + return controller, remainder __all__ = ('Controller',) diff --git a/ironic/api/controllers/v1/collection.py b/ironic/api/controllers/v1/collection.py index 6e1b1faf3..c669b9309 100644 --- a/ironic/api/controllers/v1/collection.py +++ b/ironic/api/controllers/v1/collection.py @@ -19,6 +19,38 @@ from ironic.api.controllers import link from ironic.api import types as atypes +def has_next(collection, limit): + """Return whether collection has more items.""" + return len(collection) and len(collection) == limit + + +def get_next(collection, limit, url=None, key_field='uuid', **kwargs): + """Return a link to the next subset of the collection.""" + if not has_next(collection, limit): + return None + + fields = kwargs.pop('fields', None) + # NOTE(saga): If fields argument is present in kwargs and not None. It + # is a list so convert it into a comma seperated string. + if fields: + kwargs['fields'] = ','.join(fields) + q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) + + last_item = collection[-1] + # handle items which are either objects or dicts + if hasattr(last_item, key_field): + marker = getattr(last_item, key_field) + else: + marker = last_item.get(key_field) + + next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { + 'args': q_args, 'limit': limit, + 'marker': marker} + + return link.make_link('next', api.request.public_url, + url, next_args)['href'] + + class Collection(base.Base): next = str @@ -34,23 +66,13 @@ class Collection(base.Base): def has_next(self, limit): """Return whether collection has more items.""" - return len(self.collection) and len(self.collection) == limit + return has_next(self.collection, limit) def get_next(self, limit, url=None, **kwargs): """Return a link to the next subset of the collection.""" - if not self.has_next(limit): - return atypes.Unset - resource_url = url or self._type - fields = kwargs.pop('fields', None) - # NOTE(saga): If fields argument is present in kwargs and not None. It - # is a list so convert it into a comma seperated string. - if fields: - kwargs['fields'] = ','.join(fields) - q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) - next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { - 'args': q_args, 'limit': limit, - 'marker': getattr(self.collection[-1], self.get_key_field())} - - return link.make_link('next', api.request.public_url, - resource_url, next_args)['href'] + the_next = get_next(self.collection, limit, url=resource_url, + key_field=self.get_key_field(), **kwargs) + if the_next is None: + return atypes.Unset + return the_next diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index f75029eaa..cb20fcfd8 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -606,12 +606,13 @@ class ConductorManager(base_manager.BaseConductorManager): node_id, purpose='node rescue') as task: node = task.node - # Record of any pre-existing agent_url should be removed. - utils.remove_agent_url(node) if node.maintenance: raise exception.NodeInMaintenance(op=_('rescuing'), node=node.uuid) + # Record of any pre-existing agent_url should be removed. + utils.wipe_token_and_url(task) + # driver validation may check rescue_password, so save it on the # node early i_info = node.instance_info @@ -758,6 +759,9 @@ class ConductorManager(base_manager.BaseConductorManager): handle_failure(e, _('Failed to unrescue. Exception: %s'), log_func=LOG.exception) + + utils.wipe_token_and_url(task) + if next_state == states.ACTIVE: task.process_event('done') else: @@ -1228,6 +1232,8 @@ class ConductorManager(base_manager.BaseConductorManager): error = (_('Failed to validate power driver interface for node ' '%(node)s. Error: %(msg)s') % {'node': node.uuid, 'msg': e}) + log_traceback = not isinstance(e, exception.IronicException) + LOG.error(error, exc_info=log_traceback) else: try: power_state = task.driver.power.get_power_state(task) @@ -1235,6 +1241,8 @@ class ConductorManager(base_manager.BaseConductorManager): error = (_('Failed to get power state for node ' '%(node)s. Error: %(msg)s') % {'node': node.uuid, 'msg': e}) + log_traceback = not isinstance(e, exception.IronicException) + LOG.error(error, exc_info=log_traceback) if error is None: if power_state != node.power_state: @@ -1246,7 +1254,6 @@ class ConductorManager(base_manager.BaseConductorManager): else: task.process_event('done') else: - LOG.error(error) node.last_error = error task.process_event('fail') diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py index c64dc9f5a..b30fdee5e 100644 --- a/ironic/conductor/utils.py +++ b/ironic/conductor/utils.py @@ -451,16 +451,23 @@ def cleaning_error_handler(task, msg, tear_down_cleaning=True, task.process_event('fail', target_state=target_state) +def wipe_token_and_url(task): + """Remove agent URL and token from the task.""" + info = task.node.driver_internal_info + info.pop('agent_secret_token', None) + info.pop('agent_secret_token_pregenerated', None) + # Remove agent_url since it will be re-asserted + # upon the next deployment attempt. + info.pop('agent_url', None) + task.node.driver_internal_info = info + + def wipe_deploy_internal_info(task): """Remove temporary deployment fields from driver_internal_info.""" - info = task.node.driver_internal_info if not fast_track_able(task): - info.pop('agent_secret_token', None) - info.pop('agent_secret_token_pregenerated', None) - # Remove agent_url since it will be re-asserted - # upon the next deployment attempt. - info.pop('agent_url', None) + wipe_token_and_url(task) # Clear any leftover metadata about deployment. + info = task.node.driver_internal_info info['deploy_steps'] = None info.pop('agent_cached_deploy_steps', None) info.pop('deploy_step_index', None) @@ -473,11 +480,9 @@ def wipe_deploy_internal_info(task): def wipe_cleaning_internal_info(task): """Remove temporary cleaning fields from driver_internal_info.""" - info = task.node.driver_internal_info if not fast_track_able(task): - info.pop('agent_url', None) - info.pop('agent_secret_token', None) - info.pop('agent_secret_token_pregenerated', None) + wipe_token_and_url(task) + info = task.node.driver_internal_info info['clean_steps'] = None info.pop('agent_cached_clean_steps', None) info.pop('clean_step_index', None) diff --git a/ironic/drivers/modules/iscsi_deploy.py b/ironic/drivers/modules/iscsi_deploy.py index 41c83be61..8a4843136 100644 --- a/ironic/drivers/modules/iscsi_deploy.py +++ b/ironic/drivers/modules/iscsi_deploy.py @@ -673,7 +673,7 @@ class ISCSIDeploy(agent_base.AgentDeployMixin, agent_base.AgentBaseMixin, return states.DEPLOYWAIT @METRICS.timer('ISCSIDeploy.write_image') - @base.deploy_step(priority=90) + @base.deploy_step(priority=80) @task_manager.require_exclusive_lock def write_image(self, task): """Method invoked when deployed using iSCSI. @@ -701,7 +701,7 @@ class ISCSIDeploy(agent_base.AgentDeployMixin, agent_base.AgentBaseMixin, node.save() @METRICS.timer('ISCSIDeploy.prepare_instance_boot') - @base.deploy_step(priority=80) + @base.deploy_step(priority=60) def prepare_instance_boot(self, task): if not task.driver.storage.should_write_image(task): task.driver.boot.prepare_instance(task) diff --git a/ironic/tests/unit/api/controllers/v1/test_root.py b/ironic/tests/unit/api/controllers/v1/test_root.py index b3e58b817..78d3053e4 100644 --- a/ironic/tests/unit/api/controllers/v1/test_root.py +++ b/ironic/tests/unit/api/controllers/v1/test_root.py @@ -17,6 +17,7 @@ from unittest import mock from webob import exc as webob_exc from ironic.api.controllers import v1 as v1_api +from ironic.api.controllers.v1 import versions from ironic.tests import base as test_base from ironic.tests.unit.api import base as api_base @@ -28,6 +29,130 @@ class TestV1Routing(api_base.BaseApiTest): mock.ANY, mock.ANY) + def test_min_version(self): + response = self.get_json( + '/', + headers={ + 'Accept': 'application/json', + 'X-OpenStack-Ironic-API-Version': + versions.min_version_string() + }) + self.assertEqual({ + 'id': 'v1', + 'links': [ + {'href': 'http://localhost/v1/', 'rel': 'self'}, + {'href': 'https://docs.openstack.org//ironic/latest' + '/contributor//webapi.html', + 'rel': 'describedby', 'type': 'text/html'} + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'version': { + 'id': 'v1', + 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], + 'status': 'CURRENT', + 'min_version': versions.min_version_string(), + 'version': versions.max_version_string() + }, + 'chassis': [ + {'href': 'http://localhost/v1/chassis/', 'rel': 'self'}, + {'href': 'http://localhost/chassis/', 'rel': 'bookmark'} + ], + 'nodes': [ + {'href': 'http://localhost/v1/nodes/', 'rel': 'self'}, + {'href': 'http://localhost/nodes/', 'rel': 'bookmark'} + ], + 'ports': [ + {'href': 'http://localhost/v1/ports/', 'rel': 'self'}, + {'href': 'http://localhost/ports/', 'rel': 'bookmark'} + ], + 'drivers': [ + {'href': 'http://localhost/v1/drivers/', 'rel': 'self'}, + {'href': 'http://localhost/drivers/', 'rel': 'bookmark'} + ], + }, response) + + def test_max_version(self): + response = self.get_json( + '/', + headers={ + 'Accept': 'application/json', + 'X-OpenStack-Ironic-API-Version': + versions.max_version_string() + }) + self.assertEqual({ + 'id': 'v1', + 'links': [ + {'href': 'http://localhost/v1/', 'rel': 'self'}, + {'href': 'https://docs.openstack.org//ironic/latest' + '/contributor//webapi.html', + 'rel': 'describedby', 'type': 'text/html'} + ], + 'media_types': { + 'base': 'application/json', + 'type': 'application/vnd.openstack.ironic.v1+json' + }, + 'version': { + 'id': 'v1', + 'links': [{'href': 'http://localhost/v1/', 'rel': 'self'}], + 'status': 'CURRENT', + 'min_version': versions.min_version_string(), + 'version': versions.max_version_string() + }, + 'allocations': [ + {'href': 'http://localhost/v1/allocations/', 'rel': 'self'}, + {'href': 'http://localhost/allocations/', 'rel': 'bookmark'} + ], + 'chassis': [ + {'href': 'http://localhost/v1/chassis/', 'rel': 'self'}, + {'href': 'http://localhost/chassis/', 'rel': 'bookmark'} + ], + 'conductors': [ + {'href': 'http://localhost/v1/conductors/', 'rel': 'self'}, + {'href': 'http://localhost/conductors/', 'rel': 'bookmark'} + ], + 'deploy_templates': [ + {'href': 'http://localhost/v1/deploy_templates/', + 'rel': 'self'}, + {'href': 'http://localhost/deploy_templates/', + 'rel': 'bookmark'} + ], + 'drivers': [ + {'href': 'http://localhost/v1/drivers/', 'rel': 'self'}, + {'href': 'http://localhost/drivers/', 'rel': 'bookmark'} + ], + 'events': [ + {'href': 'http://localhost/v1/events/', 'rel': 'self'}, + {'href': 'http://localhost/events/', 'rel': 'bookmark'} + ], + 'heartbeat': [ + {'href': 'http://localhost/v1/heartbeat/', 'rel': 'self'}, + {'href': 'http://localhost/heartbeat/', 'rel': 'bookmark'} + ], + 'lookup': [ + {'href': 'http://localhost/v1/lookup/', 'rel': 'self'}, + {'href': 'http://localhost/lookup/', 'rel': 'bookmark'} + ], + 'nodes': [ + {'href': 'http://localhost/v1/nodes/', 'rel': 'self'}, + {'href': 'http://localhost/nodes/', 'rel': 'bookmark'} + ], + 'portgroups': [ + {'href': 'http://localhost/v1/portgroups/', 'rel': 'self'}, + {'href': 'http://localhost/portgroups/', 'rel': 'bookmark'} + ], + 'ports': [ + {'href': 'http://localhost/v1/ports/', 'rel': 'self'}, + {'href': 'http://localhost/ports/', 'rel': 'bookmark'} + ], + 'volume': [ + {'href': 'http://localhost/v1/volume/', 'rel': 'self'}, + {'href': 'http://localhost/volume/', 'rel': 'bookmark'} + ] + }, response) + class TestCheckVersions(test_base.TestCase): diff --git a/ironic/tests/unit/api/test_root.py b/ironic/tests/unit/api/test_root.py index 9a512d7ad..b784762f3 100644 --- a/ironic/tests/unit/api/test_root.py +++ b/ironic/tests/unit/api/test_root.py @@ -44,9 +44,10 @@ class TestRoot(base.BaseApiTest): self.assertNotIn('<html', response.json['error_message']) def test_no_html_errors2(self): - response = self.delete('/v1', expect_errors=True) + response = self.delete('/', expect_errors=True) self.assertEqual(http_client.METHOD_NOT_ALLOWED, response.status_int) - self.assertIn('Not Allowed', response.json['error_message']) + self.assertIn('malformed or otherwise incorrect', + response.json['error_message']) self.assertNotIn('<html', response.json['error_message']) @@ -68,8 +69,8 @@ class TestV1Root(base.BaseApiTest): expected_resources = (['chassis', 'drivers', 'nodes', 'ports'] + additional_expected_resources) self.assertEqual(sorted(expected_resources), sorted(actual_resources)) - self.assertIn({'type': 'application/vnd.openstack.ironic.v1+json', - 'base': 'application/json'}, data['media_types']) + self.assertEqual({'type': 'application/vnd.openstack.ironic.v1+json', + 'base': 'application/json'}, data['media_types']) version1 = data['version'] self.assertEqual('v1', version1['id']) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index b97f2f2b4..7ab03e175 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -2767,11 +2767,14 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin, @mock.patch('ironic.conductor.task_manager.acquire', autospec=True) def test_do_node_rescue(self, mock_acquire): self._start_service() + dii = {'agent_secret_token': 'token', + 'agent_url': 'http://url', + 'other field': 'value'} task = self._create_task( node_attrs=dict(driver='fake-hardware', provision_state=states.ACTIVE, instance_info={}, - driver_internal_info={'agent_url': 'url'})) + driver_internal_info=dii)) mock_acquire.side_effect = self._get_acquire_side_effect(task) self.service.do_node_rescue(self.context, task.node.uuid, "password") @@ -2782,7 +2785,8 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin, err_handler=conductor_utils.spawn_rescue_error_handler) self.assertIn('rescue_password', task.node.instance_info) self.assertIn('hashed_rescue_password', task.node.instance_info) - self.assertNotIn('agent_url', task.node.driver_internal_info) + self.assertEqual({'other field': 'value'}, + task.node.driver_internal_info) def test_do_node_rescue_invalid_state(self): self._start_service() @@ -2985,16 +2989,22 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin, autospec=True) def test__do_node_unrescue(self, mock_unrescue): self._start_service() + dii = {'agent_url': 'http://url', + 'agent_secret_token': 'token', + 'other field': 'value'} node = obj_utils.create_test_node(self.context, driver='fake-hardware', provision_state=states.UNRESCUING, target_provision_state=states.ACTIVE, - instance_info={}) + instance_info={}, + driver_internal_info=dii) with task_manager.TaskManager(self.context, node.uuid) as task: mock_unrescue.return_value = states.ACTIVE self.service._do_node_unrescue(task) node.refresh() self.assertEqual(states.ACTIVE, node.provision_state) self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertEqual({'other field': 'value'}, + node.driver_internal_info) @mock.patch.object(manager, 'LOG', autospec=True) @mock.patch('ironic.drivers.modules.fake.FakeRescue.unrescue', diff --git a/releasenotes/notes/unrescue-token-ae664a17343e0610.yaml b/releasenotes/notes/unrescue-token-ae664a17343e0610.yaml new file mode 100644 index 000000000..7ce3273e7 --- /dev/null +++ b/releasenotes/notes/unrescue-token-ae664a17343e0610.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Removes stale agent token on rescue and unrescue operations. Previously it + would cause subsequent rescue operations to fail. diff --git a/zuul.d/ironic-jobs.yaml b/zuul.d/ironic-jobs.yaml index 6a8d8b7dd..91f45a2c7 100644 --- a/zuul.d/ironic-jobs.yaml +++ b/zuul.d/ironic-jobs.yaml @@ -642,6 +642,7 @@ IRONIC_ENABLED_BOOT_INTERFACES: pxe IRONIC_IPXE_ENABLED: False IRONIC_BOOT_MODE: uefi + IRONIC_RAMDISK_TYPE: tinyipa IRONIC_AUTOMATED_CLEAN_ENABLED: False IRONIC_DEFAULT_BOOT_OPTION: netboot IRONIC_VM_SPECS_RAM: 4096 |