我们知道作为OpenStack的用户,如果不是admin是没有权限创建public的image的。但是有些时候可能一个用户同时在多个tenant里面,此时这些tenant都需要同一个image。此时如果在所有的tenant里面都上传同一个image,这将会非常的浪费资源和不方便。
本文主要讲述了如果通过命令行,在多个tenant里面进行image的分享,并会基于代码解析一下Glance是如何实现的。
Share image between tenants
创建测试用的image
# curl -LO https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img
# . rc.tenant1
# glance image-create --name cirros-0.4.0 --container-format bare --disk-format qcow2 --file cirros-0.4.0-x86_64-disk.img
+------------------+----------------------------------------------------------------------------------+
| Property | Value |
+------------------+----------------------------------------------------------------------------------+
| checksum | 443b7623e27ecf03dc9e01ee93f67afe |
| container_format | bare |
| created_at | 2019-05-15T13:50:51Z |
| disk_format | qcow2 |
| id | ad5cc4e9-8556-4821-b30f-585523cd73a2 |
| locations | [{"url": "file:///var/lib/glance/images/ad5cc4e9-8556-4821-b30f-585523cd73a2", |
| | "metadata": {}}] |
| min_disk | 0 |
| min_ram | 0 |
| name | cirros-0.4.0 |
| owner | 865c376595194706b59f61657a25dd53 |
| protected | False |
| size | 12716032 |
| status | active |
| tags | [] |
| updated_at | 2019-05-15T13:50:56Z |
| virtual_size | None |
| visibility | private |
+------------------+----------------------------------------------------------------------------------+
# glance image-list
+--------------------------------------+--------------+
| ID | Name |
+--------------------------------------+--------------+
| ad5cc4e9-8556-4821-b30f-585523cd73a2 | cirros-0.4.0 |
+--------------------------------------+--------------+
Tenant1分享image给tenant2
# glance member-create ad5cc4e9-8556-4821-b30f-585523cd73a2 3745ef9fe62049cabeeffa29109448ea
+--------------------------------------+----------------------------------+---------+
| Image ID | Member ID | Status |
+--------------------------------------+----------------------------------+---------+
| ad5cc4e9-8556-4821-b30f-585523cd73a2 | 3745ef9fe62049cabeeffa29109448ea | pending |
+--------------------------------------+----------------------------------+---------+
在Tenant2查看分享过来的image
# . rc..tenant2
# glance image-list
+----+------+
| ID | Name |
+----+------+
+----+------+
可以发现这个时候tenant2还是无法看到tenant1分享过来的image的。
此时tenant1需要将image的id(ad5cc4e9-8556-4821-b30f-585523cd73a2)告诉tenant2,并且tenant2需要accept tenant1分享过来的image才行。
# glance member-update ad5cc4e9-8556-4821-b30f-585523cd73a2 3745ef9fe62049cabeeffa29109448ea accepted
+--------------------------------------+----------------------------------+----------+
| Image ID | Member ID | Status |
+--------------------------------------+----------------------------------+----------+
| ad5cc4e9-8556-4821-b30f-585523cd73a2 | 3745ef9fe62049cabeeffa29109448ea | accepted |
+--------------------------------------+----------------------------------+----------+
# glance image-list
+--------------------------------------+--------------+
| ID | Name |
+--------------------------------------+--------------+
| ad5cc4e9-8556-4821-b30f-585523cd73a2 | cirros-0.4.0 |
+--------------------------------------+--------------+
可以看到这个时候tenant2就可以使用分享过来的image了。
一些疑问
- 为什么accept了tenant1分享过来的image后,tenant2就看到这个image了?
- cirros-0.4.0明明是属于tenant1的为什么tenant2可以修改它的member状态?
为什么accept了tenant1分享过来的image后,tenant2就看到这个image了?
OpenStack是通过policy.json文件来控制用户的权限的。glance的policy.json可以直接从它的源码里面找到,在glance/etc目录下。该文件的内容如下
# cat /etc/glance/policy.json
{
"context_is_admin": "role:admin",
"default": "role:admin",
"add_image": "",
"delete_image": "",
"get_image": "",
"get_images": "",
"modify_image": "",
"publicize_image": "role:admin",
"communitize_image": "",
"copy_from": "",
...
"add_member": "",
"delete_member": "",
"get_member": "",
"get_members": "",
"modify_member": "",
...
}
从policy.json来看,针对image和member,除了publicize_image之外,好像没有对image和member的操作做任何限制。可以去看看源代码里面是怎么处理的。
首先可以看一下glance是如何在list的时候过滤image的。
# glanece.api.v2.images:ImagesController
def index(self, req, marker=None, limit=None, sort_key=None,
sort_dir=None, filters=None, member_status='accepted'):
...
image_repo = self.gateway.get_repo(req.context)
try:
images = image_repo.list(marker=marker, limit=limit,
sort_key=sort_key,
sort_dir=sort_dir,
filters=filters,
member_status=member_status)
...
可以看到index函数的参数中定义了member_status=‘accepted’,通过该参数来获取member状态是accepted的image。
不过此处还是会有疑问,虽然指定了过滤条件member_status=‘accepted’,但image明明属于tenant1,tenant2是通过什么方式获取到share过来的image的呢?
此外policy.json里面get_images是allow all的,在上面的list函数中也没有做tenant的过滤,只是单纯的调用了image_repo的list函数,glance是如何确保tenant1在list image的时候只获取属于自己的image的呢?
从上面的代码来看,在index里面只是简单的调用了image_repo.list。看来上面2个疑问要到gateway获取的image_repo中去查找了。
去看一下gateway.get_repo的实现
# glance.gateway:Gateway
def get_repo(self, context):
image_repo = glance.db.ImageRepo(context, self.db_api)
store_image_repo = glance.location.ImageRepoProxy(
image_repo, context, self.store_api, self.store_utils)
quota_image_repo = glance.quota.ImageRepoProxy(
store_image_repo, context, self.db_api, self.store_utils)
policy_image_repo = policy.ImageRepoProxy(
quota_image_repo, context, self.policy)
notifier_image_repo = glance.notifier.ImageRepoProxy(
policy_image_repo, context, self.notifier)
if property_utils.is_property_protection_enabled():
property_rules = property_utils.PropertyRules(self.policy)
pir = property_protections.ProtectedImageRepoProxy(
notifier_image_repo, context, property_rules)
authorized_image_repo = authorization.ImageRepoProxy(
pir, context)
else:
authorized_image_repo = authorization.ImageRepoProxy(
notifier_image_repo, context)
return authorized_image_repo
上面这段代码其实类似于实现了一套pipeline。
当调用image_repo.list时,相当于是调用了authorized_image_repo的list函数,然后在authorized_image_repo的list函数里面又调用了notifier_image_repo或者pir的list函数,以此类推,最终调用了image_repo的list函数。
在这个过程中有些是在list image前进行的check,如policy_image_repo.list会通过policy.json的配置来确认操作的用户是否有 get_images的权限。虽然现在对get_images没有设置任何权限的。
而有些则是在把image list出来后,返回给用户前,对image做了一些修改,如authorized_image_repo。我们知道当tenant1把image分享给tenant2后,tenant2也是可以使用image的,那如何防止tenant2对image做修改呢?authorized_image_repo就是做这个事情的。authorized_image_repo确保了只有admin或者owner才能对image做修改,而其它的用户只能用不能改。
# glance.db:ImageRepo
def list(self, marker=None, limit=None, sort_key=None,
sort_dir=None, filters=None, member_status='accepted'):
sort_key = ['created_at'] if not sort_key else sort_key
sort_dir = ['desc'] if not sort_dir else sort_dir
db_api_images = self.db_api.image_get_all(
self.context, filters=filters, marker=marker, limit=limit,
sort_key=sort_key, sort_dir=sort_dir,
member_status=member_status, return_tag=True)
images = []
for db_api_image in db_api_images:
db_image = dict(db_api_image)
image = self._format_image_from_db(db_image, db_image['tags'])
images.append(image)
return images
可以看到在ImageRepo里面也没有对image的过滤,看来要进一步到db_api.image_get_all中去确认了。
# glance.db.sqlalchemy.api
def _select_images_query(context, image_conditions, admin_as_user,
member_status, visibility):
session = get_session()
img_conditional_clause = sa_sql.and_(*image_conditions)
regular_user = (not context.is_admin) or admin_as_user
query_member = session.query(models.Image).join(
models.Image.members).filter(img_conditional_clause)
if regular_user:
member_filters = [models.ImageMember.deleted == False]
member_filters.extend([models.Image.visibility == 'shared'])
if context.owner is not None:
member_filters.extend([models.ImageMember.member == context.owner])
if member_status != 'all':
member_filters.extend([
models.ImageMember.status == member_status])
query_member = query_member.filter(sa_sql.and_(*member_filters))
query_image = session.query(models.Image).filter(img_conditional_clause)
if regular_user:
visibility_filters = [
models.Image.visibility == 'public',
models.Image.visibility == 'community',
]
query_image = query_image .filter(sa_sql.or_(*visibility_filters))
query_image_owner = None
if context.owner is not None:
query_image_owner = session.query(models.Image).filter(
models.Image.owner == context.owner).filter(
img_conditional_clause)
if query_image_owner is not None:
query = query_image.union(query_image_owner, query_member)
else:
query = query_image.union(query_member)
return query
else:
# Admin user
return query_image
def image_get_all(context, filters=None, marker=None, limit=None,
sort_key=None, sort_dir=None,
member_status='accepted', is_public=None,
admin_as_user=False, return_tag=False, v1_mode=False):
...
query = _select_images_query(context,
img_cond,
admin_as_user,
member_status,
visibility)
...
总算通过上面的代码可以知道如果不是admin,glance是通过query = query_image.union(query_image_owner, query_member)生成的query语句来确保,tenant只获取自己tenant和share给自己tenant并且是accepted状态的image的。
cirros-0.4.0明明是属于tenant1的为什么tenant2可以修改它的member状态?
接着来回答第二个问题,cirros-0.4.0明明是属于tenant1的tenant2为什么可以修改它的member状态呢?
def update(self, req, image_id, member_id, status):
...
image = self._lookup_image(req, image_id)
member_repo = self._get_member_repo(req, image)
member = self._lookup_member(req, image, member_id)
try:
member.status = status
member_repo.save(member)
return member
...
看来主要是要看一下
为什么能够获取到image
为什么能够获取到并且更新member
先来看一下为什么能够获取到image,前面那部分pipeline的处理和list image是一样的。直接到db处理那部分去看看glance是如何处理的。
# glance.db.sqlalchemy.api
def _image_get(context, image_id, session=None, force_show_deleted=False):
"""Get an image or raise if it does not exist."""
_check_image_id(image_id)
session = session or get_session()
try:
query = session.query(models.Image).options(
sa_orm.joinedload(models.Image.properties)).options(
sa_orm.joinedload(
models.Image.locations)).filter_by(id=image_id)
# filter out deleted images if context disallows it
if not force_show_deleted and not context.can_see_deleted:
query = query.filter_by(deleted=False)
image = query.one()
except sa_orm.exc.NoResultFound:
msg = "No image found with ID %s" % image_id
LOG.debug(msg)
raise exception.ImageNotFound(msg)
# Make sure they can look at it
if not is_image_visible(context, image):
msg = "Forbidding request, image %s not visible" % image_id
LOG.debug(msg)
raise exception.Forbidden(msg)
return image
从上面的代码可以发现,获取image本身没有过滤,倒是通过is_image_visible来过滤的。
来看看is_image_visible函数做了什么。
# glance.db.utils
def is_image_visible(context, image, image_member_find, status=None):
"""Return True if the image is visible in this context."""
# Is admin == image visible
if context.is_admin:
return True
# No owner == image visible
if image['owner'] is None:
return True
# Public or Community visibility == image visible
if image['visibility'] in ['public', 'community']:
return True
# Perform tests based on whether we have an owner
if context.owner is not None:
if context.owner == image['owner']:
return True
# Figure out if this image is shared with that tenant
if 'shared' == image['visibility']:
members = image_member_find(context,
image_id=image['id'],
member=context.owner,
status=status)
if members:
return True
# Private image
return False
这样第一个疑问就明白了,原来只要member是当前tenant,那glance也会认为是visible的。
再来看看第二个疑问,为什么能够获取到并且更新member。
# glance.db:ImageMemberRepo
def save(self, image_member, from_state=None):
image_member_values = self._format_image_member_to_db(image_member)
try:
new_values = self.db_api.image_member_update(self.context,
image_member.id,
image_member_values)
except (exception.NotFound, exception.Forbidden):
raise exception.NotFound()
image_member.updated_at = new_values['updated_at']
def get(self, member_id):
try:
db_api_image_member = self.db_api.image_member_find(
self.context,
self.image.image_id,
member_id)
if not db_api_image_member:
raise exception.NotFound()
except (exception.NotFound, exception.Forbidden):
raise exception.NotFound()
image_member = self._format_image_member_from_db(
db_api_image_member[0])
return image_member
可以看到真正的处理在db_api的image_member_update和image_member_find中。
def image_member_update(context, memb_id, values):
"""Update an ImageMember object."""
session = get_session()
memb_ref = _image_member_get(context, memb_id, session)
_image_member_update(context, memb_ref, values, session)
return _image_member_format(memb_ref)
def _image_member_update(context, memb_ref, values, session=None):
"""Apply supplied dictionary of values to a Member object."""
_drop_protected_attrs(models.ImageMember, values)
values["deleted"] = False
values.setdefault('can_share', False)
memb_ref.update(values)
memb_ref.save(session=session)
return memb_ref
def _image_member_get(context, memb_id, session):
"""Fetch an ImageMember entity by id."""
query = session.query(models.ImageMember)
query = query.filter_by(id=memb_id)
return query.one()
def image_member_find(context, image_id=None, member=None,
status=None, include_deleted=False):
"""Find all members that meet the given criteria.
Note, currently include_deleted should be true only when create a new
image membership, as there may be a deleted image membership between
the same image and tenant, the membership will be reused in this case.
It should be false in other cases.
:param image_id: identifier of image entity
:param member: tenant to which membership has been granted
:include_deleted: A boolean indicating whether the result should include
the deleted record of image member
"""
session = get_session()
members = _image_member_find(context, session, image_id,
member, status, include_deleted)
return [_image_member_format(m) for m in members]
def _image_member_find(context, session, image_id=None,
member=None, status=None, include_deleted=False):
query = session.query(models.ImageMember)
if not include_deleted:
query = query.filter_by(deleted=False)
if not context.is_admin:
query = query.join(models.Image)
filters = [
models.Image.owner == context.owner,
models.ImageMember.member == context.owner,
]
query = query.filter(sa_sql.or_(*filters))
if image_id is not None:
query = query.filter(models.ImageMember.image_id == image_id)
if member is not None:
query = query.filter(models.ImageMember.member == member)
if status is not None:
query = query.filter(models.ImageMember.status == status)
return query.all()
可以看到_image_member_find会把所有image属于自己和member是自己的member给找出来。当然在修改member的情况下,是会通过models.ImageMember.image_id == image_id进行过滤的。
而获取到member后进行update的时候是直接通过memb_id进行操作的。
好了,到此为止如果share image给其它的tenant,和对这部分代码的分析就结束了。