Merge "Implementation of Inject metadata properties"

This commit is contained in:
Zuul 2018-01-24 09:40:03 +00:00 committed by Gerrit Code Review
commit 44a9cf68cc
12 changed files with 409 additions and 3 deletions

View File

@ -0,0 +1,62 @@
[DEFAULT]
[image_import_opts]
#
# From glance
#
#
# Image import plugins to be enabled for task processing.
#
# Provide list of strings reflecting to the task Objects
# that should be included to the Image Import flow. The
# task objects needs to be defined in the 'glance/async/
# flows/plugins/*' and may be implemented by OpenStack
# Glance project team, deployer or 3rd party.
#
# By default no plugins are enabled and to take advantage
# of the plugin model the list of plugins must be set
# explicitly in the glance-image-import.conf file.
#
# The allowed values for this option is comma separated
# list of object names in between ``[`` and ``]``.
#
# Possible values:
# * no_op (only logs debug level message that the
# plugin has been executed)
# * Any provided Task object name to be included
# in to the flow.
# (list value)
#image_import_plugins = [no_op]
[inject_metadata_properties]
#
# From glance
#
#
# Specify name of user roles to be ignored for injecting metadata
# properties in the image.
#
# Specify name of the user roles
#
# Possible values:
# * List containing user roles. For example: [admin,member]
#
# (list value)
#ignore_user_roles = admin
#
# Dictionary contains metadata properties to be injected in image.
#
# Possible values:
# * Dictionary containing key/value pairs. Key characters
# length should be <= 255. For example: k1:v1,k2:v2
#
#
# (dict value)
#inject =

View File

@ -0,0 +1,4 @@
[DEFAULT]
wrap_width = 80
output_file = etc/glance-image-import.conf.sample
namespace = glance

View File

@ -0,0 +1,101 @@
# Copyright 2018 NTT DATA, Inc.
# All Rights Reserved.
#
# 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
from taskflow.patterns import linear_flow as lf
from taskflow import task
from glance.i18n import _
CONF = cfg.CONF
inject_metadata_opts = [
cfg.ListOpt('ignore_user_roles',
default='admin',
help=_("""
Specify name of user roles to be ignored for injecting metadata
properties in the image.
Possible values:
* List containing user roles. For example: [admin,member]
""")),
cfg.DictOpt('inject',
default={},
help=_("""
Dictionary contains metadata properties to be injected in image.
Possible values:
* Dictionary containing key/value pairs. Key characters
length should be <= 255. For example: k1:v1,k2:v2
""")),
]
CONF.register_opts(inject_metadata_opts, group='inject_metadata_properties')
class _InjectMetadataProperties(task.Task):
def __init__(self, context, task_id, task_type, image_repo, image_id):
self.context = context
self.task_id = task_id
self.task_type = task_type
self.image_repo = image_repo
self.image_id = image_id
super(_InjectMetadataProperties, self).__init__(
name='%s-InjectMetadataProperties-%s' % (task_type, task_id))
def execute(self):
"""Inject custom metadata properties to image
:param image_id: Glance Image ID
"""
user_roles = self.context.roles
ignore_user_roles = CONF.inject_metadata_properties.ignore_user_roles
if not [role for role in user_roles if role in ignore_user_roles]:
properties = CONF.inject_metadata_properties.inject
if properties:
image = self.image_repo.get(self.image_id)
image.extra_properties.update(properties)
self.image_repo.save(image)
def get_flow(**kwargs):
"""Return task flow for inject_image_metadata.
:param task_id: Task ID.
:param task_type: Type of the task.
:param image_repo: Image repository used.
:param image_id: Image_ID used.
:param context: Context used.
"""
task_id = kwargs.get('task_id')
task_type = kwargs.get('task_type')
image_repo = kwargs.get('image_repo')
image_id = kwargs.get('image_id')
context = kwargs.get('context')
return lf.Flow(task_type).add(
_InjectMetadataProperties(context, task_id, task_type, image_repo,
image_id),
)

View File

@ -216,6 +216,11 @@ class PropertyRules(object):
def check_property_rules(self, property_name, action, context):
roles = context.roles
# Include service roles to check if an action can be
# performed on the property or not
if context.service_roles:
roles.extend(context.service_roles)
if not self.rules:
return True

View File

@ -24,14 +24,26 @@ CONF = cfg.CONF
CONF.import_group("profiler", "glance.common.wsgi")
logging.register_options(CONF)
CONFIG_FILES = ['glance-api-paste.ini', 'glance-api.conf']
CONFIG_FILES = ['glance-api-paste.ini',
'glance-image-import.conf',
'glance-api.conf']
def _get_config_files(env=None):
if env is None:
env = os.environ
dirname = env.get('OS_GLANCE_CONFIG_DIR', '/etc/glance').strip()
return [os.path.join(dirname, config_file) for config_file in CONFIG_FILES]
config_files = []
for config_file in CONFIG_FILES:
cfg_file = os.path.join(dirname, config_file)
# As 'glance-image-import.conf' is optional conf file
# so include it only if it's existing.
if config_file == 'glance-image-import.conf' and (
not os.path.exists(cfg_file)):
continue
config_files.append(cfg_file)
return config_files
def _setup_os_profiler():

View File

@ -30,6 +30,7 @@ import glance.api.middleware.context
import glance.api.versions
import glance.async.flows.api_image_import
import glance.async.flows.convert
import glance.async.flows.plugins.inject_image_metadata
import glance.async.taskflow_executor
import glance.common.config
import glance.common.location_strategy
@ -107,7 +108,9 @@ _manage_opts = [
(None, [])
]
_image_import_opts = [
('image_import_opts', glance.async.flows.api_image_import.api_import_opts)
('image_import_opts', glance.async.flows.api_image_import.api_import_opts),
('inject_metadata_properties',
glance.async.flows.plugins.inject_image_metadata.inject_metadata_opts)
]

View File

@ -0,0 +1,128 @@
# Copyright 2018 NTT DATA, Inc.
# All Rights Reserved.
#
# 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.
import mock
import os
import glance_store
from oslo_config import cfg
import glance.async.flows.plugins.inject_image_metadata as inject_metadata
from glance.common import utils
from glance import domain
from glance import gateway
from glance.tests.unit import utils as test_unit_utils
import glance.tests.utils as test_utils
CONF = cfg.CONF
UUID1 = 'c80a1a6c-bd1f-41c5-90ee-81afedb1d58d'
TENANT1 = '6838eb7b-6ded-434a-882c-b344c77fe8df'
class TestInjectImageMetadataTask(test_utils.BaseTestCase):
def setUp(self):
super(TestInjectImageMetadataTask, self).setUp()
glance_store.register_opts(CONF)
self.config(default_store='file',
stores=['file', 'http'],
filesystem_store_datadir=self.test_dir,
group="glance_store")
glance_store.create_stores(CONF)
self.work_dir = os.path.join(self.test_dir, 'work_dir')
utils.safe_mkdirs(self.work_dir)
self.config(work_dir=self.work_dir, group='task')
self.context = mock.MagicMock()
self.img_repo = mock.MagicMock()
self.task_repo = mock.MagicMock()
self.image_id = mock.MagicMock()
self.gateway = gateway.Gateway()
self.task_factory = domain.TaskFactory()
self.img_factory = self.gateway.get_image_factory(self.context)
self.image = self.img_factory.new_image(image_id=UUID1,
disk_format='qcow2',
container_format='bare')
task_input = {
"import_from": "http://cloud.foo/image.qcow2",
"import_from_format": "qcow2",
"image_properties": {'disk_format': 'qcow2',
'container_format': 'bare'}
}
task_ttl = CONF.task.task_time_to_live
self.task_type = 'import'
self.task = self.task_factory.new_task(self.task_type, TENANT1,
task_time_to_live=task_ttl,
task_input=task_input)
def test_inject_image_metadata_using_non_admin_user(self):
context = test_unit_utils.get_fake_context(roles='member')
inject_image_metadata = inject_metadata._InjectMetadataProperties(
context, self.task.task_id, self.task_type, self.img_repo,
self.image_id)
self.config(inject={"test": "abc"},
group='inject_metadata_properties')
with mock.patch.object(self.img_repo, 'get') as get_mock:
image = mock.MagicMock(image_id=self.image_id,
extra_properties={"test": "abc"})
get_mock.return_value = image
with mock.patch.object(self.img_repo, 'save') as save_mock:
inject_image_metadata.execute()
get_mock.assert_called_once_with(self.image_id)
save_mock.assert_called_once_with(image)
self.assertEqual({"test": "abc"}, image.extra_properties)
def test_inject_image_metadata_using_admin_user(self):
context = test_unit_utils.get_fake_context(roles='admin')
inject_image_metadata = inject_metadata._InjectMetadataProperties(
context, self.task.task_id, self.task_type, self.img_repo,
self.image_id)
self.config(inject={"test": "abc"},
group='inject_metadata_properties')
inject_image_metadata.execute()
with mock.patch.object(self.img_repo, 'get') as get_mock:
get_mock.assert_not_called()
with mock.patch.object(self.img_repo, 'save') as save_mock:
save_mock.assert_not_called()
def test_inject_image_metadata_empty(self):
context = test_unit_utils.get_fake_context(roles='member')
inject_image_metadata = inject_metadata._InjectMetadataProperties(
context, self.task.task_id, self.task_type, self.img_repo,
self.image_id)
self.config(inject={}, group='inject_metadata_properties')
inject_image_metadata.execute()
with mock.patch.object(self.img_repo, 'get') as get_mock:
get_mock.assert_not_called()
with mock.patch.object(self.img_repo, 'save') as save_mock:
save_mock.assert_not_called()

View File

@ -97,6 +97,21 @@ def fake_get_verifier(context, img_signature_certificate_uuid,
return verifier
def get_fake_context(user=USER1, tenant=TENANT1, roles=None, is_admin=False):
if roles is None:
roles = ['member']
kwargs = {
'user': user,
'tenant': tenant,
'roles': roles,
'is_admin': is_admin,
}
context = glance.context.RequestContext(**kwargs)
return context
class FakeDB(object):
def __init__(self, initialize=True):

View File

@ -0,0 +1,72 @@
---
features:
- |
Made provision to inject image metadata properties to non-admin
images during creation of image using 'image-import' API.
upgrade:
- |
- There are two methods to create images:
- Method A:
.. code-block:: none
POST /v2/images
PUT /v2/images/{image_id}/file
- Method B:
.. code-block:: none
POST /v2/images
PUT /v2/images/{image_id}/stage
POST /v2/images/{image_id}/import
The long term goal is to make end-users use Method B to create images
and cross-services like Nova to use Method A until changes are made to
use Method B. To restrict end-users from using Method A to create
images, you will need to allow only admin or service users to call
"upload_image" API as shown below.
.. code-block:: none
upload_image": "role:admin or (service_user_id:<uuid of nova user>) or
(service_roles:<service user role>)"
"service_role" is the role which is created for the service user
and assigned to the trusted services.
- To use this feature below configurations are required:
You will need to configure 'glance-image-import.conf' file as shown
below:
.. code-block:: none
[image_import_opts]
image_import_plugins = [inject_image_metadata]
[inject_metadata_properties]
ignore_user_roles = admin,...
inject = "property1":"value",...
The first section "image_import_opts" is used to enable/plug the task
using `image_import_plugins` parameter by giving plugin name.
Plugin name is nothing but the module name under
glance/async/flows/plugins/
You don't want to allow end-users to create metadata properties
you want to be injected automatically during creation of images.
So, you will need to protect such metadata properties using
property protection configuration file as shown below.
Only admin or service user will be able to create metadata
property 'property1'.
.. code-block:: none
[property1]
create = admin,service_role
read = admin,service_role,member,_member_
update = admin
delete = admin

View File

@ -24,6 +24,7 @@ data_files =
etc/glance-manage.conf
etc/glance-registry.conf
etc/glance-scrubber.conf
etc/glance-image-import.conf
etc/glance-api-paste.ini
etc/glance-registry-paste.ini
etc/policy.json
@ -55,6 +56,7 @@ oslo.config.opts =
glance.scrubber = glance.opts:list_scrubber_opts
glance.cache= glance.opts:list_cache_opts
glance.manage = glance.opts:list_manage_opts
glance = glance.opts:list_image_import_opts
oslo.config.opts.defaults =
glance.api = glance.common.config:set_cors_middleware_defaults
glance.database.migration_backend =
@ -73,6 +75,7 @@ glance.flows.import =
glance.image_import.plugins =
no_op = glance.async.flows.plugins.no_op:get_flow
inject_image_metadata=glance.async.flows.plugins.inject_image_metadata:get_flow
[build_sphinx]
builder = html man

View File

@ -68,6 +68,7 @@ commands =
oslo-config-generator --config-file etc/oslo-config-generator/glance-scrubber.conf
oslo-config-generator --config-file etc/oslo-config-generator/glance-cache.conf
oslo-config-generator --config-file etc/oslo-config-generator/glance-manage.conf
oslo-config-generator --config-file etc/oslo-config-generator/glance-image-import.conf
[testenv:api-ref]
# This environment is called from CI scripts to test and publish