1 Glance基本框架圖
組件 |
描述 |
A client |
任何使用Glance服務的應用。 |
REST API |
通過REST方式暴露Glance的使用接口。 |
Database Abstraction Layer (DAL) |
介於Glance和數據庫之間的應用程序編程接口。 |
Glance Domain Controller |
一個中間件,實現了Glance的主要功能,比如授權、通知、規則、數據庫連接等功能。 |
Glance Store |
負責與各種后端存儲類型進行交互,提供了一個統一的接口來訪問后端存儲。 |
Registry Layer |
通過使用單獨的服務用於domain和DAL之間的安全交互的一個可選層。 |
2 Glance體系結構圖
由上圖可以看出Glance組件主要由glance-api和glance-registry兩個服務組成,glance-api是進入Glance的入口,負責接收用戶的RESTful請求,然后通過后台的Swift、Ceph等存儲系統完成鏡像的存儲與獲取。
與glance-api服務一樣,glance-registry也是一個WSGI server服務,不過不同的是glance-registry處理的是與鏡像元數據相關的RESTful請求。Glance-api接收到用戶的RESTful請求后,如果該請求與元數據相關,則將其轉發給glance-registry服務。
glance-registry會解析請求的內容,並與數據庫進行交互,存取或更新鏡像的元數據,這里的元數據是指保存在數據庫中的關於鏡像的一些信息,Glance的DB模塊存儲的僅僅是鏡像的元數據。
3 Glance源碼結構
看OpenStack的一個組件相關的代碼結構時,一般都是先看該組件源碼中的setup.cfg文件中的內容,特別是[entry_points]里console_scripts的內容,它寫明glance的各項服務的入口點:
console_scripts = glance-api = glance.cmd.api:main glance-cache-prefetcher = glance.cmd.cache_prefetcher:main glance-cache-pruner = glance.cmd.cache_pruner:main glance-cache-manage = glance.cmd.cache_manage:main glance-cache-cleaner = glance.cmd.cache_cleaner:main glance-control = glance.cmd.control:main glance-manage = glance.cmd.manage:main glance-registry = glance.cmd.registry:main glance-replicator = glance.cmd.replicator:main glance-scrubber = glance.cmd.scrubber:main glance-glare = glance.cmd.glare:main
服務 |
描述 |
glance-cache-* |
4個對Image Cache進行管理的工具。 |
glance-manage |
用於Glance數據庫的管理。 |
glance-replicator |
用於實現鏡像的復制。 |
glance-scrubber |
用於清理已經刪除的Image。 |
glance-control |
用於控制glance-api、glance-registry和glance-scrubber這三個服務進程的工具。 |
glance-glare |
Glare API服務,目前還在開發中。 |
4 Glance Domain model實現
以下是domain model的各個層描述表:
層 |
描述 |
Authorization |
認證層:提供了一個鏡像本身或其屬性是否可以改變的驗證 |
Property protection
|
屬性保護層:該層是可選的,如果你在配置文件中設置了property_protection_file參數,它就變得可用。可以通過配置文件指明訪問權限。 |
Notifier |
消息通知層:關於鏡像變化的消息和使用鏡像時發生的錯誤和警告都會被添加到消息隊列中。 |
Policy |
規則定義層:定義鏡像操作的訪問規則,規則在/etc/policy.json文件中定義,該層進行監視並實施。 |
Quota |
配額限制層:如果管理者對某用戶定義了鏡像大小的鏡像上傳上限,則若該用戶上傳了超過該限額的鏡像,則會上傳失敗。 |
Location |
鏡像位置定位層:通過glance_store與后台存儲進行交互,例如上傳、下載和管理圖像位置。 1. 添加新位置時檢查位置URI是否正確; 2. 鏡像位置改變時,刪除存儲后端保存的鏡像數據; 3. 防止鏡像位置重復; |
Database |
數據庫層:實現與數據庫進行交互的API。 |
5 Glance的鏡像介紹及鏡像的狀態轉換
Image是Glance所管理的主要資源。類似於VMware的VM模板,它預先安裝了OS。以下圖是鏡像的狀態演變圖。
鏡像狀態 |
描述 |
queued |
表示Glance注冊表中已保留該圖像標識,但還沒有圖像數據上傳到Glance。 |
saving |
表示圖像的原始數據正在上傳到Glance。 |
active |
表示在Glance中完全可用的圖像。 |
deactivated |
表示不允許任何非管理員用戶訪問圖像數據。 |
killed |
表示在上載圖像數據期間發生錯誤,並且圖像不可讀。 |
deleted |
Glance保留了關於圖像的信息,但不再可用。 此狀態下的圖像將在以后自動刪除。 |
pending_delete |
Glance尚未刪除圖像數據。 處於此狀態的圖像無法恢復。 |
6 Glance的Task
一般來說,對Image的操作有import、export、clone等幾種。Glance把這些操作統一起來抽象出了Task的概念來方便管理。Task是針對Image的異步操作,具有的一些屬性包括id、owner、狀態等。Glance同時也實現了統一的JSON格式的API來操作這些Task,比如創建、刪除、查詢狀態等。
在Glance中的任務狀態有以下幾種:
任務狀態 |
描述 |
pending |
表示該任務標識符已被保留給Glance中的任務。 還沒有開始處理。 |
processing |
表示該任務正在執行中。 |
success |
表示該任務在Glance中已經成功運行。 |
failure |
表示執行任務期間發生錯誤,並且無法繼續處理。 |
7 Glance組件重要流程分析
7.1 Glance服務啟動過程
Glance服務主要包括兩個服務,一個是glance-api服務,一個是glance-registry服務。
glance-api服務啟動時,其入口是/cmd/api.py里的main函數並給相對應的代碼加上了注釋:
def main(): try: # 初始化glance.common.config模塊的CONF對象 config.parse_args() # 更新所有默認配置值,比如更新oslo.middleware模塊的默認值 config.set_config_defaults() # 設置glance.common.wsgi模塊的eventlet對象的hub的實現方式,比如poll或select wsgi.set_eventlet_hub() # 啟動該服務日志模塊 logging.setup(CONF, 'glance') # 配置該服務事件通知功能的默認值 notifier.set_defaults() # 創建該服務的事件通知功能 if cfg.CONF.profiler.enabled: _notifier = osprofiler.notifier.create("Messaging", oslo_messaging, {}, notifier.get_transport(), "glance", "api", cfg.CONF.bind_host) osprofiler.notifier.set(_notifier) osprofiler.web.enable(cfg.CONF.profiler.hmac_keys) else: osprofiler.web.disable() # 初始化glance.common.wsgi模塊的Server類 server = wsgi.Server(initialize_glance_store=True) # 用給定的應用程序啟動wsgi服務 server.start(config.load_paste_app('glance-api'), default_port=9292) # 等待所有服務運行完畢 server.wait() except KNOWN_EXCEPTIONS as e: fail(e) if __name__ == '__main__': main()
對於這行代碼:
config.load_paste_app('glance-api'),
它的功能是根據paste配置文件建立並返回一個WSGI應用程序。glance-api的paste配置文件是源代碼中的/etc/glance-api-paste.ini,並根據傳入的參數”glance-api”來找到對應的配置:
[pipeline:glance-api] pipeline = cors healthcheck http_proxy_to_wsgi versionnegotiation osprofiler unauthenticated-context rootapp [composite:rootapp] paste.composite_factory = glance.api:rootapp /: apiversions /v1: apiv1app /v2: apiv2app [app:apiversions] paste.app_factory = glance.api.versions:create_resource [app:apiv1app] paste.app_factory = glance.api.v1.router:API.factory [app:apiv2app] paste.app_factory = glance.api.v2.router:API.factory
從該配置文件我們可以看到返回的wsgi應用程序是一個經過了多個中間件(比如cors healthcheck等等)包裝的,只有最后一個rootapp不是中間件,該rootapp也可以看到其section類型是composite的,可以看到它會調用glance.api模塊的root_app_factory函數:
def root_app_factory(loader, global_conf, **local_conf): if not CONF.enable_v1_api and '/v1' in local_conf: del local_conf['/v1'] if not CONF.enable_v2_api and '/v2' in local_conf: del local_conf['/v2'] return paste.urlmap.urlmap_factory(loader, global_conf, **local_conf)
我們主要關注的是v2版本的,可以知道其對應的是glance.api.v2.router:API類:
class API(wsgi.Router): """WSGI router for Glance v2 API requests.""" def __init__(self, mapper): custom_image_properties = images.load_custom_properties() reject_method_resource = wsgi.Resource(wsgi.RejectMethodController()) schemas_resource = schemas.create_resource(custom_image_properties) mapper.connect('/schemas/image', controller=schemas_resource, action='image', conditions={'method': ['GET']}, body_reject=True) mapper.connect('/schemas/image', controller=reject_method_resource, action='reject', allowed_methods='GET')
這里只列出一部分,重點是看mapper.connect里的格式,它描述的是請求API對應的controller和action。
glance-registry服務啟動時跟glance-api類似,不過glance-api的監聽端口是9292,glance-registry的監聽端口是9191。
7.2 列舉鏡像過程
當我們使用命令openstack image list查看鏡像文件時,從后台日志中可以看到這個請求GET /v2/images,該請求會在route.py進行匹配,可以知道它使用的是images這個controller的index函數:
mapper.connect('/images', controller=images_resource, action='index', conditions={'method': ['GET']})
從index函數中我們可以看到以下關鍵代碼:
image_repo = self.gateway.get_repo(req.context) images = image_repo.list(marker=marker, limit=limit, sort_key=sort_key, sort_dir=sort_dir, filters=filters, member_status=member_status) if len(images) != 0 and len(images) == limit: result['next_marker'] = images[-1].image_id result['images'] = images return result
我們需要再看看get_repo函數:
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
可以看到這里經過了多個類的封裝,其實就是glance domain module的實現,返回時是一個經過多個類封裝后的對象,這里封裝的類都是繼承自domain_proxy.Repo基類的:
class Repo(object): def __init__(self, base, item_proxy_class=None, item_proxy_kwargs=None): self.base = base self.helper = Helper(item_proxy_class, item_proxy_kwargs) def get(self, item_id): return self.helper.proxy(self.base.get(item_id)) def list(self, *args, **kwargs): items = self.base.list(*args, **kwargs) return [self.helper.proxy(item) for item in items]
所以它能夠做到把方法一層一層往上傳遞。
glance domain model的各層的實現功能上面我們已經講過了,這里主要針對該list來講,從源代碼來看,該list方法繼承類有重寫list方法的層有auth層、policy層和db層,首先在auth層,該層調用到policy層,該層進行權限檢查,查看此行為是否有權限,然后傳遞到db層,我們主要看下db層的list方法實現:
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
這些代碼中最重要的代碼是image_get_all方法的調用,返回的是從數據庫中查詢到的image的信息。
image_get_all方法實現是在\glance\db\sqlalchemy\api.py文件中,sqlalchemy模塊是對數據庫的操作進行了封裝的,這里不詳細描述它里面的實現細節。
7.3 上傳鏡像過程
就通過命令行開啟debug模式上傳鏡像,比如:
openstack image create "oop" --file test_image.img --disk-format raw --container-format bare --public --debug
從輸出的內容可以看到該命令執行一共發出了三個請求:
(1) 第一個請求:GET call to glance-api for http://controller:9292/v2/schemas/image used request id req-507983f9-643a-4330-a658-5b9f6803e91d
通過glance/api/v2/routesr.py.API定義的路由映射可知,該請求會對應到/v2/schemas.py.Controller類的image函數:
def image(self, req): return self.image_schema.raw()
image_schema的raw方法實現:
def raw(self): raw = super(PermissiveSchema, self).raw() raw['additionalProperties'] = {'type': 'string'} return raw
Schema的raw方法實現:
def raw(self): raw = { 'name': self.name, 'properties': self.properties, 'additionalProperties': False, } if self.definitions: raw['definitions'] = self.definitions if self.required: raw['required'] = self.required if self.links: raw['links'] = self.links return raw
從上述代碼可以知道獲得的是鏡像所支持的屬性字典定義,調用者就可以根據這些信息來驗證用戶輸入的參數是否有效。
(2) 第二個請求:POST call to glance-api for http://controller:9292/v2/images used request id req-e4cdac4f-8294-4a14-8308-1fbd46f3ce9c HTTP/1.1 201 Created
通過glance/api/v2/routesr.py.API定義的路由映射可知它這里是調用了/v2/images.py.ImageController類的create函數:
def create(self, req, image, extra_properties, tags): image_factory = self.gateway.get_image_factory(req.context) image_repo = self.gateway.get_repo(req.context) try: image = image_factory.new_image(extra_properties=extra_properties, tags=tags, **image) image_repo.add(image) return image
這里返回了兩個對象image_factory和image_repo,也是經過了glance domain module封裝的對象。
在image_factory對象所對應的責任鏈上,我們這里只看db層new_image方法實現即可,其它層都是一些校驗檢查之類的:
def new_image(self, image_id=None, name=None, visibility='shared', min_disk=0, min_ram=0, protected=False, owner=None, disk_format=None, container_format=None, extra_properties=None, tags=None, **other_args): extra_properties = extra_properties or {} self._check_readonly(other_args) self._check_unexpected(other_args) self._check_reserved(extra_properties) if image_id is None: image_id = str(uuid.uuid4()) created_at = timeutils.utcnow() updated_at = created_at status = 'queued' return Image(image_id=image_id, name=name, status=status, created_at=created_at, updated_at=updated_at, visibility=visibility, min_disk=min_disk, min_ram=min_ram, protected=protected, owner=owner, disk_format=disk_format, container_format=container_format, extra_properties=extra_properties, tags=tags or [])
可以看到new_image函數最終會返回一個image對象,對象里面包含了該鏡像的屬性值,比如鏡像id,鏡像創建時間等等,並且將該對象的狀態置為queued。
在image_repo對象對應的責任鏈上,我們主要看location和db層。
location 層的add方法的實現:
def add(self, image): result = super(ImageRepoProxy, self).add(image) self._set_acls(image) return result
可以看到先調用了db 層的add方法,查看db層的add方法實現:
def add(self, image): # 獲取到對應數據庫相關字段的屬性組合成字典返回 image_values = self._format_image_to_db(image) if (image_values['size'] is not None and image_values['size'] > CONF.image_size_cap): raise exception.ImageSizeLimitExceeded # the updated_at value is not set in the _format_image_to_db # function since it is specific to image create image_values['updated_at'] = image.updated_at # 根據image_values字典中的值創建一個image對象 new_values = self.db_api.image_create(self.context, image_values) self.db_api.image_tag_set_all(self.context, image.image_id, image.tags) image.created_at = new_values['created_at'] image.updated_at = new_values['updated_at']
主要看self.db_api.image_create函數的實現:
def image_create(context, values, v1_mode=False): """Create an image from the values dictionary.""" image = _image_update(context, values, None, purge_props=False) if v1_mode: image = db_utils.mutate_image_dict_to_v1(image) return image
這個函數的主要實現在於_image_update的實現,這個函數的實現比較長,我只寫出比較重要的代碼:
def _image_update(context, values, image_id, purge_props=False, from_state=None): # 獲取一個跟數據庫表有關系對象映射的對象 image_ref = models.Image() # 將values對象里的鍵值對更新到image_ref對象中 image_ref.update(values) values = _validate_image(image_ref.to_dict()) _update_values(image_ref, values) # 將image_ref中的值插入到數據庫表中,save里的實現是數據庫表和python對象的 關系對象映射里的實現 image_ref.save(session=session) return image_get(context, image_ref.id) 這里看下image_get函數的實現: def image_get(context, image_id, session=None, force_show_deleted=False, v1_mode=False): # 根據image_id從數據庫中查找出對應的鏡像信息並以對象形式返回 image = _image_get(context, image_id, session=session, force_show_deleted=force_show_deleted) # 為圖像的位置字段生成合適的字典列表,比如鏡像的url,其實就是將上面的image對象里包含的值全部展開,變成字典形式的字符串返回 image = _normalize_locations(context, image.to_dict(), force_show_deleted=force_show_deleted) if v1_mode: image = db_utils.mutate_image_dict_to_v1(image) return image
現在回到location層的add函數中,接着是執行self._set_acls(image)代碼,下面是_set_acls函數的實現:
def _set_acls(self, image): public = image.visibility == 'public' member_ids = [] if image.locations and not public: member_repo = _get_member_repo_for_store(image, self.context, self.db_api, self.store_api) member_ids = [m.member_id for m in member_repo.list()] # 調用store.set_acls,設置image的讀寫權限(這里的set_als的實現是在glance_store項目代碼里的) for location in image.locations: self.store_api.set_acls(location['url'], public=public, read_tenants=member_ids, context=self.context)
可以說這第二個請求基本上都是跟創建鏡像的元數據相關,並將其保存到數據庫中,但后面也看到會需要跟glance-store項目進行交互進行鏡像的讀寫權限的設置。
glance-store 向 glance-api 提供文件 backend.py 作為 store 操作的統一入口。
(2) 第三個請求:PUT call to glance-api for
http://controller:9292/v2/images/251c497f-2482-426f-9403-f026529c9e3b/file used request id req-0cc373cf-5917-41c4-8349-29ad8d14c757
通過glance/api/v2/routesr.py.API定義的路由映射可知它這里是調用了/v2/image_data.py.ImageDataController類的upload函數,這里只列出一些重要代碼:
def upload(self, req, image_id, data, size): # 跟之前一樣,獲取一個經過多層封裝的責任鏈對象 image_repo = self.gateway.get_repo(req.context) # 根據image_id從數據庫中取出image的信息並以對象形式返回,且該對象也經過了多層的封裝 image = image_repo.get(image_id) image.status = 'saving' # 更新數據庫條目 image_repo.save(image, from_state='queued') # set_data的調用也會在domain model各層中傳遞,該實現是使用了Helper代理類實現的 image.set_data(data, size) # 更新數據庫條目,比如狀態此時會更新為saving狀態 image_repo.save(image, from_state='saving')
這里我們主要關注location層的set_data的實現,因為是在該層將鏡像數據通過glance-store的相關接口存儲到后端存儲的,只列出關鍵性代碼:
def set_data(self, data, size=None): # 調用glance-store項目的接口來存儲鏡像並接收返回值 location, size, checksum, loc_meta = self.store_api.add_to_backend( CONF, self.image.image_id, # 這里使用了eventle庫來讀取要上傳的鏡像數據 utils.LimitingReader(utils.CooperativeReader(data), CONF.image_size_cap), size, context=self.context, verifier=verifier) # 將存儲后返回的數據進行更新 self.image.locations = [{'url': location, 'metadata': loc_meta, 'status': 'active'}] self.image.size = size self.image.checksum = checksum # 鏡像status的狀態更改為active self.image.status = 'active'
上面最主要的代碼是store_api.add_to_backend的調用,該項目代碼實現是在glance-store項目里,以下列出該函數的實現:
def add_to_backend(conf, image_id, data, size, scheme=None, context=None, verifier=None): if scheme is None: # 從配置文件中獲取默認的存儲方式是什么,比如rbd scheme = conf['glance_store']['default_store'] # 根據schema的類型獲取對應的后端存儲對象 store = get_store_from_scheme(scheme) # 每個存儲后端對象的add()方法的調用的封裝 return store_add_to_backend(image_id, data, size, store, context, verifier) 查看store_add_to_backend函數的實現,這里只列出關鍵代碼: def store_add_to_backend(image_id, data, size, store, context=None, verifier=None): # 調用后端存儲對象的add方法存儲鏡像數據 (location, size, checksum, metadata) = store.add(image_id, data, size, context=context, verifier=verifier) return (location, size, checksum, metadata)
因為我們后端存儲使用的是Ceph的rbd存儲,所以我們直接看塊存儲的add方法實現,只列出關鍵代碼:
def _create_image(self, fsid, conn, ioctx, image_name, size, order, context=None): # 創建一個rbd image對象 loc = self._create_image(fsid, conn, ioctx, image_name, image_size, order) # 往rbd image對象中寫數據 with rbd.Image(ioctx, image_name) as image: bytes_written = 0 offset = 0 chunks = utils.chunkreadable(image_file, self.WRITE_CHUNKSIZE) for chunk in chunks: if image_size == 0: chunk_length = len(chunk) length = offset + chunk_length bytes_written += chunk_length LOG.debug(_("resizing image to %s KiB") % (length / units.Ki)) image.resize(length) LOG.debug(_("writing chunk at offset %s") % (offset)) offset += image.write(chunk, offset) checksum.update(chunk) if verifier: verifier.update(chunk) if loc.snapshot: image.create_snap(loc.snapshot) image.protect_snap(loc.snapshot)