glance: support relying on tags to extract image id

Deprecated amp_image_id option with the new amp_image_tag option.

Also switched devstack plugin to rely on the tag to update the image
used for new load balancers.

Implements: blueprint use-glance-tags-to-manage-image
Change-Id: Ibc28b2220565667e15ca2b2674e55074d6126ec3
This commit is contained in:
Ihar Hrachyshka 2016-02-25 15:26:57 +01:00
parent ad84b40f42
commit fb53fe2340
17 changed files with 415 additions and 16 deletions

View File

@ -177,7 +177,10 @@ function octavia_start {
fi
OCTAVIA_AMP_IMAGE_ID=$(glance image-list | grep ${OCTAVIA_AMP_IMAGE_NAME} | awk '{print $2}')
iniset $OCTAVIA_CONF controller_worker amp_image_id ${OCTAVIA_AMP_IMAGE_ID}
if [ -n "$OCTAVIA_AMP_IMAGE_ID" ]; then
glance image-tag-update ${OCTAVIA_AMP_IMAGE_ID} ${OCTAVIA_AMP_IMAGE_TAG}
fi
iniset $OCTAVIA_CONF controller_worker amp_image_tag ${OCTAVIA_AMP_IMAGE_TAG}
create_amphora_flavor

View File

@ -37,6 +37,7 @@ OCTAVIA_AMP_SSH_KEY_NAME=${OCTAVIA_AMP_SSH_KEY_NAME:-"octavia_ssh_key"}
OCTAVIA_AMP_FLAVOR_ID=${OCTAVIA_AMP_FLAVOR_ID:-"10"}
OCTAVIA_AMP_IMAGE_NAME=${OCTAVIA_AMP_IMAGE_NAME:-"amphora-x64-haproxy"}
OCTAVIA_AMP_IMAGE_FILE=${OCTAVIA_AMP_IMAGE_FILE:-${OCTAVIA_DIR}/diskimage-create/${OCTAVIA_AMP_IMAGE_NAME}.qcow2}
OCTAVIA_AMP_IMAGE_TAG="amphora"
OCTAVIA_HEALTH_KEY=${OCTAVIA_HEALTH_KEY:-"insecure"}

View File

@ -132,9 +132,12 @@
[controller_worker]
# amp_active_retries = 10
# amp_active_wait_sec = 10
# Glance parameters to extract image ID to use for amphora. Only one of
# parameters is needed. Using tags is the recommended way to refer to images.
# amp_image_id =
# amp_image_tag =
# Nova parameters to use when booting amphora
# amp_flavor_id =
# amp_image_id =
# amp_ssh_key_name =
# amp_ssh_allowed_access = True
# amp_network =
@ -218,6 +221,19 @@
# vrrp_garp_refresh_interval = 5
# vrrp_garp_refresh_count = 2
[glance]
# The name of the glance service in the keystone catalog
# service_name =
# Custom glance endpoint if override is necessary
# endpoint =
# Region in Identity service catalog to use for communication with the OpenStack services.
# region_name =
# Endpoint type in Identity service catalog to use for communication with
# the OpenStack services.
# endpoint_type = publicURL
[nova]
# The name of the nova service in the keystone catalog
# service_name =

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from glanceclient import client as glance_client
from neutronclient.neutron import client as neutron_client
from novaclient import client as nova_client
from oslo_log import log as logging
@ -19,6 +20,7 @@ from octavia.common import keystone
from octavia.i18n import _LE
LOG = logging.getLogger(__name__)
GLANCE_VERSION = '2'
NEUTRON_VERSION = '2.0'
NOVA_VERSION = '2'
@ -85,3 +87,35 @@ class NeutronAuth(object):
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Error creating Neutron client."))
return cls.neutron_client
class GlanceAuth(object):
glance_client = None
@classmethod
def get_glance_client(cls, region, service_name=None, endpoint=None,
endpoint_type='publicURL'):
"""Create glance client object.
:param region: The region of the service
:param service_name: The name of the glance service in the catalog
:param endpoint: The endpoint of the service
:param endpoint_type: The endpoint_type of the service
:return: a Glance Client object.
:raises Exception: if the client cannot be created
"""
if not cls.glance_client:
kwargs = {'region_name': region,
'session': keystone.get_session(),
'interface': endpoint_type}
if service_name:
kwargs['service_name'] = service_name
if endpoint:
kwargs['endpoint'] = endpoint
try:
cls.glance_client = glance_client.Client(
GLANCE_VERSION, **kwargs)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Error creating Glance client."))
return cls.glance_client

View File

@ -210,8 +210,16 @@ controller_worker_opts = [
cfg.StrOpt('amp_flavor_id',
default='',
help=_('Nova instance flavor id for the Amphora')),
cfg.StrOpt('amp_image_tag',
default='',
help=_('Glance image tag for the Amphora image to boot. '
'Use this option to be able to update the image '
'without reconfiguring Octavia. '
'Ignored if amp_image_id is defined.')),
cfg.StrOpt('amp_image_id',
default='',
deprecated_for_removal=True,
deprecated_reason='Superseded by amp_image_tag option.',
help=_('Glance image id for the Amphora image to boot')),
cfg.StrOpt('amp_ssh_key_name',
default='',
@ -377,6 +385,20 @@ neutron_opts = [
help=_('Endpoint interface in identity service to use')),
]
glance_opts = [
cfg.StrOpt('service_name',
help=_('The name of the glance service in the '
'keystone catalog')),
cfg.StrOpt('endpoint', help=_('A new endpoint to override the endpoint '
'in the keystone catalog.')),
cfg.StrOpt('region_name',
help=_('Region in Identity service catalog to use for '
'communication with the OpenStack services.')),
cfg.StrOpt('endpoint_type', default='publicURL',
help=_('Endpoint interface in identity service to use')),
]
# Register the configuration options
cfg.CONF.register_opts(core_opts)
cfg.CONF.register_opts(amphora_agent_opts, group='amphora_agent')
@ -396,6 +418,7 @@ cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
cfg.CONF.register_opts(keystone_authtoken_v3_opts,
group='keystone_authtoken_v3')
cfg.CONF.register_opts(nova_opts, group='nova')
cfg.CONF.register_opts(glance_opts, group='glance')
cfg.CONF.register_opts(neutron_opts, group='neutron')

View File

@ -174,6 +174,10 @@ class NoReadyAmphoraeException(OctaviaException):
message = _LE('There are not any READY amphora available.')
class GlanceNoTaggedImages(OctaviaException):
message = _LE("No Glance images are tagged with %(tag)s tag.")
class NoSuitableAmphoraException(OctaviaException):
message = _LE('Unable to allocate an amphora due to: %(msg)s')

View File

@ -21,7 +21,8 @@ import six
class ComputeBase(object):
@abc.abstractmethod
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
def build(self, name="amphora_name", amphora_flavor=None,
image_id=None, image_tag=None,
key_name=None, sec_groups=None, network_ids=None,
config_drive_files=None, user_data=None, server_group_id=None):
"""Build a new amphora.
@ -29,6 +30,7 @@ class ComputeBase(object):
:param name: Optional name for Amphora
:param amphora_flavor: Optionally specify a flavor
:param image_id: ID of the base image for the amphora instance
:param image_tag: tag of the base image for the amphora instance
:param key_name: Optionally specify a keypair
:param sec_groups: Optionally specify list of security groups
:param network_ids: A list of network IDs to attach to the amphora

View File

@ -27,21 +27,23 @@ class NoopManager(object):
super(NoopManager, self).__init__()
self.computeconfig = {}
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
def build(self, name="amphora_name", amphora_flavor=None,
image_id=None, image_tag=None,
key_name=None, sec_groups=None, network_ids=None,
config_drive_files=None, user_data=None, port_ids=None,
server_group_id=None):
LOG.debug("Compute %s no-op, build name %s, amphora_flavor %s, "
"image_id %s, key_name %s, sec_groups %s, network_ids %s,"
"config_drive_files %s, user_data %s, port_ids %s,"
"server_group_id %s",
self.__class__.__name__, name, amphora_flavor, image_id,
"image_id %s, image_tag %s, key_name %s, sec_groups %s, "
"network_ids %s, config_drive_files %s, user_data %s, "
"port_ids %s, server_group_id %s",
self.__class__.__name__,
name, amphora_flavor, image_id, image_tag,
key_name, sec_groups, network_ids, config_drive_files,
user_data, port_ids, server_group_id)
self.computeconfig[(name, amphora_flavor, image_id, key_name,
user_data)] = (
self.computeconfig[(name, amphora_flavor, image_id, image_tag,
key_name, user_data)] = (
name, amphora_flavor,
image_id, key_name, sec_groups,
image_id, image_tag, key_name, sec_groups,
network_ids, config_drive_files,
user_data, port_ids, 'build')
compute_id = uuidutils.generate_uuid()
@ -84,12 +86,14 @@ class NoopComputeDriver(driver_base.ComputeBase):
super(NoopComputeDriver, self).__init__()
self.driver = NoopManager()
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
def build(self, name="amphora_name", amphora_flavor=None,
image_id=None, image_tag=None,
key_name=None, sec_groups=None, network_ids=None,
config_drive_files=None, user_data=None, port_ids=None,
server_group_id=None):
compute_id = self.driver.build(name, amphora_flavor, image_id,
compute_id = self.driver.build(name, amphora_flavor,
image_id, image_tag,
key_name, sec_groups, network_ids,
config_drive_files, user_data, port_ids,
server_group_id)

View File

@ -26,11 +26,40 @@ from octavia.i18n import _LE, _LW
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_group('glance', 'octavia.common.config')
CONF.import_group('keystone_authtoken', 'octavia.common.config')
CONF.import_group('networking', 'octavia.common.config')
CONF.import_group('nova', 'octavia.common.config')
def _extract_amp_image_id_by_tag(client, image_tag):
images = list(client.images.list(
filters={'tag': [image_tag]},
sort='created_at'))
if not images:
raise exceptions.GlanceNoTaggedImages(tag=image_tag)
image_id = images[-1]['id']
num_images = len(images)
if num_images > 1:
LOG.warn(
_LW("A single Glance image should be tagged with %(tag)s tag, "
"but %(num)d found. Using %(image_id)s."),
{'tag': image_tag, 'num': num_images, 'image_id': image_id}
)
return image_id
def _get_image_uuid(client, image_id, image_tag):
if image_id:
if image_tag:
LOG.warn(
_LW("Both amp_image_id and amp_image_tag options defined. "
"Using the former."))
return image_id
return _extract_amp_image_id_by_tag(client, image_tag)
class VirtualMachineManager(compute_base.ComputeBase):
'''Compute implementation of virtual machines via nova.'''
@ -41,10 +70,16 @@ class VirtualMachineManager(compute_base.ComputeBase):
endpoint=CONF.nova.endpoint,
region=CONF.nova.region_name,
endpoint_type=CONF.nova.endpoint_type)
self._glance_client = clients.GlanceAuth.get_glance_client(
service_name=CONF.glance.service_name,
endpoint=CONF.glance.endpoint,
region=CONF.glance.region_name,
endpoint_type=CONF.glance.endpoint_type)
self.manager = self._nova_client.servers
self.server_groups = self._nova_client.server_groups
def build(self, name="amphora_name", amphora_flavor=None, image_id=None,
def build(self, name="amphora_name", amphora_flavor=None,
image_id=None, image_tag=None,
key_name=None, sec_groups=None, network_ids=None,
port_ids=None, config_drive_files=None, user_data=None,
server_group_id=None):
@ -53,6 +88,7 @@ class VirtualMachineManager(compute_base.ComputeBase):
:param name: optional name for amphora
:param amphora_flavor: image flavor for virtual machine
:param image_id: image ID for virtual machine
:param image_tag: image tag for virtual machine
:param key_name: keypair to add to the virtual machine
:param sec_groups: Security group IDs for virtual machine
:param network_ids: Network IDs to include on virtual machine
@ -84,6 +120,8 @@ class VirtualMachineManager(compute_base.ComputeBase):
server_group = None if server_group_id is None else {
"group": server_group_id}
image_id = _get_image_uuid(
self._glance_client, image_id, image_tag)
amphora = self.manager.create(
name=name, image=image_id, flavor=amphora_flavor,
key_name=key_name, security_groups=sec_groups,

View File

@ -78,6 +78,7 @@ class ComputeCreate(BaseComputeTask):
name="amphora-" + amphora_id,
amphora_flavor=CONF.controller_worker.amp_flavor_id,
image_id=CONF.controller_worker.amp_image_id,
image_tag=CONF.controller_worker.amp_image_tag,
key_name=key_name,
sec_groups=CONF.controller_worker.amp_secgroup_list,
network_ids=[CONF.controller_worker.amp_network],

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import glanceclient.v2
import mock
import neutronclient.v2_0
import novaclient.v2
@ -98,3 +99,42 @@ class TestNeutronAuth(base.TestCase):
region="test-region", service_name="neutronEndpoint1",
endpoint="test-endpoint", endpoint_type='publicURL')
self.assertIs(bc1, bc2)
class TestGlanceAuth(base.TestCase):
def setUp(self):
CONF.set_override(group='keystone_authtoken', name='auth_version',
override='2', enforce_type=True)
# Reset the session and client
clients.GlanceAuth.glance_client = None
keystone._SESSION = None
super(TestGlanceAuth, self).setUp()
def test_get_glance_client(self):
# There should be no existing client
self.assertIsNone(
clients.GlanceAuth.glance_client
)
# Mock out the keystone session and get the client
keystone._SESSION = mock.MagicMock()
bc1 = clients.GlanceAuth.get_glance_client(
region=None, endpoint_type='publicURL')
# Our returned client should also be the saved client
self.assertIsInstance(
clients.GlanceAuth.glance_client,
glanceclient.v2.client.Client
)
self.assertIs(
clients.GlanceAuth.glance_client,
bc1
)
# Getting the session again should return the same object
bc2 = clients.GlanceAuth.get_glance_client(
region="test-region", service_name="glanceEndpoint1",
endpoint="test-endpoint", endpoint_type='publicURL')
self.assertIs(bc1, bc2)

View File

@ -30,6 +30,7 @@ class TestNoopComputeDriver(base.TestCase):
self.name = "amphora_name"
self.amphora_flavor = "m1.tiny"
self.image_id = self.FAKE_UUID_2
self.image_tag = "faketag"
self.key_name = "key_name"
self.sec_groups = "default"
self.network_ids = self.FAKE_UUID_3
@ -41,18 +42,21 @@ class TestNoopComputeDriver(base.TestCase):
self.server_group_id = self.FAKE_UUID_1
def build(self):
self.driver.build(self.name, self.amphora_flavor, self.image_id,
self.driver.build(self.name, self.amphora_flavor,
self.image_id, self.image_tag,
self.key_name, self.sec_groups, self.network_ids,
self.confdrivefiles, self.user_data,
self.server_group_id)
self.assertEqual((self.name, self.amphora_flavor, self.image_id,
self.assertEqual((self.name, self.amphora_flavor,
self.image_id, self.image_tag,
self.key_name, self.sec_groups, self.network_ids,
self.config_drive_files, self.user_data,
self.server_group_id, 'build'),
self.driver.driver.computeconfig[(self.name,
self.amphora_flavor,
self.image_id,
self.image_tag,
self.key_name,
self.sec_groups,
self.network_ids,

View File

@ -17,6 +17,7 @@ from novaclient import exceptions as nova_exceptions
from oslo_config import cfg
from oslo_utils import uuidutils
from octavia.common import clients
from octavia.common import constants
from octavia.common import data_models as models
from octavia.common import exceptions
@ -27,6 +28,62 @@ import octavia.tests.unit.base as base
CONF = cfg.CONF
class Test_GetImageUuid(base.TestCase):
def test__get_image_uuid_tag(self):
client = mock.Mock()
with mock.patch.object(nova_common,
'_extract_amp_image_id_by_tag',
return_value='fakeid') as extract:
image_id = nova_common._get_image_uuid(client, '', 'faketag')
self.assertEqual('fakeid', image_id)
extract.assert_called_with(client, 'faketag')
def test__get_image_uuid_notag(self):
client = mock.Mock()
image_id = nova_common._get_image_uuid(client, 'fakeid', '')
self.assertEqual('fakeid', image_id)
def test__get_image_uuid_id_beats_tag(self):
client = mock.Mock()
image_id = nova_common._get_image_uuid(client, 'fakeid', 'faketag')
self.assertEqual('fakeid', image_id)
class Test_ExtractAmpImageIdByTag(base.TestCase):
def setUp(self):
super(Test_ExtractAmpImageIdByTag, self).setUp()
client_mock = mock.patch.object(clients.GlanceAuth,
'get_glance_client')
self.client = client_mock.start().return_value
def test_no_images(self):
self.client.images.list.return_value = []
self.assertRaises(
exceptions.GlanceNoTaggedImages,
nova_common._extract_amp_image_id_by_tag, self.client, 'faketag')
def test_single_image(self):
images = [
{'id': uuidutils.generate_uuid(), 'tag': 'faketag'}
]
self.client.images.list.return_value = images
image_id = nova_common._extract_amp_image_id_by_tag(self.client,
'faketag')
self.assertIn(image_id, images[0]['id'])
def test_multiple_images_returns_one_of_images(self):
images = [
{'id': image_id, 'tag': 'faketag'}
for image_id in [uuidutils.generate_uuid() for i in range(10)]
]
self.client.images.list.return_value = images
image_id = nova_common._extract_amp_image_id_by_tag(self.client,
'faketag')
self.assertIn(image_id, [image['id'] for image in images])
class TestNovaClient(base.TestCase):
def setUp(self):
@ -108,6 +165,15 @@ class TestNovaClient(base.TestCase):
self.manager.manager.create.side_effect = Exception
self.assertRaises(exceptions.ComputeBuildException, self.manager.build)
def test_build_extracts_image_id_by_tag(self):
expected_id = 'fakeid-by-tag'
with mock.patch.object(nova_common, '_get_image_uuid',
return_value=expected_id):
self.manager.build(image_id='fakeid', image_tag='tag')
self.assertEqual(expected_id,
self.manager.manager.create.call_args[1]['image'])
def test_delete(self):
amphora_id = self.manager.build(amphora_flavor=1, image_id=1,
key_name=1, sec_groups=1,

View File

@ -27,6 +27,7 @@ import octavia.tests.unit.base as base
AMP_FLAVOR_ID = 10
AMP_IMAGE_ID = 11
AMP_IMAGE_TAG = 'glance_tag'
AMP_SSH_KEY_NAME = None
AMP_NET = uuidutils.generate_uuid()
AMP_SEC_GROUPS = []
@ -62,6 +63,7 @@ class TestComputeTasks(base.TestCase):
conf = oslo_fixture.Config(cfg.CONF)
conf.config(group="controller_worker", amp_flavor_id=AMP_FLAVOR_ID)
conf.config(group="controller_worker", amp_image_id=AMP_IMAGE_ID)
conf.config(group="controller_worker", amp_image_tag=AMP_IMAGE_TAG)
conf.config(group="controller_worker",
amp_ssh_key_name=AMP_SSH_KEY_NAME)
conf.config(group="controller_worker", amp_network=AMP_NET)
@ -95,6 +97,7 @@ class TestComputeTasks(base.TestCase):
name="amphora-" + _amphora_mock.id,
amphora_flavor=AMP_FLAVOR_ID,
image_id=AMP_IMAGE_ID,
image_tag=AMP_IMAGE_TAG,
key_name=AMP_SSH_KEY_NAME,
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],
@ -154,6 +157,7 @@ class TestComputeTasks(base.TestCase):
name="amphora-" + _amphora_mock.id,
amphora_flavor=AMP_FLAVOR_ID,
image_id=AMP_IMAGE_ID,
image_tag=AMP_IMAGE_TAG,
key_name=AMP_SSH_KEY_NAME,
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],
@ -212,6 +216,7 @@ class TestComputeTasks(base.TestCase):
name="amphora-" + _amphora_mock.id,
amphora_flavor=AMP_FLAVOR_ID,
image_id=AMP_IMAGE_ID,
image_tag=AMP_IMAGE_TAG,
key_name=None,
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],
@ -268,6 +273,7 @@ class TestComputeTasks(base.TestCase):
name="amphora-" + _amphora_mock.id,
amphora_flavor=AMP_FLAVOR_ID,
image_id=AMP_IMAGE_ID,
image_tag=AMP_IMAGE_TAG,
key_name=AMP_SSH_KEY_NAME,
sec_groups=AMP_SEC_GROUPS,
network_ids=[AMP_NET],

View File

@ -0,0 +1,9 @@
---
features:
- Glance image containing the latest Amphora image can now be referenced
using a Glance tag. To use the feature, set amp_image_tag in
[controller_worker]. Note that amp_image_id should be unset for the new
feature to take into effect.
upgrade:
- amp_image_id option is deprecated and will be removed in one of the next
releases. Operators are adviced to migrate to the new amp_image_tag option.

View File

@ -28,6 +28,7 @@ oslo.service>=1.0.0 # Apache-2.0
oslo.utils>=3.5.0 # Apache-2.0
PyMySQL>=0.6.2 # MIT License
python-barbicanclient>=3.3.0 # Apache-2.0
python-glanceclient>=1.2.0 # Apache-2.0
python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0
pyOpenSSL>=0.14 # Apache-2.0
WSME>=0.8 # MIT

View File

@ -0,0 +1,147 @@
..
This work is licensed under a Creative Commons Attribution 3.0 Unported
License.
http://creativecommons.org/licenses/by/3.0/legalcode
===============================================================
Allow to use Glance image tag to refer to desired Amphora image
===============================================================
https://blueprints.launchpad.net/octavia/+spec/use-glance-tags-to-manage-image
Currently, Octavia allows to define the Glance image ID to be used to boot new
Amphoras. This spec suggests another way to define the desired image, by using
Glance tagging mechanism.
Problem description
===================
The need to hardcode image ID in the service configuration file has drawbacks.
Specifically, when an updated image is uploaded into Glance, the operator is
required to orchestrate configuration file update on all Octavia nodes and then
restart all Octavia workers to apply the change. It is both complex and error
prone.
Proposed change
===============
The spec suggests an alternative way to configure the desired Glance image to
be used for Octavia: using Glance image tagging feature.
Glance allows to tag an image with any tag which is represented by a string
value.
With the proposed change, Octavia operator will be able to tell Octavia to use
an image with the specified tag. Then Octavia will talk to Glance to determine
the exact image ID that is marked with the tag, before booting a new Amphora.
Alternatives
------------
Alternatively, we could make Nova talk to Glance to determine the desired image
ID based on the tag provided by Octavia. This approach is not supported by Nova
community because they don't want to impose the complexity into their code
base.
Another alternative is to use image name instead of its ID. Nova is capable of
fetching the right image from Glance by name as long as the name is unique.
This is not optimal in case when the operator does not want to remove the old
Amphora image right after a new image is uploaded (for example, if the operator
wants to test the new image before cleaning up the old one).
Data model impact
-----------------
None.
REST API impact
---------------
None.
Security impact
---------------
Image tags should be managed by the same user that owns the images themselves.
Notifications impact
--------------------
None.
Other end user impact
---------------------
The proposed change should not break existing mechanism. To achieve that, the
new mechanism will be guarded with a new configuration option that will store
the desired Glance tag.
Performance Impact
------------------
If the feature is used, Octavia will need to reach to Glance before booting a
new Amphora. The performance impact is well isolated and is not expected to be
significant.
Other deployer impact
---------------------
The change couples Octavia with Glance. It should not be an issue since there
are no use cases to use Octavia without Glance installed.
The new feature deprecates amp_image_id option. Operators that still use the
old image referencing mechanism will be adviced to switch to the new option.
Eventually, the old mechanism will be removed from the tree.
Developer impact
----------------
None.
Implementation
==============
Assignee(s)
-----------
Primary assignee:
ihrachys (Ihar Hrachyshka)
Work Items
----------
* introduce glanceclient integration into nova compute driver
* introduce new configuration option to store the glance tag
* introduce devstack plugin support to configure the feature
* provide documentation for the new feature
Dependencies
============
None.
Testing
=======
Unit tests will be written to cover the feature.
Octavia plugin will be switched to using the new glance image referencing
mechanism. Tempest tests will be implemented to test the new feature.
Documentation Impact
====================
New feature should be documented in operator visible guides.
References
==========