OpenStack Glance Share Image to Other tenant

我们知道作为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,和对这部分代码的分析就结束了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章