我們知道作爲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,和對這部分代碼的分析就結束了。