glance支持延迟删除镜像的功能,个人觉得挺实用的,特别是在误删除的情况下。从某种程度来说,也算是对数据一种另类保护吧。


大致实现原理是:有个delayed_delete设置是否开启延迟删除的开关,如果为True的话,每次删除镜像的时候都会把镜像的状态置为pending_delete,记录此刻的delete_time,有个scrubber的后台进程会每隔一段时间(wakeup_time)去check是否有pending_delete的镜像要删除,删除的判断标准是:该镜像被删除的那个时刻的delete_time + scrub_time <= time.time(),scrub_time是镜像要隔多少秒才真正被擦除掉。


开启delayed_delete

[root@controller2~(keystone_admin)]#vim/etc/glance/glance-api.confdelayed_delete=True


来看glance api端删除镜像的时候判断是否开启了delayed_delete的代码

#v1的apiglance/api/v1/p_w_picpath.py@utils.mutatingdefdelete(self,req,id):"""Deletesthep_w_picpathandallitschunksfromtheGlance:paramreq:TheWSGI/WebobRequestobject:paramid:Theopaquep_w_picpathidentifier:raises:HttpBadRequestifp_w_picpathregistryisinvalid:raises:HttpNotFoundifp_w_picpathoranychunkisnotavailable:raises:HttpUnauthorizedifp_w_picpathoranychunkisnotdeleteablebytherequestinguser"""self._enforce(req,'delete_p_w_picpath')p_w_picpath=self.get_p_w_picpath_meta_or_404(req,id)ifp_w_picpath['protected']:msg=_("Imageisprotected")LOG.warn(msg)raiseHTTPForbidden(explanation=msg,request=req,content_type="text/plain")ifp_w_picpath['status']=='pending_delete':msg=(_("Forbiddentodeletea%sp_w_picpath.")%p_w_picpath['status'])LOG.warn(msg)raiseHTTPForbidden(explanation=msg,request=req,content_type="text/plain")elifp_w_picpath['status']=='deleted':msg=_("Image%snotfound.")%idLOG.warn(msg)raiseHTTPNotFound(explanation=msg,request=req,content_type="text/plain")ifp_w_picpath['location']andCONF.delayed_delete:#这里做了判断status='pending_delete'else:status='deleted'。。。。。。#v2的apiglance/api/v2/p_w_picpath.py@utils.mutatingdefdelete(self,req,p_w_picpath_id):p_w_picpath_repo=self.gateway.get_repo(req.context)try:p_w_picpath=p_w_picpath_repo.get(p_w_picpath_id)p_w_picpath.delete()#跟进去看p_w_picpath_repo.remove(p_w_picpath)except(glance_store.Forbidden,exception.Forbidden)ase:LOG.debug("Usernotpermittedtodeletep_w_picpath'%s'",p_w_picpath_id)raisewebob.exc.HTTPForbidden(explanation=e.msg)except(glance_store.NotFound,exception.NotFound)ase:msg=(_("Failedtofindp_w_picpath%(p_w_picpath_id)stodelete")%{'p_w_picpath_id':p_w_picpath_id})LOG.warn(msg)raisewebob.exc.HTTPNotFound(explanation=msg)exceptglance_store.exceptions.InUseByStorease:msg=(_("Image%(id)scouldnotbedeleted""becauseitisinuse:%(exc)s")%{"id":p_w_picpath_id,"exc":e.msg})LOG.warn(msg)raisewebob.exc.HTTPConflict(explanation=msg)exceptglance_store.exceptions.HasSnapshotase:raisewebob.exc.HTTPConflict(explanation=e.msg)exceptexception.InvalidImageStatusTransitionase:raisewebob.exc.HTTPBadRequest(explanation=e.msg)exceptexception.NotAuthenticatedase:raisewebob.exc.HTTPUnauthorized(explanation=e.msg)glance/domain/proxy.pyclassImage(object):def__init__(self,base,member_repo_proxy_class=None,member_repo_proxy_kwargs=None):self.base=baseself.helper=Helper(member_repo_proxy_class,member_repo_proxy_kwargs)name=_proxy('base','name')p_w_picpath_id=_proxy('base','p_w_picpath_id')status=_proxy('base','status')created_at=_proxy('base','created_at')updated_at=_proxy('base','updated_at')visibility=_proxy('base','visibility')min_disk=_proxy('base','min_disk')min_ram=_proxy('base','min_ram')protected=_proxy('base','protected')locations=_proxy('base','locations')checksum=_proxy('base','checksum')owner=_proxy('base','owner')disk_format=_proxy('base','disk_format')container_format=_proxy('base','container_format')size=_proxy('base','size')virtual_size=_proxy('base','virtual_size')extra_properties=_proxy('base','extra_properties')tags=_proxy('base','tags')defdelete(self):self.base.delete()#这里的base来自glance/domain/__init__.pyglance/domain/__init__.pyclassImage(object):valid_state_targets={#Eachkeydenotesa"current"stateforthep_w_picpath.Corresponding#valueslistthevalidstatestowhichwecanjumpfromthat"current"#state.#NOTE(flwang):Inv2,wearedeprecatingthe'killed'status,soit's#allowedtorestorep_w_picpathfrom'saving'to'queued'sothatupload#canberetried.'queued':('saving','active','deleted'),'saving':('active','killed','deleted','queued'),'active':('pending_delete','deleted','deactivated'),'killed':('deleted',),'pending_delete':('deleted',),'deleted':(),'deactivated':('active','deleted'),}def__init__(self,p_w_picpath_id,status,created_at,updated_at,**kwargs):self.p_w_picpath_id=p_w_picpath_idself.status=statusself.created_at=created_atself.updated_at=updated_atself.name=kwargs.pop('name',None)self.visibility=kwargs.pop('visibility','private')self.min_disk=kwargs.pop('min_disk',0)self.min_ram=kwargs.pop('min_ram',0)self.protected=kwargs.pop('protected',False)self.locations=kwargs.pop('locations',[])self.checksum=kwargs.pop('checksum',None)self.owner=kwargs.pop('owner',None)self._disk_format=kwargs.pop('disk_format',None)self._container_format=kwargs.pop('container_format',None)self.size=kwargs.pop('size',None)self.virtual_size=kwargs.pop('virtual_size',None)extra_properties=kwargs.pop('extra_properties',{})self.extra_properties=ExtraProperties(extra_properties)self.tags=kwargs.pop('tags',[])ifkwargs:message=_("__init__()gotunexpectedkeywordargument'%s'")raiseTypeError(message%list(kwargs.keys())[0])defdelete(self):#base调用的是这个delete方法ifself.protected:raiseexception.ProtectedImageDelete(p_w_picpath_id=self.p_w_picpath_id)ifCONF.delayed_deleteandself.locations:#跟v1api同样的判断self.status='pending_delete'else:self.status='deleted'#v2api有gateway、proxy、domain这些概念,留个悬念,下次弄清楚。

这里是官方对gateway、domain、proxy的介绍:http://docs.openstack.org/developer/glance/domain_model.html


修改glance-scrubber.conf文件

[root@controller2~(keystone_admin)]#egrep-v"^$|^#"/etc/glance/glance-scrubber.conf[DEFAULT]scrub_time=300delayed_delete=truesend_identity_headers=truewakeup_time=60daemon=Trueadmin_user=glanceadmin_password=glanceadmin_tenant_name=serviceauth_url=http://controller2:35357/v2.0auth_region=RegionOneregistry_host=controller2registry_port=9191[database]connection=mysql+pymysql://glance:glance@controller2/glance?charset=utf8[oslo_concurrency][oslo_policy][glance_store]default_store=rbdstores=rbd,http,cinderrbd_store_pool=p_w_picpathsrbd_store_user=glancerbd_store_ceph_conf=/etc/ceph/ceph.confrbd_store_chunk_size=8filesystem_store_datadirs=/var/lib/glance/p_w_picpaths


启动glance-srubber服务

[root@controller2~(keystone_admin)]#serviceopenstack-glance-scrubberstart


接下来看glance scrubber的启动过程

glance/cmd/scrubber.pydefmain():CONF.register_cli_opts(scrubber.scrubber_cmd_cli_opts)CONF.register_opts(scrubber.scrubber_cmd_opts)try:config.parse_args()logging.setup(CONF,'glance')glance_store.register_opts(config.CONF)glance_store.create_stores(config.CONF)#会调用glance_store/backend.py的create_stores函数,初始化SCHEME_TO_CLS_MAPglance_store.verify_default_store()app=scrubber.Scrubber(glance_store)#会作为下面的store_apiifCONF.daemon:#让glance-scrubber以daemon方式存在server=scrubber.Daemon(CONF.wakeup_time)server.start(app)server.wait()else:app.run()exceptRuntimeErrorase:sys.exit("ERROR:%s"%e)if__name__=='__main__':main()


Daemon类

glance/scrubber.pyclassDaemon(object):def__init__(self,wakeup_time=300,threads=100):LOG.info(_LI("StartingDaemon:wakeup_time=%(wakeup_time)s""threads=%(threads)s"),{'wakeup_time':wakeup_time,'threads':threads})self.wakeup_time=wakeup_timeself.event=eventlet.event.Event()#Thispoolisusedforperiodicinstantiationofscrubberself.daemon_pool=eventlet.greenpool.GreenPool(threads)defstart(self,application):self._run(application)defwait(self):try:self.event.wait()exceptKeyboardInterrupt:msg=_LI("DaemonShutdownonKeyboardInterrupt")LOG.info(msg)def_run(self,application):LOG.debug("Runningapplication")self.daemon_pool.spawn_n(application.run,self.event)#这里也用eventleteventlet.spawn_after(self.wakeup_time,self._run,application)#application就是下面Scrubber的instanceLOG.debug("Nextrunscheduledin%sseconds",self.wakeup_time)


Scrubber类

classScrubber(object):def__init__(self,store_api):LOG.info(_LI("Initializingscrubberwithconfiguration:%s"),six.text_type({'registry_host':CONF.registry_host,'registry_port':CONF.registry_port}))self.store_api=store_apiregistry.configure_registry_client()registry.configure_registry_admin_creds()#glance/registry/client/v2或v1/api.py,初始好_CLIENT_CREDS,获得registryclient需要#Herewecreatearequestcontextwithcredentialstosupport#delayeddeletewhenusingmulti-tenantbackendstorageadmin_user=CONF.admin_useradmin_tenant=CONF.admin_tenant_name#需要配置,授权用的,要获得registry的clientinstanceifCONF.send_identity_headers:#之前没enablesend_identity_headers,一直授权失败,难道有坑?#Whenregistryisoperatingintrusted-authmoderoles=[CONF.admin_role]self.admin_context=context.RequestContext(user=admin_user,tenant=admin_tenant,auth_token=None,roles=roles)self.registry=registry.get_registry_client(self.admin_context)else:ctxt=context.RequestContext()self.registry=registry.get_registry_client(ctxt)auth_token=self.registry.auth_tokenself.admin_context=context.RequestContext(user=admin_user,tenant=admin_tenant,auth_token=auth_token)self.db_queue=get_scrub_queue()self.pool=eventlet.greenpool.GreenPool(CONF.scrub_pool_size)#每隔wakeup_time秒就会执行这个run函数defrun(self,event=None):delete_jobs=self._get_delete_jobs()ifdelete_jobs:list(self.pool.starmap(self._scrub_p_w_picpath,delete_jobs.items()))#对后面可迭代对象迭代执行_scrub_p_w_picpath函数#_scrub_p_w_picpath函数def_scrub_p_w_picpath(self,p_w_picpath_id,delete_jobs):iflen(delete_jobs)==0:returnLOG.info(_LI("Scrubbingp_w_picpath%(id)sfrom%(count)dlocations."),{'id':p_w_picpath_id,'count':len(delete_jobs)})success=Trueforimg_id,loc_id,uriindelete_jobs:try:self._delete_p_w_picpath_location_from_backend(img_id,loc_id,uri)exceptException:success=Falseifsuccess:p_w_picpath=self.registry.get_p_w_picpath(p_w_picpath_id)ifp_w_picpath['status']=='pending_delete':self.registry.update_p_w_picpath(p_w_picpath_id,{'status':'deleted'})#利用上面获得的registryclient更新p_w_picpath的状态,registry是跟数据库打交道的LOG.info(_LI("Image%shasbeenscrubbedsuccessfully"),p_w_picpath_id)else:LOG.warn(_LW("Oneormorep_w_picpathlocationscouldn'tbescrubbed""frombackend.Leavingp_w_picpath'%s'in'pending_delete'""status")%p_w_picpath_id)#_delete_p_w_picpath_location_from_backend函数def_delete_p_w_picpath_location_from_backend(self,p_w_picpath_id,loc_id,uri):ifCONF.metadata_encryption_key:uri=crypt.urlsafe_decrypt(CONF.metadata_encryption_key,uri)#uri有加密,就先解密try:LOG.debug("Scrubbingp_w_picpath%sfromalocation.",p_w_picpath_id)try:self.store_api.delete_from_backend(uri,self.admin_context)#store_api是glance_store/__init__.pyexceptstore_exceptions.NotFound:LOG.info(_LI("Imagelocationforp_w_picpath'%s'notfoundin""backend;Markingp_w_picpathlocationdeletedin""db."),p_w_picpath_id)ifloc_id!='-':db_api.get_api().p_w_picpath_location_delete(self.admin_context,p_w_picpath_id,int(loc_id),'deleted')LOG.info(_LI("Image%sisscrubbedfromalocation."),p_w_picpath_id)exceptExceptionase:LOG.error(_LE("Unabletoscrubp_w_picpath%(id)sfromalocation.""Reason:%(exc)s")%{'id':p_w_picpath_id,'exc':encodeutils.exception_to_unicode(e)})raise


#_get_delete_jobs函数,获取要删除的镜像的dictdef_get_delete_jobs(self):try:records=self.db_queue.get_all_locations()#ScrubDBQueue类的get_all_locations函数exceptExceptionaserr:LOG.error(_LE("Cannotgetscrubjobsfromqueue:%s")%encodeutils.exception_to_unicode(err))return{}delete_jobs={}forp_w_picpath_id,loc_id,loc_uriinrecords:ifp_w_picpath_idnotindelete_jobs:delete_jobs[p_w_picpath_id]=[]delete_jobs[p_w_picpath_id].append((p_w_picpath_id,loc_id,loc_uri))returndelete_jobs#ScrubDBQueue类的get_all_locations函数defget_all_locations(self):"""Returnsalistofp_w_picpathidandlocationtuplefromscrubqueue.:returns:alistofp_w_picpathid,locationidandurituplefromscrubqueue"""ret=[]forp_w_picpathinself._get_all_p_w_picpaths():deleted_at=p_w_picpath.get('deleted_at')ifnotdeleted_at:continue#NOTE:Stripoffmicrosecondswhichmayoccurafterthelast'.,'#Example:2012-07-07T19:14:34.974216date_str=deleted_at.rsplit('.',1)[0].rsplit(',',1)[0]delete_time=calendar.timegm(time.strptime(date_str,"%Y-%m-%dT%H:%M:%S"))ifdelete_time+self.scrub_time>time.time():#判断是否到了清除的时间continueforlocinp_w_picpath['location_data']:ifloc['status']!='pending_delete':#判断是否是pending_delete状态continueifself.metadata_encryption_key:#判断镜像uri是否加密uri=crypt.urlsafe_encrypt(self.metadata_encryption_key,loc['url'],64)else:uri=loc['url']ret.append((p_w_picpath['id'],loc['id'],uri))returnret


下面都是关于glance_store,算是glance的子项目了,专门和后端真正存储打交道的。

glance_store/__init__.pyfrom.backendimport*#noqafrom.driverimport*#noqafrom.exceptionsimport*#noqa#来看store_api.delete_from_backend函数glance_store/backend.pydefdelete_from_backend(uri,context=None):"""Removeschunksofdatafrombackendspecifiedbyuri."""loc=location.get_location_from_uri(uri,conf=CONF)store=get_store_from_uri(uri)returnstore.delete(loc,context=context)#get_store_from_uri函数defget_store_from_uri(uri):"""GivenaURI,returnthestoreobjectthatwouldhandleoperationsontheURI.:paramuri:URItoanalyze"""scheme=uri[0:uri.find('/')-1]#形如得到的会是这样的file、rbdreturnget_store_from_scheme(scheme)#get_store_from_scheme函数,从SCHEME_TO_CLS_MAP中获取对应的schemamappingdefget_store_from_scheme(scheme):"""Givenascheme,returntheappropriatestoreobjectforhandlingthatscheme."""ifschemenotinlocation.SCHEME_TO_CLS_MAP:raiseexceptions.UnknownScheme(scheme=scheme)scheme_info=location.SCHEME_TO_CLS_MAP[scheme]store=scheme_info['store']ifnotstore.is_capable(capabilities.BitMasks.DRIVER_REUSABLE):#Driverinstanceisn'tstatelesssoitcan't#bereusedsafelyandneedrecreation.store_entry=scheme_info['store_entry']store=_load_store(store.conf,store_entry,invoke_load=True)store.configure()try:scheme_map={}loc_cls=store.get_store_location_class()forschemeinstore.get_schemes():scheme_map[scheme]={'store':store,'location_class':loc_cls,'store_entry':store_entry}location.register_scheme_map(scheme_map)exceptNotImplementedError:scheme_info['store']=storereturnstore#上面配置的stores是rbd,获得的就是glance_store/_drivers/rbd.py@capabilities.checkdefdelete(self,location,context=None):"""Takesa`glance_store.location.Location`objectthatindicateswheretofindthep_w_picpathfiletodelete.:paramlocation:`glance_store.location.Location`object,suppliedfromglance_store.location.get_location_from_uri():raises:NotFoundifp_w_picpathdoesnotexist;InUseByStoreifp_w_picpathisinuseorsnapshotunprotectfailed"""loc=location.store_locationtarget_pool=loc.poolorself.poolself._delete_p_w_picpath(target_pool,loc.p_w_picpath,loc.snapshot)#_delete_p_w_picpath函数def_delete_p_w_picpath(self,target_pool,p_w_picpath_name,snapshot_name=None,context=None):"""DeleteRBDp_w_picpathandsnapshot.:paramp_w_picpath_name:Image'sname:paramsnapshot_name:Imagesnapshot'sname:raises:NotFoundifp_w_picpathdoesnotexist;InUseByStoreifp_w_picpathisinuseorsnapshotunprotectfailed"""withself.get_connection(conffile=self.conf_file,rados_id=self.user)asconn:withconn.open_ioctx(target_pool)asioctx:try:#Firstremovesnapshot.ifsnapshot_nameisnotNone:withrbd.Image(ioctx,p_w_picpath_name)asp_w_picpath:try:p_w_picpath.unprotect_snap(snapshot_name)p_w_picpath.remove_snap(snapshot_name)exceptrbd.ImageNotFoundasexc:msg=(_("SnapOperatingException""%(snap_exc)s""Snapshotdoesnotexist.")%{'snap_exc':exc})LOG.debug(msg)exceptrbd.ImageBusyasexc:log_msg=(_LE("SnapOperatingException""%(snap_exc)s""Snapshotisinuse.")%{'snap_exc':exc})LOG.error(log_msg)raiseexceptions.InUseByStore()#Thendeletep_w_picpath.rbd.RBD().remove(ioctx,p_w_picpath_name)exceptrbd.ImageHasSnapshots:log_msg=(_LE("Removep_w_picpath%(img_name)sfailed.""Ithassnapshot(s)left.")%{'img_name':p_w_picpath_name})LOG.error(log_msg)raiseexceptions.HasSnapshot()exceptrbd.ImageBusy:log_msg=(_LE("Removep_w_picpath%(img_name)sfailed.""Itisinuse.")%{'img_name':p_w_picpath_name})LOG.error(log_msg)raiseexceptions.InUseByStore()exceptrbd.ImageNotFound:msg=_("RBDp_w_picpath%sdoesnotexist")%p_w_picpath_nameraiseexceptions.NotFound(message=msg)


参考链接

eventlet常用函数介绍http://www.cnblogs.com/Security-Darren/p/4168233.html


以上过程,理解不对的地方,还请指正,见谅!