本文主要是介绍openstack zun源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
容器服务启动过程
项目包括三个服务,分别是zun-api
,zun-wsproxy
,zun-compute
,均使用systemctl
来管理启动停止,相关的服务文件如 zun-api.service
在/etc/systemd/system
或 /usr/lib/systemd/system
(nova cinder等在这里)中。nova cinder等是自动创建的,而zun的是手动创建的,指定了创建位置。
zun-api启动过程
该文件内的execstart
指定了启动脚本所在位置,如 /usr/bin/zun-api
,该脚本是个python代码,启动就是调用了zun.cmd.api.main
进行启动
启动之后解析命令行参数,如果设置的有配置文件,则从配置文件中读取数据
重点在zun_service.WSGIService()
,构建该对象时,在__init__
方法中,app.load_app()
加载了api-paste.ini
文件,该文件中构建的app只有一个,在zun.api.app.app_factory()
,这里调用zun.api.app.setup_app
构建了wsgi应用(即pecan对象
,该对象中有application的实现,对应方法是__call__
),然后又使用wsgi.Server
构建了wsgi服务,传入参数有self.app,也就是该wsgi server和构建的wsgi app关联上了
而该wsgi服务的创建是由oslo_service
提供的。
paste
提供的是如何调用配置文件来启动wsgi应用,以及定义执行流程,还有一些路由的功能,通过composite
部分实现,但是zun的配置文件中没有这块,路由不是通过paste实现的,而是pecan实现的
oslo.service
提供了一个框架,用于使用其他 OpenStack 应用程序建立的模式定义新的长期运行服务。它还包括长时间运行的应用程序可能需要使用 SSL 或 WSGI、执行定期操作、与 systemd 交互等的实用程序。
zun-compute启动过程
zun-compute的启动,本质上是rpc server
的启动
zun-compute安装之后,创建启动service文件,放置在/etc/systemd/system/zun-compute.service
中,其中有/usr/local/bin/zun-compute
这个命令,查看可以看到其中执行了·zun.cmd.compute.main()·,该函数内进行·rpc server·的创建,即构建了zun.compute.manager.Manager
对象。也就是zun-compute启动的时候,就启动了rpc server
zun创建容器全流程分析
1、systemctl start zun-api
启动WSGI Service,对接应用主要为zun/api/controllers/v1/containers.py
(容器相关代码)
2、以创建容器流程做全过程分析
zun 的其他操作比如 start、stop、kill 容器等实现原理也类似
zun-api详细流程分析
容器服务入口为 zun-api
,主要代码实现在 zun/api/controllers/v1/containers.py
以及 zun/compute/api.py
创建容器代码分析
请求体样例
{"name": "aaa","image": "cirros","image_driver": "docker","run": true,"auto_heal": false,"mounts": [],"security_groups": ["default"],"interactive": true,"hints": {},"nets": []
}
路由走向:
zun/api/root.py(RootController)
–> zun/api/controllers/v1/__init__.py(Controller)
– > zun/api/controllers/v1/containers.py(ContainersController.post(1.20version)
–> _do_post
_do_post()
方法是创建容器的方法,核心处理代码如下:
...
# 检查 policy,验证用户是否具有创建 container 权限的 API 调用
policy.enforce(context, "container:create", action="container:create")
...
# 检查安全组是否存在,根据传递的名称返回安全组的 ID。
security_group_id = self._check_security_group(context, {'name': sg})
...
# 检查 quota 配额。
self._check_container_quotas(context, container_dict)
...
# 检查网络配置,比如 port 是否存在、network id 是否合法,最后构建内部的 network 对象模型字典。注意,这一步只检查并没有创建 port。
requested_networks = utils.build_requested_networks(context, nets)
...
# 根据传递的参数,构造 container 对象模型。
new_container = objects.Container(context, **container_dict)
...
# 检查 volume 配置,如果传递的是 volume id,则检查该 volume 是否存在,如果没有传递 volume id 只指定了 size,则调用 Cinder API 创建新的 volume。
self._build_requested_volumes(context, new_container, mounts)
...
# 调用zun/compute/api.py中对应的方法创建容器
compute_api.container_create(context, new_container, **kwargs)
经过以上处理之后流程进入zun/compute/api.py
内的API.container_create
,在这里执行以下步骤:
# 使用 FilterScheduler 调度 container,返回宿主机的 host 对象。这个和 nova-scheduler 非常类似,只是 Zun 集成到 zun-api 中了。目前支持的 filters 如 CPUFilter、RamFilter、LabelFilter、ComputeFilter、RuntimeFilter 等。
host_state = self._schedule_container(context, new_container, extra_spec)
...
# image validation: 检查镜像是否存在,这里会远程调用 zun-compute 的 image_search 方法,其实就是调用 docker search。这里主要为了实现快速校验,避免到了 compute 节点才发现 image 不合法。
if CONF.api.enable_image_validation:try:images = self.rpcapi.image_search(context, new_container.image,new_container.image_driver, True, new_container.registry,host_state['host'])
...
# record action: 和 Nova 的 record action 一样,记录 container 的操作日志。
self._record_action_start(context, new_container, container_actions.CREATE)
...
# 调用zun/compute/rpcapi.py的API.container_create
self.rpcapi.container_create
注意:image_search方法只支持从dockerhub搜索,搜不到不要紧,后续会继续执行。但是在zun-compute中会看到错误如下:ERROR zun.compute.manager [None req-8044425d-a6ef-4d51-b2ea-06f5e18d93fb admin admin] Unexpected exception while searching image: Image searching is not supported in registry: harbor129.com: OperationNotSupported: Image searching is not supported in registry: harbor129.com
接下来进入 zun/compute/rpcapi.py
(API.container_create)。这个API中的函数即为zun服务提供给RPC调用的接口,其他服务在调用前需要先import这个模块。
def container_create(self, context, host, container, limits,requested_networks, requested_volumes, run,pci_requests):self._cast(host, 'container_create', limits=limits,requested_networks=requested_networks,requested_volumes=requested_volumes,container=container,run=run,pci_requests=pci_requests)
self._cast
: 通过rpc
远程异步调用 zun-compute
的 container_create()
方法,zun-api
任务结束。
zun.compute.rpcapi.API
只是暴露给其他服务的RPC调用接口,Compute服务的RPC Server在接收到RPC请求后,真正完成任务的是zun.compute.manager模块。从zun.compute.rpcapi.API到zun.compute.manager.Manager的过程就是RPC调用的过程。
进入 zun/compute/rpcapi.py(API.container_create)
--> zun/common/rpc_service.py
,这个文件内构建的rpc的客户端class API
和服务端class Service
从
zun/common/rpc.py
(get_client
获取rpc的client,构建过程由oslo_messaging模块提供),其中的TRANSPORT
参数由本文件内的init
函数执行初始化,而init
函数的执行通过zun.common.config.parse_args()
调用,parse_args
被zun.common.service.prepare_service
调用,prepare_service
又被zun.cmd.api.main
调用,即在启动zun-api
构建WSGI
服务时,就已经进行了配置项的初始化。因此在rpc.py文件内找不到init的调用之处。
zun-compute详细流程分析
在zun/compute/rpcapi.py
中走到self._cast(host, 'container_create'
时,注意这里的container_create
,会到zun/compute/manager.py
中寻找对应的方法。因该文件中只有Manager
类,后续会直接说该文件中的方法名,不再说类名。
def container_create(self, context, limits, requested_networks,requested_volumes, container, run, pci_requests=None):@utils.synchronized(container.uuid)def do_container_create():# 等待 volume 创建完成,状态变为 avaiable。# -->zun.container.docker.driver.DockerDriver.is_volume_available()# -->zun.volume.driver.Cinder.is_volume_avalable() self._wait_for_volumes_available(context, requested_volumes, container)# 这一步的目的是将cinder api创建的卷映射到宿主机上,并挂载到对应目录上self._attach_volumes(context, container, requested_volumes)# 如果使用本地盘,检查本地的 quota 配额。self._check_support_disk_quota(context, container)# 创建容器created_container = self._do_container_create( context, container, requested_networks, requested_volumes, pci_requests, limits)if run:# 创建容器后如设置启动,调用 Docker客户端 启动容器self._do_container_start(context, created_container)utils.spawn_n(do_container_create)
重点过程分析
self._attach_volumes
首先说明一下,容器挂载卷的全流程如下所示:
以上1-2-3
就在self._attach_volumes(context, container, requested_volu mes)
这段逻辑中完成,因为可能是多卷挂载,因此还会进入_attach_volume
进行处理,这里是针对一个卷的处理过程,细节如下:
-
文字说明:
self._attach_volumes
:这一步的目的是将cinder api创建的卷映射到宿主机上,并完成挂载。代码往后一直找,最终落脚点在zun.volume.driver.Cinder.attach()
。其中的
cinder.attach_volume(volmap)
进入CinderWorkflow._do_attach_volume
实现功能如下:
self._connect_volume
(这一步是核心,实际执行1-2
的方法): connect volume 就是把 volume attach(映射)到 container 所在的宿主机上,建立连接的的协议通过initialize_connection
获取,如果是 LVM 类型则一般通过 iscsi,如果是 Ceph rbd 则直接使用 rbd map。
这里所需connectors是由os_brick.initiator.connectors
提供的,其中有常用的iscsi
和rbd
,在这里真正的调用系统命令来完成连接_mount_device()
实现功能如下:ensure mountpoit tree: 检查挂载点路径是否存在,如果不存在则调用 mkdir 创建目录。make filesystem:如果是新的 volume,挂载时由于没有文件系统因此会失败,此时会创建文件系统。do mount: 一切准备就绪,调用 OS 的 mount 接口挂载 volume 到指定的目录点上。最终执行在zun.common.mount.do_mount(),最后交给oslo_concurrency去具体执行挂载命令,通过subprocess.Popen构建子进程形式执行
注:在dashboard上指定的挂载点(destination)是容器内的路径,宿主机的挂载点在
ensure mountpoit tree
会自动创建(路径在zun.conf中的state_path参数配置的目录下mnt内),本例中在`/var/lib/zun/mnt/zun.volume.uuid目录下,这里的uuid对应的是zun库volume表的uuid字段,不是cinder中的id,cinder中的id对应的是zun库volume表的cinder_volume_id。
简单说,
1-2
的过程与容器无关,因此使用的是cinder volume id
,与容器映射的目录肯定就是容器相关,使用的就是容器的volume uuid。
这两个字段都在zun的volumes表中
以上的挂载实现的是卷挂载到容器宿主机的过程
_do_container_create
self._do_container_create
--> _do_container_create_base
,到这里开始执行具体操作,本质上就是组织创建容器需要的一系列参数,最后向docker提供的接口发起创建容器的请求,完成容器的创建。简要内容如下:
# 调用 Docker 拉取或者加载镜像。
self.driver.pull_image,pull or load image
# 具体执行流程:从zun/compute/manager.py(_do_container_create_base)-->zun/container/docker/driver.py(DockerDriver,配置文件配置了container_driver指向这里) -->zun/image/docker/driver.py(DockerDriver.pull_image)# 镜像拉取完成,开始创建容器,调用 Docker 创建容器。
create container: 从zun/compute/manager.py(_do_container_create_base)-->zun/container/docker/driver.py(DockerDriver.create)其中涉及到创建 docker network、创建 neutron port,最后再创建容器,向docker api发起请求创建
调用 Dokcer 拉取镜像、创建容器、启动容器的代码位于
zun/container/docker/driver.py
,该模块基本就是对社区Docker SDK for Python
的封装
创建容器过程中的网络相关流程
1、zun.api.controller.v1.containers.ContainersController._do_post
创建容器接口
2、zun.common.utils.build_requested_networks
通过调用 neutron 客户端构建请求的网络。请求参数中如果有网络相关的参数(其实就是网络的uuid),就使用。如果没有调用neutron可用network,如果还没有就抛出异常。返回可用的network 数据供后续使用
3、zun.compute.manager.Manager.container_create()
--> _do_container_create()
--> _do_container_create_base(拉取镜像开始调用驱动构建容器)
4、zun.container.docker.driver.DockerDriver.create()
,这个方法在调用python docker sdk的create_container()
方法之前,还做了很多工作,其中包括网络相关配置,具体如下:
-
1、创建
network api
,根据zun.network.network.api()
中通过配置文件获取network driver
可知,driver默认是kuryr
(参见zun.conf.network),这里最后返回的api是zun.network.kuryr_network
中的KuryrNetwork对象,并进行了初始化。 -
2、
self._provision_network
该方法字面意思:供应网络,是检查docker的network是否存在,不存在就创建。也对应了该方法的名称 -->_get_or_create_docker_network
(这里将neutron_net_id
赋值给了docker_net_name
。即原本是存在neutron中的network,但是docker没有,因此用neutron的uuid作为docker network的名字创建一个docker network) -
3、
network_api.list_networks(names=[docker_net_name])
这里的network_api就是第一步中的KuryrNetwork对象,该对象内对该list_networks调用指向的是docker客户端的networks方法,该方法在docker包的api.network.NetworkApiMixin.networks
中实现。最终调用的是docker networks ls
命令。(之前利用neutron的network创建过容器后,容器删除后docker创建的network依旧存在,后续如果还用neutron的这个network创建容器,那么会直接使用而不是新建)。如果不存在network,就创建,创建最终指向的方法是这个类中的create_network,等价于执行docker network create
,最终真正实现的docker供应网络 -
4、
self._process_exposed_ports
这一步测试时因为exposed_ports是None,所以啥也没干,具体要干嘛没有深究 -
5、
self._process_networking_config
该方法内先进行了一些数据参数获取的过程,直到下边方法
network_api.create_or_update_port
-->zun.network.kuryr_network.KuryrNetwork.create_or_update_port
见名知意,创建或更新port,返回addresses(ip)和port(neutron层面的port),处理逻辑大体如下:- 1、如果前端传过来的有port参数(前端如果同时设置了network和port,中间处理过程中会从二者中pop出列表中的后者,默认情况下会是带有port的。如果没有选择port,那么pop出来的就不会有port参数),则会将port补上deviceid,deviceowner binding host id等内容,然后更新到neutron.ports表中,也就是说该port被占用了。如果有pci相关还要进行操作。先有port的情况下,该port已经有ip地址等信息
- 2、如果没有port参数,就是用neutron创建一个port出来,创建port时,deviceid等信息都有,因此直接保存到数据库即可
(对应infoq文章的Docker libnetwork 会首先 POST 调用 kuryr 的/IpamDriver.RequestAddressAPI 请求分配 IP,但显然前面 Zun 已经创建好了 port,port 已经分配好了 IP,因此这个方法其实就是走走过场。如果直接调用 docker 命令指定 kuryr 网络创建容器,则会调用该方法从 Neutron 中创建一个 port。) - 3、docker.create_endpoint_config (docker为docker.api.container.ContainerApiMixin.create_endpoint_config)
- 4、docker.create_networking_config
- 5、docker.create_container --> docker.api.ContainerApiMixin.create_container 组织一堆参数,然后直接发送post请求交给docker进行处理了。
- 6、self._setup_network_for_container()
network_api.connect_container_to_network
-->zun.network.kuryr_network.KuryrNetwork.connect_container_to_network
这里还有个self.create_or_update_port
前面已经执行过一次了。后续就交给docker执行了
其实 kuryr 只干了一件事,那就是把 Zun 申请的 port(neutron中的port) 绑定到容器中。即 neutron–kuryr–container,kuryr是桥梁
查询容器
查询容器列表
dashboard访问url:http://192.168.221.129/dashboard/api/zun/containers/
zun-api:zun/api/controllers/v1/containers.py
路由走向:
api/root.py(RootController)
–> api/controllers/v1/__init__.py(Controller)
– > api/controllers/v1/containers.py(ContainersController.get_all)
– >_get_containers_collection
)
_get_containers_collection
做了一些验证,还有请求参数验证是否符合要求,默认情况下kwargs是{},最终进入objects.Container.list
(在模型中定义方法),指向到zun.db.api
的数据库操作中,在这里首先获取DB api
对象,配置从配置项中获取,同时后端映射采用的zun.db.sqlalchemy.api
,最终是sqlalchemy从数据库获取数据返回给dashboard
注:此过程不经过zun-compute
查询单个容器
dashboard访问url: http://192.168.221.129/dashboard/api/zun/containers/fa9cb58d-a638-4e26-a85a-dac5739f3bd5
zun-api: zun/api/controllers/v1/containers.py
路由走向:
api/root.py(RootController)
–>api/controllers/v1/__init__.py(Controller)
–>api/controllers/v1/containers.py(ContainersController.get_one)
核心逻辑:
# 获取该容器的基本信息,读取zun数据库得到
container = utils.get_container(container_ident)utils.get_container--> zun.common.utils.get_container-->zun.api.utils.get_resource-->zun.objects.container.get_by_uuid or get_by_name如果有container.host,最终会调用rpc,运行到zun.compute.manager.Manager.container_show--> zun.container.docker.driver.DockerDriver.show -->实际执行docker inspect命令得到数据 -->数据填充到container中(self._populate_container)返回
删除容器
删除一个容器
dashboard DELETE
:http://192.168.221.129/dashboard/api/zun/containers/
request body [“82d4e8f6-3e7b-428d-94f1-747175cd9cbc”]
zun-api: zun/api/controllers/v1/containers.py
路由走向:
api/root.py(RootController)
–> api/controllers/v1/__init__.py(Controller)
–> api/controllers/v1/containers.py(ContainersController.delete)
如果container.host
存在,即主机节点可以连上,调用zun.compute.api.container_delete
,通过rpc异步调用zun.compute.manager.Manager.container_delete
--> _do_container_delete
。否则调用zun.objects.container.ContainerBase.destroy
。destroy
仅仅干了一件事,就是从数据库中删除这个容器。
_do_container_delete
...
# 调用 zun/container/driver/docker.py 中的delete方法,清理容器的网络,然后向docker服务发起实质性删除容器的请求
self.driver.delete(context, container, force)
...
# 卸载卷,详见下方分析
self._detach_volumes(context, container, reraise=reraise)
...
# 摧毁容器在数据库中的内容
container.destroy(context)
卸载卷(detach volume)
卸载卷(detach volume
):zun.compute.manager.Manager._detach_volumes
zun源码中调用_detach_volumes
的只有在删除container
和container failed
时才会调用。
核心处理逻辑如下:
zun.volume.driver.Cinder.detach
def detach(self, context, volmap):self._unmount_device(volmap) # 这一步执行是linux命令 umount,卸载卷cinder = cinder_workflow.CinderWorkflow(context)cinder.detach_volume(context, volmap)
- 删除的细节:
1、umount mountpoint(zun.common.mount.py 66行)
2、递归的删除mountpoint,即挂载的目录。到这里是zun的代码执行,后续的是通过调用cinder的api执行的
3、detach_volume(cinderworkflow.py 152行)
1、begin_detaching:
--> zun.volume.cinder_api.py(begin_detaching 136行)
--> cinderclient.v3.volumes.py(VolumeManager)
--> cinderclient.v2.volumes.py(VolumeManager.begin_detaching, os-begin_detaching 具体发起post请求,url是 /volumes/volume-id/action)
这一步目的是将volume的状态改为detaching 即分离中 Update volume status to detaching。 2、如果volume mapping表中还有该volume的信息,要执行断开连接的操作,通过os_brick包完成该操作3、执行分离卷
zun.volume.cinder_api.py(detach)
--> cinderclient.v2.volumes.py(VolumeManager.detach)执行 os-detach,cinderclient发送请求。/volumes/volume-id/action 这一步目的从服务器分离卷。卷状态必须为in-use.。4、如果要删除volume,还要执行删除操作并等待删除完成,delete_volume
compute.manager._detach_volumes
--> container.docker.driver.delete_volume
--> volume.driver.delete
--> volume.cinder_workerflow.delete_volume
--> cinder_api.delete_volume
--> cinderclient.v2.volumes.delete
--> cinderclient.base._delete 至此发起删除请求/volumes/volume-id?cascade=True给cinder服务
删除卷的前提条件:
Volume status must be available, in-use, error, error_restoring, error_extending, error_managing, and must not be migrating, attached, awaiting-transfer, belong to a group, have snapshots or be disassociated from snapshots after volume transfer.
从上可以看出,detach volume与docker毫无关系,整个过程就是zun源码执行umount
,而后通过cinderclient
对卷detach操作(类似卸载u盘的操作,先执行卸载,后拔出,与u盘同理,如果卷被使用着就卸载umount不掉)
从原生使用_detach_volumes
的地方,即删除container
时调用,在detach
之前,先进行了container的删除操作。但是只要卷挂载的目录没有被正在使用,就能够执行_detach_volumes
,可以手动执行 umount /dev/sdc
进行测试。
通过 websocket 实现远程容器访问
虚拟机可以通过 VNC 远程登录,物理服务器可以通过 SOL(IPMI Serial Over LAN)实现远程访问,容器则可以通过 websocket 接口实现远程交互访问。
Docker 原生支持 websocket 连接,参考Attach to a container via a websocket ,websocket 地址为/containers/{id}/attach/ws
,不过只能在计算节点访问,那如何通过 API 访问呢,和 Nova、Ironic 实现完全一样,也是通过 proxy 代理转发实现的,负责 container
的 websocket
转发的进程为 zun-wsproxy
。
当调用 zun-compute
的 container_attach()
(代码在zun/compute/manager.py
中)方法时,zun-compute
会把 container
的 websocket_url
以及 websocket_token
保存到数据库中.。zun-wsproxy
则可读取 container
的 websocket_url
作为目标端进行转发。
相应代码在 zun\websocket\websocketproxy.py
中的_new_websocket_client()
-
具体流程
1、获取wsproxy地址的请求:
页面进入详情页时,前端发起http://192.168.221.129/dashboard/project/container/containers/aa6a20d9-6bf6-42ab-9811-a6c435cb0fce/console
,
zun-ui
会转发到zun-api
,至zun.api.controllers.v1.containers.ContainersController.attach
-->zun.compute.api.API.container_attach
–>zun.compute.rpcapi.API.container_attach
(在这里通过rpc
拿到access token
,该值保存在container
表中的websocket_token
字段,是在ws-proxy
服务中生成的token值。此时,与配置文件(/etc/zun.conf
)中的websocket_proxy.base_url
组成ws代理访问地址返回,最终返回给前端,注意这里是代理地址,真正地址是通过配置文件中的docker_remote_api_host
生成的,保存在数据库的websocket_url
中)
–>zun.compute.manager.Manager.container_attach
(完成websocket_url
和token
的生成并保存到数据库中,url实现在zun.container.docker.driver.DockerDriver.get_websocket_url
)ws-proxy
服务:代码在zun/websocket
中,实现的python包是websockify
,启动服务脚本在/usr/local/bin/zun-wsproxy
,调用zun.cmd.wsproxy.main
启动,在其中启动了websocket proxy
服务。即start_server
,该代码的实现在websockify.websocket.WebSocketServer.start_server
,注释中有提到如果连接是websockets
客户端,则为每个新的客户端连接调用new_websocket_client()
方法(该方法注释中写道,在建立新的websocket
连接后调用),该方法必须被overridden(重写)。在zun.websocket.websocketproxy.ZunProxyRequestHandlerBase
中被重写,而后被ZunProxyRequestHandler
继承,最终在zun.cmd.wsproxy.main
中被调用,new_websocket_client
的落脚点在_new_websocket_client
中,主要实现了WebSocketClient
对象并建立连接(连接地址是数据库中的websocket_url
字段),最后建立代理(self.do_websocket_proxy
)。
WebSocketClient
的连接是通过python包websocket-client
处理的。
整个流程:浏览器(ws://192.168.221.129:6784/?token=6d9d7c43-f887-4ac8-ba11-bb0b37a89174&uuid=aa6a20d9-6bf6-42ab-9811-a6c435cb0fce),访问该地址,到达zun-wsproxy
服务,该服务为每一个新的客户端连接创建 WebSocketClient
对象,最后通过do_websocket_proxy
进行代理转发到WebSocketClient
对象中设置的目标地址,即数据库中的websocket_url
字段对应的地址
更新容器
目前只支持更新cpu、name、memory
(看页面似乎disk也行,但是代码的patch方法中没有disk)
路由走向:
api/root.py(RootController)
–>api/controllers/v1/__init__.py(Controller)
– >api/controllers/v1/containers.py(ContainersController.patch)
源码中可看到,如果更新了cpu或者内存,会先检查配额,而后向·zun-compute·发起请求,进行相应的更新
对存在的容器进行重命名方法是rename(POST)api/controllers/v1/containers.py(ContainersController.rename)
,在zun-api
完成操作
原生的update按钮不知为何无法点击?存疑
容器配额quota
创建容器时的配额检查
创建容器时,会设置cpu
、memory
、如果用户没有输入,默认值在 zun/conf/container_driver.py
中。即容器驱动中设置容器的一些默认参数。disk并不在其中
另外,创建容器时会检查quota,具体代码逻辑在 zun/api/controllers/v1/containers.py
中的_check_container_quotas
方法,检查项如下所示:
deltas = {'containers': 0 if update_container else 1,'cpu': container_delta_dict.get('cpu', 0),'memory': container_delta_dict.get('memory', 0),'disk': container_delta_dict.get('disk', 0)
}
# container_delta_dict 是创建容器时提交的容器参数
项目配额
zun-ui
没有提供容器配额相关页面,因此后续操作通过命令行进行
初始配额设置位置zun/conf/quota.py
查询默认配额
查询默认配额GET
:/container/v1/quotas/{project_id}/defaults
这个接口获取的是配置文件(zun/conf/quota.py
)中设置的数据
获取项目配额
命令:zun quota-get project_id
接口:GET /container/v1/quotas/{project_id}
路由落脚点在:zun/api/controllers/v1/quotas.py(QuotaController.get)
代码执行过程:
QuotaController.get
–>QuotaController._get_quotas
,通过QUOTAS对象
–zun/common/quota.py(QUOTAS,QUOTAS.register_resources(resources))
。创建resources时
–>CountableResource(继承自BaseResource)
–>BaseResource(default方法)
–> zun/conf/quota.py
(该代码内全是设置的默认值,上边执行的命令在数据库没有数据时,就从此处获取默认配置)
具体执行到conf/quota.py
中获取参数的代码在zun/common/quota.py(DbQuotaDriver.get_defaults
for resource in resources.values():quotas[resource.name] = default_quotas.get(resource.name, resource.default)
这里的resource.default
就是获取默认值,resource是CountableResource
对象,继承自BaseResource,有方法default,但是该方法有@property装饰器,因此可以用属性的调用方法调用
zun.conf
也能配置这个参数
quota-defaults Print a default quotas for a project
quota-delete Delete quotas for a project
quota-get Print a quotas for a project with usages (optional)
quota-update Print an updated quotas for a project
quota-class-get Print a quotas for a quota class
quota-class-update Print an updated quotas for a quota class
更多信息参考 sample-config.html
增加和修改项目配额
增加和修改项目配额接口PUT
:/container/v1/quotas/{project_id}
请求体:
{"disk": 200, "cpu": 30,"containers": 80,"memory": 102400
}
这里的一个参数对应quotas表中的一个resource字段,即resource字段内记录的是以上四个选项。另外这里使用postman发起请求时,不是
json
格式,content-type
需要设置成application/x-www-form-urlencoded
代码执行完之后数据会写入quotas
表中,表中原本不需要有该条记录。每次执行put请求,都不是更新,而是新增记录,取数据取的最新的记录进行返回。数据库的updated_at字段发现未生效。
删除配额
接口:DELETE
:/container/v1/quotas/{project_id}
返回null,会将数据库中这个project_id
相关的所有记录全部删除
镜像的增删改查
官方没有
image api
只有container api
,以下内容从源码中分析得到。如有错漏,请告知
zun-ui提供的镜像功能(非镜像仓库)
选择拉取镜像
提供的功能点:
1、pull image
,从配置的镜像仓库拉取指定镜像(镜像名)到指定主机上
2、删除镜像(注意不要手动在服务器删除zun拉取的镜像,否则zun-ui会删不掉该镜像在数据库的数据,执行不到这一步还看不到错误输出。实际应该在zun/container/docker/driiver.py
-->delete_image
第一行就有问题了,因为查不到这个镜像了)
3、过滤搜索镜像,即页面上的搜索框(注意这里走的接口仍然是获取所有镜像,前端完成的过滤,与通常理解的搜索逻辑不同)
4、镜像列表
本质上这里执行的是docker pull alpine
,从镜像仓库拉取镜像到本地
拉取完成后,在对应的host
上通过docker images
命令可以看到拉取成功的镜像。
API(无官方文档):
-
1、获取所有镜像信息:
GET
/container/v1/images
-->zun.api.controllers.v1.images.ImageController.get_all
-
2、获取单个镜像信息:
GET
/container/v1/images/{image_id}
-->zun.api.controllers.v1.images.ImageController.get_one
-
3、创建新的镜像
pull image
:
POST
/container/v1/images
-->zun.api.controllers.v1.images.ImageController.post
请求体:{"repo":"alpine","host":"e8228dc2-bc81-4488-a5fc-11548252b009"}
zun-api
详细过程:... # 获取host信息 host = _get_host(image_dict.pop('host')) ... # 将image信息存入数据库 new_image.pull(context) ... # 调用zun-compute的image_pull接口 pecan.request.compute_api.image_pull(context, new_image)
zun-compute
过程,具体逻辑在zun/compute/manager.py
中的_do_image_pull
方法中。_do_image_pull()
–>zun.container.docker.driver.DockerDriver.pull_image
(无论image driver是docker还是glance,都会先进这个地方,这是container driver,则必然是docker,默认的imagedriverlist从conf.image_driver中得到,只有glance和docker,配置文件可以指定以覆盖这个配置文件)
–>zun.image.docker/glance.driver.DockerDriver/GlanceDriver.pull_image
(在此处最终调用相应的代码拉取镜像,或docker或glance,docker的就交给docker包操作的,最终执行的过程就是docker pull
)镜像拉取完成后数据image对象保存到数据库中
-
4、删除镜像:
DELETE
/container/v1/images/{image_id}
-->zun.api.controllers.v1.images.ImageController.delete
-
5、
search
方法(注意,该接口不是zun-ui镜像页面的搜索框对应的方法。该接口请求的是docker api
中的search images
接口):GET /container/v1/images/search?image=ubuntu&image_driver=docker&exact_match=false
注意:该接口原生版本有问题,是无法使用的,具体问题如下:
1、schema.query_param_search
中没有image
参数,而接口需要的第一个参数就是image
2、pecan.request.compute_api.image_search
与创建容器时指定镜像后最终调用的是同一个接口。但是这里少了一个registry参数,会导致最终调用zun.compute.manager.Manager.image_search
时报错,缺少位置参数。改成如下:return pecan.request.compute_api.image_search(context, image, image_driver, exact_match, None)
不能传递命名参数,只能传递位置参数,因为
zun.compute.api.API.image_search
中接受的是*args
位置参数,而不是**kwargs
。
页面中也未见到调用该接口的地方
通过命令行创建的镜像zun-ui看不到,或通过zun-ui pull的镜像,命令行也看不到,但是数据库是有数据的,造成该现象的原因可能是命令和页面看的不是一个项目所导致
容器commit
通过原生zun-ui,是没有提供类似docker commit
功能的入口的,但是源码中提供有接口完成此功能。
zun-api中commit接口
def commit(self, container_ident, **kwargs):"""Create a new image from a container's changes.:param container_ident: UUID or Name of a container."""container = utils.get_container(container_ident)check_policy_on_container(container.as_dict(), "container:commit")utils.validate_container_state(container, 'commit')LOG.debug('Calling compute.container_commit %s ', container.uuid)context = pecan.request.contextcompute_api = pecan.request.compute_apipecan.response.status = 202return compute_api.container_commit(context, container,kwargs.get('repository', None),kwargs.get('tag', None))
核心只在最后调用compute完成此功能
zun-compute中commit
def container_commit(self, context, container, repository, tag=None):# NOTE(miaohb): Glance is the only driver that support image# uploading in the current version, so we have hard-coded here.# https://bugs.launchpad.net/zun/+bug/1697342# 从上提到的glance是支持镜像上传的唯一驱动。docker是不支持的。创建glance镜像,此处并未上传镜像,尚未使用docker commit从容器创建一个新的镜像snapshot_image = self.driver.create_image(context, repository,glance.GlanceDriver())...self._do_container_commit(context, snapshot_image, container,repository, tag)
_do_container_commit
核心内容如下:
...
# ensure the container is paused before doing commit
# commit之前要确保容器状态是暂停
container = self.driver.pause(context, container)
...
# 执行docker commit操作
# 流程经过 zun/container/docker/driver.py(commit) --> docker/api/container.py(commit) 实质上向docker服务提供的api发起commit操作
container_image_id = self.driver.commit(context, container, repository, tag)# 获取image,核心执行的是docker/api/image.py(get_image)
# Get a tarball of an image. Similar to the ``docker save`` command. 可以看到执行的是docker save操作,返回镜像数据
container_image = self.driver.get_image(repository + ':' + tag)
...
# 如果之前操作了暂停容器,此时要将容器恢复
container = self.driver.unpause(context, container)
...
# 将镜像上传到glance,当前版本不支持上传到dockerhub等仓库
self._do_container_image_upload(context, snapshot_image,container_image_id,container_image, tag)
docker api
见 create a new image from a container
Export an image get_image接口
浏览器通过非attach
方式访问容器的可行性研究
docker exec
docker exec
命令的流程(该命令在docker api中相当于执行俩api,分别是create an exec instance
和 start an exec instance
)
该命令的实质是让容器能像宿主机中一样执行命令。只是可以通过指定i
和t
参数,来得到一个tty
,方便操作。最终实质是要执行的命令。
因此要执行的命令是不可缺少的。这也决定了通过该命令开一个终端是不现实的,因为没有命令不会给你显示伪终端。而要执行命令,如bash,sh等,又不确定每个容器内都存在,就导致不能使用该方案。
在python对应接口中,api文档中给了四个参数分别是 container_ident,command,run,interactive
run在代码中默认值是true,即exec create
之后还要执行exec run
,实际就是docker api中的start,interactive(代码中默认值false)代表了docker api中的两个参数AttachStdin和Tty(zun.container.docker.driver.DockerDriver.exec_create中进行了赋值,二者值与interactive保持一致)
执行流程:
zun.api.controllers.v1.containers.ContainersController.execute
–>zun.compute.manager.Manager.container_exec
–>zun.container.docker.driver.DockerDriver.execute_create
–>docker.api.exec_api.ExecApiMixin.exec_create
,在这里拼装了请求体,拼装了url,最后发起post请求,self._url
和self._post_json
在–>docker.api.client.APIClient
中实现,post请求调用的是requests发起的
如果run是true就要start
–>zun.container.docker.driver.DockerDriver.execute_run
–>docker.api.exec_api.ExecApiMixin.exec_start
这一步对接的就是docker api的start exec接口(这里将detach、tty、stream参数全部写死为false了)
但是start传递的参数比docker api要多,而且docker api没有返回值,这里却有返回值可以接收
start之后,还要执行 exec_inspect 获取容器的简单信息,对应的也是docker api中的inspect an exec instance
ssh方式
在使用 autodl 平台时,发现该平台提供给用户的就是容器,可以在浏览器访问,执行exit也不会导致容器停止。经过调研发现该平台是通过ssh方式访问容器的。
但是ssh方式也不可行,ssh连接需要服务器内安装有sshd服务并开启。每个容器是否存在该服务是不确定的,因此该方案也不行
关于ssh方式实现访问可参考xterm.js
研究该问题的初衷是因为浏览器上进入容器后,在容器内执行
exit
命令,会使容器直接退出,状态变为Stopped
,这是因为本质上是通过docker attach containerid
的方式进入容器的,而该方式在容器内执行exit,就是会导致容器停止。希望通过其他方案不让容器退出,但是目前看没有可行性
paste配置文件详解
# 这是一个composite段,表示这将会根据一些条件将web请求调度到不同的应用
[composite:main]
use = egg:Paste#urlmap # 表示我们将使用Paste egg包中urlmap来实现composite
/ = home
/blog = blog # 根据web请求的path的前缀进行一个到应用的映射(map)
/wiki = wiki
/cms = config:cms.ini # 映射到了另外一个配置文件,Paste Deploy再根据这个文件进行载入
# app是一个callable object,接受的参数(environ,start_response)app需要完成的任务是响应envrion中的请求,准备好响应头和消息体,然后交给start_response处理,并返回响应消息体
[app:home]
use = egg:Paste#static # Paste包中的一个简单程序,它只处理静态文件
# 它需要一个配置文件document_root,后面的值可以是一个变量,形式为%(var)s相应的值应该在[DEFAULT]字段指明以便Paste读取。
document_root = %(here)s/htdocs [filter-app:blog] # filter是一个callable object,其唯一参数是(app),这是WSGI的application对象,filter需要完成的工作是将application包装成另一个application(“过滤”),并返回这个包装后的application。
use = egg:Authentication#auth
next = blogapp # 在正式调用blogapp之前,我会调用egg:Authentication#auth进行一个用户的验证,随后才会调用blogapp进行处理
roles = admin
htpasswd = /home/me/users.htpasswd[app:blogapp] # 定义了blogapp,并指明了需要的database参数。
use = egg:BlogApp
database = sqlite:/home/me/blog.db[app:wiki]
# call(表示使用call方法):模块的完成路径名字:应用变量的完整名字
use = call:mywiki.main:application # 使用call的话,相应的函数,类,实例中必须实现call()方法。
database = sqlite:/home/me/wiki.db
# pipeline就是简化了filter-app,不然你想,如果我有十个filter,那不是要写十
# 个filter-app(有next),通过pipeline,我就可以把这些filter都连起来写在一行,很方便。但要注意的是这些filter需要有一个app作为结尾。
[pipeline:main]
pipeline = cors request_id osprofiler authtoken api_v1
# 定义WSGI应用,main表示只有一个应用,有多个应用的话main改为应用名字
# 定义application需要运行的Python code
# 这种方式必须明确指定使用的protocol(此例中是paste.app_factory),value值表
# 示需要import的内容。此例中是import zun.api.app,然后检测app_factory执行。
[app:api_v1]
paste.app_factory = zun.api.app:app_factory
# 就是一个过滤
[filter:authtoken]
acl_public_routes = /, /v1
paste.filter_factory = zun.api.middleware.auth_token:AuthTokenMiddleware.factory[filter:osprofiler]
paste.filter_factory = zun.common.profiler:WsgiMiddleware.factory[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = zun
uwsgi部署实现
参考官方文档 https://docs.openstack.org/zun/latest/contributor/mod-wsgi.html
创建文件 /etc/zun/zun-uwsgi.ini
,放在其他位置也是可以的
[uwsgi]
http = 0.0.0.0:9517
wsgi-file = <path_to_zun>/zun/api/app.wsgi
plugins = python
# This is running standalone
master = true
# Set die-on-term & exit-on-reload so that uwsgi shuts down
exit-on-reload = true
die-on-term = true
# uwsgi recommends this to prevent thundering herd on accept.
thunder-lock = true
# Override the default size for headers from the 4k default. (mainly for keystone token)
buffer-size = 65535
enable-threads = true
# Set the number of threads usually with the returns of command nproc
threads = 8
# Make sure the client doesn't try to re-use the connection.
add-header = Connection: close
# Set uid and gip to a appropriate user on your server. In many
# installations ``zun`` will be correct.
uid = zun
gid = zun
启动:uwsgi ./zun-uwsgi.ini
后台启动:uwsgi -d ./zun-uwsgi.ini
依旧是使用paste启动wsgi 应用,对比二者启动方式发现,这里仅是用uwsgi
代替了 zun.cmd.api.main
中的wsgi server功能,不再走zun提供的wsgi服务,而是走wsgi-file
指定的文件,如zun/api/app.wsgi
,其余不变,仅此而已
如果要默认使用uwsgi启动zun-api,还得修改/etc/systemd/system/zun-api.service
postman测试接口说明
当无法通过dashboard完成请求时,需要使用postman或其他工具直接访问zun-api
提供的接口
获取服务接口
通过openstack endpoint list
命令可以看到各个组件的接口,如下所示:
如zun-api
对应:http://controller:9517/v1 ,然后再到各组件的api文档中查看各个请求的接口url,拼接后就是完整的请求路径。
使用组成的url直接通过浏览器访问是会报401错的。因为没有携带认证信息
通过以下命令获取token
source /etc/keystone/admin_openrc
这个文件由自己决定放在哪里,内容大概如下:
export OS_PROJECT_DOMAIN_NAME=Default
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_NAME=admin
export OS_USERNAME=admin
export OS_PASSWORD=hy@123
export OS_AUTH_URL=http://controller:5000/v3
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2
通过openstack token issue
可以拿到当前用户的token,下图中的id就是
然后请求上边的url时将token加入header中。参数名是 X-Auth-Token 值就是token。这样就可以测试所有api了。
发现的一些问题
1、服务器重启
创建有容器的服务器如果发生重启,此时挂载的硬盘会全部丢失。发生这种问题是致命的。容器挂载硬盘这块的实现关系如下:
cinder卷
--> map到宿主机/dev/sdb
--> mount /var/lib/zun/mnt/zun.volume.uuid 目录
--> /var/lib/zun/mnt/zun.volume.uuid:/data
服务器重启后cinder卷还在,容器也在,但是中间两步丢失了,因此在服务器重启后再次重启容器时需要完成中间两步即可。这部分处理逻辑是源码中的attach_volume
,因此可以复用该代码进行适当调整实现服务器重启后挂载卷功能,映射关系保持不变。
2、容器启动失败
此处的启动失败特指之前创建好的可以正常运行的容器,因为某些原因状态变为stopped
,当再次启动该容器时由于外在原因(如docker服务未启动、kuryr-libnetwork未启动)导致容器启动失败,源码中的处理逻辑是直接走_fail_container
的逻辑,在该部分代码中会执行_detach_volumes
,这里会将卷从容器卸载,并且如果卷设置的是卸载后自动删除,还会将卷直接删掉。这种bug是不可承受的。
当前思考的处理方式是在容器启动失败时,判断容器状态,如果状态是stopped,并且当前正在执行的状态是starting,则只卸载卷,而不删除卷,再新建容器将这个卷挂到新容器上实现数据的保留。
参考链接:
OpenStack 容器服务 Zun 初探与原理分析
container service api
zun configuration options
installing the api via wsgi
Pecan web 框架简介
pecan框架的使用
about
欢迎关注我的博客
这篇关于openstack zun源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!