OpenStack 虚拟机挂载数据卷过程分析
如何把块设备挂载到虚拟机
如何把一个块设备提供给虚拟机使用,qemu-kvm
只需要通过--drive
参数指定即可。如果使用libvirt,以CLI virsh
为例,可以通过attach-device
子命令挂载设备给虚拟机使用,该命令包含两个必要参数,一个是domain
,即虚拟机id,另一个是xml文件,文件包含设备的地址信息。
1 |
|
iSCSI设备需要先把lun设备映射到宿主机本地,然后当做本地设备挂载即可。一个简单的demo xml为:
1 |
|
可见source
就是lun设备映射到本地的路径。
值得一提的是,libvirt支持直接挂载rbd image
(宿主机需要包含rbd内核模块),通过rbd协议访问image,而不需要先map
到宿主机本地,一个demo xml文件为:
1 |
|
所以我们Cinder如果使用LVM driver,则需要先把LV加到iSCSI target中,然后映射到计算节点的宿主机,而如果使用rbd driver,不需要映射到计算节点,直接挂载即可。
以上介绍了存储的一些基础知识,有了这些知识,再去理解OpenStack nova和cinder就非常简单了,接下来我们开始进入我们的正式主题,分析OpenStack虚拟机挂载数据卷的流程。
nova 侧代码
VolumeAttachmentController
接口一览
1
2
3
4
5
6
7
8('/servers/{server_id}/os-volume_attachments', {
'GET': [server_volume_attachments_controller, 'index'], # list虚拟机挂载的卷
'POST': [server_volume_attachments_controller, 'create'], # attach卷
}),
('/servers/{server_id}/os-volume_attachments/{id}', {
'GET': [server_volume_attachments_controller, 'show'], # show虚拟机挂载的卷
'DELETE': [server_volume_attachments_controller, 'delete'] # detach卷
})index 方法查询虚拟机所有挂载的卷,nova volume-attachments命令调用的是这个接口。
实现是查询nova数据库block_device_mapping表,这个表是nova中非常重要的一个表,很多接口的实现都和它密切相关。
index方法并没有将bdms的所有字段返回,而是_translate_attachment_detail_view处理后,仅仅返回了几个字段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def index(self, req, server_id):
"""Returns the list of volume attachments for a given instance."""
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
limited_list = common.limited(bdms, req)
results = []
for bdm in limited_list:
if bdm.volume_id:
va = _translate_attachment_detail_view(
bdm, show_tag=show_tag,
show_delete_on_termination=show_delete_on_termination)
results.append(va)
return {'volumeAttachments': results}show 方法的实现和index的基本一致,查询block_device_mapping表,然后_translate_attachment_detail_view处理后返回。
1
2
3
4
5
6
7
8
9def show(self, req, server_id, id):
"""Return data about the given volume attachment."""
bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
context, volume_id, instance.uuid)
return {'volumeAttachment': _translate_attachment_detail_view(
bdm, show_tag=show_tag,
show_delete_on_termination=show_delete_on_termination)}block_device_mapping 表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30mysql> desc block_device_mapping;
+-----------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------+--------------+------+-----+---------+----------------+
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| deleted_at | datetime | YES | | NULL | |
| id | int(11) | NO | PRI | NULL | auto_increment |
| device_name | varchar(255) | YES | | NULL | |
| delete_on_termination | tinyint(1) | YES | | NULL | |
| snapshot_id | varchar(36) | YES | MUL | NULL | |
| volume_id | varchar(36) | YES | MUL | NULL | |
| volume_size | int(11) | YES | | NULL | |
| no_device | tinyint(1) | YES | | NULL | |
| connection_info | mediumtext | YES | | NULL | |
| instance_uuid | varchar(36) | YES | MUL | NULL | |
| deleted | int(11) | YES | | NULL | |
| source_type | varchar(255) | YES | | NULL | |
| destination_type | varchar(255) | YES | | NULL | |
| guest_format | varchar(255) | YES | | NULL | |
| device_type | varchar(255) | YES | | NULL | |
| disk_bus | varchar(255) | YES | | NULL | |
| boot_index | int(11) | YES | | NULL | |
| image_id | varchar(36) | YES | | NULL | |
| tag | varchar(255) | YES | | NULL | |
| attachment_id | varchar(36) | YES | | NULL | |
| uuid | varchar(36) | YES | UNI | NULL | |
| backup_id | varchar(36) | YES | | NULL | |
| volume_type | varchar(255) | YES | | NULL | |
+-----------------------+--------------+------+-----+---------+----------------+create方法attach卷
delete方法detach卷
nova-api 挂载盘流程
- _check_volume_already_attached_to_instance() : 检查卷是否已经挂载到虚拟机,其实现也是查询block_device_mapping表,如果查询不到,说明卷没有挂载到虚拟机。
1
2
3
4
5
6
7
8try:
objects.BlockDeviceMapping.get_by_volume_and_instance(
context, volume_id, instance.uuid)
msg = _("volume %s already attached") % volume_id
raise exception.InvalidVolume(reason=msg)
except exception.VolumeBDMNotFound:
pass - _create_volume_bdm(): 即在
block_device_mapping
表中创建对应的记录,由于API节点无法拿到目标虚拟机挂载后的设备名,比如/dev/vdb
,只有计算节点才知道自己虚拟机映射到哪个设备。因此bdm不是在API节点创建的,而是通过RPC请求到虚拟机所在的计算节点创建,请求方法为reserve_block_device_name
,该方法首先调用libvirt分配一个设备名,比如/dev/vdb
,然后创建对应的bdm实例。
1 |
|
- _check_attach_and_reserve_volume(): 调用 cinder api 创建 attachment_create 接口,然后把返回的 attachment_id 更新block_device_mapping表中。
1 |
|
- self.compute_rpcapi.attach_volume() : rpc 计算节点的 attach_volume()方法。
nova-compute 挂载盘流程
attach_volume(): 位于nova/compute/manager.py,先是调用
driver_bdm = driver_block_device.convert_volume(bdm)
将 bdm(dict) 对象转化成DriverVolumeBlockDevice
对象, 然后调用对象的attach方法,方法位于 nova/virt/block_device.py。get_volume_connector():该方法首先调用的是
virt_driver.get_volume_connector(instance)
,其中virt_driver
这里就是libvirt
,该方法位于nova/virt/libvirt/driver.py
,其实就是调用os-brick的get_connector_properties
:
1 |
|
os-brick是从Cinder项目分离出来的,专门用于管理各种存储系统卷的库,代码仓库为os-brick。其中get_connector_properties
方法位于os_brick/initiator/connector.py
:
1 |
|
该方法最重要的工作就是返回该计算节点的信息(如ip、操作系统类型等)以及initiator name(参考第2节内容)。
- volume_api.initialize_connection(): 调用Cinder API的
initialize_connection
方法,返回 connection_info:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24def _legacy_volume_attach(self, context, volume, connector, instance,
volume_api, virt_driver,
do_driver_attach=False):
connection_info = volume_api.initialize_connection(context,
volume_id,
connector)
'''connection_info数结构如下所示
{
'driver_volume_type': 'iscsi',
'data': {
'auth_password': 'YZ2Hceyh7VySh5HY',
'target_discovered': False,
'encrypted': False,
'qos_specs': None,
'target_iqn': 'iqn.2010-10.org.openstack:volume-8b1ec3fe-8c57-45ca-a1cf-a481bfc8fce2',
'target_portal': '11.0.0.8:3260',
'volume_id': '8b1ec3fe-8c57-45ca-a1cf-a481bfc8fce2',
'target_lun': 1,
'access_mode': 'rw',
'auth_username': 'nE9PY8juynmmZ95F7Xb7',
'auth_method': 'CHAP'
}
}'''
virt_driver.attach_volume()
:此时到达libvirt层,代码位于nova/virt/libvirt/driver.py
,该方法分为如下几个步骤:_connect_volume()
该方法会调用nova/virt/libvirt/volume/iscsi.py
的connect_volume()
方法,该方法其实是直接调用os-brick的connect_volume()
方法,该方法位于os_brick/initiator/connector.py
中ISCSIConnector
类中的connect_volume
方法,该方法会调用前面介绍的iscsiadm
命令的discovory
以及login
子命令,即把lun设备映射到本地设备。
_get_volume_config()
获取volume
的信息,其实也就是我们生成xml需要的信息,最重要的就是拿到映射后的本地设备的路径,如/dev/disk/by-path/ip-10.0.0.2:3260-iscsi-iqn.2010-10.org.openstack:volume-060fe764-c17b-45da-af6d-868c1f5e19df-lun-0
,返回的conf最终会转化成xml格式。该代码位于nova/virt/libvirt/volume/iscsi.py
:
1 |
|
终于到了最后一步,该步骤其实就类似于调用virsh attach-device
命令把设备挂载到虚拟机中,该代码位于nova/virt/libvirt/guest.py
:
1 |
|
libvirt的工作完成,此时volume已经挂载到虚拟机中了。
volume_api.attach()
回到nova/virt/block_device.py
,最后调用了volume_api.attach()
方法,该方法向Cinder发起API请求。此时cinder-api通过RPC请求到cinder-volume
,代码位于cinder/volume/manager.py
,该方法没有做什么工作,其实就是更新数据库,把volume状态改为in-use
。
cinder 侧代码
在nova中,volume_api.initialize_connection()
调用Cinder API的initialize_connection方法。该方法又会RPC请求给volume所在的cinder-volume
服务节点。代码位置为cinder/volume/manager.py
,该方法也是分阶段的。
- driver.validate_connector()
该方法不同的driver不一样,对于LVM + iSCSI来说,就是检查有没有initiator
字段,即nova-compute节点的initiator信息
1 |
|
driver.create_export()
: 该方法位于cinder/volume/targets/iscsi.py
:
1 |
|
该方法最重要的操作是调用了create_iscsi_target方法, create_iscsi_target方法通过写入配置文件的方法创建target。
create_iscsi_target(): 该方法位于
cinder/volume/targets/tgt.py
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class TgtAdm(iscsi.ISCSITarget):
# target 配置文件模板
VOLUME_CONF = textwrap.dedent("""
<target %(name)s>
backing-store %(path)s
driver %(driver)s
%(chap_auth)s
%(target_flags)s
write-cache %(write_cache)s
</target>
""")
def create_iscsi_target(self, name, tid, lun, path,
chap_auth=None, **kwargs):
# 渲染配置文件模板
volume_conf = self.VOLUME_CONF % {
'name': name, 'path': path, 'driver': driver,
'chap_auth': chap_str, 'target_flags': target_flags,
'write_cache': write_cache}
LOG.debug('Creating iscsi_target for Volume ID: %s', vol_id)
volumes_dir = self.volumes_dir
volume_path = os.path.join(volumes_dir, vol_id)
# 写入配置文件
utils.robust_file_write(volumes_dir, vol_id, volume_conf)driver.initialize_connection()
:返回connection_info,该方法位于cinder/volume/drivers/iscsi.py
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31def initialize_connection(self, volume, connector):
"""Initializes the connection and returns connection info.
The iscsi driver returns a driver_volume_type of 'iscsi'.
The format of the driver data is defined in _get_iscsi_properties.
Example return value::
{
'driver_volume_type': 'iscsi',
'data': {
'auth_password': 'YZ2Hceyh7VySh5HY',
'target_discovered': False,
'encrypted': False,
'qos_specs': None,
'target_iqn': 'iqn.2010-10.org.openstack:volume-8b1ec3fe-8c57-45ca-a1cf-a481bfc8fce2',
'target_portal': '11.0.0.8:3260',
'volume_id': '8b1ec3fe-8c57-45ca-a1cf-a481bfc8fce2',
'target_lun': 1,
'access_mode': 'rw',
'auth_username': 'nE9PY8juynmmZ95F7Xb7',
'auth_method': 'CHAP'
}
}
"""
iscsi_properties = self._get_iscsi_properties(volume,
connector.get(
'multipath'))
return {
'driver_volume_type': self.iscsi_protocol,
'data': iscsi_properties
}