OpenStack是一个IaaS开源项目,实现公有云和私有云的部署及管理,目前已经成为了最流行的一种开源云解决方案。其中提供计算服务组件Nova、网络服务组件Neutron以及块存储服务组件Cinder是OpenStack的最为核心的组件。这里我们重点关注Nova和Cinder组件,Neutron组件将在下一篇文章中详细介绍。
Nova组件为OpenStack提供计算服务(Compute as Service),类似AWS的EC2服务。Nova管理的主要对象为云主机(server),用户可通过Nova API申请云主机(server)资源。云主机通常对应一个虚拟机,但不是绝对,也有可能是一个容器(docker driver)或者裸机(对接ironic driver)。
Nova创建一台云主机的三个必要参数为:
extra specs
,可以实现设置io限速、cpu拓扑等功能。创建一台云主机的CLI为:
nova boot --image ${IMAGE_ID} --flavor m1.small --nic net-id=${NETWORK_ID} int32bit-test-1
使用nova list
可以查看租户的所有云主机列表。
Cinder组件为OpenStack提供块存储服务(Block Storage as Service),类似AWS的EBS服务。Cinder管理的主要对象为数据卷(volume),用户通过Cinder API可以对volume执行创建、删除、扩容、快照、备份等操作。
创建一个volume有两个必要参数:
创建一个20G的volume:
cinder create --volume-type ssd --name int32bit-test-volume 20
Cinder目前最典型的应用场景就是为Nova云主机提供云硬盘功能,用户可以把一个volume卷挂载到Nova的云主机中,当作云主机的一个虚拟块设备使用。
挂载volume是在Nova端完成的:
nova volume-attach ${server_id} ${volume_id}
Cinder除了能够为Nova云主机提供云硬盘功能,还能为裸机、容器等提供数据卷功能。john griffith写了一篇博客介绍如何使用Cinder为Docker提供volume功能:Cinder providing block storage for more than just Nova。
本文接下来将重点介绍OpenStack如何将volume挂载到虚拟机中,分析Nova和Cinder之间的交互过程。
iSCSI是一种通过TCP/IP共享块设备的协议,通过该协议,一台服务器能够把本地的块设备共享给其它服务器。换句话说,这种协议实现了通过internet向设备发送SCSI指令。
iSCSI server端称为Target
,client端称为Initiator
,一台服务器可以同时运行多个Target,一个Target可以认为是一个物理存储池,它可以包含多个backstores
,backstore就是实际要共享出去的设备,实际应用主要有两种类型:
/dev/sda
,也可以是一个LVM卷。除了以上两类,还有pscsi、ramdisk等。
backstore需要添加到指定的target中,target会把这些物理设备映射成逻辑设备,并分配一个id,称为LUN(逻辑单元号)。
为了更好的理解iSCSI,我们下节将一步步手动实践下如何使用iSCSI。
首先我们准备一台iscsi server服务器作为target,这里以CentOS 7为例,安装并启动iscsi服务:
yum install targetcli -y
systemctl enable target
systemctl start target
运行targetcli
检查是否安装成功:
int32bit $ targetcli
targetcli shell version 2.1.fb41
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
/> ls
o- / .................................... [...]
o- backstores ......................... [...]
| o- block ............. [Storage Objects: 0]
| o- fileio ............ [Storage Objects: 0]
| o- pscsi ............. [Storage Objects: 0]
| o- ramdisk ........... [Storage Objects: 0]
o- iscsi ....................... [Targets: 0]
o- loopback .................... [Targets: 0]
如果正常的话会进入targetcli shell,在根目录下运行ls
命令可以查看所有的backstores和iscsi target。
具体的targetcli命令可以查看官方文档,这里需要说明的是,targetcli shell是有context session(上下文),简单理解就是类似Linux的文件系统目录,你处于哪个目录位置,对应不同的功能,比如你在/backstores
目录则可以对backstores进行管理,你在/iscsi
目录,则可以管理所有的iscsi target。你可以使用pwd
查看当前工作目录,cd
切换工作目录,help
查看当前工作环境的帮助信息,ls
查看子目录结构等,你可以使用tab
键补全命令,和我们Linux shell操作非常相似,因此使用起来还是比较顺手的。
为了简单起见,我们创建一个fileio类型的backstore,首先我们cd
到/backstores/fileio
目录:
/> cd /backstores/fileio
/backstores/fileio> create test_fileio /tmp/test_fileio.raw 2G write_back=false Created fileio test_fileio with size 2147483648
我们创建了一个名为test_fileio
的fileio类型backstore,文件路径为/tmp/test_fileio.raw
,大小为2G,如果文件不存在会自动创建。
创建了backstore后,我们创建一个target,cd
到/iscsi
目录:
/iscsi> create iqn.2017-09.me.int32bit:int32bit
Created target iqn.2017-09.me.int32bit:int32bit.
Created TPG 1.
Default portal not created, TPGs within a target cannot share ip:port.
/iscsi>
以上我们创建了一个名为int32bit
的target,前面的iqn.2017-09.me.int32bit
是iSCSI Qualified Name (IQN),具体含义参考wikipedia-ISCSI,这里简单理解为一个独一无二的namespace就好。使用ls
命令我们发现创建一个目录iqn.2017-09.me.int32bit:int32bit
(注意:实际上并不是目录,我们暂且这么理解)。
创建完target后,我们还需要把这个target export出去,即进入监听状态,我们称为portal,创建portal也很简单:
/iscsi> cd iqn.2017-09.me.int32bit:int32bit/tpg1/portals/
/iscsi/iqn.20.../tpg1/portals> create 10.0.0.4
Using default IP port 3260
Created network portal 10.0.0.4:3260.
以上10.0.0.4
是server的ip,不指定端口的话就会使用默认的端口3260。
target创建完毕,此时我们可以把我们之前创建的backstore加到这个target中:
/iscsi/iqn.20.../tpg1/portals> cd ../luns
/iscsi/iqn.20...bit/tpg1/luns> create /backstores/fileio/test_fileio
Created LUN 0.
此时我们的target包含有一个lun设备了:
/iscsi/iqn.20...bit/tpg1/luns> ls /iscsi/iqn.2017-09.me.int32bit:int32bit/
o- iqn.2017-09.me.int32bit:int32bit ...................................................................................... [TPGs: 1]
o- tpg1 ................................................................................................... [no-gen-acls, no-auth]
o- acls .............................................................................................................. [ACLs: 0]
o- luns .............................................................................................................. [LUNs: 1]
| o- lun0 .......................................................................... [fileio/test_fileio (/tmp/test_fileio.raw)]
o- portals ........................................................................................................ [Portals: 0]
接下来我们配置client端,即iSCSI Initiator:
yum install iscsi-initiator-utils -y
systemctl enable iscsid iscsi
systemctl start iscsid iscsi
拿到本机的initiator name:
int32bit $ cat /etc/iscsi/initiatorname.iscsi
InitiatorName=iqn.1994-05.com.redhat:e0db637c5ce
client需要连接server target,还需要ACL认证,我们在server端增加client的访问权限,在server端运行:
int32bit $ targetcli
targetcli shell version 2.1.fb41
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
/> cd /iscsi/iqn.2017-09.me.int32bit:int32bit/tpg1/acls
/iscsi/iqn.20...bit/tpg1/acls> create iqn.1994-05.com.redhat:e0db637c5ce
Created Node ACL for iqn.1994-05.com.redhat:e0db637c5ce
Created mapped LUN 0.
注意:以上我们没有设置账户和密码,client直接就能登录。
一切准备就绪,接下来让我们在client端连接我们的target吧。
首先我们使用iscsiadm
命令自动发现本地可见的target列表:
int32bit $ iscsiadm --mode discovery --type sendtargets --portal 10.0.0.4 | grep int32bit
10.0.0.4:3260,1 iqn.2017-09.me.int32bit:int32bit
发现target后,我们登录验证后才能使用:
int32bit $ iscsiadm -m node -T iqn.2017-09.me.int32bit:int32bit -l
Logging in to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] (multiple)
Login to [iface: default, target: iqn.2017-09.me.int32bit:int32bit, portal: 10.0.0.4,3260] successful.
我们可以查看所有已经登录的target:
int32bit $ iscsiadm -m session
tcp: [173] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-1e062767-f0bc-40fb-9a03-7b0df61b5671 (non-flash)
tcp: [198] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-060fe764-c17b-45da-af6d-868c1f5e19df (non-flash)
tcp: [199] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-757f6281-8c71-430e-9f7c-5df2e3008b46 (non-flash)
tcp: [203] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (non-flash)
tcp: [205] 10.0.0.4:3260,1 iqn.2017-09.me.int32bit:int32bit (non-flash)
此时target已经自动映射到本地块设备,我们可以使用lsblk
查看:
int32bit $ lsblk --scsi
NAME HCTL TYPE VENDOR MODEL REV TRAN
sda 0:0:2:0 disk ATA INTEL SSDSC2BX40 DL2B
sdb 0:0:3:0 disk ATA INTEL SSDSC2BX40 DL2B
sdc 0:0:4:0 disk ATA INTEL SSDSC2BX40 DL2B
sdd 0:0:5:0 disk ATA INTEL SSDSC2BX40 DL2B
sde 0:0:6:0 disk ATA INTEL SSDSC2BX40 DL2B
sdf 0:0:7:0 disk ATA INTEL SSDSC2BX40 DL2B
sdg 0:2:0:0 disk DELL PERC H330 Mini 4.26
sdh 183:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdi 208:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdj 209:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdk 213:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdm 215:0:0:0 disk LIO-ORG test_fileio 4.0 iscsi
可见映射本地设备为/dev/shm
。接下来就可以当作本地硬盘一样使用了。
以上我们是通过target服务器的一个本地文件以块形式共享的,通常这只是用来测试,生产环境下一般都通过商业存储提供真实的块设备来共享。OpenStack Cinder如果使用的LVM driver,则是通过LVM卷共享的,这其实不难实现,只需要把LVM对应LV PATH加到block backstore即可,本文后面会重点介绍这种情况。
前面我们使用的targetcli
是Datera公司开发的,不仅提供了这个CLI工具,Datera还提供了一个Python库-rtslib,该项目地址为rtslib。可能由于某些原因,社区fork自rtslib项目,并单独维护了一个分支,命名为“free branch”,即rtslib-fb项目,目前这两个分支可能不兼容,因此确保targetcli、rtslib以及configshell是在同一个版本分支,要么全是fb,要么全是non-fb。
OpenStack社区基于rtstool封装了一个CLI工具,即我们要介绍的cinder-rtstool工具。该工具使用起来非常简单,我们查看它的help
信息:
$ cinder-rtstool --help
Usage:
cinder-rtstool create [device] [name] [userid] [password] [iser_enabled] [-a] [-pPORT]
cinder-rtstool add-initiator [target_iqn] [userid] [password] [initiator_iqn]
cinder-rtstool delete-initiator [target_iqn] [initiator_iqn]
cinder-rtstool get-targets
cinder-rtstool delete [iqn]
cinder-rtstool verify
cinder-rtstool save [path_to_file]
该工具主要运行在target端,即cinder-volume所在节点,其中create
命令用于快速创建一个target
,并把设备加到该target
中,当然也包括创建对应的portal
。add-initiator
对应就是创建acls
,get-targets
列出当前服务器的创建的所有target
。其它命令不过多介绍,基本都能大概猜出什么功能。
Ceph是开源分布式存储系统,具有高扩展性、高性能、高可靠性等优点,同时提供块存储服务(rbd)、对象存储服务(rgw)以及文件系统存储服务(cephfs)。目前也是OpenStack的主流后端存储,为OpenStack提供统一共享存储服务。使用ceph作为OpenStack后端存储,至少包含以下几个优点:
ceph的更多知识可以参考官方文档,这里我们仅仅简单介绍下rbd。
前面我们介绍的iSCSI有个target
的概念,存储设备必须加到指定的target
中,映射为lun。rbd中也有一个pool
的概念,rbd创建的虚拟块设备实例我们称为image
,所有的image
必须包含在一个pool
中。这里我们暂且不讨论pool
的作用,简单理解是一个namespace
即可。
我们可以通过rbd
命令创建一个rbd image
:
$ rbd -p test2 create --size 1024 int32bit-test-rbd --new-format
$ rbd -p test2 ls int32bit-test-rbd
centos7.raw
$ rbd -p test2 info int32bit-test-rbd
rbd image 'int32bit-test-rbd':
size 1024 MB in 256 objects
order 22 (4096 kB objects)
block_name_prefix: rbd_data.9beee82ae8944a
format: 2
features: layering
flags:
以上我们通过create
子命令创建了一个name为int32bit-test-rbd
,大小为1G的 image
,其中-p
的参数值test2
就是pool
名称。通过ls
命令可以查看所有的image
列表,info
命令查看image
的详细信息。
iSCSI创建lun设备后,Initiator端通过login
把设备映射到本地。rbd image
则是通过map
操作映射到本地的,在client端安装ceph client包并配置好证书后,只需要通过rbd map
即可映射到本地中:
$ rbd -p test2 map int32bit-test-rbd
/dev/rbd0
此时我们把创建的image
映射到了/dev/rbd0
中,作为本地的一个块设备,现在可以对该设备像本地磁盘一样使用。
如何把一个块设备提供给虚拟机使用,qemu-kvm
只需要通过--drive
参数指定即可。如果使用libvirt,以CLI virsh
为例,可以通过attach-device
子命令挂载设备给虚拟机使用,该命令包含两个必要参数,一个是domain
,即虚拟机id,另一个是xml文件,文件包含设备的地址信息。
$ virsh help attach-device
NAME
attach-device - attach device from an XML file
SYNOPSIS
attach-device [--persistent] [--config] [--live] [--current]
DESCRIPTION
Attach device from an XML .
OPTIONS
[--domain] domain name, id or uuid
[--file] XML file
--persistent make live change persistent
--config affect next boot
--live affect running domain
--current affect current domain
iSCSI设备需要先把lun设备映射到宿主机本地,然后当做本地设备挂载即可。一个简单的demo xml为:
2ed1b04c-b34f-437d-9aa3-3feeb683d063
可见source
就是lun设备映射到本地的路径。
值得一提的是,libvirt支持直接挂载rbd image
(宿主机需要包含rbd内核模块),通过rbd协议访问image,而不需要先map
到宿主机本地,一个demo xml文件为:
所以我们Cinder如果使用LVM driver,则需要先把LV加到iSCSI target中,然后映射到计算节点的宿主机,而如果使用rbd driver,不需要映射到计算节点,直接挂载即可。
以上介绍了存储的一些基础知识,有了这些知识,再去理解OpenStack nova和cinder就非常简单了,接下来我们开始进入我们的正式主题,分析OpenStack虚拟机挂载数据卷的流程。
这里我们先以Ciner使用LVM driver为例,iSCSI驱动使用lioadm
,backend配置如下:
[lvm]
iscsi_helper=lioadm
volume_driver=cinder.volume.drivers.lvm.LVMVolumeDriver
volume_backend_name=lvm
volume_group = cinder-volumes
OpenStack源码阅读方法可以参考如何阅读OpenStack源码,这里不过多介绍。这里需要说明的是,Nova中有一个数据库表专门用户存储数据卷和虚拟机的映射关系的,这个表名为block_device_mapping
,其字段如下:
MariaDB [nova]> 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 | |
+-----------------------+--------------+------+-----+---------+----------------+
Cinder中也有一个单独的表volume_attachment
用来记录挂载情况:
MariaDB [cinder]> desc volume_attachment;
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| deleted_at | datetime | YES | | NULL | |
| deleted | tinyint(1) | YES | | NULL | |
| id | varchar(36) | NO | PRI | NULL | |
| volume_id | varchar(36) | NO | MUL | NULL | |
| attached_host | varchar(255) | YES | | NULL | |
| instance_uuid | varchar(36) | YES | | NULL | |
| mountpoint | varchar(255) | YES | | NULL | |
| attach_time | datetime | YES | | NULL | |
| detach_time | datetime | YES | | NULL | |
| attach_mode | varchar(36) | YES | | NULL | |
| attach_status | varchar(255) | YES | | NULL | |
+---------------+--------------+------+-----+---------+-------+
13 rows in set (0.00 sec)
接下来我们从nova-api开始一步步跟踪其过程。
nova-api挂载volume入口为nova/api/openstack/compute/volumes.py
,controller为VolumeAttachmentController
,create
就是虚拟机挂载volume的方法。
该方法首先检查该volume是不是已经挂载到这个虚拟机了:
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
for bdm in bdms:
if bdm.volume_id == volume_id:
_msg = _("Volume %(volume_id)s have been attaced to "
"instance %(server_id)s.") % {
'volume_id': volume_id,
'server_id': server_id}
raise exc.HTTPConflict(explanation=_msg)
然后调用nova/compute/api.py
的attach_volume
方法,该方法的工作内容为:
(1) create_volume_bdm()
即在block_device_mapping
表中创建对应的记录,由于API节点无法拿到目标虚拟机挂载后的设备名,比如/dev/vdb
,只有计算节点才知道自己虚拟机映射到哪个设备。因此bdm不是在API节点创建的,而是通过RPC请求到虚拟机所在的计算节点创建,请求方法为reserve_block_device_name
,该方法首先调用libvirt分配一个设备名,比如/dev/vdb
,然后创建对应的bdm实例。
(2) check_attach_and_reserve_volume()
这里包含check_attach
和reserve_volume
两个过程,check_attach
就是检查这个volume能不能挂载,比如status必须为avaliable
,或者支持多挂载情况下状态为in-use
或者avaliable
。该方法位置为nova/volume/cinder.py
的check_attach
方法。而reserve_volume
是由Cinder完成的,nova-api会调用cinder API。该方法其实不做什么工作,仅仅是把volume的status置为attaching
。该方法流程:nova-api -> cinder-api -> reserver_volume
,该方法位于cinder/volume/api.py
:
@wrap_check_policy
def reserve_volume(self, context, volume):
expected = {'multiattach': volume.multiattach,
'status': (('available', 'in-use') if volume.multiattach
else 'available')}
result = volume.conditional_update({'status': 'attaching'}, expected)
if not result:
expected_status = utils.build_or_str(expected['status'])
msg = _('Volume status must be %s to reserve.') % expected_status
LOG.error(msg)
raise exception.InvalidVolume(reason=msg)
LOG.info(_LI("Reserve volume completed successfully."),
resource=volume)
(3) RPC计算节点的attach_volume()
此时nova-api会向目标计算节点发起RPC请求,由于rpcapi.py
的attach_volume
方法调用的是cast
方法,因此该RPC是异步调用。由此,nova-api的工作结束,剩下的工作由虚拟机所在的计算节点完成。
nova-compute接收到RPC请求,callback函数入口为nova/compute/manager.py
的attach_volume
方法,该方法会根据之前创建的bdm实例参数转化为driver_block_device
,然后调用该类的attach
方法,这就已经到了具体的硬件层,它会根据volume的类型实例化不同的具体类,这里我们的类型是volume,因此对应为DriverVolumeBlockDevice
,位于nova/virt/block_device.py
。
我们看其attach
方法,该方法是虚拟机挂载卷的最重要方法,也是实现的核心。该方法分好几个阶段,我们一个一个阶段看。
(1) get_volume_connector()
该方法首先调用的是virt_driver.get_volume_connector(instance)
,其中virt_driver
这里就是libvirt
,该方法位于nova/virt/libvirt/driver.py
,其实就是调用os-brick的get_connector_properties
:
def get_volume_connector(self, instance):
root_helper = utils.get_root_helper()
return connector.get_connector_properties(
root_helper, CONF.my_block_storage_ip,
CONF.libvirt.iscsi_use_multipath,
enforce_multipath=True,
host=CONF.host)
os-brick是从Cinder项目分离出来的,专门用于管理各种存储系统卷的库,代码仓库为os-brick。其中get_connector_properties
方法位于os_brick/initiator/connector.py
:
def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath,
host=None):
iscsi = ISCSIConnector(root_helper=root_helper)
fc = linuxfc.LinuxFibreChannel(root_helper=root_helper)
props = {}
props['ip'] = my_ip
props['host'] = host if host else socket.gethostname()
initiator = iscsi.get_initiator()
if initiator:
props['initiator'] = initiator
wwpns = fc.get_fc_wwpns()
if wwpns:
props['wwpns'] = wwpns
wwnns = fc.get_fc_wwnns()
if wwnns:
props['wwnns'] = wwnns
props['multipath'] = (multipath and
_check_multipathd_running(root_helper,
enforce_multipath))
props['platform'] = platform.machine()
props['os_type'] = sys.platform
return props
该方法最重要的工作就是返回该计算节点的信息(如ip、操作系统类型等)以及initiator name
(参考第2节内容)。
(2) volume_api.initialize_connection()
终于轮到Cinder真正干点活了!该方法会调用Cinder API的initialize_connection
方法,该方法又会RPC请求给volume所在的cinder-volume
服务节点。我们略去cinder-api
,直接到cinder-volume
。
代码位置为cinder/volume/manager.py
,该方法也是分阶段的。
(1) driver.validate_connector()
该方法不同的driver不一样,对于LVM + iSCSI来说,就是检查有没有initiator
字段,即nova-compute节点的initiator信息,代码位于cinder/volume/targets/iscsi.py
:
def validate_connector(self, connector):
# NOTE(jdg): api passes in connector which is initiator info
if 'initiator' not in connector:
err_msg = (_LE('The volume driver requires the iSCSI initiator '
'name in the connector.'))
LOG.error(err_msg)
raise exception.InvalidConnectorException(missing='initiator')
return True
注意以上代码跳转过程:drivers/lvm.py -> targets/lio.py
-> targets/iscsi.py
。即我们的lvm driver会调用target
相应的方法,这里我们用的是lio
,因此调到lio.py
,而lio
又继承自iscsi
,因此跳到iscsi.py
。下面分析将省去这些细节直接跳转。
(2) driver.create_export()
该方法位于cinder/volume/targets/iscsi.py
:
def create_export(self, context, volume, volume_path):
# 'iscsi_name': 'iqn.2010-10.org.openstack:volume-00000001'
iscsi_name = "%s%s" % (self.configuration.iscsi_target_prefix,
volume['name'])
iscsi_target, lun = self._get_target_and_lun(context, volume)
chap_auth = self._get_target_chap_auth(context, iscsi_name)
if not chap_auth:
chap_auth = (vutils.generate_username(),
vutils.generate_password())
# Get portals ips and port
portals_config = self._get_portals_config()
tid = self.create_iscsi_target(iscsi_name,
iscsi_target,
lun,
volume_path,
chap_auth,
**portals_config)
data = {}
data['location'] = self._iscsi_location(
self.configuration.iscsi_ip_address, tid, iscsi_name, lun,
self.configuration.iscsi_secondary_ip_addresses)
LOG.debug('Set provider_location to: %s', data['location'])
data['auth'] = self._iscsi_authentication(
'CHAP', *chap_auth)
return data
该方法最重要的操作是调用了create_iscsi_target
方法,该方法其实就是调用了cinder-rtstool
的create
方法:
command_args = ['cinder-rtstool',
'create',
path,
name,
chap_auth_userid,
chap_auth_password,
self.iscsi_protocol == 'iser'] + optional_args
self._execute(*command_args, run_as_root=True)
即create_export
方法的主要工作就是调用cinder-rtstool
工具创建target,并把设备添加到target中。
在cinder-volume节点可以通过targetcli
查看所有export
的target:
/iscsi> ls /iscsi/ 1
o- iscsi .............................................................................................................. [Targets: 5]
o- iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 ............................................... [TPGs: 1]
o- iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0 ............................................... [TPGs: 1]
o- iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40 ............................................... [TPGs: 1]
o- iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082 ............................................... [TPGs: 1]
(3) driver.initialize_connection()
这是最后一步。该方法位于cinder/volume/targets/lio.py
:
def initialize_connection(self, volume, connector):
volume_iqn = volume['provider_location'].split(' ')[1]
(auth_method, auth_user, auth_pass) = \
volume['provider_auth'].split(' ', 3)
# Add initiator iqns to target ACL
try:
self._execute('cinder-rtstool', 'add-initiator',
volume_iqn,
auth_user,
auth_pass,
connector['initiator'],
run_as_root=True)
except putils.ProcessExecutionError:
LOG.exception(_LE("Failed to add initiator iqn %s to target"),
connector['initiator'])
raise exception.ISCSITargetAttachFailed(
volume_id=volume['id'])
self._persist_configuration(volume['id'])
return super(LioAdm, self).initialize_connection(volume, connector)
该方法的重要工作就是调用cinder-rtstool
的add-initiator
子命令,即把计算节点的initiator增加到刚刚创建的target acls中。
targetcli
输出结果如下:
/iscsi> ls /iscsi/iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063/tpg1/acls/
o- acls .................................................................................................................. [ACLs: 1]
o- iqn.1994-05.com.redhat:e0db637c5ce ............................................................... [1-way auth, Mapped LUNs: 1]
o- mapped_lun0 ......................... [lun0 block/iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (rw)]
因此Cinder的主要工作就是创建volume的iSCSI target以及acls。cinder-volume工作结束,我们返回到nova-compute。
回到nova-compute的第(2)步,调用volume_api.initialize_connection()
后,执行第(3)步。
(3) virt_driver.attach_volume()
此时到达libvirt层,代码位于nova/virt/libvirt/driver.py
,该方法分为如下几个步骤。
1. _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设备映射到本地设备。
可以使用iscsiadm
查看已经connect(login)的所有volume:
$ iscsiadm -m session
tcp: [203] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063 (non-flash)
tcp: [206] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40 (non-flash)
tcp: [207] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0 (non-flash)
tcp: [208] 10.0.0.4:3260,1 iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082 (non-flash)
使用lsblk
查看映射路径:
$ lsblk --scsi
NAME HCTL TYPE VENDOR MODEL REV TRAN
... # 略去部分输出
sdh 216:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdi 217:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdj 218:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
sdk 213:0:0:0 disk LIO-ORG IBLOCK 4.0 iscsi
也可以在Linux的/dev/disk
中by-path
找到:
$ ls -l /dev/disk/by-path/
total 0
lrwxrwxrwx 1 root root 9 Sep 6 17:21 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-2ed1b04c-b34f-437d-9aa3-3feeb683d063-lun-0 -> ../../sdk
lrwxrwxrwx 1 root root 9 Sep 8 17:34 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-70347e2a-cdfc-4575-a891-3973ec264ec0-lun-0 -> ../../sdi
lrwxrwxrwx 1 root root 9 Sep 8 17:29 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-980eaf85-9d63-4e1e-9e47-75f1a14ecc40-lun-0 -> ../../sdh
lrwxrwxrwx 1 root root 9 Sep 8 17:35 ip-10.0.0.4:3260-iscsi-iqn.2010-10.org.openstack:volume-db6aa94d-64cc-4996-805e-f768346d8082-lun-0 -> ../../sdj
2. _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
:
def get_config(self, connection_info, disk_info):
"""Returns xml for libvirt."""
conf = super(LibvirtISCSIVolumeDriver,
self).get_config(connection_info, disk_info)
conf.source_type = "block"
conf.source_path = connection_info['data']['device_path']
conf.driver_io = "native"
return conf
3. guest.attach_device()
终于到了最后一步,该步骤其实就类似于调用virsh attach-device
命令把设备挂载到虚拟机中,该代码位于nova/virt/libvirt/guest.py
:
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
self._domain.attachDeviceFlags(conf.to_xml(), flags=flags)
libvirt的工作完成,此时volume已经挂载到虚拟机中了。
(4) 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
,并创建对应的attach
记录。
到此,OpenStack整个挂载流程终于结束了,我们是从Nova的视角分析,如果从Cinder的视角分析,其实Cinder的工作并不多,总结有如下三点:
reserve_volume
: 把volume的状态改为attaching
,阻止其它节点执行挂载操作。initialize_connection
: 创建target、lun、acls等。attach_volume
: 把volume状态改为in-use
,挂载成功。前面我们分析了LVM + lio的volume挂载流程,如果挂载rbd会有什么不同呢。这里我们不再详细介绍其细节过程,直接从cinder-volume的initialize_connection
入手。我们前面已经分析cinder-volume的initialize_connection
步骤:
driver.validate_connector()
driver.create_export()
driver.initialize_connection()
这些步骤对应ceph rbd就特别简单。因为rbd不需要像iSCSI那样创建target、创建portal,因此rbd driver的create_export()
方法为空:
def create_export(self, context, volume, connector):
"""Exports the volume."""
pass
initialize_connection()
方法也很简单,直接返回rbd image信息,如pool、image name、mon地址以及ceph配置信息等。
def initialize_connection(self, volume, connector):
hosts, ports = self._get_mon_addrs()
data = {
'driver_volume_type': 'rbd',
'data': {
'name': '%s/%s' % (self.configuration.rbd_pool,
volume.name),
'hosts': hosts,
'ports': ports,
'auth_enabled': (self.configuration.rbd_user is not None),
'auth_username': self.configuration.rbd_user,
'secret_type': 'ceph',
'secret_uuid': self.configuration.rbd_secret_uuid,
'volume_id': volume.id,
'rbd_ceph_conf': self.configuration.rbd_ceph_conf,
}
}
LOG.debug('connection data: %s', data)
而前面介绍过了,rbd不需要映射虚拟设备到宿主机,因此connect_volume
方法也是为空。
剩下的工作其实就是nova-compute节点libvirt调用get_config()
方法获取ceph的mon地址、rbd image信息、认证信息等,并转为成xml格式,最后调用guest.attach_device()
即完成了volume的挂载。
因此,相对iSCSI,rbd挂载过程简单得多。
总结下整个过程,仍以LVM+LIO为例,从创建volume到挂载volume的流程如下:
attaching
。attach-device
接口把volume挂载到虚拟机。挂载过程总结为以下流图:
需要注意的是,以上分析都是基于老版本的attach API
,社区从Ocata版本开始引入和开发新的volume attach API
,整个流程可能需要重新设计,具体可参考add new attch apis,这个新的API设计将更好的实现多挂载(multi-attach)以及更好地解决cinder和nova状态不一致问题。
以上的流程图可能看不太清楚,可以直接在Draw sequence diagrams online in seconds查看原始图,以下是flow源码:
title OpenStack attach volume flow
participant client
participant nova-api
participant cinder
participant nova-compute
participant libvirt
client -> nova-api: volume_attach
activate client
activate nova-api
note over nova-api: check if volume has been attached
nova-api->nova-compute: reserve_block_device_name
activate nova-compute
nova-compute->libvirt: get device name for instance
activate libvirt
libvirt->nova-compute: return /dev/vdb
deactivate libvirt
note over nova-compute: create bdm
nova-compute->nova-api: return new bdm
deactivate nova-compute
note over nova-api: check attach
nova-api->cinder: reserve_volume
activate cinder
note over cinder: set volume status to 'attaching'
cinder->nova-api: done
deactivate cinder
nova-api->nova-compute: attach_volume
deactivate nova-api
deactivate client
activate nova-compute
note over nova-compute: convert bdm to block device driver
note over nova-compute: get_volume_connector
nova-compute->cinder: initialize_connection
activate cinder
note over cinder: require driver initialized
note over cinder: validate connector
note over cinder: create export
note over cinder: do driver initialize connection
cinder->nova-compute: return connection info
deactivate cinder
nova-compute->libvirt: attach_volume
activate libvirt
note over libvirt: connect volume
note over libvirt: get volume conf and convert to xml
note over libvirt: attach device
libvirt->nova-compute: done
deactivate libvirt
nova-compute->cinder:attach_volume
activate cinder
note over cinder: set volume status to 'in-use'
note over cinder: create attachment
cinder->nova-compute: return attachment
deactivate cinder
note over nova-compute: END
deactivate nova-compute