Remove unused glance client integration

The glance client integration and create_image_from_instance method
became dead code after the legacy workflow removal in commit
4179c3527c. The legacy workflow used
snapshot-based cold migration (creating an image snapshot, deleting
the VM, and recreating from the snapshot), which required glance
client access. This was replaced with Nova's native cold migration
support, eliminating the need for glance integration.

This change removes:
- python-glanceclient dependency from requirements.txt
- glance_client configuration options
- NovaHelper.create_image_from_instance() dead method
- Related test coverage for removed methods
- OpenStackClients.glance() client initialization

The removal eliminates an unnecessary runtime dependency and reduces
configuration complexity with zero impact on users.

Closes-Bug: #2126959
Generated-By: claude-code
Change-Id: I8c53555eee4c7cd42965e1d3d066dc5edcb3e054
Signed-off-by: Sean Mooney <work@seanmooney.info>
This commit is contained in:
Sean Mooney
2025-10-06 20:10:47 +01:00
parent 24afb27d3c
commit 13721b3804
10 changed files with 31 additions and 238 deletions

View File

@@ -0,0 +1,10 @@
---
other:
- |
The experimental glance client integration has been removed from Watcher.
The glance client and create_image_from_instance method became dead code
after the legacy workflow removal in commit 4179c3527c, which replaced
the snapshot-based cold migration approach with Nova's native cold
migration support. This removal eliminates the python-glanceclient
dependency and glance_client configuration options with no user-facing
impact.

View File

@@ -33,7 +33,6 @@ pecan>=1.3.2 # BSD
PrettyTable>=0.7.2 # BSD
gnocchiclient>=7.0.1 # Apache-2.0
python-cinderclient>=3.5.0 # Apache-2.0
python-glanceclient>=2.9.1 # Apache-2.0
python-keystoneclient>=3.15.0 # Apache-2.0
python-novaclient>=14.1.0 # Apache-2.0
python-observabilityclient>=1.1.0 # Apache-2.0

View File

@@ -15,7 +15,6 @@ from oslo_config import cfg
import warnings
from cinderclient import client as ciclient
from glanceclient import client as glclient
from gnocchiclient import client as gnclient
from ironicclient import client as irclient
from keystoneauth1 import adapter as ka_adapter
@@ -69,7 +68,6 @@ class OpenStackClients:
self._session = None
self._keystone = None
self._nova = None
self._glance = None
self._gnocchi = None
self._cinder = None
self._monasca = None
@@ -130,28 +128,6 @@ class OpenStackClients:
session=self.session)
return self._nova
@exception.wrap_keystone_exception
def glance(self):
if self._glance:
return self._glance
# NOTE(dviroel): This integration is classified as Experimental due to
# the lack of documentation and CI testing. It can be marked as
# supported or deprecated in future releases, based on improvements.
debtcollector.deprecate(
("Glance is an experimental integration and may be "
"deprecated in future releases."),
version="2025.2", category=PendingDeprecationWarning)
glanceclient_version = self._get_client_option('glance', 'api_version')
glance_endpoint_type = self._get_client_option('glance',
'endpoint_type')
glance_region_name = self._get_client_option('glance', 'region_name')
self._glance = glclient.Client(glanceclient_version,
interface=glance_endpoint_type,
region_name=glance_region_name,
session=self.session)
return self._glance
@exception.wrap_keystone_exception
def gnocchi(self):
if self._gnocchi:

View File

@@ -39,7 +39,6 @@ class NovaHelper:
self.osc = osc if osc else clients.OpenStackClients()
self.cinder = self.osc.cinder()
self.nova = self.osc.nova()
self.glance = self.osc.glance()
self._is_pinned_az_available = None
def is_pinned_az_available(self):
@@ -462,72 +461,6 @@ class NovaHelper:
return status
def create_image_from_instance(self, instance_id, image_name,
metadata={"reason": "instance_migrate"}):
"""This method creates a new image from a given instance.
It waits for this image to be in 'active' state before returning.
It returns the unique UUID of the created image if successful,
None otherwise.
:param instance_id: the uniqueid of
the instance to backup as an image.
:param image_name: the name of the image to create.
:param metadata: a dictionary containing the list of
key-value pairs to associate to the image as metadata.
"""
LOG.debug(
"Trying to create an image from instance %s ...", instance_id)
# Looking for the instance
instance = self.find_instance(instance_id)
if not instance:
LOG.debug("Instance not found: %s", instance_id)
return None
else:
host_name = getattr(instance, 'OS-EXT-SRV-ATTR:host')
LOG.debug(
"Instance %(instance)s found on host '%(host)s'.",
{'instance': instance_id, 'host': host_name})
# We need to wait for an appropriate status
# of the instance before we can build an image from it
if self.wait_for_instance_status(instance, ('ACTIVE', 'SHUTOFF'),
5,
10):
image_uuid = self.nova.servers.create_image(instance_id,
image_name,
metadata)
image = self.glance.images.get(image_uuid)
if not image:
return None
# Waiting for the new image to be officially in ACTIVE state
# in order to make sure it can be used
status = image.status
retry = 10
while status != 'active' and status != 'error' and retry:
time.sleep(5)
retry -= 1
# Retrieve the instance again so the status field updates
image = self.glance.images.get(image_uuid)
if not image:
break
status = image.status
LOG.debug("Current image status: %s", status)
if not image:
LOG.debug("Image not found: %s", image_uuid)
else:
LOG.debug(
"Image %(image)s successfully created for "
"instance %(instance)s",
{'image': image_uuid, 'instance': instance_id})
return image_uuid
return None
def delete_instance(self, instance_id):
"""This method deletes a given instance.

View File

@@ -60,7 +60,6 @@ _DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'qpid.messaging=INFO',
'keystoneclient=INFO', 'stevedore=INFO',
'eventlet.wsgi.server=WARN', 'iso8601=WARN',
'requests=WARN', 'neutronclient=WARN',
'glanceclient=WARN',
'apscheduler=WARN']
Singleton = service.Singleton

View File

@@ -28,7 +28,6 @@ from watcher.conf import datasources
from watcher.conf import db
from watcher.conf import decision_engine
from watcher.conf import exception
from watcher.conf import glance_client
from watcher.conf import gnocchi_client
from watcher.conf import grafana_client
from watcher.conf import grafana_translators
@@ -60,7 +59,6 @@ maas_client.register_opts(CONF)
monasca_client.register_opts(CONF)
models.register_opts(CONF)
nova_client.register_opts(CONF)
glance_client.register_opts(CONF)
gnocchi_client.register_opts(CONF)
keystone_client.register_opts(CONF)
grafana_client.register_opts(CONF)

View File

@@ -1,43 +0,0 @@
# Copyright (c) 2016 Intel Corp
#
# Authors: Prudhvi Rao Shedimbi <prudhvi.rao.shedimbi@intel.com>
#
# 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 oslo_config import cfg
glance_client = cfg.OptGroup(name='glance_client',
title='Configuration Options for Glance')
GLANCE_CLIENT_OPTS = [
cfg.StrOpt('api_version',
default='2',
help='Version of Glance API to use in glanceclient.'),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'internal', 'admin',
'publicURL', 'internalURL', 'adminURL'],
help='Type of endpoint to use in glanceclient.'),
cfg.StrOpt('region_name',
help='Region in Identity service catalog to use for '
'communication with the OpenStack service.')]
def register_opts(conf):
conf.register_group(glance_client)
conf.register_opts(GLANCE_CLIENT_OPTS, group=glance_client)
def list_opts():
return [(glance_client, GLANCE_CLIENT_OPTS)]

View File

@@ -16,7 +16,6 @@ from unittest import mock
from cinderclient import client as ciclient
from cinderclient.v3 import client as ciclient_v3
from glanceclient import client as glclient
from gnocchiclient import client as gnclient
from gnocchiclient.v1 import client as gnclient_v1
from ironicclient import client as irclient
@@ -154,43 +153,6 @@ class TestClients(base.TestCase):
nova_cached = osc.nova()
self.assertEqual(nova, nova_cached)
@mock.patch.object(glclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance(self, mock_session, mock_call):
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
mock_call.assert_called_once_with(
CONF.glance_client.api_version,
interface=CONF.glance_client.endpoint_type,
region_name=CONF.glance_client.region_name,
session=mock_session)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_diff_vers(self, mock_session):
CONF.set_override('api_version', '1', group='glance_client')
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
self.assertEqual(1.0, osc.glance().version)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_diff_endpoint(self, mock_session):
CONF.set_override('endpoint_type',
'internalURL', group='glance_client')
osc = clients.OpenStackClients()
osc._glance = None
osc.glance()
self.assertEqual('internalURL', osc.glance().http_client.interface)
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_glance_cached(self, mock_session):
osc = clients.OpenStackClients()
osc._glance = None
glance = osc.glance()
glance_cached = osc.glance()
self.assertEqual(glance, glance_cached)
@mock.patch.object(gnclient, 'Client')
@mock.patch.object(clients.OpenStackClients, 'session')
def test_clients_gnocchi(self, mock_session, mock_call):

View File

@@ -35,7 +35,6 @@ CONF = conf.CONF
@mock.patch.object(clients.OpenStackClients, 'nova')
@mock.patch.object(clients.OpenStackClients, 'cinder')
@mock.patch.object(clients.OpenStackClients, 'glance')
class TestNovaHelper(base.TestCase):
def setUp(self):
@@ -116,7 +115,7 @@ class TestNovaHelper(base.TestCase):
server.migrate.side_effect = side_effect
def test_get_compute_node_by_hostname(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
hypervisor_id = utils.generate_uuid()
hypervisor_name = "fake_hypervisor_1"
@@ -162,7 +161,7 @@ class TestNovaHelper(base.TestCase):
self.assertIs(nodes[index], result)
def test_get_compute_node_by_uuid(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
hypervisor_id = utils.generate_uuid()
hypervisor_name = "fake_hypervisor_1"
@@ -190,7 +189,7 @@ class TestNovaHelper(base.TestCase):
self.assertIs(result, nova_mock.servers.list.return_value)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_stop_instance(self, mock_glance, mock_cinder, mock_nova):
def test_stop_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
instance_id = utils.generate_uuid()
server = self.fake_server(instance_id)
@@ -235,7 +234,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_start_instance(self, mock_glance, mock_cinder, mock_nova):
def test_start_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
instance_id = utils.generate_uuid()
server = self.fake_server(instance_id)
@@ -280,7 +279,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_delete_instance(self, mock_glance, mock_cinder, mock_nova):
def test_delete_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
instance_id = utils.generate_uuid()
@@ -297,8 +296,7 @@ class TestNovaHelper(base.TestCase):
self.assertTrue(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_resize_instance(self, mock_glance, mock_cinder,
mock_nova):
def test_resize_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'status', 'VERIFY_RESIZE')
@@ -316,8 +314,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(is_success)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_live_migrate_instance(self, mock_glance, mock_cinder,
mock_nova):
def test_live_migrate_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'OS-EXT-SRV-ATTR:host',
@@ -360,7 +357,7 @@ class TestNovaHelper(base.TestCase):
@mock.patch.object(time, 'sleep', mock.Mock())
def test_live_migrate_instance_with_task_state(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'OS-EXT-SRV-ATTR:host',
@@ -384,7 +381,7 @@ class TestNovaHelper(base.TestCase):
@mock.patch.object(time, 'sleep', mock.Mock())
def test_live_migrate_instance_no_destination_node(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
self.destination_node = None
@@ -399,7 +396,7 @@ class TestNovaHelper(base.TestCase):
self.assertTrue(is_success)
def test_watcher_non_live_migrate_instance_not_found(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
self.fake_nova_find_list(nova_util, fake_find=None, fake_list=None)
@@ -410,8 +407,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(is_success)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_abort_live_migrate_instance(self, mock_glance, mock_cinder,
mock_nova):
def test_abort_live_migrate_instance(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'OS-EXT-SRV-ATTR:host',
@@ -450,7 +446,7 @@ class TestNovaHelper(base.TestCase):
self.instance_uuid, self.source_node, self.destination_node))
def test_non_live_migrate_instance_no_destination_node(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
setattr(server, 'OS-EXT-SRV-ATTR:host',
@@ -464,38 +460,7 @@ class TestNovaHelper(base.TestCase):
)
self.assertTrue(is_success)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_create_image_from_instance(self, mock_glance, mock_cinder,
mock_nova):
nova_util = nova_helper.NovaHelper()
instance = self.fake_server(self.instance_uuid)
image = mock.MagicMock()
setattr(instance, 'OS-EXT-SRV-ATTR:host', self.source_node)
setattr(instance, 'OS-EXT-STS:vm_state', "stopped")
self.fake_nova_find_list(
nova_util,
fake_find=instance,
fake_list=instance)
image_uuid = 'fake-image-uuid'
nova_util.nova.servers.create_image.return_value = image
glance_client = mock.MagicMock()
mock_glance.return_value = glance_client
glance_client.images = {image_uuid: image}
instance = nova_util.create_image_from_instance(
self.instance_uuid, "Cirros"
)
self.assertIsNotNone(instance)
nova_util.glance.images.get.return_value = None
instance = nova_util.create_image_from_instance(
self.instance_uuid, "Cirros"
)
self.assertIsNone(instance)
def test_enable_service_nova_compute(self, mock_glance, mock_cinder,
mock_nova):
def test_enable_service_nova_compute(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
nova_services = nova_util.nova.services
nova_services.enable.return_value = mock.MagicMock(
@@ -520,8 +485,7 @@ class TestNovaHelper(base.TestCase):
nova_util.nova.services.enable.assert_called_with(
service_uuid=mock.ANY)
def test_disable_service_nova_compute(self, mock_glance, mock_cinder,
mock_nova):
def test_disable_service_nova_compute(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
nova_services = nova_util.nova.services
nova_services.disable_log_reason.return_value = mock.MagicMock(
@@ -559,8 +523,7 @@ class TestNovaHelper(base.TestCase):
return volume
@mock.patch.object(time, 'sleep', mock.Mock())
def test_swap_volume(self, mock_glance, mock_cinder,
mock_nova):
def test_swap_volume(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
server = self.fake_server(self.instance_uuid)
self.fake_nova_find_list(nova_util, fake_find=server, fake_list=server)
@@ -581,8 +544,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_wait_for_volume_status(self, mock_glance, mock_cinder,
mock_nova):
def test_wait_for_volume_status(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
# verify that the method will return True when the status of volume
@@ -607,8 +569,7 @@ class TestNovaHelper(base.TestCase):
timeout=2)
@mock.patch.object(api_versions, 'APIVersion', mock.MagicMock())
def test_check_nova_api_version(self, mock_glance, mock_cinder,
mock_nova):
def test_check_nova_api_version(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
# verify that the method will return True when the version of nova_api
@@ -625,8 +586,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_wait_for_instance_status(self, mock_glance, mock_cinder,
mock_nova):
def test_wait_for_instance_status(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
instance = self.fake_server(self.instance_uuid)
@@ -658,8 +618,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
@mock.patch.object(time, 'sleep', mock.Mock())
def test_confirm_resize(self, mock_glance, mock_cinder,
mock_nova):
def test_confirm_resize(self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
instance = self.fake_server(self.instance_uuid)
self.fake_nova_find_list(nova_util, fake_find=instance, fake_list=None)
@@ -675,7 +634,7 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(result)
def test_get_compute_node_list(
self, mock_glance, mock_cinder, mock_nova):
self, mock_cinder, mock_nova):
nova_util = nova_helper.NovaHelper()
hypervisor1_id = utils.generate_uuid()
hypervisor1_name = "fake_hypervisor_1"

View File

@@ -36,7 +36,7 @@ class TestListOpts(base.TestCase):
self.base_sections = [
'DEFAULT', 'api', 'database', 'watcher_decision_engine',
'watcher_applier', 'watcher_datasources', 'watcher_planner',
'nova_client', 'glance_client', 'gnocchi_client', 'grafana_client',
'nova_client', 'gnocchi_client', 'grafana_client',
'grafana_translators', 'cinder_client',
'monasca_client', 'ironic_client', 'keystone_client',
'neutron_client', 'watcher_clients_auth', 'collector',