侧边栏壁纸
博主头像
AI研究僧

hycj89@163.com

  • 累计撰写 1,899 篇文章
  • 累计创建 179 个标签
  • 累计收到 1 条评论
标签搜索

目 录CONTENT

文章目录

openstack zun源码分析

AI研究僧
2023-02-07 / 0 评论 / 0 点赞 / 1,011 阅读 / 11,058 字

容器服务启动过程

项目包括三个服务,分别是zun-apizun-wsproxyzun-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-computecontainer_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_argszun.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![在这里插入图片描述](https://img-blog.csdnimg.cn/1aa01a89ad6b47dcb9dca639b454b077.png) 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提供的,其中有常用的iscsirbd,在这里真正的调用系统命令来完成连接

    _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.destroydestroy仅仅干了一件事,就是从数据库中删除这个容器。

_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的只有在删除containercontainer 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 代理转发实现的,负责 containerwebsocket 转发的进程为 zun-wsproxy

当调用 zun-computecontainer_attach() (代码在zun/compute/manager.py中)方法时,zun-compute 会把 containerwebsocket_url 以及 websocket_token 保存到数据库中.。zun-wsproxy 则可读取 containerwebsocket_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_urltoken的生成并保存到数据库中,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

创建容器时的配额检查

创建容器时,会设置cpumemory、如果用户没有输入,默认值在 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.context
    compute_api = pecan.request.compute_api
    pecan.response.status = 202
    return 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 apicreate a new image from a container
Export an image get_image接口

浏览器通过非attach方式访问容器的可行性研究

docker exec

docker exec命令的流程(该命令在docker api中相当于执行俩api,分别是create an exec instancestart an exec instance

该命令的实质是让容器能像宿主机中一样执行命令。只是可以通过指定it参数,来得到一个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._urlself._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框架的使用

博主关闭了所有页面的评论