这篇博文静静的呆在草稿箱大半年了,如果不是因为某些原因被问到,以及因为忽略它而导致的损失,否则我也不知道什么时候会将它完成。感谢这段时间经历的挫折,让我知道不足,希望你能给我更大的决心!
本文试图详细地描述openstack创建虚拟机的完整过程,从用户发起请求到虚拟机成功运行,包括客户端请求的发出、keystone身份验证、nova-api接收请求、nova-scheduler调度、nova-computer创建、nova-network分配网络。对于每一个模块在创建虚拟机的过程中所负责的功能和执行的操作,进行较为详细描述和讨论。为了方便描述,本文假设所有的服务ip地址为localhost,端口使用服务的默认设置端口。openstack为F版。下图为创建虚拟机的一个大概流程图,粗糙地表示下。接下来将对每一个模块进行详细介绍,如有错误,欢迎拍砖!
客户端
创建虚拟机的第一步是需要客户端调用nova-api,发送创建虚拟机请求。目前openstack提供两种客户端:
1 命令行指令nova,通过指定命令行参数,就可以请求nova-api创建虚拟机,一个最简单的创建虚拟机指令如下:
nova boot vm_name --flavor flavor_id --image image_uuid
2 网页交互页面horizon,这是通过web操作页面来调用nova-api创建虚拟机,比较简单易用。选定相关参数后,点击create就可以了。
这两种客户端除了UI不一样以外,功能方面基本都是一样。就创建虚拟机来讲,它们需要完成:
- 用户身份验证。客户端构造一个body格式如下:
{"auth": {
"passwordCredentials": {"username": self.user,
"password": self.password}}
"tenantName": "admin" }
向keystone发送HTTP请求(keystone的服务地址:nova命令一般通过环境变量OS_AUTH_URL设置,horizon则通过openstack_dashboard.local.local_settings.OPENSTACK_KEYSTONE_URL设置),url为http://localhost:5000/v2.0/tokens 。如果验证通过,keystone则返回token_id和serviceCatalog。身份验证通过后,客户端才可对nova-api发送创建请求。因为客户端只配置了keystone的服务地址和端口,对于nova-api的服务地址它是不知道的,所以需要先通过keystone验证,然后由keystone将nova-api的服务地址放在serviceCatalog中返回给它。其实serviceCatalog中不仅仅包含nova-api的服务地址,还有glance-api、cinder-api等服务的地址,这些地址在以后的镜像下载、块存储请求时会用到。token_id被放在请求nova-api的headers中,用作nova-api验证。
- 对传入flavor和image参数进行验证,以确定资源是否有效(对于一些非必须参数,此处省略讨论)。对于nova boot,flavor_id和image_uuid需要通过命令行参数指定,novaclient.v1_1.shell._boot()分别向nova-api的"http://localhost:8774/v2/project_id/flavors/flavor_id"和"http://localhost:8774/v2/project_id/images/image_id"发送HTTP请求,验证资源是否存在。对于horizon,它首先执行horizon.api.nova.flavor_list()和horizon.api.glance.image_list_detailed(),获取所有可用的flavor ids和image ids,创建虚拟机时只能从这些可用的id中选择。注意这里nova boot和horizon对于image的验证有些不同,nova boot是请求nova-api,nova-api再请求glance-api以验证image uuid,而horizon是直接请求glance-api获取所有的image ids。
- 最后设置好请求参数后(除name、flavor和image外,其它可使用默认值),通过novaclient.v1_1.ServerManager.create()发送创建虚拟机请求,该函数主要是将请求参数构造成规定格式的body,然后向nova-api发送HTTP请求。关于body的模样,可看看novaclient.v1_1.BootingManagerWithFind._boot() 。以上面的nova boot命令为例,body内容如下:
{'server': {'flavorRef': '1',
'hypervisor_type': 'QEMU',
'imageRef': 'd3670457-16f5-4c70-913f-6fc7b76706e4',
'max_count': 1,
'min_count': 1,
'name': 'test-ic'}}
这里的flavorRef对应的是数据库instance_types的flavorid字段(非id字段),上面命令行传入的flavor_id也是指数据库的flavorid字段。向nova-api的http://localhost:8774/v2/project_id/flavors/flavorid请求时,它通过nova.api.openstack.compute.views.flavors.ViewBuilder,将数据库的id字段作为instance_type_id,将flavorid作为id进行返回的。
可以看出,无论nova boot还是horizon,最后都是通过novaclient向nova-api发送请求的。novaclient是针对nova-api做了一层封装,如获取验证token-id,以特定的格式构造HTTP请求headers和body,解析请求的结果等。其实可以不用nova boot命令行和horizon,甚至novaclient都不需要,我们完全可以设定好HTTP请求的headers和body,直接请求nova-api。不过这三个工具将这些繁琐的工作替我们做了,我们只需要填写参数就可以了。最后注意下,nova命令其实只是novaclient的一个entry point:novaclient.shell.main(),而nova boot实际上调用的是novaclient.v1_1.shell.do_boot()。
keystone
由上可知,在客户端发起创建虚拟机请求时,keystone需要对客户端的用户名和密码进行验证。keystone-all与nova-api一样,api的发布没有采用任何框架,而是使用router、paste类库,从头写的,实现风格上又与nova-api有点差异。keystone-all服务会监听两个端口:localhost:5000,即publicURL,一般用户使用密码可以访问;localhost:35357,即adminURL,只能使用admin账户和密码访问。在创建虚拟机流程中,调用的keystone的api有两个(其实,每次请求基本都会调用这两个api):
1 http://localhost:5000/v2.0/tokens,请求该api来获取token_id和serviceCatalog,由客户端调用。keystone会将该api的请求交给keystone.TokenController.authenticate()处理。该函数主要完成:
- 对客户端传过来的name、password、tenant_id进行验证。keystone的数据库user表中保存了user相关信息,包括password加密后的hash值。password的加密使用了sha512算法,由passlib库提供。密码验证如下:passlib.hash.sha512_crypt.verify(password, hashed)。
- 根据CONF.signing.token_format配置项,为客户端的每一次请求生成一个token_id。目前支持的选择有两个UUID、PKI,默认为UUID。所以在默认情况下,一个token_id就是一个随机产生的uuid。token_id有一个有效时间,从token_id的生成的时刻开始算起。有效时间可通过CONF.token.expiration配置项设置,默认值为86400s。
- 构建serviceCatalog,这里面就是一堆endpoints,包括nova-api、glance-api、cinder-api、keystone-api等。根据配置项CONF.catalog.driver,可以从数据库的endpoint表中获取,也可以从模板文件中获取。
2 http://localhost:35357/v2.0/tokens/token_id,对请求的token_id进行验证,由nova-api调用。nova-api接收到请求后,首先使用请求携带的token_id来访问该api,以验证请求是否通过验证。当然nova-api需要在body里加上admin的账户和密码信息,这些信息需要在api-paste.ini中配置,还有keystone的服务地址,因为在验证没通过之前,不能使用客户端传过来的endpoints。glance-api、cinder-api等在接收到客户端请求后,都会调用该api对客户端的token_id进行验证。该api的请求交给keystone.TokenController.validate_token()处理,其实就是使用请求的token_id进行一次数据库查询。
nova-api
nova-api是一个统称,它是一类服务的集合。如openstack之nova-api服务流程分析所说,在默认配置下,它包含ec2(与EC2兼容的API),osapi_compute(openstack compute自己风格的API),osapi_volume(openstack volume服务API),metadata(metadata 服务API)等api服务。每个服务提供不同的api,不过虽然ec2和osapi_compute的api不同,但功能是相同的。这里,创建虚拟机请求的api是:
http://localhost:8774/v2/project_id/servers,由osapi_compute服务发布。该api是RESTFUL的,以POST方法请求该api,经过几层middleware处理后,最终交给nova.api.openstack.compute.servers.Controller.create()处理。它们主要完成以下功能:
- 验证客户端的token_id。通过keystone.middleware.auth_token.AuthProtocol.__call__()进行验证,它是一个middleware,通过api-paste.ini配置。如上面keystone所讲,它是通过请求http://localhost:35357/v2.0/tokens/token_id实现的。
- 检查创建虚拟机参数是否有效与合法,由nova.api.openstack.compute.servers.Controller.create()实现。如,检查虚拟机name是否符合命名规范,flavor_id是否在数据库中存在,image_uuid是否是正确的uuid格式等。
- 检查instance、vcpu、ram的数量是否超过限制,并预留所需资源,由nova.quota.QUOTAS管理。每个project拥有的资源都是有限的,如创建虚拟机的个数,vcpu个数,内存大小,volume个数等。默认情况下,所有project的拥有的资源数量相等,由quota_instances、quota_cores、quota_ram、quota_volumes等配置项指定。使用admin账户,以PUT方法请求http://localhost:8774/v2/admin_project/os-quota-sets/project_id,可为特定的project设置配额。
- 检查metadata长度是否超过限制,inject file的个数是否超过限制,由nova.quota.QUOTAS管理检测。一般情况下,这些参数都为空,都能通过。
- 检查指定的network和fixed ip是否有效,如network是否存在,fixed ip是否属于该network,fixed ip有没有被分配等。一般情况下,该参数也为空,由network自动分配。
- 检查image、disk、ramdisk是否存在且可用,这个是向glance-api(http://localhost:9292/v1/images/image_id)请求,获取返回数据进行判断的。并判断flavor是否满足该image的最小配置需求,如内存,虚拟磁盘是否满足image的最小值。
- 当所有资源充足,并且所有传参都有效时,更新数据库,新建一条instance记录,vm_states设为BUILDING,task_state设为SCHEDULING。如果没有给虚拟机指定security group,那么将默认使用default。instance与security group的关联非常隐蔽,在db.instance_create()中,注意观察。
- 提交预留资源,完成资源分配。通过rpc call,将所有参数传给nova-scheduler的nova.scheduler.manager.SchedulerManager.run_instance(),由它执行虚拟机调度。
在token_id验证通过的情况下,nova-api的主要任务是资源配额和参数检查,并创建数据库。如果你使用dashboard,此时你在页面上将会看到虚拟机处于scheduling状态。不过该状态持续时间很短,几乎察觉不到,除非nova-scheduler出问题了。
nova-scheduler
与nova-api提供外部服务不同,nova各组件之间相互调用,使用的是以rabbitmq作为消息队列的RPC调用。这里忽略RPC的实现细节,只需知道rcp call调用哪个的节点的哪个服务就可以了。nova-scheduler的run_instance()从nova-api接收到的参数中只使用到了request_spec和filter_properties,其余参数将直接传递给nova-compute去创建虚拟机。request_spec包含虚拟机的type、number、uuids等信息,在调度中需要用这些信息作为参考。filter_properties包含指定调度规则信息,如force_hosts指定调度到特定的节点,ignore_hosts不调度到某些节点等。nova-scheduler在接收到nova-api的请求后,由nova.scheduler.filter_scheduler.FilterScheduler.scheduler_run_instances(),它主要完成以下任务:
- 获取现存所有计算节点及其信息,调用nova.scheduler.host_manager.HostManager.get_all_host_states()获取。这些信息包括compute_nodes表中的信息,及由每个nova-compute通过定时任务_publish_service_capabilitites上传的capabilities信息,包括cpu、vcpu、内存、磁盘等大小和使用情况。
- 使用指定的filter对上面返回的节点进行过滤,调用nova.scheduler.filters.HostFilterHander.get_filtered_hosts()实现。这些filter可通过配置项scheduler_default_filters指定,它可以包含nova.scheduler.filters中任何已经实现的filter。如RamFilter,它用来过滤内存不足以创建虚拟机的host;ComputeFilter用来过滤一些service无效的host。也可以在这里添加自定义的filter,然后添加到scheduler_default_filters配置项中就行了。不过这里需要注意一点,通过force_hosts和forces_nodes指定的hosts,将不经过filter过滤,直接返回。
- 使用weighers对经过过滤的host进行排序,调用nova.scheduler.weights.HostWeightHander.get_weighed_objects()实现,在F版中只实现了一个RamWeigher,只以内存大小对hosts做了一个排序。后面的版本可能觉得使用Weigher,又使用WeigherHander,仅仅是对内存大小做排序,有点小题大做了,在2012.2.4中用一个函数nova.scheduler.least_cost.weighted_sum()就实现排序。但在2013.2中又回到原来的结构了。
- 从排名前n个hosts中,随机选取一个用于创建虚拟机。n可通过配置项scheduler_host_subset_size设置,默认值为1,即选取最优节点进行创建。并更新scheduler保存host的资源信息,如内存、磁盘、vcpu等。使得调度下一个虚拟机时,使用的资源信息及时更新。
- 更新数据库设置instance的host=NULL,scheduled_at=now(),然后通过RPC调用上面选取host的nova-compute进行创建虚拟机。
nova-scheduler相对而言,逻辑比较简单,代码也不难。不过需要注意在node-scheduler执行过程中,不会改变虚拟机的vm_state和tast_state。所以在horizon上也不会有变化。
nova-compute
nova-scheduler选定host后,随即通过rpc调用,调用host的nova-compute服务的nova.compute.manager.ComputeManager.run_instance()。由它执行真正的创建虚拟机操作。不过在介绍之前,需要简单提及一下nova-compute的两个定时任务:
- update_available_resource。该任务首先获取当前计算节点的cpu个数、总内存大小、总磁盘大小,数据由nova.virt.libvirt..driver.LibvirtDriver.get_available_resource()获取。然后从数据库中查找到运行在该节点上的所有虚拟机信息,统计出这些虚拟机所使用的vcpu个数、内存大小、磁盘大小。并将计算节点的总资源数量减去虚拟机使用的数量,得到空闲内存大小和空闲磁盘大小。然后更新数据库compute_node信息。这里需要注意从数据库中获取的虚拟机使用资源数量并不是一定是计算节点消耗的资源数量,如1)虚拟机使用磁盘镜像为qcow2格式时,它的大小要小于或等于实际分配的大小;2)也有可能因为数据不同步,使得统计的数据与实际的资源使用不一致。
- _report_driver_status。该任务是用于定时更新当前计算节点的capabilities信息,同样也是通过LibvirtDriver来获取资源数据的,不过在计算已使用资源方面,是直接使用通过调用multiprocessing、libvirt、os等返回的cpu个数、内存、磁盘使用情况。并附加上host_ip信息。并由定时任务_publish_service_capabilities通过rpc call转发到nova.scheduler.host_manager.HostManager.service_states中。
这两个定时任务为nova-scheduler提供了实时的host信息,所以才能实现准确调度。由于capabilities信息与compute_node表中信息有很大的相似度,所以调度过程中很少用到。nova-scheduler调度到nova-compute的run_instance()主要完成什么功能呢:
- 检查虚拟机是否已创建,及instance的image大小是否超过root的大小,超过则报错。
- 更新数据库,将instance的vm_state=BUILDING,task_state=NULL状态,horizon上面会有反应,但时间很短。
- 给虚拟机分配资源,包括cpu、内存、磁盘等。更新数据库,设置instance的host字段。
- 分配网络,首先将instance的tast_state=NETWORKING,然后通过rpc调用nova-network的nova.network.manager.NetworkManager.allocate_for_instance(),返回网络信息。nova-network处理的详细将在下面的nova-network模块讨论。
- 建立块设备,将task_state=BLOCK_DEVICE_MAPPING。因为未给分配块设备,这步将不进行操作,有待后面讨论。
- 将task_state=SPAWNING,如果该类型的instance第一次在该计算节点上创建,该状态要持续几分钟,因为需要下载镜像。
- 根据上面配置的虚拟机信息,生成xml,写入libvirt,xml文件,生成console.log文件。
- 下载镜像,kernel、ramdisk、image,通过glance api下载。它们首先会被放在FLAGS.instances_path的_base目录下,然后copy一份到instance的目录下面。这样,如果这个计算节点上创建相同虚拟机时,首先查找_base中是否已经下载,如果已经下载过了,则直接copy就可以了。对于image,一般采用qcow2格式,作为qemu-img的backing_file新建一个image使用,这样可以节约空间。
- 向下载过后的磁盘文件,注入指定的内容,如public_key、/etc/network/interfaces、root密码、指定的文件路径和内容、/etc/vmuuid等。原理比较简单,将下载的image使用mount命令进行挂载,然后将要写入的内容下到特定的位置。
- 最后使用上面生成的xml,调用libvirt创建虚拟机,等待虚拟机正常运行。
- 更新数据库,将instance的vm_state=ACTIVE、task_state=None。
nova-compute做的事情还是挺多的,生成虚拟机xml配置文件、下载镜像并注入文件、调用libvirt创建虚拟机等,这里对于libvirt还需继续研究。
nova-network
nova-network是nova-compute在创建虚拟机之前,被其调用nova.network.manager.FlatDHCPManger.allocate_for_instance(),为虚拟机分配fixed ip和floating ip(如果auto_assign_floating_ip为True),并返回网络信息。这里network采用 FlatDHCP模式,multihost=True。查看代码可知 FlatDHCPManager继承RPCAllocateFixedIP, FloatingIP, NetworkManager三个class,根据python属性访问流程(参考 python对象之属性访问流程)可知,调用FlatDHCPManger.allocate_for_instance()首先执行FloatingIP.allocate_for_instance(),再由它调用NetworkManger.allocate_for_instance()。主要完成任务如下:
- 获取网络信息,用户可指定network_uuid,否则将直接获取networks表中所有network。
- 给每一个network,在数据库创建一个virtual interface,给instance uuid和network id置为相应的值。注意virtual interface表与其它的表不太一样,当删除一个virtual interface时,将直接删除该记录,而不是将deleted=1。
- 从数据库中找出一个network id等于该network或为NULL的fixed ip,设置instance uuid、host、allocated=True、virtual_interface_id。更新dnsmasq的conf文件(默认在nova源码目录下),同时给dnsmasq发送HUP信号,重读配置文件,当虚拟机动态获取IP时,dnsmasq根据接收到的mac,查询该配置文件,返回分配给该虚拟机的ip。这里需要注意下,dnsmasq的启动参数--dhcp-script=/usr/bin/nova-dhcpbridge,在虚拟机请求和释放ip时该脚本会被调用,用来设置fixed ip的leased字段。
- 如果auto_assign_floating_ip为True,则给虚拟机分配floating ip。这里分配的floating ip原本不属于任何project,分配过后才设置它的project_id和auto_assigned字段。并将fixed_ip_id字段设为上面分配到fixed ip。
- 在对外出口网卡上绑定floating ip,设置iptables nat的nova-network-PREREOUTING、nova-network-OUTPUT和nova-network-float-snat表,做SNAT和DNAT转换。这样就可以通过floating ip从外部访问虚拟机了。
- 通过instance uuid查询数据库,获取它的vif、network、floating ip、fixed ip等信息,以nova.network.model.NetworkInfo结构构造,返回给nova-compute。