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,和對這部分代碼的分析就結束了。

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