8.5 OpenStack 源码剖析与改造 ============================ |image0| 8.5.1 虚拟机是如何创建出来的? ------------------------------ 生成xml,准备网络(plug_vif)创建domain 是在 ``_create_domain_and_network`` 这个函数中 |image1| 这个函数,只有在 ``spawn`` 、\ ``_hard_reboot`` 和 ``finish_migration`` 才会执行。 通过libvirt的接口创建虚拟机 |image2| 8.5.2 同步虚机电源状态 ---------------------- 虚拟机在 hypervisor 的真实状态和 OpenStack 数据库管理的状态可能有出入。 比如客户可能在虚拟机内部自己执行 ``shutdown -h now`` ,那么虚机就进入合理正常的关机状态了,可这个状态并不是由 OpenStack 触发的,所以OpenStack 需要定时任务去检查真实的电源状态,并实时更新。 这个定时任务是利用 ``eventlet`` 这个库去做的 .. code:: python class ComputeManager(manager.Manager): def __init__(self, compute_driver=None, *args, **kwargs): # CONF.sync_power_state_pool_size 默认是 1000 self._sync_power_pool = eventlet.GreenPool( size=CONF.sync_power_state_pool_size) self._syncs_in_progress = {} 默认是 600s,就是10分钟同步一次数据库的状态。 |image3| 具体的函数是在这个 ``_sync_instance_power_state`` |image4| 注意这个函数只有两个地方会调用 一个是上面 时间间隔 10分钟 的定时任务,时间明显有点长。 还有一个是由 libvirt 触发的\ ``lifecycle event`` 事件,这个只要在虚拟机在hypervisor层发生状态变动时,就会调用。 |image5| 8.5.3 创建快照代码解读? ------------------------ 8.5.4 虚拟机状态 ---------------- vm_state 描述虚拟机当前的稳定状态,其值可以在 ``nova/compute/vm_states.py``\ 看到 :: ACTIVE BUILDING PAUSED SUSPENDED STOPPED RESCUED RESIZED SOFT_DELETED DELETED ERROR SHELVED power_state 描述的是从hypervisor传过来的状态,其值可在\ ``nova/compute/power_state.py`` :: NOSTATE RUNNING PAUSED SHUTDOWN CRASHED SUSPENDED task_state 描述的是中间任务状态, :: spawning networking scheduling block_device_mapping 在创建虚拟机时,会有几次会产生虚拟机状态(vm_state和task_state)的上报(到ceilomet er)。 nova 提供了一个配置项:notify_on_state_change,本意是想,如果配置\ ``vm_state``\ 就只在vm_state 第一次,在\ ``manager.py:2050``\ 的函数 ``_do_build_and_run_instance``\ 里,看instance.save() 8.5.5 快照镜像如何实现? ------------------------ nova-api 的入口如下 |image6| 接着会调用 nova/compute/api.py |image7| 在nova-compute 层面:nova/compute/manager.py:_snapshot_instance() |image8| 接下来会调用 ``nova/virt/libvirt/driver.py:snapshot()`` |image9| 先获取imagebackend的类型,然后找到对应的backend .. code:: python disk_path, source_format = libvirt_utils.find_disk(virt_dom) source_type = libvirt_utils.get_disk_type_from_path(disk_path) ... snapshot_backend = self.image_backend.snapshot(instance, disk_path, image_type=source_type) 接下来,会调用对应的imagebackend的\ ``snapshot_extract`` 方法。 |image10| ``snapshot_extract`` 方法最终会调用\ ``nova/virt/images.py:_convert_image()`` ,可以看出其实底层调用的是 ``qemu-img`` 提供的\ ``convert`` 接口。 |image11| 如果是qcow2的backend,不是调用这边,而是调用 ``nova/virt/libvirt/utils.py:extract_snapshot()`` .. figure:: C:\Users\wangbm\AppData\Roaming\Typora\typora-user-images\1551944122412.png :alt: 1551944122412 1551944122412 如果要查询镜像压缩的时间,可以在compute上执行这个命令 :: grep -E "Start executing commands|End executing commands" /var/log/nova/nova-compute.log 以上,就是整个镜像创建的过程。 独立磁盘模式的暂时不支持,原因分析如下。 在\ ``libvirt_utils.get_disk_type_from_path`` 里没有相应的修改,导致返回的是lvm。 |image12| 后面的imagebackend也相应的变成 lvm的 |image13| 然后会进入 lvm这个backend的init函数。由于\ ``path`` 是\ ``/dev/sdb`` 并不是一个lv,所以这边会报错。 .. figure:: C:\Users\wangbm\AppData\Roaming\Typora\typora-user-images\1551940635806.png :alt: 1551940635806 1551940635806 下次修改方法:一个是最开始获取\ ``source_type``\ 时判断为isolate,一个是后面 ``isolate``\ 的\ ``extract_snapshot`` 也要和lvm一样实现一下。 8.5.6 宿主机资源采集上报 ------------------------ compute的资源上报,是在 ``nova/compute/resource_tracker.py:_init_compute_node`` 里。 从宿主机上获取数据:\ ``update_available_resource`` 函数下的 ``resources = self.driver.get_available_resource(self.nodename)`` 其调用的函数是\ ``virt/libvirt/driver.py`` 里的 ``get_available_resource`` 函数 |image14| 从数据库获取旧数据 ``self.compute_node = self._get_compute_node(context)`` 8.5.7 虚拟机的ip是如何分配的? ------------------------------ 这一节主要讲两个点: 1. Port 是如何创建的? 2. network_info 是如何组装的? 虚拟机配置网络可以cloudinit从 ConfigDrive 里取得网络信息,然后配置。而这里的网络信息是怎么来的呢,这就是本节要讲的内容,它就是 ``network_info`` 信息,它的最终去向是 ConfigDrive。 在创建虚拟机时,需要的资源有很多,而网络就是其中一种(\ ``network_info``\ ),其他的还有 ``block_device_info`` 。 当你看完本节后,你会发现,network_info 其实是创建完 port 后,根据port的信息组装成一个对象。 虚拟机创建资源的代码是在 ``nova/compute/manager.py`` 里的 ``_build_resource()`` 里 |image15| 这里只讲一下 ``network_info``\ ,其他的我都忽略了。 |image16| 在这个函数\ ``_allocate_network()`` ,主要做如下两件事。 |image17| 继续进入这个函数,如果不指定重试次数,默认是一次。 |image18| 接下来就是调用我们非常熟悉的 ``_create_port_minimal`` 函数去创建port |image19| 如果你全局搜索,你会发现,在 network/rpc.py 下也有这个函数,这个是通过 nova interface-attach 为虚拟机添加网卡,从 nova-api 那边发起的 rpc 请求,才会走到这里 |image20| 上面创建完port了,后面最后一个函数,我已经标出来了,开始组装获取 network_info 对象。 |image21| 上面是创建虚拟机时,创建 network_info 对象的过程。 如果虚拟机已存在,那么如果获取呢,可以用这个函数 .. code:: python network_info = compute_utils.get_nw_info_for_instance(instance) 8.5.8 手动引入上下文环境 ------------------------ 有两种方式可以生成context 1. 如果有请求req(在nova-api里),可以使用这种 |image22| 2. 其他地方可以使用这种 |image23| 8.5.9 指定ip时检查allocation_pools ---------------------------------- 在原生的 neutron 中,当你指定 ip(172.20.22.64) 来创建虚拟机时,假如子网的 allocation_pools 是 172.20.20.100 - 172.20.20.200 ,那 neutron 是不会去检查你指定的ip是否在 allocation_pools 中的。 先来看看,port 是如何创建的 |image24| 若要解决这个问题,可以参考原生代码中,在为子网添加allocation_pool时,验证是否合法的的逻辑,代码如下 |image25| 然后在 ``neutron\neutron\db\ipam_pluggable_backend.py`` 文件中添加我们检查 ip是否在 allocation_pools 中的逻辑代码。 |image26| .. code:: python # 代码如下:方便复制 @staticmethod def _is_ip_in_allocation_pools(ip_address, allocation_pools): from neutron.ipam.exceptions import InvalidIpForAllocationPools for ap in allocation_pools: ap_start_ip = netaddr.IPAddress(ap['start']) ap_end_ip = netaddr.IPAddress(ap['end']) if ap_start_ip <= ip_address <= ap_end_ip: return True raise InvalidIpForAllocationPools(ip_address='ip_address') def _validate_allocation_pool_for_fixed_ip(self, subnet, fixed): ip_address = netaddr.IPAddress(fixed["ip_address"]) allocation_pools = subnet["allocation_pools"] return self._is_ip_in_allocation_pools(ip_address, allocation_pools) 然后还要定义一个异常类型 |image27| 若指定的ip在allocation pool 里,则正常创建,若不在allocation 里,就会在 nova-compute 日志中报错。 |image28| 可以发现我们的ip 172.20.22.64 并不在子网的allocation pool,理所当然在nova的日志中可以看到相应的报错。 |image29| 8.5.10 attach port时ip占用提示 ------------------------------ 当你调用 ``os-interface`` (指定了ip)接口给一台虚拟机添加一张网卡时,若这个ip已经被使用。 nova-api 返回的结果令人无法理解: :: [{"computeFault": {"message": "Unexpected API Error. Please report this at http://bugs.launchpad.net/nova/ and attach the Nova API log if possible.\n", "code": 500}}]. 究其原因,是 nova 在调用neutron的api 创建port时,如果ip已被占用,必须neutron会抛出 IpAddressAlreadyAllocated,而在 neutronclient 只有 IpAddressInUseClient 的异常,并不匹配,在neutronclient 端与neutron 对应的异常应该为 IpAddressAlreadyAllocatedClient 。 |image30| 如何让nova-api能够返回具体的错误信息呢? 解决方法有两种, 一种是,在 neutronclient/common/exceptions.py 里添加 IpAddressAlreadyAllocatedClient 异常。 并且在nova 创建port的代码处,捕获这个异常 |image31| 这种要改两个组件,而且要将neutronclient 的代码也管理起来,较为麻烦 一种是,只改neutron,在neutron/ipam/exceptions.py 添加一个与 neutronclient 相对应的异常。 |image32| 然后修改 neutron/ipam/drivers/neutrondb_ipam/drivers.py 修改异常类型 |image33| 通过 postman 进行模拟,已经可以返回具体的信息 |image34| 另附:neutron 是如何判断ip是否已经占用?代码如下 |image35| 8.5.11 nova-compute 如何启动的? -------------------------------- 从 /usr/bin/nova-compute 这个文件可以了解到nova-compute的入口是 ``nova.cmd.compute:main()`` |image36| 从这个入口进去,会开启一个 ``nova-compute`` 的服务。 |image37| 当调用 service.Service.create 时(create 是一个工厂函数),实际是返回实例化的 service.Service 对象。当没有传入 manager 时,就会以binary 里的为准。比如binary 是\ ``nova-compute``\ ,那manager_cls 就是 ``compute_manager``\ ,对应的manager 导入路径,会从配置里读取。 |image38| 8.5.12 往 spec_obj 里添加对象 ----------------------------- 在 nova-scheduler 里的 过滤器里,有俩非常重要的对象。 - host_state:包含每台 host 的所有信息 - spec_obj:包含创建虚拟机请求的所有信息 |image39| 有时候,spec_obj 里并没有我们想要的信息(比如虚拟机的 metadata),这时候,我们就要手动添加。 这里我以 metadata 为例,来讲一下这个添加过程。 首先第一点要清楚的是,spec_obj 其实是 RequestSpec 对象:nova/objects/request_spec.py,所以下面的修改都是在这个文件里进行。 要往这个对象加属性,第一步是要定义这个字段。 |image40| 往 instance_fields 追加属性名,完了后,这个属性会出现在 ``request_spec['instance_properties']`` 里 |image41| 最后在 ``from_primitives`` 这个函数里,把这个新属性赋值给 request_spec 对象。 .. code:: python spec.metadata = request_spec['instance_properties'].get('metadata', {}) 8.5.13 支持指定子网和指定ip --------------------------- 在 nova-api 接收请求处。 |image42| |image43| 对 network_info 进行解析,然后塞给 request 对象返回。 |image44| 8.5.14 HTTP 状态码 ------------------ 在 标准库 WebOB 中 :: Exception HTTPException HTTPOk * 200 - :class:`HTTPOk` * 201 - :class:`HTTPCreated` * 202 - :class:`HTTPAccepted` * 203 - :class:`HTTPNonAuthoritativeInformation` * 204 - :class:`HTTPNoContent` * 205 - :class:`HTTPResetContent` * 206 - :class:`HTTPPartialContent` HTTPRedirection * 300 - :class:`HTTPMultipleChoices` * 301 - :class:`HTTPMovedPermanently` * 302 - :class:`HTTPFound` * 303 - :class:`HTTPSeeOther` * 304 - :class:`HTTPNotModified` * 305 - :class:`HTTPUseProxy` * 307 - :class:`HTTPTemporaryRedirect` * 308 - :class:`HTTPPermanentRedirect` HTTPError HTTPClientError * 400 - :class:`HTTPBadRequest` * 401 - :class:`HTTPUnauthorized` * 402 - :class:`HTTPPaymentRequired` * 403 - :class:`HTTPForbidden` * 404 - :class:`HTTPNotFound` * 405 - :class:`HTTPMethodNotAllowed` * 406 - :class:`HTTPNotAcceptable` * 407 - :class:`HTTPProxyAuthenticationRequired` * 408 - :class:`HTTPRequestTimeout` * 409 - :class:`HTTPConflict` * 410 - :class:`HTTPGone` * 411 - :class:`HTTPLengthRequired` * 412 - :class:`HTTPPreconditionFailed` * 413 - :class:`HTTPRequestEntityTooLarge` * 414 - :class:`HTTPRequestURITooLong` * 415 - :class:`HTTPUnsupportedMediaType` * 416 - :class:`HTTPRequestRangeNotSatisfiable` * 417 - :class:`HTTPExpectationFailed` * 422 - :class:`HTTPUnprocessableEntity` * 423 - :class:`HTTPLocked` * 424 - :class:`HTTPFailedDependency` * 428 - :class:`HTTPPreconditionRequired` * 429 - :class:`HTTPTooManyRequests` * 431 - :class:`HTTPRequestHeaderFieldsTooLarge` * 451 - :class:`HTTPUnavailableForLegalReasons` HTTPServerError * 500 - :class:`HTTPInternalServerError` * 501 - :class:`HTTPNotImplemented` * 502 - :class:`HTTPBadGateway` * 503 - :class:`HTTPServiceUnavailable` * 504 - :class:`HTTPGatewayTimeout` * 505 - :class:`HTTPVersionNotSupported` * 511 - :class:`HTTPNetworkAuthenticationRequired` 8.5.15 不同存储方式xml ---------------------- lvm .. code:: xml
qcow2 .. code:: xml
config(configdrive) .. code:: xml
isolate .. code:: xml
其他都好理解,这个qcow2的类型,有一点点的不一样。 他是先创建一个在LVM存储池中,创建一个LV。 然后为这个LV,创建一个软连接,通过 ``df -Th`` 可以看到 这个LV 挂载到 一个目录下。这个目录下有一个名为 disk 的qcow2文件,而这个qcow2 文件的backing file 是指向一个 base 镜像文件(raw格式)。 |image45| 8.5.16 独立磁盘与LVM -------------------- **独立磁盘** 优点:虚拟机磁盘互不影响,IO不共享,不会因为一块物理盘挂了而影响所有的虚拟机。 缺点:无法像 LVM 存储池那样,做到精准而灵活的资源分配,有可能造成资源浪费或资源不足。 |image46| **LVM** 优点:可以根据业务或者其他条件(如介质类型),对多块物理盘组合成一个VG,实现精准而灵活的资源分配 缺点:不同虚拟机之间,磁盘IO共享,不同客户之间的性能会出现争抢的情况;稳定性不好,如果一块盘坏了,有可能有多个虚拟机受到影响。 8.5.17 主机组的使用 ------------------- 在 flavor 上有一个 extra_spec 可以设置多个参数,nova 本身自带了许多,当然这个参数可以自定义。 这个参数有什么用呢,需要搭配主机组使用。 只要在主机组上设置的metadata 的 key-value 和 extra_spec 的key-value一样就可以实现宿主机的过滤。 |image47| 8.5.18 生成config drive ----------------------- |image48| |image49| :: network_metadata inst_md.ip_info # {'fixed_ip6s': [u'2001:1001::3'], 'fixed_ips': [], 'floating_ips': []} .. figure:: C:\Users\wangbm\AppData\Roaming\Typora\typora-user-images\1562565550118.png :alt: 1562565550118 1562565550118 8.5.19 修改nova-api的接口 ------------------------- nova api 的接口参数是在 nova/api/openstack/compute/schemas/ 目录下,如创建虚拟机的接口如下: |image50| 这些参数字段后面的类型是用来做参数类型的校验的。 在application 函数的头部,可以发现有如下这几种装饰器, |image51| 装饰器里会传入一个参数,就是 schema 的内容,通过它可以约束一个请求内的参数合法性。 schemas 的文件都统一放在 ``nova\api\openstack\compute\schemas`` 目录下。 对于创建虚拟机来说,它的 schemas 文件是在上面目录下的 ``servers.py`` 但并不是说,创建虚拟机的所有参数校验规则只在这里,nova 这边引入了 stevedore,它是 oslo 项目中为OpenStack其他项目提供动态加载功能的公共组件库(详细可以看这篇文章:https://blog.csdn.net/bill_xiang_/article/details/78852717)。通过它可以用添加扩展的方式,给 servers 更新 schemas。 |image52| 关键在于这个 map 函数,它会循环所有的扩展() |image53| 将调用传进去的 ``self._create_extension_schema`` 将已加载到的扩展schemas 更新到主schemas里去。 :: # [ext.obj.name for ext in self.create_schema_manager.extensions] ['MultipleCreate', 'BlockDeviceMapping', 'BlockDeviceMappingV1', 'AvailabilityZone', 'UserData', 'Keypairs', 'SchedulerHints', 'SecurityGroups', 'ConfigDrive'] |image54| 装饰器 schemas 的定义如下: |image55| 比如我们使用的 novaclient 发出的请求,是 v2.3.7的,所以 create() 顶部的五个schema 装饰器,上面四个都会空跑,不会进行校验,只有最后一个才会进入检验逻辑。 |image56| 是不是很呐闷,为什么创建server 的接口,会知道关联上面那9个 schemas,其实那各自代表一种资源,属于扩展资源(它们的中心是核心资源)。 那这些扩展资源是如何关联到核心资源的,这是在哪定义的呢? 其实就是 nova 的 setup.cfg 里定义好的。 就像下面指明了 server 的 create 接口会去加载这9个扩展资源。 |image57| 搜索一下 server_create 方法 还真的只有这 9 个资源里才会定义。 |image58| 这就神奇了,servers 这个核心资源是如何加载到这些资源的,看了下代码是使用 stevedore 这个模块去动态加载,然后还会校验这些资源是否都有 ``server_create`` ,只有有这个函数,才会被正常加载进来。 |image59| 8.5.20 往数据库中加字段 ----------------------- 先先定义好migrate 文件 .. code:: python from sqlalchemy import MetaData, Text, Table, Column def upgrade(migrate_engine): meta = MetaData(bind=migrate_engine) instances = Table('instances', meta, autoload=True) if not hasattr(instances.c, 'host_routes'): instances.create_column(Column('host_routes', Text(), default=[])) 然后在 nova/objects/instances.py 中的fields加入相应的字段。 然后在 nova/db/sqlalchemy/models.py 也要加相应的 Column 最后再执行这个命令,同步数据库 :: nova-manage db sync 8.5.21 如何加action ------------------- :: self._record_action_start(context, instance, instance_actions.REBOOT) 8.5.22 如何生成ConfigDrive -------------------------- |image60| 8.5.23 创建时修改虚拟机密码 --------------------------- 使用下面这条命令,输入你要的密码,可以生成一串加密过字符,将这串字符传cloudinit,原封不动地放入 /etc/shadow 就可以实现密码设置。 .. code:: shell python -c 'import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw,crypt.METHOD_SHA512) if (pw==getpass.getpass("Confirm: ")) else exit())' -------------- |image61| .. |image0| image:: http://image.iswbm.com/20200602135014.png .. |image1| image:: http://image.iswbm.com/20190526144846.png .. |image2| image:: http://image.iswbm.com/20190529135942.png .. |image3| image:: http://image.iswbm.com/20190530204839.png .. |image4| image:: http://image.iswbm.com/20190530204505.png .. |image5| image:: http://image.iswbm.com/20190530210912.png .. |image6| image:: http://image.iswbm.com/20190508110723.png .. |image7| image:: http://image.iswbm.com/20190508111109.png .. |image8| image:: http://image.iswbm.com/20190508095028.png .. |image9| image:: http://image.iswbm.com/20190508111527.png .. |image10| image:: http://image.iswbm.com/FhRPy4B1xEI9SfoD2RcunJl15ZE3 .. |image11| image:: http://image.iswbm.com/FuyMWZS6HF4g3rPwTlLcereZxg4L .. |image12| image:: http://image.iswbm.com/FnJA8RNIvJN2lAEXbKtJDpOLg1vg .. |image13| image:: http://image.iswbm.com/FnGyI8jCQFLCGi0pGVmI3SV6pDrv .. |image14| image:: http://image.iswbm.com/FrbE6oEZ3vtTWwDfMNQ16MGi6SWr .. |image15| image:: http://image.iswbm.com/20190906093751.png .. |image16| image:: http://image.iswbm.com/20190906094703.png .. |image17| image:: http://image.iswbm.com/20190906214536.png .. |image18| image:: http://image.iswbm.com/20190906100119.png .. |image19| image:: http://image.iswbm.com/20190906213038.png .. |image20| image:: http://image.iswbm.com/20190906210825.png .. |image21| image:: http://image.iswbm.com/20190906213823.png .. |image22| image:: http://image.iswbm.com/20190426153322.png .. |image23| image:: http://image.iswbm.com/20190426152148.png .. |image24| image:: http://image.iswbm.com/20190526141815.png .. |image25| image:: http://image.iswbm.com/20190526142453.png .. |image26| image:: http://image.iswbm.com/20190526134519.png .. |image27| image:: http://image.iswbm.com/20190526141226.png .. |image28| image:: http://image.iswbm.com/20190526134543.png .. |image29| image:: http://image.iswbm.com/20190526134618.png .. |image30| image:: http://image.iswbm.com/20190526140213.png .. |image31| image:: http://image.iswbm.com/20190526140301.png .. |image32| image:: http://image.iswbm.com/20190526140315.png .. |image33| image:: http://image.iswbm.com/20190526140336.png .. |image34| image:: http://image.iswbm.com/20190526140410.png .. |image35| image:: http://image.iswbm.com/20190526143235.png .. |image36| image:: http://image.iswbm.com/20190526205152.png .. |image37| image:: http://image.iswbm.com/20190526165007.png .. |image38| image:: http://image.iswbm.com/20190526204328.png .. |image39| image:: http://image.iswbm.com/20191008173211.png .. |image40| image:: http://image.iswbm.com/20191008174046.png .. |image41| image:: http://image.iswbm.com/20191008210127.png .. |image42| image:: http://image.iswbm.com/20190529203441.png .. |image43| image:: http://image.iswbm.com/20190529215953.png .. |image44| image:: http://image.iswbm.com/20190529215825.png .. |image45| image:: http://image.iswbm.com/20190627213044.png .. |image46| image:: http://image.iswbm.com/20190627213609.png .. |image47| image:: http://image.iswbm.com/20190627215038.png .. |image48| image:: http://image.iswbm.com/20190708100902.png .. |image49| image:: http://image.iswbm.com/20190708103119.png .. |image50| image:: http://image.iswbm.com/20190719170825.png .. |image51| image:: http://image.iswbm.com/20190730140551.png .. |image52| image:: http://image.iswbm.com/20190826210813.png .. |image53| image:: http://image.iswbm.com/20190826211542.png .. |image54| image:: http://image.iswbm.com/20190826211737.png .. |image55| image:: http://image.iswbm.com/20190730142527.png .. |image56| image:: http://image.iswbm.com/20190730143003.png .. |image57| image:: http://image.iswbm.com/20190830091540.png .. |image58| image:: http://image.iswbm.com/20190830092203.png .. |image59| image:: http://image.iswbm.com/20190830093613.png .. |image60| image:: http://image.iswbm.com/20190912135302.png .. |image61| image:: http://image.iswbm.com/20200607174235.png