1 Cinder架构图
Cinder是在虚拟机和具体存储设备之间引入了一层“逻辑存储卷”的抽象,Cinder本身并不是一种存储技术,只是提供一个中间的抽象层,Cinder通过调用不同存储后端类型的驱动接口来管理相对应的后端存储,为用户提供统一的卷相关操作的存储接口。
由上图可以看出,目前的Cinder组件主要由cinder-api、cinder-scheduler、cinder-volume以及cinder-backup几个服务所组成,它们之间通过消息队列进行通信。
2 Cinder源码结构
跟Glance一样,先看setup.cfg文件里[entry_points]里console_scripts的内容,它写明Cinder的各项服务的入口点:
console_scripts = cinder-api = cinder.cmd.api:main cinder-backup = cinder.cmd.backup:main cinder-manage = cinder.cmd.manage:main cinder-rootwrap = oslo_rootwrap.cmd:main cinder-rtstool = cinder.cmd.rtstool:main cinder-scheduler = cinder.cmd.scheduler:main cinder-volume = cinder.cmd.volume:main cinder-volume-usage-audit = cinder.cmd.volume_usage_audit:main
服务 | 描述 |
cinder-api | 进入Cinder的HTTP接口。 |
cinder-backup | 用于提供存储卷的备份功能,支持将块存储卷备份到OpenStack备份存储后端,比如Swift、Ceph等。 |
cinder-manage | 用于cinder管理的命令行接口。 |
cinder-rtstool | 伴随LIO(Linux-IO Target)支持而增加的工具。 |
cinder-scheduler | 根据预定的策略选择合适的cinder-volume节点来处理用户的请求。 |
cinder-volume | 通过相关驱动程序架构直接与块存储服务进行交互。 |
cinder-volume-usage-audit | 用于卷使用情况统计。 |
3 Cinder各服务功能概述
从前面我们可以看到Cinder内部服务中主要是由cinder-api、cinder-scheduler、cinder-volume和cinder-backup组成的,而且各服务之间是使用高级消息队列进行通信的,以下对各服务进行概述。
3.1 cinder-api
cinder-api的作用主要是为用户提供Restful风格的接口,接收client的请求,在该服务中可以对用户的权限和传入的参数进行提前的检查,无误后方才将请求信息交给消息队列,由后续的其它服务根据消息队列信息进行处理。
3.2 cinder-scheduler
cinder-scheduler是一个调度器,用于选择合适的存储节点,该服务中包含过滤器算法和权重计算算法,Cinder默认的过滤算法有三个:
(1)AvailabilityZoneFilter过滤算法:判断cinder host的availability zone是否与目标zone一致,否则过滤掉该节点;
(2)CapacityFilter过滤算法:判断host的可用存储空间是否不小于要分配卷的大小,否则过滤掉该节点;
(3)CapabilitiesFilter过滤算法:检查host的属性否和volume type中的extra specs相同,不相同则过滤掉该节点。
通过指定的过滤算法可能会得到一系列的host,这时还需使用权重计算算法来计算各节点的权重值,权重值最大的会认为是最优节点,cinder-scheduler会基于消息队列服务的rpc调用来让最优节点对请求进行处理,以下列出几个计算权重的算法:
(1)AllocatedCapacityWeigher算法:存储空间使用最小的节点为最优节点;
(2)CapacityWeigher算法:可用存储空间最大的节点成为最优节点;
(3)ChanceWeigher算法:随机选择一个节点作为最优节点。
3.3 cinder-volume
cinder-volume是部署在存储节点上的服务,cinder-volume的主要功能是对后端存储进行一层抽象封装,为用户提供统一的接口,cinder-volume通过调用后端存储驱动API来进行存储相关的操作。cinder-volume服务执行的功能可以由如下表列出:
卷操作 | 创建卷 |
克隆卷 | |
扩展卷 | |
删除卷 | |
卷虚机从操作 | 挂载卷到虚拟机 |
从虚拟机里分离出卷 | |
卷-快照操作 | 创建卷的快照 |
从已有卷快照创建卷 | |
删除快照 | |
卷-镜像操作 | 从镜像创建卷 |
从卷创建镜像 |
3.4 cinder-backup
cinder-backup的功能是将volume备份到别的存储设备上去,以后可以通过restone操作恢复。
cinder-backup跟volume做快照还是有很大区别的,以下列出主要的几点区别:
(1)快照依赖于源volume,如果源volume被删除了,那快照是无用的,但backup是不依赖于源volume的,因为源volume也被备份到备份存储设备上去了,通过restone操作可以完全恢复。
(2)快照和源volume通常放在一起,由同一个volume provider管理,而backup存放在独立的备份设备上,有自己的备份方案和实现,和volume provider没有关系。
(3)cinder-backup是具有容灾功能的,但因此备份往往需要较大的空间,而快照snapshot则提供volume provider内快捷的回溯功能。
我们可以通过使用cinder backup-create命令创建某个卷的备份,通过cinder backup-list命令查看创建的备份。
4 Cinder组件重要流程分析
4.1 Cinder服务启动流程
Cinder的主要服务包括cinder-api、cinder-scheduler和cinder-volume等,这里介绍下cinder-api服务的启动流程,其它类似。
cinder-api服务启动流程
从/cmd/api.py的main函数中我们可以看到以下几行代码:
def main(): objects.register_all() gmr_opts.set_defaults(CONF) CONF(sys.argv[1:], project='cinder', version=version.version_string()) config.set_middleware_defaults() logging.setup(CONF, "cinder") python_logging.captureWarnings(True) utils.monkey_patch() gmr.TextGuruMeditation.setup_autorun(version, conf=CONF) rpc.init(CONF) launcher = service.process_launcher() server = service.WSGIService('osapi_volume') launcher.launch_service(server, workers=server.workers) launcher.wait()
(1)register_all函数是导入/cinder/object目录下的多个模块
(2)接着是加载配置文件、设置中间件和初始化日志模块
(3)初始化rpc模块,用于与其它服务进行通信
(4)启动wsgi服务,监听客户端发送的请求
4.2 创建卷过程分析
比如客户端敲入命令:openstack volume create oop2 --size=1 --debug
首先我们知道该请求会先在cinder-api服务中进行处理,该请求首先会匹配到/cinder/api/v2/volumes.py文件的create函数:
def create(self, req, body): ...# 对卷的参数进行检查,比如卷名、size参数,参数赋予kwargs字典等 # 调用/cinder/volume/api.py里的create函数进行创建 new_volume = self.volume_api.create(context, size, volume.get('display_name'), volume.get('display_description'), **kwargs) retval = self._view_builder.detail(req, new_volume) return retval
查看api.py文件里的create函数:
def create(self, context, size, name, description, ...): # 验证该用户是否有权限在该project下做这个操作 check_policy(context, 'create_from_image' if image_id else 'create') # 获取调用cinder-scheduler服务端方法的rpc客户端 sched_rpcapi = (self.scheduler_rpcapi if ( not cgsnapshot and not source_cg and not group_snapshot and not source_group) else None) # 获取调用cinder-volume服务端方法的rpc客户端 volume_rpcapi = (self.volume_rpcapi if ( not cgsnapshot and not source_cg and not group_snapshot and not source_group) else None) # 调用/cinder/volume/flows/api/create_volume.py的get_flow方法,并返回api入口流 flow_engine = create_volume.get_flow(self.db, self.image_service, availability_zones, create_what, sched_rpcapi, volume_rpcapi)
在cinder组件的代码中我们可以经常看到在处理一个操作时,会创建一个类flow的实例对象,然后往该对象中添加任务来进行工作流程的处理,查看get_flow函数的实现:
def get_flow(db_api, image_service_api, availability_zones, create_what, scheduler_rpcapi=None, volume_rpcapi=None): """Constructs and returns the api entrypoint flow. This flow will do the following: 1. Inject keys & values for dependent tasks. 2. Extracts and validates the input keys & values. 3. Reserves the quota (reverts quota on any failures). 4. Creates the database entry. 5. Commits the quota. 6. Casts to volume manager or scheduler for further processing. """ flow_name = ACTION.replace(":", "_") + "_api" # 获取类flow的实例化对象,可以添加task到flow对象 api_flow = linear_flow.Flow(flow_name) # 添加ExtractVolumeRequestTask,该任务是将接收到的一系列参数值通过一系列条件的验证将验证结果存储起来供其它task使用 api_flow.add(ExtractVolumeRequestTask( image_service_api, availability_zones, rebind={'size': 'raw_size', 'availability_zone': 'raw_availability_zone', 'volume_type': 'raw_volume_type'})) # 添加多个task # QuotaReserveTask: 为卷保留配额,失败时可以回滚 # EntryCreateTask: 将卷的创建过程写入数据库,比如卷的状态,此时卷的状态未"creating" # QuotaCommitTask: 用于提交保留 api_flow.add(QuotaReserveTask(), EntryCreateTask(), QuotaCommitTask()) # 将请求通过基于消息队列服务的rpc方式发送到scheduler或者volume管理程序去处理请求 if scheduler_rpcapi and volume_rpcapi: # This will cast it out to either the scheduler or volume manager via # the rpc apis provided. # 添加VolumeCastTask # VolumeCastTask: 将卷创建工作转移到scheduler或volume管理程序去处理,也就表示工作流程从api服务中转到 # 其它服务中去执行 api_flow.add(VolumeCastTask(scheduler_rpcapi, volume_rpcapi, db_api)) # Now load (but do not run) the flow using the provided initial data. return taskflow.engines.load(api_flow, store=create_what)
上面的代码中比较关键的代码是VolumeCastTask该任务类它会调用它的execute函数,execute函数中会调用_cast_create_volume函数:
def _cast_create_volume(self, context, request_spec, filter_properties): # 这里判断用户是否有指定host进行创建卷操作,如果没有则将创建任务交给 scheduler管理程序去完成 # 如果用户有指定host则跳过scheduler,直接将创建任务交给volume管理程序去完成 if not source_volume_ref: # Cast to the scheduler and let it handle whatever is needed # to select the target host for this volume. # 这里会直接调用到SchedulerAPI.create_volume函数 # SchedulerAPI.create_volume函数会通过消息异步调用SchedulerManager.create_volume函数, # 也就是/cinder/scheduler/manager.py中的create_volume函数 self.scheduler_rpcapi.create_volume( context, volume, snapshot_id=snapshot_id, image_id=image_id, request_spec=request_spec, filter_properties=filter_properties) else: # Bypass the scheduler and send the request directly to the volume # manager. volume.host = source_volume_ref.host volume.cluster_name = source_volume_ref.cluster_name volume.scheduled_at = timeutils.utcnow() volume.save() if not cgsnapshot_id: self.volume_rpcapi.create_volume( context, volume, request_spec, filter_properties, allow_reschedule=False)
到这里cinder-api服务就完成它所有的工作了,它会通过消息异步调用cinder-scheduler服务里面的函数,以下创建卷的工作流程开始在cinder-scheduler服务中进行,查看scheduler服务中的manager.py文件中的create_volume函数:
def create_volume(self, context, volume, snapshot_id=None, image_id=None, request_spec=None, filter_properties=None): # 确保调度程序已经准备好 self._wait_for_scheduler() try: # 这里会调用/cinder/scheduler/flows/create_volume.py中的get_flow函数 flow_engine = create_volume.get_flow(context, self.driver, request_spec, filter_properties, volume, snapshot_id, image_id)
主要查看get_flow函数的实现:
def get_flow(context, driver_api, request_spec=None, filter_properties=None, volume=None, snapshot_id=None, image_id=None): """Constructs and returns the scheduler entrypoint flow. This flow will do the following: 1. Inject keys & values for dependent tasks. 2. Extract a scheduler specification from the provided inputs. 3. Use provided scheduler driver to select host and pass volume creation request further. """ create_what = { 'context': context, 'raw_request_spec': request_spec, 'filter_properties': filter_properties, 'volume': volume, 'snapshot_id': snapshot_id, 'image_id': image_id, } flow_name = ACTION.replace(":", "_") + "_scheduler" # 获取类flow实例对象 scheduler_flow = linear_flow.Flow(flow_name) # This will extract and clean the spec from the starting values. # ExtractSchedulerSpecTask: 从请求规范中提取规范对象 scheduler_flow.add(ExtractSchedulerSpecTask( rebind={'request_spec': 'raw_request_spec'})) # This will activate the desired scheduler driver (and handle any # driver related failures appropriately). # ScheduleCreateVolumeTask: 激活scheduler程序的驱动程序并处理任何后续故障 scheduler_flow.add(ScheduleCreateVolumeTask(driver_api)) # Now load (but do not run) the flow using the provided initial data. return taskflow.engines.load(scheduler_flow, store=create_what)
ScheduleCreateVolumeTask任务会执行它的execute函数,execute函数会调用过滤器FilterScheduler的schedule_create_volume函数来选择最佳的存储后端并将工作流程过渡到volume管理服务中:
def schedule_create_volume(self, context, request_spec, filter_properties): # 选择一个最合适的backend来进行准备创建的卷的存储 backend = self._schedule(context, request_spec, filter_properties) if not backend: raise exception.NoValidBackend(reason=_("No weighed backends " "available")) backend = backend.obj volume_id = request_spec['volume_id'] # 更新数据库当前卷的状态 updated_volume = driver.volume_update_db(context, volume_id, backend.host, backend.cluster_name) self._post_select_populate_filter_properties(filter_properties, backend) # context is not serializable filter_properties.pop('context', None) # 通过消息队列请求调用volume_rpcapi.create_volume # VolumeAPI.create_volume会通过消息队列远程调用VolumeManager.create_volume # 最后会调用到/cinder/volume/manager.py中的create_volume函数,也就是创建卷的工作流程会转入到cinder-volume的服务中 self.volume_rpcapi.create_volume(context, updated_volume, request_spec, filter_properties, allow_reschedule=True)
至此cinder-scheduler服务的工作也已经全部完成了,接下来的工作会调用进cinder-volume服务的manager.py文件中create_volume函数,该函数也是通过建立flow实例对象,然后添加任务来完成创建工作,我们直接看get_flow函数:
def get_flow(context, manager, db, driver, scheduler_rpcapi, host, volume, allow_reschedule, reschedule_context, request_spec, filter_properties, image_volume_cache=None): """Constructs and returns the manager entrypoint flow. This flow will do the following: 1. Determines if rescheduling is enabled (ahead of time). 2. Inject keys & values for dependent tasks. 3. Selects 1 of 2 activated only on *failure* tasks (one to update the db status & notify or one to update the db status & notify & *reschedule*). 4. Extracts a volume specification from the provided inputs. 5. Notifies that the volume has started to be created. 6. Creates a volume from the extracted volume specification. 7. Attaches a on-success *only* task that notifies that the volume creation has ended and performs further database status updates. """ flow_name = ACTION.replace(":", "_") + "_manager" # 获取类flow实例对象 volume_flow = linear_flow.Flow(flow_name) # This injects the initial starting flow values into the workflow so that # the dependency order of the tasks provides/requires can be correctly # determined. create_what = { 'context': context, 'filter_properties': filter_properties, 'request_spec': request_spec, 'volume': volume, } # ExtractVolumeRefTask: 提取给定卷ID的卷引用 volume_flow.add(ExtractVolumeRefTask(db, host, set_error=False)) retry = filter_properties.get('retry', None) # Always add OnFailureRescheduleTask and we handle the change of volume's # status when reverting the flow. Meanwhile, no need to revert process of # ExtractVolumeRefTask. do_reschedule = allow_reschedule and request_spec and retry volume_flow.add(OnFailureRescheduleTask(reschedule_context, db, driver, scheduler_rpcapi, do_reschedule)) LOG.debug("Volume reschedule parameters: %(allow)s " "retry: %(retry)s", {'allow': allow_reschedule, 'retry': retry}) # ExtractVolumeSpecTask: 结合数据库存取的该卷的信息,提供有用的、易分析的卷相关数据结构给其它任务使用 # NotifyVolumeActionTask: 在卷开始创建时执行相关的通知信息 # CreateVolumeFromSpecTask: 该任务是根据卷的规格真实创建卷 # CreateVolumeOnFinishTask: 当卷创建成功后,会使用该任务将卷在数据库的状态更新为available volume_flow.add(ExtractVolumeSpecTask(db), NotifyVolumeActionTask(db, "create.start"), CreateVolumeFromSpecTask(manager, db, driver, image_volume_cache), CreateVolumeOnFinishTask(db, "create.end")) # Now load (but do not run) the flow using the provided initial data. return taskflow.engines.load(volume_flow, store=create_what)
上面代码中进行卷创建的工作是在CreateVolumeFromSpecTask该任务中,该任务类首先是执行execute函数,execute函数中ongoing根据要创建的卷的类型调用相对应的方法来进行卷的创建:
# 根据不同类型的卷调用不同的方法来创建 if create_type == 'raw': model_update = self._create_raw_volume(volume, **volume_spec) elif create_type == 'snap': model_update = self._create_from_snapshot(context, volume, **volume_spec) elif create_type == 'source_vol': model_update = self._create_from_source_volume( context, volume, **volume_spec) elif create_type == 'source_replica': model_update = self._create_from_source_replica( context, volume, **volume_spec)
这里我们卷类型应该是raw,则它会调用_create_raw_volume函数:
ret = self.driver.create_volume(volume)
在该函数中,它会根据配置文件中指定的后端存储类型来调用相对应的在driver目录下的逻辑代码,比如我们配置的后端存储是Ceph的块设备存储,那里它就会调用到/cinder/volume/drivers/rbd.py文件的create_volume函数来进行卷的创建。
4.3 挂载卷到虚拟机过程分析
在OpenStack的搭建示例中,使用的是lvm后端存储,然后再使用ISCSI的方式来进行访问后端存储卷,这里我们采用的后端存储方式是Ceph的块设备存储,libvirt是支持直接挂载rbd image的,然后通过rbd协议来访问image,以下是挂载操作过程的源码分析分析,大部分的工作其实都是在nova组件服务中完成的。
我们可以通过敲入命令nova volume-attach instance-name volume-id来将volume-id的卷挂载到实例instance-name中,这个操作首先会调用到/nova/api/openstack/compute/volumes.py中的create函数:
def create(self, req, server_id, body): """Attach a volume to an instance.""" context = req.environ['nova.context'] context.can(vol_policies.BASE_POLICY_NAME) context.can(va_policies.POLICY_ROOT % 'create') volume_id = body['volumeAttachment']['volumeId'] device = body['volumeAttachment'].get('device') instance = common.get_instance(self.compute_api, context, server_id) if instance.vm_state in (vm_states.SHELVED, vm_states.SHELVED_OFFLOADED): _check_request_version(req, '2.20', 'attach_volume', server_id, instance.vm_state) device = self.compute_api.attach_volume(context, instance, volume_id, device) # The attach is async attachment = {} attachment['id'] = volume_id attachment['serverId'] = server_id attachment['volumeId'] = volume_id attachment['device'] = device # TODO(justinsb): How do I return "accepted" here? return {'volumeAttachment': attachment}
这里的关键执行时调用了/nova/compute/api.py的attach_volume函数:
def attach_volume(self, context, instance, volume_id, device=None, disk_bus=None, device_type=None): if device and not block_device.match_device(device): raise exception.InvalidDevicePath(path=device) is_shelved_offloaded = instance.vm_state == vm_states.SHELVED_OFFLOADED if is_shelved_offloaded: return self._attach_volume_shelved_offloaded(context, instance, volume_id, device, disk_bus, device_type) return self._attach_volume(context, instance, volume_id, device, disk_bus, device_type)
这里主要是根据虚拟机的当前状态来判断执行哪个函数,我们当前的虚拟机是在运行的,它走的逻辑是_attach_volume函数,我们查看该函数:
def _attach_volume(self, context, instance, volume_id, device, disk_bus, device_type): # 远程目的主机确定设备名和更新数据库并返回相关信息对象 volume_bdm = self._create_volume_bdm( context, instance, device, volume_id, disk_bus=disk_bus, device_type=device_type) try: # 这个函数中会通过远程调用cinder-volume服务里的函数来检查该卷是否是可添加的且更新数据库的状态 self._check_attach_and_reserve_volume(context, volume_id, instance) self.compute_rpcapi.attach_volume(context, instance, volume_bdm) except Exception: with excutils.save_and_reraise_exception(): volume_bdm.destroy() return volume_bdm.device_name
前面一些代码主要都是用以检查卷和更新数据库信息的,可以看到后面调用到attach_volume继续attach的任务,在attach_volume函数中通过rpc方式调用到目的计算节点上的函数进行任务的执行,这里调用到的是目的计算节点上/nova/compute/manager.py中的attach_volume函数,该函数里根据之前的参数信息转换了一个driver_block_device类实例对象,然后调用_attach_volume函数并将该实例作为参数传入:
def _attach_volume(self, context, instance, bdm): context = context.elevated() # 因为我们创建的volume,所以这里调用的attach方法是/nova/virt/block_device.py里的attach函数 bdm.attach(context, instance, self.volume_api, self.driver, do_check_attach=False, do_driver_attach=True) info = {'volume_id': bdm.volume_id} self._notify_about_instance_usage( context, instance, "volume.attach", extra_usage_info=info)
查看attach函数:
def attach(self, context, instance, volume_api, virt_driver, do_check_attach=True, do_driver_attach=False, **kwargs): # 获取有关要attach的volume的信息对象 volume = volume_api.get(context, self.volume_id) if do_check_attach: # 检查volume的状态属性是否符合attach的要求 volume_api.check_attach(context, volume, instance=instance) volume_id = volume['id'] context = context.elevated() # 返回该计算节点信息,比如节点名、节点ip、操作系统架构等 connector = virt_driver.get_volume_connector(instance) # 获取卷的信息,比如如果是存储在Ceph集群的,会包含集群名、monitor节点ip等,确保拥有的信息能访问集群中的该volume connection_info = volume_api.initialize_connection(context, volume_id, connector) if 'serial' not in connection_info: connection_info['serial'] = self.volume_id self._preserve_multipath_id(connection_info) # If do_driver_attach is False, we will attach a volume to an instance # at boot time. So actual attach is done by instance creation code. if do_driver_attach: # 远程调用cinder组件服务中的函数去获取该卷的加密元数据 encryption = encryptors.get_encryption_metadata( context, volume_api, volume_id, connection_info) virt_driver.attach_volume( context, connection_info, instance, self['mount_device'], disk_bus=self['disk_bus'], device_type=self['device_type'], encryption=encryption) self['connection_info'] = connection_info if self.volume_size is None: self.volume_size = volume.get('size') mode = 'rw' if 'data' in connection_info: mode = connection_info['data'].get('access_mode', 'rw')
这段代码中initialize_connection函数主要都是远程调用了volume组件服务来获取信息,这里我们可以详细看下这个函数在cinder组件服务里所做的事情,主要查看/cinder/volume/manager.py的initialize_connection函数:
def initialize_connection(self, context, volume, connector): # TODO(jdg): Add deprecation warning # 验证driver是否已经初始化 utils.require_driver_initialized(self.driver) # 对于rbd驱动没有重写该方法,所以没做任何事 self.driver.validate_connector(connector) # 对于rbd driver,该方法是空的,因为rbd不需要像ISCSI那样创建target、创建protal model_update = self.driver.create_export(context.elevated(), volume, connector) if model_update: volume.update(model_update) volume.save() # 对于ceph rbd驱动,这里是获取有关该卷所在集群的信息,比如monitor、ip、secret_uuid等 conn_info = self.driver.initialize_connection(volume, connector) conn_info = self._parse_connection_options(context, volume, conn_info) LOG.info(_LI("Initialize volume connection completed successfully."), resource=volume) return conn_info
注意这里的driver是根据配置文件的配置的后端存储类型的driver,由于我们配置的是Ceph的块设备作为后端存储,因此其实例driver调用的函数都是/cinder/volume/drivers/rbd.py里的函数。
现在我们回到nova组件代码中,查看virt_driver.attach_volume调用的是/nova/virt/libvirt/driver.py中的attach_volume函数:
def attach_volume(self, context, connection_info, instance, mountpoint, disk_bus=None, device_type=None, encryption=None): guest = self._host.get_guest(instance) disk_dev = mountpoint.rpartition("/")[2] bdm = { 'device_name': disk_dev, 'disk_bus': disk_bus, 'device_type': device_type} # Note(cfb): If the volume has a custom block size, check that # that we are using QEMU/KVM and libvirt >= 0.10.2. The # presence of a block size is considered mandatory by # cinder so we fail if we can't honor the request. data = {} if ('data' in connection_info): data = connection_info['data'] if ('logical_block_size' in data or 'physical_block_size' in data): if ((CONF.libvirt.virt_type != "kvm" and CONF.libvirt.virt_type != "qemu")): msg = _("Volume sets block size, but the current " "libvirt hypervisor '%s' does not support custom " "block size") % CONF.libvirt.virt_type raise exception.InvalidHypervisorType(msg) disk_info = blockinfo.get_info_from_bdm( instance, CONF.libvirt.virt_type, instance.image_meta, bdm) self._connect_volume(connection_info, disk_info) if disk_info['bus'] == 'scsi': disk_info['unit'] = self._get_scsi_controller_max_unit(guest) + 1 conf = self._get_volume_config(connection_info, disk_info) self._check_discard_for_attach_volume(conf, instance) state = guest.get_power_state(self._host) live = state in (power_state.RUNNING, power_state.PAUSED) if encryption: encryptor = self._get_volume_encryptor(connection_info, encryption) encryptor.attach_volume(context, **encryption) guest.attach_device(conf, persistent=True, live=live)
这段代码中关键的地方有首先获取该该虚拟机的实例对象,然后将一系列参数都封装进conf对象中,然后guest调用attach_device函数来完成挂载工作,查看该函数:
def attach_device(self, conf, persistent=False, live=False): """Attaches device to the guest. :param conf: A LibvirtConfigObject of the device to attach :param persistent: A bool to indicate whether the change is persistent or not :param live: A bool to indicate whether it affect the guest in running state """ flags = persistent and libvirt.VIR_DOMAIN_AFFECT_CONFIG or 0 flags |= live and libvirt.VIR_DOMAIN_AFFECT_LIVE or 0 # 把conf中的信息转换成xml的格式,然后可通过libvirt工具将卷挂载到guest中 device_xml = conf.to_xml() if six.PY3 and isinstance(device_xml, six.binary_type): device_xml = device_xml.decode('utf-8') LOG.debug("attach device xml: %s", device_xml) self._domain.attachDeviceFlags(device_xml, flags=flags)
这一段代码的主要工作就是将conf转换成libvirt挂载卷需要的xml格式的文件,然后由libvirt提供的功能来卷挂载到虚拟机实例上去。