
OpenStack Developer, Opensource Lover :- )
前言在去年(2021)的 AWS re:Invent 上,我非常诧异于 AWS 居然能够如此神速的向市场推出 AWS Private 5G 服务。要知道,就在不久前(2021 年 2 月 24 日),FCC(美国联邦通信委员会)才宣布了 C 波段 3.7GHz-3.98GHz 的 5G 频段拍卖结果 —— 本轮拍卖创造了 FCC 有史以来最高的拍卖记录,总金额达 811 亿美元,由 21 家运营商获得共 5684 个频段牌照。可以想象,AWS Private 5G 服务必然是 AWS 蓄谋已久的 “重磅炸弹”。笔者作为 ICT 从业者,自然也对其充满了无限的好奇,不妨就今天来尝试解读一下 AWS Private 5G 的技术内幕。近几年 AWS 在 5G ICT 领域的部署既然说是蓄谋已久,当然也不能无稽之谈。首先不妨再次回顾一下笔者所了解到的,AWS 近些年在 5G ICT 领域中所做的战略部署。AWS 与 Verizon 合作推出的 Private MEC 解决方案2020 年 8 月,Verizon 将 5G 专网和 MEC 平台部署在 AWS Outposts 上。通过 AWS 与 Verizon 合作推出的 Private MEC 解决方案,Verizon 的客户可以使用私有的 MEC 平台,快速地部署实时的 MEC App,例如:智能物流、预测性维护、机器人技术、工厂自动化等应用,并以此降低成本、提高安全性、精确度和效率。该方案可以应用于工厂、仓库、大型商业园区等特定的场景中,提供了一个安全且边缘的专用计算平台,支持统一的连接、计算和存储,并且无需客户支出成本高昂的网络和 IT 基础设施。同时,因为采用了 AWS Outposts 作为 IT 底座,所以该方案还可以为客户提供可靠、安全、高带宽、低延迟的 AWS 服务、API、以及在 AWS Outposts 上运行的所有工具。AWS 与 Vodafone Business 合作推出 MEC 解决方案同样是,2020 年 8 月,AWS 还与 Vodafone(沃达丰 )合作推出 MEC 解决方案。区别于前面提到的解决方案使用了 AWS Outposts 作为 IT 底座。AWS 与 Vodafone Business 合作推出 MEC 解决方案采用的是 Amazon Wavelength 作为 IT 底座。通过在 Vodafone 的 4G / 5G Network Edge DC(网络边缘数据中心)中嵌入 AWS Wavelength 所提供的计算和存储服务,能够帮助客户侧的开发人员为 UE(移动设备)等终端用户构建低延迟的 5G MEC 应用程序。例如:变更监控以及设施安全、智慧汽车、智慧城市、医疗保健和实时交互视频流。与 AWS Outposts 相同的点是,AWS Wavelength 也可以为客户提供可靠、安全、高带宽、低延迟的 AWS 服务、API、以及在 AWS Wavelength 上运行的所有工具。使得开发者在任何的电信边缘云上也能够获得与在 AWS 中心云上一致的服务体验。AWS 与 ERICSSON、Telefonica Germany 合作推出公有云化的 5G 专网解决方案2020 年 9 月,AWS 与 ERICSSON(爱立信 5G 设备提供商)、Telefonica Germany(西班牙电信德国公司)合作推出公有云化的 5G 专网解决方案。从官宣的总体架构看,有 ERICSSON 提供的 5GC CP NFs(5G 核心网控制面网元)以 CNF 的形式运行在 AWS Region 之上,而 UPF 以及 5G NR 的 CU-CP 则以 VNF 的形式运行在部署到客户现场的 AWS Outpost 上。其中,对于 5GC CP NFs,推荐使用了 AWS EKS 进行承载;对于 5GC UPF 和 5G NR CU-CP 则推荐使用了 AWS EC2 进行承载。对于运行在 AWS Outputs 上的 VNF 可以基于 SR-IOV、DPDK 等的网络优化技术,同时结合 AWS Nitro System 技术,通过将 Hypervisor、安全、存储等相关的工作负载 Offload 到专用硬件板卡上,可以基本实现等价于物理机性能的 VNF,使得 5G UP 的转发速率可以达到惊人的 100Gbps。另外,根据 Telefonica Germany 的业务需求,也考虑了使用 SONiC 或 P4 白盒交换机来实现 UPF 设备,继而支持 Tbps 级别转发,完全可以同时满足 toC / toB UPF 的性能需求。AWS 与 Dish 合作推出公有云化的 5G 专网和 5G 公网解决方案2021 年 4 月,Dish 官网宣布,选择了 AWS 作为其首选的云提供商,并将在 AWS 上构建其 5G 网络。根据双方的合作协议,两家公司将携手为 toC 消费者和 toB 企业客户提供 5G 公网和 5G 专网服务。Dish 准备将 5G Core 和 RAN/O-RAN 全部 xNF(VNF / CNF)化,并部署于 AWS 上。同时,Dish 自身的的 BOSS 系统也将部署于 AWS 之上。Dish 认为,在 AWS 上部署 5G 网络会有利于节省 CT 网络建设的成本和加速 IT 应用的创新。节省 CT 网络建设的成本:Dish 只需将 5G 网络部署于 AWS 上,而无需自己投资建设、运维相关的网络硬件和 IT 基础设施。这意味着一个可能的行业趋势 —— 设备商以后不必向运营商提供物理硬件了,只需提供网络功能软件即可。加速 IT 应用的创新:利用 AWS 的 AI、云编排和 CI/CD 等通用的软件堆栈和服务能力,可帮助 Dish 及其客户的开发人员简化和加速 5G 行业应用和解决方案的创新。AWS 与 Swisscom 合作推出混合云化的云原生 5G 核心网解决方案2021 年 6 月,AWS 宣布正式成为 Swisscom(瑞士电信)的首选公有云提供商。Swisscom 希望通过推行云优先战略,来提升 IT 敏捷性与运营的效率,继而缩短全新 ICT 产品和服务的上市时间。Swisscom 将 AWS 作为全面数字化转型的关键部分,会陆续的把大量业务支撑系统都迁移至 AWS,包括:企业资源管理系统(ERP)运营支撑系统(OSS)业务支撑系统(BSS)数据分析系统联络中心系统通信配置系统等等此外,Swisscom 还将利用 AWS 成熟的云基础设施和服务,探索如何在云中建立可靠、可扩展、安全且具高性价比的 5G 核心网,为客户快速开发和部署 5G 服务。具体地说,Swisscom 将尝试把当前传统基础设施上的 5G 网络迁移至 AWS 之上的由云原生 5G 核心网所支撑的全新 5G SA 网络。Swisscom 预期,在云原生 5G SA 核心网投入使用之后,可以有效降低运营成本,并通过虚拟化和自动化网络功能来提高可靠性和可扩展性,加快部署新功能和面向客户的应用。AWS Private 5G在回顾了近两年来 AWS 与生态合作伙伴共同推出的那么多 5G 相关解决方案之后,可以理解 AWS Private 5G 的推出是那么的自然而然了。在 AWS re:Invent 上,AWS CEO Adam Selipsky 表示:“通过 AWS Private 5G,您可以在几天内而不是几个月内,建立和扩展一张移动蜂窝通讯专网。您可以获得移动技术的所有优点,而无需经历漫长的规划周期、复杂的集成和高昂的前期成本。”目前 AWS Private 5G 以预览版的托管服务的形式对市场发布,其旨在降低 5G 专网建设的复杂性,简化 5G 专网的部署,通过托管服务的方式来消除企业 5G 专网目前多面临的挑战,包括:前期的网络架构设计、网络规划、硬件和软件的采购与集成、安全法规的许可和定价模型,以及随着流量增长而暴露的扩展需求。简而言之,AWS Private 5G 是一项全新的托管服务,通过 AWS 提供的所有必需的硬件和软件,客户可以轻松部署、管控和扩展自己的 5G 专用蜂窝网络。极大地降低任何对此解决方案感到兴趣的企业的准入门槛。综上,我们可以简要的总结一下 AWS Private 5G 的产品理念:完全云化的 5G 专网:这意味着 5G 专网天然的具备了云计算的关键特性,包括:按需付费、弹性伸缩、高可靠、IT 敏捷性。这是节省 CT 网络建设成本和加速 IT 应用创新的关键。完全托管的 5G 专网:这意味着 5G 专网的订购是完全自服务的、合规的、简易的。用 Adam Selipsky 的话来说就是:“它非常易用,您只需要告诉我们建设网络的位置和容量,我们将为您提供所有必需的硬件、软件和 SIM 卡。一旦设备通电,5G 专网就会非常轻松地自动配置和建立一个移动网络。您只需要将 SIM 插入 UE(终端设备),然后一切就都联网了。”简而言之,AWS Private 5G 服务集成打包了建设一个 5G 专网所需要的 5G 共享频谱(默认使用 CBRS(美国公民宽带无线电服务),也支持其他许可频谱)、Small Cell 基站设备、通用服务器设备、5G 核心网软件和 RAN 软件、SIM 卡、以及集成了 SIM 卡资源和 Traffic Group 资源的的 AWS IAM(Identity and Access Management)控制台。用户只需点击几下鼠标即可进行购买,然后网络管理员就能够直接控制、设置移动设备在其 5G 专网络的资源访问权限以及带宽流量了。5G CP 和 UP 集中式部署。5G CP 集中式部署,UP 分布式部署。并且所有的这些都由 AWS 负责交付和维护,同时,AWS Private 5G 不需要预付费用或按设备来收费,只需为网络容量和吞吐量付费即可,这是一种完全的云消费定价模式。可见,AWS 这一次的重磅炸弹再次引领了行业的革新理念 —— 像其他任何 IT 采购一样采购 5G 专网。AWS 这一举动也将对整个市场产生重大影响。更重要的是,AWS 有一个明确的目标,它将 5G 专网视为通向更高价值服务和构建企业数字化转型的重要抓手。Demo 视频https://www.youtube.com/watch?v=kP3oEhWRh5E
目录 目录 相关文章 Socket 与 HTTP 的区别 生产实践考虑 网络断开重连问题 Heartbeat 心跳机制 使用非阻塞模式下的 select 函数进行 Socket 连接检查 会话过期问题 同步还是异步问题 数据缓存问题 完全断开连接问题 相关文章 NOTE:本文假设你已经对 Socket 的使用有一定的了解。 Python Module_Socket_网络编程 Socket 与 HTTP 的区别 首先通过对比法来了解两者不同的特性: HTTP:超文本传输协议,首先它是一个协议,并且是基于 TCP/IP 协议(传输层)之上的应用层协议,要想通过 HTTP 来进行通信,首先需要双方建立起 TCP/IP 连接,因为TCP/IP 主要解决的问题是数据如何在网络中传输,而 HTTP 协议主要解决的问题是如何包装需要传输的数据。所以 HTTP 协议能够支持使用 Header 信息来详细规定浏览器与服务器之间的通信规则。HTTP 连接是基于 Request-Response 的非持久(短)连接,其连接的生命周期通过 Request 界定,一个 Request 一个 Response,此次 HTTP 连接的生命周期结束。对于这点在 HTTP 1.1 中进行了改进,支持 Keep-alive,允许在一个 HTTP 连接的生命周期中,可以发送多个 Request,接收多个 Response,但由 Request 来界定生命周期的本质没有改变。 Socket:首先需要注意的是 Socket 不是一种协议,而是一种调用接口(API)或称之为 TCP/IP 的基本操作单元。Socket 实际上是对 TCP/IP 协议的封装,通过 Socket,就能够应用 TCP/IP 协议来建立连接并传输数据。Socket 连接是持久化(长)连接,这种连接的生命周期能够自主控制,也就是说客户端和服务器端一旦建立连接后,理想状态下连接会持久存在,直到一方自动发出断开指令。 生产实践考虑 下面列出在生产项目中应用 Socket 所需要注意的几点: 网络断开重连问题 连接会话和身份认证问题 同步和异步问题 数据缓存问题 完全断开连接问题 网络断开重连问题 的确,Socket 连接在理想的网络环境下是持久的长连接,但实际网络环境是复杂的,网络抖动、路由宕机等各种网络问题都会导致 Socket 连接被动断开。而且 Socket 没有提供「自动重连」的机制,所以解决网络断开重连问题,是 Socket 程序稳定性的重要保证。 思路:在 发送 和 接收 前检查 Socket 连接是否依然生效,若不生效,则重新建立 Socket 连接。 那么首先需要解决的是:如何判断 Socket 连接状态是否 ACTIVE? NOTE:一般的来说,「判断 Socket 连接状态是否 ACTIVE」都是服务端的功能需求,因为服务端需要以此来作为是否回收连接资源的依据。而客户端则无需特别在意,因为即便断开了连接也只需捕获异常、重新连接、重新发送即可。 Heartbeat 心跳机制 别名定义: 客户端 Socket == cli-socket 服务端 Socket == ser-socket 一般的 Socket 应用程序逻辑中,ser-socket 应该能够感知到 cli-socket 的断开,并且执行相应的断开逻辑处理,释放相应的 Socket 连接资源。但实际是,ser-socket 无法有效的区分 cli-socket 是处于长时间空闲还是处于 offline 的状态,所以也无法确定 cli-socket 的连接是否已经断开。为解决这个问题,程序员所提出的思路就是,屏蔽「长时间空闲」的场景,让 cli-socket 看起来始终是忙碌的(不断发送「无用包」),直到其静默即表示连接断开。 这就是较为通用的用于保证连接质量的心跳机制,而 cli-socket 发送的无用包也称之为心跳包。所谓心跳包就是 cli-socket 定时发送简单的协议信息给 ser-socket,以此让 ser-socket 知道 cli-socket 依旧 online。相对的, ser-socket 就会认为 cli-socket 已经断开。注意,发包方可以是 cli-socket 也可以是 ser-socket,但出于效率的考虑(减轻服务器压力),一般由 cli-socket 承担。如果是流式 Socket(for TCP),则使用 send 发出;如果是数据报式 Socket(for UDP),则使用 sendto 发出。还有一点需要说明,心跳包实际是一个自定义协议包,由开发者制定,并在 cli-socket 和 ser-socket 中遵守。 NOTE:如果仅为了确定 ser-socket 是否 online,可以用 TCP 协议自带的心跳包,应用 socket.socket.setsockopt 的 SO_KEEPALIVE 属性,来设置发包时间间隔。SO_KEEPALIVE 是操作系统的底层机制,用于维护每一个 TCP 连接。但SO_KEEPALIVE 并不能用于替代心跳机制,因为其仅能确保 ser-socket 一方的连接状态。 使用非阻塞模式下的 select 函数进行 Socket 连接检查 以异步(非阻塞)模式建立连接 s.setblocking(0),如果 select 函数返回的值为 1 (表示 Socket 可读),但使用 recv 函数读取到的数据长度为 0,并且 errno != EINTR and errno != EAGAIN,则说明该 Socket 已经断开 NOTE:需要注意的是,在非阻塞模式下,即便 recv 函数的返回值小于等于 0,依旧不足以证明问题。此时还需要继续判断 errno ?= EINTR,如果 Yes,则说明此次 recv 是由于程序接收到 EINTR 中断信号后返回的,Socket 连接仍然正常。除此之外,如果 write 写的太快,很有可能会把 Buffer 写满,这时的 errno == EAGAIN。根据实际需要,如果 errno == EAGAIN 的话,建议重试几次。当然,Read 也有类似的情况。 会话过期问题 如果服务端程序应用了 Session 机制,那么在实现客户端程序时除了需要考虑 Socket 连接的问题之外,还需要考虑 Session 是否过期的问题。 在 发送 和 接收 前首先检查连接是否有效,然后检查会话是否过期: 连接失效:则重新建立连接,并且重新创建 session 连接有效,但会话过期:则重新创建 session 连接有效,会话有效:PASS 同步还是异步问题 选择同步还是异步模式是非常重要的,使用了错误的连接模式将无法达到预期效果。例如高并发需求没能达到,例如程序的稳定性没能提高等等,如何进行选择需要结合实际的应用场景: 在高并发且不关注执行结果的场景中使用异步模式。 在对程序执行的稳定性,对执行结果响应的准确性都有很高要求的场景下使用同步模式,并且需要保证一次 send 和 recv 的原子性。 NOTE:对于后者,应该尽可以能仅保证 send 和 recv 原子操作是同步的,以此来优化效率。 数据缓存问题 Socket 的 recv 具有缓存功能,如果其中一方发送的数据量超过了另一方 recv 所允许一次接受的最大数据量,数据会被截短,并将剩余的数据缓冲在接收端。再次调用 recv 时,剩余的数据会从缓冲区取出并删除。这一特性与 HTTP 连接方式有很大区别,表示 Socket 连接无法像 HTTP 连接那般,一次 Request 对应的一次 Response 就能完成一次操作单元,而是很可能需要任意次的 send 以及任意次的 recv 才能完成。换句话说就是,需要开发者来保证 send 和 recv 的完整性,你需要手动的整理、组合出完整的发送和响应结果数据。经常的,为了得到一个完整的响应结果可能需要进行多次 recv。 完全断开连接问题 调用 Socket 的 close 函数并不会马上断开 Socket 连接,一般的我们会在 close 之前调用 shutdown 函数来确保连接会被正常关闭。而且 shutdown 提供了多种不同的关闭方式: SHUT_RD:关闭读,不能使用 read/recv SHUT_WR:关闭写,不能使用 send/write SHUT_RDWR:关闭读写,不能使用 send/write/recv/read NOTE:在客户端程序中,一般我们会选择使用 SHUT_WR 方式,立即停止写操作,但可以继续将响应数据读完。而在服务端程序中一般会选择 SHUT_RD 模式,立即停止对客户端请求的读取,但会继续完成响应。当然了,在某些对精度要求不要的场景中,SHUT_RDWR 是不错的选择。
目录 目录 前文列表 将已存在的虚拟机恢复到指定时间点 恢复为新建虚拟机 灾难恢复 恢复细节 恢复增量备份数据 以 RDM 的方式创建虚拟磁盘 创建虚拟机 Sample of VirtualMachineConfigSpec Demo of VirtualMachineConfigSpec 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT VMware 虚拟化编程(11) — VMware 虚拟机的全量备份与增量备份方案 VMware 虚拟化编程(12) — VixDiskLib Sample 程序使用 VMware 虚拟化编程(13) — VMware 虚拟机的备份方案设计 VMware 虚拟化编程(14) — VDDK 的高级传输模式详解 将已存在的虚拟机恢复到指定时间点 使用 vSphere WS API 连接到 VC/ESXi。 使用 vSphere WS API 关闭恢复目标虚拟机的电源,因为 VixDiskLib 对正在运行的虚拟机磁盘没有写权限。 使用 VixDiskLib 连接到恢复目标虚拟机的虚拟磁盘,如果使用 SAN、HotAdd、NBDSSL 高级传输模式来进行连接,还需要在执行恢复数据操作前创建一个临时快照。对于 SAN 传输模式,如果在创建临时快照前就已经存在快照了,那么你就需要在创建临时快照前确保移除了之前的所有快照,否则 SAN 传输模式的还原会失败。 使用 VixDiskLib 的 VixDiskLib_Write 将备份数据恢复到虚拟磁盘。恢复磁盘数据的前提是需要获得恢复目标虚拟磁盘当前实际的全称(包括一些序列号),否则备份数据无法正确定位到恢复目标虚拟磁盘,因为当前恢复目标虚拟机的虚拟磁盘可能从一个或多个快照继承而来。VixDiskLib_Write 函数以偶数扇区为单位传输数据,且传输长度必须为扇区大小的偶数倍。 如果创建了临时快照,还需要使用 vSphere WS API 删除快照。尤其对于 SAN 传输模式,还需要先恢复临时快照,然后再删除临时快照。如果没有恢复临时快照,就会因为 CID 不匹配导致无法删除临时快照。如果临时快照没有删除,会导致虚拟机不能开机,而且这个临时快照还会引起后续备份的还原问题。 使用 vSphere WS API 开启恢复目标虚拟机的电源。 恢复为新建虚拟机 (灾难恢复) 使用 vSphere WS API 连接 VC/ESXi 使用 vSphere WS API 来创建新的虚拟机和虚拟磁盘,需要使用在备份时保存的虚拟机配置信息(VirtualMachineConfigInfo)。 使用 VixDiskLib 的 VixDiskLib_WriteMetadata 写入虚拟磁盘元数据。 使用 VixDiskLib 的 VixDiskLib_Write 将备份数据恢复到新建的虚拟磁盘。 使用 vSphere WS API 开启新建虚拟机的电源。 恢复细节 恢复增量备份数据 使用 vSphere WS API 关闭虚拟机电源。 使用 vSphere WS API 来创建新的虚拟机和虚拟磁盘,需要使用在备份时保存的虚拟机配置信息(VirtualMachineConfigInfo)。 使用 VixDiskLib 的 VixDiskLib_Write 把全量备份数据恢复到新建的虚拟磁盘中作为基准虚拟磁盘(Base Disk),进行全量恢复的期间,需要关闭 CBT。 使用 vSphere WS API 创建一个快照,这在高级传输模式下是必须的。 对于 SAN 传输模式的恢复,也需要禁用 CBT,否则增量数据的 VixDiskLib_Write 写操作不可用,这是因为文件系统需要统计精简置备虚拟磁盘(Thin)的分配状况以及进行延迟清零(Lazy Zeroed)的操作。 使用 VixDiskLib 的 VixDiskLib_Write 依次恢复增量备份数据到虚拟磁盘。 如果你向前还原,还原时某些相同的扇区可能会被写入多次。 如果你向后还原,必须手动记录哪些扇区已经更新过了,以避免再次写入更旧的数据。 在增量备份时,需要保存调用 QueryChangedDiskAreas 获取的相应「已修改数据库偏移量」,以此来得知要恢复哪些虚拟磁盘的扇区。 建议将备份数据直接恢复到基准虚拟磁盘,避免创建创建重做日志文件。 以 RDM 的方式创建虚拟磁盘 使用 vSphere WS API 中的 CreateVM_Task 创建 RDM 磁盘时,需要用到了一个可用且未被占用的 LUN 设备。在组装 VirtualDiskRawDiskMappingVer1BackInfo 类型虚拟磁盘的 backingInfo 时,我们可以按照下述方法来获取 LUN 设备的相关信息,并以此组装创建 RDM 磁盘所需要的参数: 调用 QueryConfigTarget 获得 ConfigTarget.ScsiDisk.Disk.CanonicalName 属性值,并设置到 VirtualDiskRawDiskMappingVer1BackInfo.deviceName 属性中。 调用 QueryConfgiTarget 获得 ConfigTarget.ScsiDisk.Disk.uuid 属性值,并设置到 VirtualDiskRawDiskMappingVer1BackInfo.lunUuid 属性中。 NOTE:有些开发者可能会使用 ESXi configInfo 对象中的 LUN uuid。需要注意的是,这样做很可能会导致错误,因为实际可用的 LUN uuid 是由 Datastore 指定的,而非 ESXi。 创建虚拟机 在实际调用 vSphere WS API 中的 CreateVM_Task 新建一个虚拟机前,需要组装一个 VirtualMachineConfigSpec 配置数据集来描述虚拟机的各项配置以及虚拟设备。大部分需要的信息都可以从 VirtualMachine Managed Object 的 VirtualMachineConfigInfo 得到,其中的 config.hardware.device 就包含了虚拟机所有虚拟设备配置信息。不同设备间的关系使用 key 的值表示,它是设备的唯一标识符。除此之外,每个设备还都拥有 controllerKey 属性,controllerKey 的值就是设备所连接的控制器的唯一标识符。我们在组装 VirtualMachineConfigSpec 时,应该使用一个负整数作为临时的 controllerKey 值,以此来保证临时 controllerKey 值不会和实际分配给这些控制器的值发送冲突。当虚拟设备关联到默认设备时,controllerKey 值应该重置为对应控制器的 key 值。 一般的,为了恢复一个新的虚拟机,我们会在备份时保留备份目标虚拟机的 VirtualMachineConfigInfo 数据,为了恢复出「一模一样」的虚拟机,甚至有些开发者会完整的将其保留。但实际上,VirtualMachineConfigInfo 中的一些数据是不需要的,而且如果强行引用到 VirtualMachineConfigSpec 中的话,还会导致虚拟机创建失败。例如,包含了「Default Devices」的 VirtualMachineConfigSpec 就会创建失败,「Default Devices」列表如下,这些设备的信息都是无需组装到 VirtualMachineConfigInfo 中的。但是,除此之外的其他控制器和虚拟设备就必须组装到 VirtualMachineConfigSpec 中了。 vim.vm.device.VirtualIDEController vim.vm.device.VirtualPS2Controller vim.vm.device.VirtualPCIController vim.vm.device.VirtualSIOController vim.vm.device.VirtualKeybord vim.vm.device.VirtualVMCIDevice vim.vm.device.VirtualPointingDevice 还有一些设备的信息如果被提供了的话也可能会出问题,组装 VirtualMachineConfigSpec 的要点如下: 从 VirtualMachineConfigSpec 中排除「Default Devices」以及 VirtualController.device 的属性域 设置 VirtualDisk.FlatVer2BackingInfo 类型虚拟磁盘的 parent backing(父磁盘) 信息为 null 将 VirtualMachineConfigInfo 中的 cpuFeatureMask 属性域下的 HostCpuIdInfo 数组项转换为 VirtualMachineCpuIdInfoSpec 的 ArrayUpdateSpec 属性值,并且添加 ArrayUpdateOperation::add 项。 Sample of VirtualMachineConfigSpec // beginning of VirtualMachineConfigSpec, ends several pages later { dynamicType = <unset>, changeVersion = <unset>, //This is the display name of the VM name = “My New VM“, version = "vmx-04", uuid = <unset>, instanceUuid = <unset>, npivWorldWideNameType = <unset>, npivDesiredNodeWwns = <unset>, npivDesiredPortWwns = <unset>, npivTemporaryDisabled = <unset>, npivOnNonRdmDisks = <unset>, npivWorldWideNameOp = <unset>, locationId = <unset>, // This is advisory, the disk determines the O/S guestId = "winNetStandardGuest", alternateGuestName = "Microsoft Windows Server 2008, Enterprise Edition", annotation = <unset>, files = (vim.vm.FileInfo) { dynamicType = <unset>, vmPathName = "[plat004-local]", snapshotDirectory = "[plat004-local]", suspendDirectory = <unset>, logDirectory = <unset>, }, tools = (vim.vm.ToolsConfigInfo) { dynamicType = <unset>, toolsVersion = <unset>, afterPowerOn = true, afterResume = true, beforeGuestStandby = true, beforeGuestShutdown = true, beforeGuestReboot = true, toolsUpgradePolicy = <unset>, pendingCustomization = <unset>, syncTimeWithHost = <unset>, }, flags = (vim.vm.FlagInfo) { dynamicType = <unset>, disableAcceleration = <unset>, enableLogging = <unset>, useToe = <unset>, runWithDebugInfo = <unset>, monitorType = <unset>, htSharing = <unset>, snapshotDisabled = <unset>, snapshotLocked = <unset>, diskUuidEnabled = <unset>, virtualMmuUsage = <unset>, snapshotPowerOffBehavior = "powerOff", recordReplayEnabled = <unset>, }, consolePreferences = (vim.vm.ConsolePreferences) null, powerOpInfo = (vim.vm.DefaultPowerOpInfo) { dynamicType = <unset>, powerOffType = "preset", suspendType = "preset", resetType = "preset", defaultPowerOffType = <unset>, defaultSuspendType = <unset>, defaultResetType = <unset>, standbyAction = "powerOnSuspend", }, // the number of CPUs numCPUs = 1, // the number of memory megabytes memoryMB = 256, memoryHotAddEnabled = <unset>, cpuHotAddEnabled = <unset>, cpuHotRemoveEnabled = <unset>, deviceChange = (vim.vm.device.VirtualDeviceSpec) [ (vim.vm.device.VirtualDeviceSpec) { dynamicType = <unset>, operation = "add", fileOperation = <unset>, // CDROM device = (vim.vm.device.VirtualCdrom) { dynamicType = <unset>, // key number of CDROM key = -42, deviceInfo = (vim.Description) null, backing = (vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo) { dynamicType = <unset>, deviceName = "", useAutoDetect = <unset>, exclusive = false, }, connectable = (vim.vm.device.VirtualDevice.ConnectInfo) { dynamicType = <unset>, startConnected = false, allowGuestControl = true, connected = false, }, // connects to this controller controllerKey = 200, unitNumber = 0, }, }, (vim.vm.device.VirtualDeviceSpec) { dynamicType = <unset>, operation = "add", fileOperation = <unset>, // SCSI controller device = (vim.vm.device.VirtualLsiLogicController) { dynamicType = <unset>, // key number of SCSI controller key = -44, deviceInfo = (vim.Description) null, backing = (vim.vm.device.VirtualDevice.BackingInfo) null, connectable = (vim.vm.device.VirtualDevice.ConnectInfo) null, controllerKey = <unset>, unitNumber = <unset>, busNumber = 0, hotAddRemove = <unset>, sharedBus = "noSharing", scsiCtlrUnitNumber = <unset>, }, }, (vim.vm.device.VirtualDeviceSpec) { dynamicType = <unset>, operation = "add", fileOperation = <unset>, // Network controller device = (vim.vm.device.VirtualPCNet32) { dynamicType = <unset>, // key number of Network controller key = -48, deviceInfo = (vim.Description) null, backing = (vim.vm.device.VirtualEthernetCard.NetworkBackingInfo) { dynamicType = <unset>, deviceName = "Virtual Machine Network", useAutoDetect = <unset>, network = <unset>, inPassthroughMode = <unset>, }, connectable = (vim.vm.device.VirtualDevice.ConnectInfo) { dynamicType = <unset>, startConnected = true, allowGuestControl = true, connected = true, }, controllerKey = <unset>, unitNumber = <unset>, addressType = "generated", macAddress = <unset>, wakeOnLanEnabled = true, }, }, (vim.vm.device.VirtualDeviceSpec) { dynamicType = <unset>, operation = "add", fileOperation = "create", // SCSI disk one device = (vim.vm.device.VirtualDisk) { dynamicType = <unset>, // key number for SCSI disk one key = -1000000, deviceInfo = (vim.Description) null, backing = (vim.vm.device.VirtualDisk.FlatVer2BackingInfo) { dynamicType = <unset>, fileName = "", datastore = <unset>, diskMode = "persistent", split = false, writeThrough = false, thinProvisioned = <unset>, eagerlyScrub = <unset>, uuid = <unset>, contentId = <unset>, changeId = <unset>, parent = (vim.vm.device.VirtualDisk.FlatVer2BackingInfo) null, }, connectable = (vim.vm.device.VirtualDevice.ConnectInfo) { dynamicType = <unset>, startConnected = true, allowGuestControl = false, connected = true, }, // controller for SCSI disk one controllerKey = -44, unitNumber = 0, // size in MB SCSI disk one capacityInKB = 524288, committedSpace = <unset>, shares = (vim.SharesInfo) null, }, }, (vim.vm.device.VirtualDeviceSpec) { dynamicType = <unset>, operation = "add", fileOperation = "create", // SCSI disk two device = (vim.vm.device.VirtualDisk) { dynamicType = <unset>, // key number of SCSI disk two key = -100, deviceInfo = (vim.Description) null, backing = (vim.vm.device.VirtualDisk.FlatVer2BackingInfo) { dynamicType = <unset>, fileName = "", datastore = <unset>, diskMode = "persistent", split = false, writeThrough = false, thinProvisioned = <unset>, eagerlyScrub = <unset>, uuid = <unset>, contentId = <unset>, changeId = <unset>, parent = (vim.vm.device.VirtualDisk.FlatVer2BackingInfo) null, }, connectable = (vim.vm.device.VirtualDevice.ConnectInfo) { dynamicType = <unset>, startConnected = true, allowGuestControl = false, connected = true, }, // controller for SCSI disk two controllerKey = -44, unitNumber = 1, // size in MB SCSI disk two capacityInKB = 131072, committedSpace = <unset>, shares = (vim.SharesInfo) null, }, } }, cpuAllocation = (vim.ResourceAllocationInfo) { dynamicType = <unset>, reservation = 0, expandableReservation = <unset>, limit = <unset>, shares = (vim.SharesInfo) { dynamicType = <unset>, shares = 100, level = "normal", }, overheadLimit = <unset>, }, memoryAllocation = (vim.ResourceAllocationInfo) { dynamicType = <unset>, reservation = 0, expandableReservation = <unset>, limit = <unset>, shares = (vim.SharesInfo) { dynamicType = <unset>, shares = 100, level = "normal", }, overheadLimit = <unset>, }, cpuAffinity = (vim.vm.AffinityInfo) null, memoryAffinity = (vim.vm.AffinityInfo) null, networkShaper = (vim.vm.NetworkShaperInfo) null, swapPlacement = <unset>, swapDirectory = <unset>, preserveSwapOnPowerOff = <unset>, bootOptions = (vim.vm.BootOptions) null, appliance = (vim.vService.ConfigSpec) null, ftInfo = (vim.vm.FaultToleranceConfigInfo) null, applianceConfigRemoved = <unset>, vAssertsEnabled = <unset>, changeTrackingEnabled = <unset>, } // end of VirtualMachineConfigSpec Demo of VirtualMachineConfigSpec // Duplicate virtual machine configuration VirtualMachineConfigSpec configSpec = new VirtualMachineConfigSpec(); // Set the VM values configSpec.setName("My New VM"); configSpec.setVersion("vmx-04"); configSpec.setGuestId("winNetStandardGuest"); configSpec.setNumCPUs(1); configSpec.setMemoryMB(256); // Set up file storage info VirtualMachineFileInfo vmfi = new VirtualMachineFileInfo(); vmfi.setVmPathName("[plat004-local]"); configSpec.setFiles(vmfi); vmfi.setSnapshotDirectory("[plat004-local]"); // Set up tools config info ToolsConfigInfo tools = new ToolsConfigInfo(); configSpec.setTools(tools); tools.setAfterPowerOn(new Boolean(true)); tools.setAfterResume(new Boolean(true)); tools.setBeforeGuestStandby(new Boolean(true)); tools.setBeforeGuestShutdown(new Boolean(true)); tools.setBeforeGuestReboot(new Boolean(true)); // Set flags VirtualMachineFlagInfo flags = new VirtualMachineFlagInfo(); configSpec.setFlags(flags); flags.setSnapshotPowerOffBehavior("powerOff"); // Set power op info VirtualMachineDefaultPowerOpInfo powerInfo = new VirtualMachineDefaultPowerOpInfo(); configSpec.setPowerOpInfo(powerInfo); powerInfo.setPowerOffType("preset"); powerInfo.setSuspendType("preset"); powerInfo.setResetType("preset"); powerInfo.setStandbyAction("powerOnSuspend"); // Now add in the devices VirtualDeviceConfigSpec[] deviceConfigSpec = new VirtualDeviceConfigSpec [5]; configSpec.setDeviceChange(deviceConfigSpec); // Formulate the CDROM deviceConfigSpec[0].setOperation(VirtualDeviceConfigSpecOperation.add); VirtualCdrom cdrom = new VirtualCdrom(); VirtualCdromIsoBackingInfo cdDeviceBacking = new VirtualCdromRemotePassthroughBackingInfo(); cdDeviceBacking.setDatastore(datastoreRef); cdrom.setBacking(cdDeviceBacking); cdrom.setKey(-42); cdrom.setControllerKey(new Integer(-200)); // Older Java required type for optional properties cdrom.setUnitNumber(new Integer(0)); deviceConfigSpec[0].setDevice(cdrom); // Formulate the SCSI controller deviceConfigSpec[1].setOperation(VirtualDeviceConfigSpecOperation.add); VirtualLsiLogicController scsiCtrl = new VirtualLsiLogicController(); scsiCtrl.setBusNumber(0); deviceConfigSpec[1].setDevice(scsiCtrl); scsiCtrl.setKey(-44); scsiCtrl.setSharedBus(VirtualSCSISharing.noSharing); // Formulate SCSI disk one deviceConfigSpec[2].setFileOperation(VirtualDeviceConfigSpecFileOperation.create); deviceConfigSpec[2].setOperation(VirtualDeviceConfigSpecOperation.add); VirtualDisk disk = new VirtualDisk(); VirtualDiskFlatVer2BackingInfo diskfileBacking = new VirtualDiskFlatVer2BackingInfo(); diskfileBacking.setDatastore(datastoreRef); diskfileBacking.setFileName(volumeName); diskfileBacking.setDiskMode("persistent"); diskfileBacking.setSplit(new Boolean(false)); diskfileBacking.setWriteThrough(new Boolean(false)); disk.setKey(-1000000); disk.setControllerKey(new Integer(-44)); disk.setUnitNumber(new Integer(0)); disk.setBacking(diskfileBacking); disk.setCapacityInKB(524288); deviceConfigSpec[2].setDevice(disk); // Formulate SCSI disk two deviceConfigSpec[3].setFileOperation(VirtualDeviceConfigSpecFileOperation.create); deviceConfigSpec[3].setOperation(VirtualDeviceConfigSpecOperation.add); VirtualDisk disk2 = new VirtualDisk(); VirtualDiskFlatVer2BackingInfo diskfileBacking2 = new VirtualDiskFlatVer2BackingInfo(); diskfileBacking2.setDatastore(datastoreRef); diskfileBacking2.setFileName(volumeName); diskfileBacking2.setDiskMode("persistent"); diskfileBacking2.setSplit(new Boolean(false)); diskfileBacking2.setWriteThrough(new Boolean(false)); disk2.setKey(-100); disk2.setControllerKey(new Integer(-44)); disk2.setUnitNumber(new Integer(1)); disk2.setBacking(diskfileBacking2); disk2.setCapacityInKB(131072); deviceConfigSpec[3].setDevice(disk2); // Finally, formulate the NIC deviceConfigSpec[4].setOperation(VirtualDeviceConfigSpecOperation.add); com.VMware.vim.VirtualEthernetCard nic = new VirtualPCNet32(); VirtualEthernetCardNetworkBackingInfo nicBacking = new VirtualEthernetCardNetworkBackingInfo(); nicBacking.setNetwork(networkRef); nicBacking.setDeviceName(networkName); nic.setAddressType("generated"); nic.setBacking(nicBacking); nic.setKey(-48); deviceConfigSpec[4].setDevice(nic); // Now that it is all put together, create the virtual machine. // Note that folderMo, resourcePool, and hostMo, are moRefs to // the Folder, ResourcePool, and Host where the VM is to be created. ManagedObjectReference taskMoRef = serviceConnection.getService().createVM_Task(folderMo, configSpec, resourcePool, hostMo);
目录 目录 前文列表 虚拟磁盘数据的传输方式 Transport Methods Local File Access NBD and NBDSSL Transport SAN Transport HotAdd Transport 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT VMware 虚拟化编程(11) — VMware 虚拟机的全量备份与增量备份方案 VMware 虚拟化编程(12) — VixDiskLib Sample 程序使用 VMware 虚拟化编程(13) — VMware 虚拟机的备份方案设计 虚拟磁盘数据的传输方式 Transport Methods 在前面的篇章中有简单的介绍过 VDDK 支持的数据传输模式: 本地文件 (Local File) 网络块设备 (NBD, Network Block Device) 局域网的加密 (NBDSSL,NBD with encryption) 存储区域网络 (SAN, Storage Area Network) 热添加的 SCSI (SCSI HotAdd) 备份程序都是通过调用 VixDiskLib 的 VixDiskLib_ConnectEx 接口来建立与 VMDK 的连接,如果传输模式参数传入为 NULL 的话,VixDiskLib 会按照默认按照「file:san:hotadd:nbd」的顺序依次尝试,值得首次成功或者全部失败。当建立连接成功之后,可以调用 VixDiskLib_ListTransportModes 查看此时连接所用的传输模式。 Local File Access Local File Access 本地文件访问模式,使用 VixDiskLib 直接读取 ESXi Host 本地 /vmfs/volumes 下的虚拟磁盘数据,简单来说就是直接读取本地 Hosted Disk 内的数据。但需要注意的是,Local File Access 并非一种网络传输的方式,所以肯定是不能被应用于备份应用的。 NBD and NBDSSL Transport NBD and NBDSSL Transport (加密)网络块设备传输模式,也叫 LAN Transport,只要在局域网络环境中就能支持,是最基础也是最通用的备份数据传输方式,当其他的传输方式不可用时,VixDiskLib 会自动回退(fall‐back)到该传输模式。NBD 将远程 ESXi 主机上的存储视为一个块设备,所以 NBD 支持镜像级别备份。NBDSSL 就是在 NBD 的基础上使用 SSL 来加密的 TCP 连接上传输的数据。 从上图可以看出,ESXi Host 会从其 VMFS 中的虚拟磁盘读取数据,然后再将数据流通过 LAN 传输到 Backup Server,所以 NBD/NBDSSL 数据传输使用的是 ESXi Host 的管理网络,而非独立的数据传输网络。这就意味着 NBD/NBDSSL 的传输速率会更低,同时也需要考虑数据传输所占用的带宽是否会影响到 ESXi Host 的正常通信。 NBD/NBDSSL 特性: 支持所有存储类型,具有很好的通用性。 支持使用虚拟机作为 Backup Proxy,这样能够最小化备份业务的性能影响。可以考虑将 Backup Proxy 运行在级别较低的资源池中。 在私有的网络环境中,可以考虑使用 NBD 代替 NBDSSL,因为前者的速度更快,占用的资源更低。当然,这个视乎数据的安全级别。 NOTE:物理备份服务器 Backup Server;虚拟备份服务器 Backup Proxy; SAN Transport SAN 传输模式 SAN Transport,VixDiskLib 的高级传输模式之一,是效率最高的数据传输模式,应用 SAN 需要 Backup Server 和 ESXi 主机共享一个 Datastore 依赖的 LUN,使 Backup Server 能够直接访问 LUN 的原始数据,并绕过了 ESXi 主机的 I/O 操作。换句话说就是 SAN 传输模式要求 Backup Server 能够通过 FC/iSCSI/SAS 访问到虚拟磁盘。 此传输模式下,VixDiskLib 会从 VC/ESXi 中获取相关 VMFS LUN 的布局信息,然后基于这些信息直接从虚拟磁盘对应的 LUN 设备中读取数据,而无需再通过 ESXi Host 和 LAN 来进行数据传输。使用 SAN 数据传输网络,达到了 LAN free 的效果。 SAN Transport 特性: 仅支持 SAN 网络存储 仅支持物理备份服务器 要求能够以 RAW 设备的形式访问 LUN 设备 不能兼容 VSAN(a network based storage solution with direct attached disks) SAN 存储设备支持包含 SATA drives 支持 Fibre Channel、iSCSI、SAS(based storage arrays) 是备份的最佳选择,但对恢复来说却不是 NOTE:使用 SAN Transport 恢复虚拟机时,如果虚拟机有一个预先存在的快照的话,那么你需要先删除掉该快照,否则将恢复失败。 HotAdd Transport HotAdd 是 VMware 提供的一种功能,允许正在运行的虚拟机动态添加 SCSI 磁盘、CPU 和内存设备配置,而 HotAdd Transport 就是基于 HotAdd 功能实现的另一种 VixDiskLib 高级传输模式。 HotAdd Transport 要求备份应用运行在 Backup Proxy 中,与 NBD/NBDSSL 一样也是使用 LAN 进行数据传输,不同在于前者需要走管理网络(ESXi 控制),而后者走的是数据/存储网络,所以 HotAdd Transport 的数据传输效率依旧比 NBD/NBDSSL 更高。Backup Proxy 以 HotAdd 传输模式连接到备份目标虚拟机创建的快照之后,会创建该快照的一个临时克隆链接(linked clone)并将这个链接 Attach 到 Backup Proxy。此时在 Backup Proxy 上会发现一块新的 SCSI 磁盘设备,然后就可以使用 VixDiskLib 来直接读取该磁盘设备中的数据,Backup Proxy 也能够想读取自己的磁盘一样来读取新的 SCSI 磁盘中的文件。 HotAdd Transport 特征: 仅支持 Backup Proxy 仅适用于备份具有 SCSI 磁盘的虚拟机,不支持备份 IDE 虚拟磁盘 NOTE 1:当使用 HotAdd 备份一个 Linux GuestOS 时,通常会按照数字顺序为虚拟机添加的 SCSI Contorller 指定一个 ID。但因为 Linux GuestOS 缺少一个接口来通知 SCSI Controller 被分配到了哪一个总线 ID,所以 HotAdd 会假设 SCSI Controller 的唯一 ID 和它的总线 ID 总是相关的。但实际上这个假设是有可能不成立的,例如:如果 Linux GuestOS 的第一个 SCSI Controller ID 0 被分配到总线 ID 0,但是你添加了一个 SCSI Controller ID 1 并将其分配到总线 ID 3。对于这种情况下,HotAdd 高级传输模式就很可能会失败,因为它期望的总线 ID 是 1,才能够继续与总线 ID 相关。为避免出现这种问题,当向虚拟机添加 SCSI Controller 时,必须严格按照数字顺序分配下一个可用的总线 ID。 NOTE 2:如果新添加的虚拟磁盘引用了一个还不存在的 SCSI Controller ID,VMware 为隐式的添加一个 SCSI Controller 来完成 bus:disk 分配。例如:如果磁盘 0:0 和 0:1 已经存在,添加一个磁盘 1:0 没有问题,VMware 会隐式的添加 SCSI Controller 1。但是如果你手动的添加磁盘 3:0 就会打破总线 ID 的顺序(SCSI Controller 1 != Bus 3)。为了避免 HotAdd 连接问题,需要严格按照数字顺序来添加虚拟磁盘。
目录 目录 前文列表 备份思路 备份算法 备份细节 连接到 vCenter 还是 ESXi 如何选择快照类型 是否开启 CBT 如何获取备份数据 如何提高备份数据的传输率 备份厚置备磁盘和精简置备磁盘有什么区别 Thin 精简置备虚拟磁盘 Thick-Lazy 延迟置零的厚置备虚拟磁盘 Thick-Eager 立即置零的厚置备虚拟磁盘 有什么磁盘类型是无法进行备份的 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT VMware 虚拟化编程(11) — VMware 虚拟机的全量备份与增量备份方案 VMware 虚拟化编程(12) — VixDiskLib Sample 程序使用 备份思路 在目标虚拟机上创建临时快照 从目标虚拟机上获取备份数据,并保存备份数据 删除临时快照 备份算法 连接到备份目标虚拟机所在的 VC/ESXi 使用 vSphere WS API 开启备份目标虚拟机的 CBT (视情况而定) 使用 vSphere WS API 的 CreateSnapshot_Task 创建备份目标虚拟机的临时快照,如果备份要求「应用一致性」那么需要设置参数 quiescent=True 获取备份目标虚拟机的虚拟磁盘数据以及虚拟机配置信息 使用 VDDK 打开并读取备份目标虚拟机的虚拟磁盘和快照文件数据 将虚拟磁盘、快照文件、配置信息等数据一起复制到备份存储 使用 vSphere WS API 删除临时快照 备份细节 连接到 vCenter 还是 ESXi? VMware 官方建议备份应用程序选择与 vCenter 服务器建立通信,而非 ESXi 主机。最主要的原因在于 vCenter 服务器为使用 vSphere WS API 的开发者提供了目标对象定位的透明性。由 vCenter 服务器负责跟踪目标虚拟机在不同 ESXi 主机之间的迁移,并且会隐式的将 SDK 操作重定向到当前运行目标虚拟机的 ESXi 主机。 并且 VMware 官方建议开发者应该最小化连接或会话的数量,以降低 vSphere 的资源负载。对备份应用程序来说,最好只创建一个会话,并在所有需要和 vSphere 进行交互的模块中共享该会话。这就意味着如果你希望备份应用程序支持多线程,那么你应该引入访问控制块来对连接的访问进行互斥。 如何选择快照类型? 创建临时快照时,需要充分考虑虚拟机备份的级别,根据不同的级别来创建不同类型的临时快照。EXAMPLE: 只关注磁盘数据完整性的备份级别 ==> 创建崩溃一致性快照 需要关注业务系统一致性的备份级别 ==> 创建 Quiseced 快照 如果是后者的话,则需要将 CreateSnapshot_Task 的「quiesce」和「memory」参数均设置为 TRUE,前置会使 FileSystem 处于静置状态,后者则允许在快照中包含开启电源状态下的虚拟机内存状态,以此获得一个「静置快照」。否则,创建出的快照会存在过渡期的系统状态,还原这种状态下的快照数据很可能会破坏业务系统的运行时状态; NOTE:「quiesce」和「memory」标识有效的保证了备份虚拟机的文件系统一致性和应用一致性。 还有一点需要注意的是,在执行完备份之后,临时快照就无用了,切记将其删除。快照会影响到虚拟机的性能,以及占用存储空间。 是否开启 CBT? CBT 是实现增量备份的底层支撑,如果你需要进行更加有利于节省存储空间增量备份的话,通常需要在第一个快照创建之前开启 CBT。但实际上 CBT 也存在着一些使用的限制,具体请浏览 《VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT》。 是否开启 CBT 的关键除了其自身使用上的限制之外,还要考虑备份目标虚拟机的重要性。如果备份目标虚拟机内运行的是核心业务系统,而且存储资源又比较充裕,那么我会建议关闭 CBT,选择使用完全备份。 1. CBT 会占用可测量的性能资源; 2. CBT 的稳定性存在着隐患; 如何获取备份数据? 获取虚拟机的备份数据需要集齐以下关键要素: Snapshot MoRef:快照实际上就是虚拟磁盘的备份版本,通过 Snapshot MoRef 我们可以获取虚拟磁盘的名称和路径等信息。例如:从 Snapshot Managed Object 的 ConfigInfo 开始,能够查找到快照中所有虚拟磁盘的 BackingInfo,我们能够从这些 BackingInfo 中找到虚拟机所有虚拟磁盘相应的 changeId。所以尤其在多虚拟磁盘的场景中,如何有效的对不同的虚拟磁盘进行标识,是一个非常重要的前提条件。 VixDiskLib 库函数:在对虚拟磁盘进行了有效的标识之后,我们需要使用 VixDiskLib 所提供的「接口函数集」来获取实际的虚拟磁盘数据。之所以将其称之为「接口函数」,是因为 VixDiskLib 向开发者透明了虚拟磁盘操作的细节。例如,调用 VixDiskLib_Open 和 VixDiskLib_Read 函数时,VixDiskLib 允许以扇区为单位(in Sector)来访问虚拟磁盘数据,所以传输的数据总量是扇区大小的整数倍。可以看出,VixDiskLib 库函数的调用就犹如接口函数调用一般的简单直接。 Metadata:虚拟磁盘的元数据,是描述了虚拟磁盘配置信息的一系列键值对,包含了磁盘卷标、LUN、分区布局、链接个数、文件属性、RDM、锁等关键信息,在还原一个虚拟磁盘的时候起到了至关重要的作用。元数据信息可以通过 VixDiskLib_ReadMetadataKeys 和 VixDiskLib_ReadMetadata 来获得。 VixMntapi:VixMntapi 库可以获取虚拟磁盘中的 GuestOS 相关信息(如:操作系统类型等)。更重要的是 VixMntapi 支持将卷直接挂载到设备节点上,这样就能够执行面向文件的备份与还原了。 如何提高备份数据的传输率? 掌握以下使用要点,就能够有效的提高备份数据的传输率: 在备份应用程序中选择使用增强型连接函数 VixDiskLib_InitEx 和 VixDiskLib_ConnectEx,增量型连接函数支持使用高级数据传输模式 SAN 和 SCSI HotAdd 如果备份场景主需要读取虚拟磁盘数据,而无需修改磁盘数据的话。调用 VixDiskLib_ConnectEx 时候可以通过传入 readOnly=TRUE 来启用高性能只读访问。 如果在 Linux 上使用 SAN 存储的场景中,可以选择「direct」直接读写模式 O_DIRECT,「direct」模式会阻止其他进程访问最新的虚拟磁盘数据,这意味着磁盘数据的读写都不会存在任何的缓存,这样能够避免提交写缓存前由于进程终止所造成的信息丢失。而且备份一个虚拟磁盘通常都是连续的读取不同扇区中的数据,开启 Cache 机制实际上反而降低了性能。如果备份应用按照下列原则进行读写操作,能够达到最佳的效率: 在 SAN 中执行读写操作的数据偏移都应该是 Page Size 的整数倍。 传输的长度应该是 Page Size 的整数倍。 数据传输的缓冲区应该是页边界对齐的。 备份厚置备磁盘和精简置备磁盘有什么区别? 厚置备磁盘在 Datastore 中会直接占用其分配的所有数据空间,而精简置备磁盘只会消耗其实际使用了的数据空间,所以备份精简置备磁盘的速度会更快。 需要注意的是,厚置备磁盘又细分为「即时清零厚置备磁盘」和「延迟清零厚置备磁盘」。对于前者的全量备份而言,是否开启 CBT 的影响并不大,因为始终都会备份其所分配的容量。但对于后者的全量备份而言就有区别了,如果在全量备份一个「延迟清零厚置备磁盘」前没有开启 CBT 的话,VixDiskLib 会读取虚拟磁盘所有的扇区,并将实际没有使用到的空扇区(本来会被延迟清零的扇区)中的脏数据全部重置为 0,然后再进行备份。也就是说如果「延迟清零厚置备磁盘」在进行全量备份之前没有开启 CBT 的话,其备份数据量上是与「即时清零厚置备磁盘」一样的,而且还损耗了置零的时间,使用这样的备份数据恢复出来的虚拟磁盘类型也是「即时清零厚置备磁盘」。所以建议在对「延迟清零厚置备磁盘」进行全量备份前开启 CBT 功能,实际上如果是增量备份的场景,无论是什么类型的虚拟磁盘,都应该建议开启 CBT。 还有一点需要注意的是,精简置备虚拟磁盘的数据空间是动态的,所以其很可能不是一片连续的数据空间,其在数据存储中实际的数据空间是在第一次写入数据时才被创建的(created on first write)。所以与使用 NBD/NBDSSL/HotAdd 的厚置备磁盘相比,精简置备磁盘在首次写入时需要额外的数据块分配性能开销,不过一旦精简置备磁盘的数据空间被创建之后,其性能就与厚置备类型相差无几了。重要的是,在虚拟机对精简置备磁盘进行随机 I/O 或写操作的同时进行备份的话,即便开启的 CBT,所备份出来的数据量也很可能会大于预期的数据量。对于这种情况,进行虚拟磁盘碎片整理可能会有助于减小备份的数据量。 Thin 精简置备虚拟磁盘 在保护之前不存在 Snapshot (changeId == '*'),开启 CBT 并执行增量保护所保护的数据量是「实际已使用」的数据空间。 在保护之前已经存在 Snapshot (changeId != '*'),开启 CBT 并执行全量保护所保护的数据量是「已分配」的数据空间。 Thick-Lazy 延迟置零的厚置备虚拟磁盘 在保护之前不存在 Snapshot (changeId == '*'),开启 CBT 并执行增量保护所保护的数据量是「实际已使用」的数据空间。 在保护之前已经存在 Snapshot (changeId != '*'),开启 CBT 并执行全量保护所保护的数据量是「已分配」的数据空间。 Thick-Eager 立即置零的厚置备虚拟磁盘 在保护之前不存在 Snapshot (changeId == '*'),开启 CBT 并执行增量保护所保护的数据量是「已分配」的数据空间。 在保护之前已经存在 Snapshot (changeId != '*'),开启 CBT 并执行全量保护所保护的数据量是「已分配」的数据空间。 有什么磁盘类型是无法进行备份的? 一般来说,RDM 类型的虚拟磁盘是无法进行备份的,因为物理兼容的 RDM 磁盘设备无法进行快照。备份应用应该忽略这些无法进行快照的独立磁盘,如果创建快照,就会抛出错误。
目录 目录 前文列表 vixDiskLibSample 安装 Sample 程序 Sample 程序使用方法 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT VMware 虚拟化编程(11) — VMware 虚拟机的全量备份与增量备份方案 vixDiskLibSample 在介绍 VDDK 时也提到过,VDDK 实际上是一系列的 C/C++ lib 库及其相关的 Docs 和 Sample 的开发工具集合。本篇主要记录 VDDK 提供的 Sample 程序的使用方法,它对于刚刚接触 VDDK 的开发者而言是非常有用的。 这些 Sample 程序代码是使用 C++ 编写的,要成功编译 Sample 程序,必须确保加载了正确的动态链接库和共享对象。在 Linux 系统,VixDiskLib 与动态链接库或者共享对象是组织在一起的,这样简化了第三方以及开源组件的打包发行。如果你按照《VMware 虚拟化编程(4) — VDDK 安装》中提供的方式进行安装,那么应该可以编译成功。当然了,每个人的环境不一样,你也可能需要进行调整。 官方给出了下列保证正确加载动态链接库的建议: Linux 默认的安装路径为 /usr/share/doc/vmware-vix-disklib/sample 在 VDDK 程序中设置路径 将 VDDK lib 库的路径加入到环境变量 NOTE:需要注意的是,VDDK 使用相对路径来加载动态链接库文件,而非绝对路径,应该要注意避免不同版本动态链接库的冲突。 安装 Sample 程序 在安装好 VDDK 之后,直接编译 Sample 程序: [root@mickeyfan-dev diskLib]# cd /usr/lib/vmware-vix-disklib/doc/samples/diskLib [root@mickeyfan-dev diskLib]# ls Makefile vixDiskLibSample.cpp [root@mickeyfan-dev disklib]# make [root@mickeyfan-dev diskLib]# ls Makefile vix-disklib-sample vixDiskLibSample.cpp NOTE 1:官方建议的 Sample 安装路径为 /usr/share/doc/vmware-vix-disklib/samples/disklib NOTE 2:某些特定的 Linux 还需要在 vixDiskLibSample.cpp 的 15 行后添加两行 include 15 #else 16 #include <stdio.h> 17 #include <string.h> NOTE 3:如果编译失败建议进行以下尝试 将 /usr/lib/vmware-vix-disklib/lib64 添加到 /etc/ld.so.conf.d/vmware-vix-disklib.conf 文件中,然后使用 root 权限运行 ldconfig 指令 添加或编辑环境变量 LD_LIBRARY_PATH=/usr/lib/vmware-vix-disklib/lib64 Sample 程序使用方法 使用方法:vixdisklibsample command [options] diskPath 指令: - -create:创建由 -cap 选项指定容量大小的稀疏类型虚拟磁盘「diskPath」 - -redo parentPath:为父虚拟磁盘「parentPath」创建一个子(重写日志)虚拟磁盘「diskPath」 - -info:显示指定虚拟磁盘「diskPath」的信息 - -dump:以十六进制的方式显示指定范围内的扇区内容 - -fill:使用 -val 选项指定的值来填充指定范围内的虚拟磁盘扇区 - -wmeta key value:将键值对(key, value)写入指定虚拟磁盘「diskPath」的元数据表中 - - rmeta key:显示元数据表中指定 key 对应的 value - -meta:显示虚拟磁盘元数据表中所有的项 - -clone sourcePath:将源 VMDK 克隆到指定的远程站点 - -readbench blocksize:使用指定的 I/O blocksize (以扇区为单位),在虚拟磁盘上读取标签。 - -writebench blocksize:使用指定的 I/O blocksize (以扇区为单位),在虚拟磁盘上写入标签。警告:这可能会覆盖磁盘上原有的数据 选项: - -adapter [ide|scsi]:在「-create」指令时,指定总线 bus 的类型,默认为 scsi 类型 - -start n:在「dump | fill」指令时,指定开始扇区,默认为 0 - -count n:在「dump | fill」指令时,指定扇区数量,默认为 1 - -val byte:在「fill」指令时,指定用于填充的字节,默认为 255 - -cap megabytes:在「-create」指令时,指定容量的大小(MB),默认为 100 - -single:打开虚拟机的单个磁盘链接而非全部磁盘链接(不指定该选项默认打开全部磁盘链接),仅支持本地磁盘,不支持远程托管磁盘。 - -multithread n:开启 n 个线程,并将指定文件拷贝到 n 个新文件中 - -host hostname:VC/vSphere 的 hostname/IP (强制项) - -user userid:host 的 username (强制项) - -password password:host 的 password (强制项) - -port port:用于连接 VC/ESXi host 的端口,默认是 443 - nfchostport port:使用 NFC 连接到 ESXi host 的端口,默认 902 - -vm moref=id:虚拟机的托管对应引用 - -libdir dir:VDDK lib 库的安装路径 - -initex configfile:配置文件的路径或文件名 - -ssmoref moref:虚拟机快照的托管对象引用 - -mode mode:传递给 VixDiskLib_ConnectEx 函数的传输模式字符串,有效的模式为:nbd, nbdssl, san, hotadd - -thumb stirng:SSL 指纹验证字符串,格式为:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx NOTE 1:要连接到 ESXi Host,则必须指定 -host,-user,-password 等强制选项,并提供 Datastore 中的 diskPath。EXAMPLE: ./vix-disklib-sample –host <esxi_ip> –user <esxi_username> –password <esxi_password> "[<datastore_name>] <vm_name>/<vm_vdisk_name>.vmdk" NOTE 2:如果要连接到 vCenter Server,还需要指定 -libdir 和 -vm 选项,DiskLibPlugin 需要通过这两个选项来连接到 vCenter Server 并定位到指定 VM。EXAMPLE: ./vix-disklib-sample –host <vc_ip> –user <vc_username> –password <vc_password> –libdir <pluginDir> -vm <vm_mor> "[<datastore_name>] <vm_name>/<vm_vdisk_name>.vmdk" NOTE 3:如果希望使用高级传输进行连接,就必须指定 -mode 和 -ssmoref 选项。这两个选择的值会被传递到 VixDiskLib_ConnectEx。注意,虚拟机的快照必须存在,因为打开正在运行的虚拟机的 bask disk 是非常危险的。EXAMPLE: ./vix-disklib-sample –host <vc_ip> –user <vc_username> –password <vc_password> –libdir <pluginDir> -vm <vm_mof> -mode san -ssmoref <snap_mor> "[<datastore_name>] <vm_name>/<vm_vdisk_name>.vmdk" NOTE 4:VixDiskLib_Create 不支持直接创建远程的托管磁盘,而是首先需要在创建一个本地磁盘,然后使用 VixDiskLib_Clone 将本地磁盘克隆并转换为托管磁盘。EXAPMLE: ./vix-disklib-sample -create -cap 1024 virtdisk.vmdk ./vix-disklib-sample -clone virtdisk.vmdk –host <vc_ip> –user <vc_username> –password <vc_password> vmfsdisk.vmdk
目录 目录 前文列表 全量备份数据的获取方式 增量备份数据的获取过程 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 VMware 虚拟化编程(10) — VMware 数据块修改跟踪技术 CBT 全量备份数据的获取方式 首先需要声明的是,无论是获取全量数据还是增量数据,其面向操作的对象都是虚拟磁盘,而非虚拟机。 获取 VMware 虚拟机的全量备份相对简单,通常有以下两种方式: 方式一:直接使用 VixDiskLib 来备份虚拟磁盘的所有内容,这种方式的缺点在于,对于「精简置备」或「厚置备延迟置零」的虚拟磁盘而言,实际上需要备份的数据可能远小于虚拟磁盘中所含有的数据。因为虚拟磁盘中有些数据可能只是没有被擦除,非当前虚拟机所实际拥有的数据。 方式二: changeId='*' 的 QueryChangedDiskAreas 调用,可以获得虚拟磁盘的全量数据。「*」表示 QueryChangedDiskAreas 应该返回虚拟磁盘中实际已分配的数据块偏移量,这里解决了方式一的缺陷。但需要注意的是,QueryChangedDiskAreas 获得的是已修改数据块的偏移量,而非实际的数据,仍然需要结合 VixDiskLib 来取得实际的磁盘数据。 NOTE:对于「厚置备置零」的虚拟磁盘来说,方式二和方式一的效果是等同的。 增量备份数据的获取过程 Step 1:对虚拟机执行第一次快照,并获取全量数据。 Step 2:通过 vShpere WS API VirtualDisk.getBacking.getChangeId 来取得 Step 1 中所创建快照的虚拟磁盘的 ChangeId。 Step 3:对虚拟机执行第二次快照。 Step 4:调用 vShpere WS API QueryChangedDiskAreas,并传入从 Step 2 取得的 ChangeId、从 Step 3 创建的快照 moRef 和指定虚拟磁盘的唯一 ID 作为实参。如此就能够获得自第一次快照时间点(前端点)到第二次快照时间点(后端点)之间,该虚拟磁盘的已修改数据块的偏移量。 Step 5:结合 Step 4 获得的已修改数据块偏移量和 VixDiskLib 所提供的的 VixDiskLib_Read 函数就能够取得该虚拟磁盘的增量数据。 Step 6:逐一对虚拟机所含有的虚拟磁盘重复 Step 2,4,5,最终获得虚拟机完整的增量数据。 NOTE:从上述过程可知,在多磁盘场景中,虚拟机的一个快照数据文件中可能包含了该虚拟机所有虚拟磁盘的增量数据。所以当我们使用 VixDiskLib_ConnectEx 并 VixDiskLib_Open 一个快照之后,还需要根据不同虚拟磁盘的已修改数据块偏移量来 VixDiskLib_Read 出其中属于该虚拟磁盘的那一份增量数据。
目录 目录 前文列表 数据块修改跟踪技术 CBT 为虚拟机开启 CBT CBT 修改数据块偏移量获取函数 QueryChangedDiskAreas changeId 一个 QueryChangedDiskAreas 的 DEMO 应用 QueryChangedDiskAreas 设计的增量差异备份算法 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟化编程(9) — VMware 虚拟机的快照 数据块修改跟踪技术 CBT CBT(Changed Block Tracking) 数据块修改跟踪技术,是 VMware 实现「增量备份」的底层支撑技术。CBT 的优势在于节约空间,它允许只备份发生了修改的数据。在 CBT 被引入之前,每次都必须要备份整个虚拟机,而不是增量备份。所谓增量备份,即仅备份两个快照时间点之间所被修改过的数据。开启了 CBT 的虚拟机会在其数据储存目录下新建一个 -ctk.vmdk 文件,用于记录数据块修改的跟踪信息。但开启 CBT 会对虚拟机的虚拟磁盘带来一些性能损失,所以默认会关闭 CBT。 CBT 的工作原理就是让 VMKernel 监控自上次快照时间点以来有那些数据块中的数据被改变了,并记录下这些被改变的数据块的偏移量,依靠这些偏移量就能够获取数据块中的修改数据了。 以下场景都支持 CBT: 存储在 VMFS 上的虚拟磁盘(SAN or Local) NFS 上的虚拟磁盘 虚拟兼容模式的 RDM(裸设备映射) 以下场景不支持 CBT: 物理兼容模式的 RDM 为虚拟机开启 CBT 默认情况下 CBT 功能是禁用的,因为它会引起一个很小但是可被测量到的性能损耗。开启 CBT 的前提条件需要虚拟机版本为 7 或更高,我们可以使用 PropertyCollector 从 VirutalMachine ManagedObject 中获取这个 CBT 的属性域,如果其中含有 changeTrackingSupported 属性值,就该虚拟机支持 CBT 功能。 使用程序设置 CBT: VirtualMachineConfigSpec configSpec = new VirtualMachineConfigSpec(); configSpec.changeTrackingEnabled = new Boolean(true); ManagedObjectReference taskMoRef = serviceConnection.getService().ReconfigVm_Task(targetVM_MoRef, configSpec); 手动设置 CBT: 右击虚拟机,选择「Edit Settings」 点击「Options」 点击「Advanced section」下的「General」,点击「Configuration Parameters」,开启「Configuration Parameters」窗口后,查找或添加「ctkEnabled」项,设置为「true」,并设置每个磁盘的「ctkEnabled=true」 在设置了 CBT 之后,需要重启虚拟机生效。 NOTE: vSphere WS API 能调用 configSpec.changeTrackingEnabled = new Boolean(true) 来动态的设置 CBT 状态,而不需关闭虚拟机。 CBT 修改数据块偏移量获取函数 QueryChangedDiskAreas VMware vSphere WS API 中提供的 QueryChangedDiskAreas Method 能够帮助开发者获得虚拟机 CBT 的功能,该函数需要提供以下参数: _this:目标 VirtualMachine moRef snapshot:虚拟机当前的 Snapshot moRef deviceKey:目标虚拟磁盘的 Id startOffset:指定开始检查 CBT 的 offset,一般可以为 0 changeId:虚拟磁盘在某个时间点上的状态标识符,是一个格式为 <UUID>/<nnn> 的数字序列字符串,如果 <UUID> 改变了,跟踪信息就会失效。 QueryChangedDiskAreas 返回的是 DiskChangeInfo 数据对象,它包含一组 DiskChangeInfo.DiskChangeExtent 元素,分别表示已修改数据块磁盘区域的开始位移和长度,DiskChangeInfo 覆盖了整个磁盘区域的开始位移和长度。 EXAMPLE:(offset,length) 表示一个发生了修改的数据块的偏移量 (117768192, 65536) (132120576, 65536) (145096704, 43122688) (265289728, 65536) (958398464, 65536) 使用 QueryChangedDiskAreas 得到已修改数据块偏移量信息的前提条件是需要在创建快照前启用 CBT 功能,如果在启用 CBT 前调用该 Method 就会触发 FileFault 错误。 changeId changeId 实际上就是虚拟磁盘在某个时间点的标识符。当我们调用 QueryChangedDiskAreas Method 时,除了可能会为形参 changeId 传入一串数字序列字符串之外,还可能会传入一个「*」号。 在使用 changeId 时,应该注意以下几点: 当虚拟机还没有快照时,虚拟磁盘的 changeId 初始值应该为 none 表示未设置的。 当虚拟机创建第一个快照时,应该将传入「*」号,表示虚拟磁盘上所有的实际已分配的区域,同时忽略稀疏类型磁盘的未分配区域。需要注意的是,只有当虚拟磁盘的 changeId 初始值为 none 时,changeId=* 才会生效。换句话说,只有当虚拟机仅拥有一个快照时调用 QueryChangedDiskAreas,才能够将 changeId 设置为「*」,并且能够以此获得虚拟机的全量数据偏移量。 在此之后的每次创建快照都会生成一个新的 changeId。如果 changeId 不再为 none,则表示虚拟机已经进行过至少一次快照,此后调用 QueryChangedDiskAreas 就能够得到自 changeId 标识的快照时间点以来所发生了修改的数据块偏移量,也就是增量数据偏移量。 总结一下,使用「*」时,存在下面两点限制: 虚拟磁盘必须存放在 VMFS 上。 启动 CBT 时,虚拟机必须没有快照存在。 获取虚拟磁盘当前的 changeId: 我们能够用过 VirutalMachine ManagedObject 的 vim.vm.device.VirtualDevice.VirtualDisk 配置项中找到虚拟机每一块虚拟磁盘的 backing 信息。如果 backing 的类型是下列中的一个,你就可以使用 BackingInfo 数据对象的 changeId 属性来获得其 changeId: vim.vm.device.VirtualDevice.VirtualDiskFlatVer2BackingInfo vim.vm.device.VirtualDevice.VirtualDiskSparseVer2BackingInfo vim.vm.device.VirtualDevice.VirtualDiskRawDiskMappingVer1BackingInfo vim.vm.device.VirtualDevice.VirtualDiskRawDiskVer2BackingInfo 最后总结一下,QueryChangedDiskAreas(..., "*") 实际上会返回虚拟磁盘被实际使用的(Thin)或者整个被分配的(Thick)的数据空间偏移量。CBT 的实现依赖于「虚拟磁盘的未分配区域」以及「 VMFS 的数据块延迟清零」两者的定义和特性。因此,CBT 只有在 VMFS 数据存储上才会返回有意义的结果。在其他存储类型上,要么就失败,要么就返回包含整个磁盘的单个内容。 在开启的 CBT 的前提下,第一次调用 QueryChangedDiskAreas(..., "*") 时,它会返回虚拟磁盘上所有 已经使用的 数据块区域,后续的调用则会返回 已修改的 数据块区域,而不是 已分配的 区域。 在没有开启 CBT 的前提下,在快照后调用 QueryChangedDiskAreas,则会返回 已分配的 区域,对于精简置备虚拟磁盘和延迟清零的厚置备磁盘而言,那些 已分配但未使用的 区域则会使用零值填充。也就是说这种情况下,即便是精简置备虚拟磁盘,其全量备份所得到的数据量等于为其所分配的数据量,而非实际所使用了的数据量。 一个 QueryChangedDiskAreas 的 DEMO String changeId; //Already initialized: changeId, snapshotMoRef, the VM ManagedObjectReferencesnapshotMoRef; ManagedObjectReferencetheVM; int diskDeviceKey; //Identifies the virtual disk. VirutalMachine.DiskChangeInfochanges; long startPostion = 0; do { changes =theVM.QueryChangedDiskAreas(snapshotMoRef, diskDeviceKey, startPostion,changeId); for (int i = 0; i < changes.changedAread.length;i++) { long length =changes.changedArea[i].length; long offset =changes.chagedArea[i].startOffset; // // Go get and save disk data here } startPosition = changes.startOffset +changes.length; } while (startPosition< diskCapacity); 在这个 DEMO 里,QueryChangedDiskAreas 被反复调用,同时开始检查位置 startPosition 在虚拟磁盘中不断往后移动。这是因为对于大型虚拟磁盘而言,ChangedDiskArea 数组很可能会占用大量的内存。 NOTE:需要注意的是,QueryChangedDiskAreas 获得的是已修改数据块的偏移量,而非实际的已修改数据。 应用 QueryChangedDiskAreas 设计的增量/差异备份算法 假设在 T1 时间点创建了一个初始的全量备份,之后在 T2、T3 时间点分别创建了增量备份。当然,你也可以使用差异备份,只是它会消耗更多的备份时间和带宽,但是拥有更少的还原时间。 T1 时间的全量备份 记录虚拟机的配置信息 VirtualMachineConfigInfo。 创建虚拟机快照,命名为 snapshot_T1。 获得并保存快照中各个虚拟磁盘的 changeId,changeId_T1。 备份调用 queryChangedDiskAreas(…, “*”) 所返回的已修改数据块扇区对应的数据。 删除快照 snapshot_T1。 T2 时间的增量备份 创建虚拟机快照,命名为 snapshot_T2。 获得并保存快照中各个虚拟磁盘的 changeId,changeId_T2。 备份调用 queryChangedDiskAreas(snapshot_T2, …, changedId_T1)返回的已修改数据块扇区对应的数据。 删除快照snapshot_T2。 T3 时间的增量备份 创建虚拟机快照,命名为 snapshot_T3 (此时你无法再获得 T1 到 T2 两个时间点之间的以修改数据块列表) 获得并保存快照中各个虚拟磁盘的 changeId,changeId_T3。 备份调用 queryChangedDiskAreas(snapshot_T3, …, changedId_T2)返回的已修改数据块扇区对应的数据。如果你希望执行差异备份,则调用 queryChangedDiskAreas(snapshot_T3, …, changedId_T1)。 4删除快照snapshot_T3。 T4 时间的灾难恢复 使用之前保存的 VirtualMachineConfigInfo 中的配置参数,创建一个没有 GuestOS,没有虚拟磁盘的新虚拟机。 从 T3 的增量备份中还原数据,并记录还原了哪些扇区。 从 T2 中还原增量备份的数据,跳过 2 中已记录(已还原)的扇区,并记录还原了哪些扇区。如果 T3 是差异备份,则跳过此步骤。 从 T1 完全备份中还原数据,并跳过 2、3 中已记录(已还原)的扇区。 打开恢复过后的虚拟机。 NOTE:从后往前还原的目的就是获取同一数据块上最新的数据,从而避免不需要的数据拷贝。
目录 目录 前文列表 VMware 虚拟机的快照 快照的执行过程 删除快照 快照类型 Quiseced Snapshot 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 VMware 虚拟化编程(8) — 多线程中的 VixDiskLib VMware 虚拟机的快照 在备份和恢复应用程序中,不管是全量备份,还是增量备份,都依赖于 vSphere 中的快照。要备份虚拟机,首先需要创建一个虚拟机的快照。快照创建成功之后,就需要找到与快照相关的虚拟磁盘。虚拟磁盘的快照文件以虚拟磁盘的基本名称命名,并且在名称后面追加了唯一的序列字符串,以保证 VMDK 文件的唯一性。如:vdisk-000032.vmdk,其中 vdisk 是虚拟磁盘的基本名称。在物理文件系统的快照磁盘文件类型如下: vm_name-000001.vmdk (配置文件): 虚拟机快照的元数据文件,记录了该次快照相关文件的信息,其中 000001 表示第一次快照。 vm_name-000001-delta.vmdk (二进制文件):称为快照数据文件或者重做日志文件(redo-log)也被称为子磁盘文件,该文件用于保存快照时间点后虚拟机所产生的更改数据(即快照数据)。应用了 in-file delta technology 技术,初始大小为 16MB,会随着虚拟机数据落盘操作的增多,而按照 16MB 的大小进行增长(降低 SCSI reservation 冲突),并且该文件的大小永远不会超过 Base Disk File 的大小。 vm_name-000001-ctk.vmdk (二进制文件):改变追踪文件,保存了自从上次快照以来的虚拟磁盘文件所发生变化的数据块偏移量信息。需要开启 CBT 功能才会生成该文件。 NOTE:在 vSphere API 的定义中,使用 Datastore 唯一标识作为前缀,结合虚拟磁盘相对于 Datastore 根目录的路径,以此来标识一块虚拟磁盘,如:[storageN] VmName/vdisk-NNNNNN.vmdk 要取得虚拟磁盘机器快照的名称和相关配置信息(capacityInKB、changeID 等),可以使用 PropertyCollector 来获取 VirtualMachine Managed Object 的 config.hardware.device 属性。在其中找到 VirtualDisk 项的 BackingInfo 属性,那就是虚拟磁盘的配置信息,有以下类型: VirtualDiskFlatVer1BackingInfo VirutalDiskFlagVer2BackingInfo VirtualDiskRawDiskMappingVer1BackingInfo VirtualDiskSparseVer1BackingInfo VirtualDiskSparseVer2BackingInfo NOTE:VirtualDiskRawDiskMappingVer1BackingInfo 类型的虚拟磁盘是无法创建快照的,所以也无法备份这一类型的虚拟磁盘。 快照的执行过程 STEP 1: STEP 2: STEP 3: 可以看出 VMware 虚拟机快照的特性有: VMware 虚拟机使用的是链式快照。 VMware 虚拟机快照的 Parent VMDK File 的访问权限为 OR 只读。 快照时间点之后新落盘的数据只会被写入到 Child VMDK File 快照数据文件中。 快照链上的任意快照文件的损坏都会导致虚拟机无法正常运行。 删除快照 从创建快照的特性中可以理解,如果希望在删除一个快照的同时保证虚拟机能够正常运行的话,那么就需要将该快照数据文件中的数据合并到 Parent VMDK File 中,以此来保证虚拟机磁盘数据的完整性。 删除虚拟机快照一般会是以下两种情况: 待删除的虚拟机快照在快照链中:delta vmdk 中的数据会向父快照的 delta vmdk 或基础虚拟磁盘文件 base vmdk 合并,然后 delta vmdk 被删除。 待删除的虚拟机快照不在快照链中(VMware 支持独立快照):不需要合并,直接删除快照数据文件。 删除 VMware 虚拟机快照的特点: 删除快照过程包括两个异步的操作:1. 从 Snapshot Manager 中将快照删除;2. vmdk 数据合并。如果 1 成功而 2 失败,就会残留 delta vmdk 文件,这样的话就需要手动进行快照文件的合并。 删除快照可能会带来大量的数据写操作,有时候可能需要删除很长的时间,并且期间虚拟机的性能会受到负面影响。 自从 vSphere 4 Update 2 开始,优化了选择删除所有虚拟机快照的过程,不再是顺序向下一层层的合并,而是各层分别直接合并到 Base vmdk 中。 快照类型 崩溃一致快照 (Crash-Consistent Snapshot):是 VMware 虚拟机的默认快照类型,相当于电脑突然断电时磁盘的状态,闪存中的数据会丢失掉。 文件系统一致快照 (File-System-Consistent Snapshot):快照时间点之前,虚拟机的文件系统会被冻结,内存中的脏数据刷盘,快照完成之后,文件系统再解冻。这样的快照能够保证文件系统的一致性,即内存中的数据不会丢失。 应用一致性 (Application-Consistent Snapshot):快照时间点前,虚拟机上运行的应用程序被冻结,内存中应用程序相关的所有脏数据刷盘,快照完成之后,应用程序再被解冻,这样的快照能够保证指定应用程序的数据是完整的,但不会保证文件系统也是完全一致的。 其中文件系统一致快照和应用一致性也统称为 Quiseced Snapshot。 Quiseced Snapshot 使用 Quiseced Snapshot 需要特殊的环境配置,主要的实现方式有两种: 使用 GuestOS 内置的应用服务或 VMware 提供的一致性驱动: 版本较新的 Windows GuestOS 提供了 VSS(Volume Shadow Copy Service) 服务,VSS 的 Requester-Writer 能够对应用程序和文件系统进行冻结和解冻操作。 对于版本较老的 Windows GuestOS,VMware 也提供了 SYNC driver 数据一致性驱动来支持应用程序和文件系统一致性快照; 而 Linux Guest,VMware 提供了仅支持持文件系统一致性的 vmsync kernel module。 使用脚本程序:如果是非 Windows GuestOS,那么就需要编写针对指定应用程序的脚本来对其进行冻结和解冻的操作。 NOTE:上述列举的 VSS、SYNC driver、vmsync kernel module 和脚本,均要依赖 VMware Tools 来调用,所以即便客户机操作系统支持上述功能,仍需安装 VMware Tools 才能完美支持 Quiseced Snapshot。例如针对 VSS,VMware tools 就提供了 VSS support 功能,它是 VMware tools 和 Windows VSS 之间交互的桥梁。 Quiseced Snapshot 的创建过程: 1. 用户发出 Quiesced Snapshot 创建请求给 vCenter,vCenter 再给虚拟机所在的 ESXi 的 Hostd Service 发出快照创建请求。 2. ESXi 上的 Hostd Service 将快照创建请求传递给 虚拟机 GuestOS 内的 VMware tools。 3. VMware tools 以 VSS Requester 的身份通知 VSS,VSS 再通知已经被注册的文件系统和各应用的 VSS writer 执行冻结操作。 4. 一旦完成冻结和内存数据落盘,VMware tools 就将完成结果通知 Hostd Service。 5. Hostd Service 执行快照操作。 6. 快照完成后,按照前面的顺序再对文件系统和各应用进行解冻。
目录 目录 前文列表 多线程注意事项 多线程中的 VixDiskLib 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VMware 虚拟化编程(7) — VixDiskLib 虚拟磁盘库详解之三 多线程注意事项 如果你的应用程序是多线程实现,那么你应该将 VMDK 操作的请求串行化。通过 VixDiskLib_Open 获取的磁盘句柄 diskHandle 并不会绑定到单个线程,而是能够跨线程使用。也就是说,你可以在一个线程中打开磁盘,然后在其他线程中使用该磁盘的句柄来进行其他的磁盘操作,但前提是你 必须串行化磁盘操作请求。否则在并发的场景中,必定会造成磁盘操作对象混乱的后果。 多线程中的 VixDiskLib 首先可以确定的是,VDDK 支持到多个 VMDK File 的并发 I/O。但存在着以下限制: VixDiskLib_InitEx or VixDiskLib_Init 只能被每个进程中的主线程调用一次。 VixDiskLib_InitEx or VixDiskLib_Init 的回调日志函数参数不能指定为 NULL。VixDiskLib 提供的默认日志函数是线程非安全的。在多线程环境中,必须提供自己实现的的线程安全日志函数。 在调用 VixDiskLib_Open 或 VixDiskLib_Close() 时,VDDK 会分别进行或取消多个 lib 库的初始化。这使得在多线程环境中,可能会导致一些需要初始化环境的 lib 库不能生效。EXAMPLE:这会使 diskhandle2 的某些磁盘操作失败。 Thread 1: VixDiskLib_Open ...... VixDiskLib_Close Thread 2: ................................... VixDiskLib_Open ...... VixDiskLib_Close 解决该问题的办法就是,使用一个指定的线程管理所有的 Open 和 Close 操作,而让其他的线程进行 Read 或 Writes 等操作。EXAMPLE: Open/Close Thread: VixDiskLib_Open ...... VixDiskLib_Open ...... VixDiskLib_Close ...... VixDiskLib_Close ...... (handle1) (handle2) (handle1) (handle2) I/O Thread 1: (owns handle1) VixDiskLib_Read ... VixDiskLib_Read ... I/O Thread 2: (owns handle2) VixDiskLib_Read ... VixDiskLib_Read ... 该图显示了两个独立 diskHandle 上的并发读取,需要注意的是,在同一个 diskHandle 上进行并发读取是不被允许的。
目录 目录 前文列表 VixDiskLib 虚拟磁盘库 VixDiskLib_GetMetadataKeys VixDiskLib_ReadMetadata 获取虚拟磁盘元数据 VixDiskLib_WriteMetadata 更新虚拟磁盘元数据表 VixDiskLib_Create 创建新的寄宿磁盘 Hosted Disk VixDiskLib_Clone 克隆 VMDK File 创建新的托管磁盘 Managed Disk VixDiskLib_Unlink 删除 VMDK File VixDiskLib_Grow 扩展 VMDK File VixDiskLib_Shrink 压缩 VMDK File VixDiskLib_Defragment 整理 VMDK File VixDiskLib_Rename 重命名 VMDK File VMware 虚拟机的快照 VixDiskLib_CreateChild 创建 VMDK File 的快照文件 VixDiskLib_Attach 读取指定快照文件的数据 保持磁盘操作的原子性 VixDiskLib_PrepareForAccess 推迟干扰虚拟磁盘访问的虚拟机操作 VixDiskLib_EndAccess 结束访问占用 Managed Disk 操作需知 Hosted Disk 操作需知 远程创建一个虚拟机的算法 回滚虚拟机到指定快照时间点上的算法 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VMware 虚拟化编程(6) — VixDiskLib 虚拟磁盘库详解之二 VixDiskLib 虚拟磁盘库 紧接上篇。 VixDiskLib_GetMetadataKeys & VixDiskLib_ReadMetadata 获取虚拟磁盘元数据 函数原型: /** * Retrieves the list of keys in the metadata table. * Key names are returned as list of null-terminated strings, * followed by an additional NULL character. * @param diskHandle [in] Handle to an open virtual disk. * @param keys [out, optional] Keynames buffer, can be NULL. * @param maxLen [in] Size of the keynames buffer. * @param requiredLen [out, optional] Space required for the keys including the double * end-of-string characters. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_GetMetadataKeys(VixDiskLibHandle diskHandle, char *keys, size_t maxLen, size_t *requiredLen); /** * Retrieves the value of a metadata entry corresponding to the supplied key. * @param diskHandle [in] Handle to an open virtual disk. * @param key [in] Key name. * @param buf [out, optional] Placeholder for key's value in the metadata store, * can be NULL. * @param bufLen [in] Size of the buffer. * @param requiredLen [out, optional] Size of buffer required for the value (including * end of string character) * @return VIX_OK if success, VIX_E_DISK_BUFFER_TOO_SMALL if too small a buffer * and other errors as applicable. */ VixError VixDiskLib_ReadMetadata(VixDiskLibHandle diskHandle, const char *key, char *buf, size_t bufLen, size_t *requiredLen); 函数调用: vixError = VixDiskLib_ReadMetadataKeys(diskHandle, &buf[0], requiredLen, NULL); vixError = VixDiskLib_ReadMetadata(diskHandle, metakey, &val[0],requiredLen, NULL); 元数据表中包含了磁盘卷标、LUN、分区布局、链接个数、文件属性、锁、RDM 封装等信息,Hosted Disk 和 Managed Disk 的元数据也并不相同。 VixDiskLibAdapterTypea dapterType VixDiskLibSectorType capacity int numLinks char * parentFileNameHint VixDiskLibGeometry biosGeo VixDiskLibGeometry physGeo typedef uint32 heads typedef uint32 cylinders typedef uint32 sectors adapterType= buslogic geometry.heads= 64 geometry.cylinders= 100 geometry.sectors= 32 uuid= 60 00 c2 93 7b a0 3a 03 9f 22 56 c5 29 93 b7 27 需要注意的是,VixDiskLib_ReadMetadataKeys 获取的虚拟磁盘的元数据表中的 key,而非 value,其需要和 VixDiskLib_ReadMetadata 结合使用。 VixDiskLib_WriteMetadata 更新虚拟磁盘元数据表 函数原型: /** * Creates or modifies a metadata table entry. * @param diskHandle [in] Handle to an open virtual disk. * @param key [in] Key name. * @param val [in] Key's value. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_WriteMetadata(VixDiskLibHandle diskHandle, const char *key, const char *val); 函数调用: vixError = VixDiskLib_WriteMetadata(diskHandle, metakey, metaVal); VixDiskLib_WriteMetadata 通过指定 Key/Value 对来更新虚拟磁盘的元数据。如果 Key 不存在,则会新添一项;如果 Key 存在,则更新 Value。Key 可以被设置为空,但是不能删除。 VixDiskLib_Create 创建新的寄宿磁盘 Hosted Disk 函数原型: /** * Creates a local disk. Remote disk creation is not supported. * @param connection [in] A valid connection. * @param path [in] VMDK file name given as absolute path * e.g. "c:\\My Virtual Machines\\MailServer\SystemDisk.vmdk". * @param createParams [in] Specification for the new disk (type, capacity ...). * @param progressFunc [in] Callback to report progress. * @param progressCallbackData [in] Callback data pointer. * @return VIX_OK if success suitable VIX error code otherwise. */ VixError VixDiskLib_Create(const VixDiskLibConnection connection, const char *path, const VixDiskLibCreateParams *createParams, VixDiskLibProgressFunc progressFunc, void *progressCallbackData); 函数调用: vixError = VixDiskLib_Create(connectionHandle, diskPath, &createParams, NULL, NULL); 当你使用 VixDiskLib_ConnectEx 或 VixDiskLib_Connect 连接到 ESX/ESXi Host 之后,就可以调用该函数创建一个新的 hosted disk(本地磁盘)。 @param createParams:需要指定 磁盘类型、适配器类型、硬件版本,容量 等信息。 typedef VixDiskLibDiskType diskType typedef VixDiskLibAdapterType adapterType typedef uint hwVersion typedef VixDiskLibSectorType capacity NOTE:在 FAT32 以及 FAT 文件系统上,VixDiskLib_Create 要求 hosted disk 的 size 小于 4GB;NTFS 文件系统上,hosted disk 的 size 小于 16TB-54KB(hex FFFFFFF0000);在 ReFS 和 exFAT 文件系统,hosted disk 的 size 小于 2^64 ‐ 1。在最新的 vSphere5.5 以及更新版本中,将支持大于 2TB 的 VMDK File。包括 NFS3 在内的基于 POSIX 的文件系统不再有 2GB 的 VMDK File Size Limit。 VixDiskLib_Clone 克隆 VMDK File 函数原型 /** * Copies a disk with proper conversion. * @param dstConnection [in] A valid connection to access the destination disk. * @param dstPath [in] Absolute path for the (new) destination disk. * @param srcConnection [in] A valid connection to access the source disk. * @param srcPath [in] Absolute path for the source disk. * @param vixCreateParams [in] creationParameters (disktype, hardware type...). * If the destination is remote, createParams is currently * ignored and disk with default size and adapter type is * created. * @param progressFunc [in] Callback to report progress (called on the same thread). * @param progressCallbackData [in] Opaque pointer passed along with the percent * complete. * @param overWrite [in] TRUE if Clone should overwrite an existing file. * @return VIX_OK if success, suitable VIX error code otherwise (network errors like * file already exists * handshake failure, ... * are all combined into a generic connect message). */ VixError VixDiskLib_Clone(const VixDiskLibConnection dstConnection, const char *dstPath, const VixDiskLibConnection srcConnection, const char *srcPath, const VixDiskLibCreateParams *vixCreateParams, VixDiskLibProgressFunc progressFunc, void *progressCallbackData, Bool overWrite); 函数调用: vixError = VixDiskLib_Clone(dstconnection, dstPath, srcConnection, srcPath, &createParams, CloneProgressFunc, NULL, TRUE); VixDiskLib_Clone 函数能够将一个 VMDK File 中的数据拷贝到另一个 VMDK File 中,支持指定 磁盘类型,磁盘大小,硬件版本 等信息。 创建新的托管磁盘 Managed Disk 因为 VixDiskLib_Create 函数仅支持创建本地的 hosted disk,所以如果你希望创建一个 Remote Managed Disk 的话,就需要使用特别的方式。 Step 1:首先调用 VixDiskLib_Create 创建一个 hosted disk Step 2:然后调用 VixDiskLib_Clone 将 hosted disk 的数据克隆到 managed disk。 VixDiskLib_Unlink 删除 VMDK File 函数原型: /** * Deletes all extents of the specified disk link. If the path refers to a * parent disk, the child (redo log) will be orphaned. * Unlinking the child does not affect the parent. * @param connection [in] A valid connection. * @param path [in] Path to the disk to be deleted. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Unlink(VixDiskLibConnection connection, const char *path); 函数调用: vixError= VixDiskLib(connectionHandle, diskpath); 该函数需要提供两个参数,连接句柄和 VMDK File Name,调用该函数能够彻底删除 VMDK File。需要注意的是,删除一个 VMDK 后,它包含的所有信息都将丢失。一般的,如果虚拟机正在运行,那么 HostOS 将会阻止你删除 VMDK File。 但如果你删除了一个电源已关闭的虚拟机的 VMDK 文件,那么这个 GuestOS 将无法启动。 VixDiskLib_Grow 扩展 VMDK File 函数原型: /** * Grows an existing disk, only local disks are grown. * @pre The specified disk is not open. * @param connection [in] A valid connection. * @param path [in] Path to the disk to be grown. * @param capacity [in] Target size for the disk. * @param updateGeometry [in] Should vixDiskLib update the geometry? * @param progressFunc [in] Callback to report progress (called on the same thread). * @param progressCallbackData [in] Opaque pointer passed along with the percent * complete. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Grow(VixDiskLibConnection connection, const char *path, VixDiskLibSectorType capacity, Bool updateGeometry, VixDiskLibProgressFunc progressFunc, void *progressCallbackData); 函数调用: vixError= VixDiskLib_Grow(connectionHandle, diskpath, size, FALSE, GrowProgressFunc, NULL); 调用 VixDiskLib_Grow 函数能够通过增加扇区数量的方式来扩展一个已存在的 VMDK File,但该函数同样仅支持 hosted disk。 VixDiskLib_Shrink 压缩 VMDK File 函数原型: /** * Shrinks an existing disk, only local disks are shrunk. * @param diskHandle [in] Handle to an open virtual disk. * @param progressFunc [in] Callback to report progress (called on the same thread). * @param progressCallbackData [in] Opaque pointer passed along with the percent * complete. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Shrink(VixDiskLibHandle diskHandle, VixDiskLibProgressFunc progressFunc, void *progressCallbackData); 函数调用: vixError= VixDiskLib_Shrink(diskHandle, ShrinkProgressFunc, NULL); 调用 VixDiskLib_Shrink 函数能够回收 VMDK File 中未被使用的空间(标记为 0 的 Blocks),该函数一般对 SPARSE 稀疏型磁盘文件而言效果更佳明显。同样的该函数仅支持 hosted disk。 NOTE:我们可以调用 VixDiskLib_Write 来将未被使用的磁盘空间置零,然后再调用 VixDiskLib_Shrink 来回收这些空间。而且调用 VixDiskLib_Shrink 并不会减少磁盘的实际容量,却可以获得更多的可用空余磁盘空间。 VixDiskLib_Defragment 整理 VMDK File 函数原型: /** * Defragments an existing disk. * @param diskHandle [in] Handle to an open virtual disk. * @param progressFunc [in] Callback to report progress (called on the same thread). * @param progressCallbackData [in] Opaque pointer passed along with the percent * complete. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Defragment(VixDiskLibHandle diskHandle, VixDiskLibProgressFunc progressFunc, void *progressCallbackData); 函数调用: vixError = VixDiskLib_Defragment(diskHandle, DefragProgressFunc, NULL); 调用函数 VixDiskLib_Defragment 能够对一个已存在的 VMDK File 进行碎片整理,碎片整理能够有效的提高磁盘的性能。需要注意的是碎片整理操作仅会对 SPARSE 稀疏型磁盘文件有效,而平面类型 FLAG 的磁盘文件则无需要进行碎片整理。 NOTE:VixDiskLib_Defragment 实际上是将 2GB 范围内的碎片数据往更低的区段移动,该操作对于 GuestOS 自带的碎片整理工具而言是透明的。VMware 会推荐一种「由内到外」的碎片整理方案,即使用 GuestOS ==> VMDK(VixDiskLib_Defragment) ==> HostOS 的顺序进行整理。 VixDiskLib_Rename 重命名 VMDK File 函数原型: /** * Renames a virtual disk. * @param srcFileName [in] Virtual disk file to rename. * @param dstFileName [in] New name for the virtual disk. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Rename(const char *srcFileName, const char *dstFileName); 函数调用: vixError = VixDiskLib_Rename(oldDiskpath, newDiskpath); 该函数需要传入两个参数,新旧的 VMDK File Name。HostOS 需要将 GuestOS 的 VMDK File 储存在一个可预见的位置,在 Rename 期间的任何文件访问都有可能造成 I/O 失败,或导致 GuestOS 故障。所以需要注意的是,调用该函数之前,需要确保关闭虚拟机电源。 VMware 虚拟机的快照 通常,我们可以通过创建一个虚拟机的快照来生成 VMDK File 的一个重写日志,这个快照包含了磁盘数据和虚拟机状态。但对于 Hosted Disk,我们也可以直接调用 VixDiskLib_CreateChild 函数来生成一个重写日志。所以在 VDDK 相关的术语中,子磁盘(Child Disk)、重做日志(Redo Log)、差异链(Delta Link)、快照数据文件(Snapshot File) 具有相同的含义。从最原始的父磁盘文件(Base File)开始,每一个子磁盘都包含了从父磁盘的原始状态到自身当前状态的集合。换句话说,每个快照数据文件都包含了从上一个快照时间点到当前快照时间点之间的更新数据。 NOTE 1:这些快照数据文件类型为 VIXDISKLIB_DISK_VMFS_SPARSE 稀疏型磁盘文件,并使用了 COW 的机制来存放更新数据,以此来节省存储空间。可参考 《再谈 COW、ROW 快照技术》 NOTE 2:子磁盘被创建之后,vm.vmdk 的指向就会被更改指向它。也就是说,此时的父磁盘是 OR 只读的状态,而子磁盘才是 RW 可读写的状态。 NOTE 3:VMware 虚拟机的第一次快照所得到的快照数据实际上是虚拟机当前状态的全量数据,而之后的快照一般均为增量数据。 VixDiskLib_CreateChild 创建 VMDK File 的快照文件 函数原型: /** * Creates a redo log from a parent disk. * @param diskHandle [in] Handle to an open virtual disk. * @param childPath [in] Redo log file name given as absolute path * e.g. "c:\\My Virtual Machines\\MailServer\SystemDisk_s0001.vmdk". * @param diskType [in] Either VIXDISKLIB_DISK_MONOLITHIC_SPARSE or * VIXDISKLIB_DISK_SPLIT_SPARSE. * @param progressFunc [in] Callback to report progress. * @param progressCallbackData [in] Callback data pointer. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_CreateChild(VixDiskLibHandle diskHandle, const char *childPath, VixDiskLibDiskType diskType, VixDiskLibProgressFunc progressFunc, void *progressCallbackData); 函数调用: vixError = VixDiskLib_CreateChild(parentHandle, diskPath, VIXDISKLIB_DISK_MONOLITHIC_SPARSE, NULL, NULL); VixDiskLib_Attach 读取指定快照文件的数据 函数原型: /** * Attaches the child disk chain to the parent disk chain. Parent handle is * invalid after attaching and child represents the combined disk chain. * @param parent [in] Handle to the disk to be attached. * @param child [in] Handle to the disk to attach. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Attach(VixDiskLibHandle parent, VixDiskLibHandle child); 函数调用: vixError = VixDiskLib_Attach(parentHandle, childHandle); 如上图,当我们希望访问 Child1 时间点的快照数据时,需要先将子磁盘 Child1a 挂载到 Child1 之后,那么实际上我们会获取到了 Child1 快照时间点时刻的虚拟机状态。类似于将虚拟机恢复到指定的快照时间点,也类似于新建了一条快照链的分支。 保持磁盘操作的原子性 当我们对虚拟机的虚拟磁盘进行操作时,需要对这些操作保证一定的原子性,也就是说我们需要保证在对虚拟磁盘的才做完成之前,不会被别的虚拟机操作打断。VDDK 5.0 之后包含了两个新的 VixDiskLib 调用 PrepareForAccess 和 EndAccess,它们用于虚拟机备份时禁用和启用存储迁移功能。这避免了在执行备份时,虚拟机移动了它的存储,从而导致旧的磁盘镜像被遗留了下来。VMware 强烈建议使用这两个调用。 VixDiskLib_PrepareForAccess 推迟干扰虚拟磁盘访问的虚拟机操作 函数原型: /** * This function is used to notify the host of the virtual machine that the * disks of the virtual machine will be opened. The host disables operations on * the virtual machine that may be adversely affected if they are performed * while the disks are open by a third party application. * * @param connectParams [in] This is always used on remote connections. * @param identity [in] An arbitrary string containing the identity of the * application. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_PrepareForAccess(const VixDiskLibConnectParams *connectParams, const char *identity); 函数调用: vixError = VixDiskLib_PrepareForAccess(&cnxParams, “vmName”); @param identity:一个标识字符串,仅用于跟踪目的,且长度不超过 50 个字符。可以是虚拟机名称或者应用程序名称。 VixDiskLib_PrepareForAccess 函数会通知 vCenter 服务器,ESXi 主机正在打开一个虚拟磁盘,ESXi 主机应该推迟所有可能会干扰这次虚拟磁盘访问的虚拟机操作。例如:该函数会禁止虚拟机的 RelocateVM_Task 迁移操作。 NOTE 1:在备份应用程序中,应该在创建虚拟机快照前调用此函数。 NOTE 2:调用该函数必须连接到 vCenter,如果直接到 ESX/ESXi Host 上调用,系统会抛出错误消息:「VDDK: HostAgent is not a vCenter, cannot disable svmotion.」 NOTE 3:在调用该函数时应该以超时时间作为释放依据,否则容易出现程序逻辑上的流动。EXAMPLE:下面的代码片段中,在备份程序中使用 PrepareForAccess 等待存储迁移(Storage vMotion)完成,设定最多只等待 10 分钟。 if(appGlobals.vmxSpec != NULL) { for (int i = 0; i < 10; i+) { vixError =VxiDiskLib_PrepareForAccess(&cnxParams, “Sample”); if (vixError == VIX_OK) { break; } else { Sleep(60000); } } } VixDiskLib_EndAccess 结束访问占用 函数原型: /** * This function is used to notify the host of a virtual machine that the * virtual machine disks are closed and that the operations which rely on the * virtual machine disks to be closed can now be allowed. * * @param connectParams [in] Always used for a remote connection. Must be the * same parameters as used in the corresponding PrepareForAccess call. * @param identity [in] An arbitrary string containing the identity of the * application. * @return VIX_OK of success, suitable VIX error code otherwise. */ VixError VixDiskLib_EndAccess(const VixDiskLibConnectParams *connectParams, const char *identity); 函数调用: vixError= VixDiskLib_EndAccess(&cnxParams, “vmName”); 每一次调用 VixDiskLib_PrepareForAcccess 都应该相应的调用一次 VixDiskLib_EndAccess 函数。该函数会通知 ESXi 主机,虚拟磁盘访问已经被关闭,被推迟的虚拟磁盘操作现在可以启用了,如 vMotion,RelocateVM_Task。 NOTE:该函数还能够在关闭所有虚拟磁盘并删除所有虚拟机快照后调用,也能够在奔溃后调用它来做一次 Cleanup 清除操作。 Managed Disk 操作需知 如果使用 VixDiskLib_Connect 打开一个 Managed Disk 的连接,必须提供有效的 vSphere 访问认证信息。 在 ESX/ESXi Host 上,VixDiskLib_Open 不能打开磁盘链中的单个连接。 如果要 在 ESX/ESXi Host 上创建一个 Managed Disk,首先需要调用 VixDiskLib_Create 创建一个 Hosted Disk,然后使用 VixDiskLib_Clone 将 Hosted Disk 转换为 Managed Disk。 VixDiskLib_Defragment 只能针对 Hosted Disk 进行碎片整理。 VixDiskLib_Grow() 只能扩展 Hosted Disk。 VixDiskLib_Unlink() 只能删除 Hosted Disk。 ESXi 5.1 之前,HotAdd 传输只能用于 vSphere 企业版或更高版本。 Hosted Disk 操作需知 除了高级传输模式以外,大多数操作都 能支持 Hosted Disk,但不包括: - VixDiskLib_ConnectEx() 扩展连接函数。 - SAN 和 HotAdd 高级传输。 - 使用 VixDiskLib_PrepareForAccess() 和 VixDiskLib_EndAccess() 延迟存储迁移。 远程创建一个虚拟机的算法 使用 VixDiskLib_Create 创建一个 2GB 大小的本地 VMDK File。 使用 VixDiskLib_Write 将 GuestOS 的镜像和应用软件写入 VMDK File。 将本地的 VMDK File 克隆到 ESX/ESXi Host 的 VMFS 文件系统上。 vixError= VixDiskLib_Clone(appGlobals.connection, appGlobals.diskPath, srcConnection,appGlobals.srcPath, &createParam, CloneProgressFunc, NULL, TRUE); # appGlloobals.connection, appGlobals.diskPath 表示 ESX/ESXi Host 上的远程 VMDK File # srcConection, appGlobals.srcPath 表示本地 VMDK File。 打开新的 GuestOS,得到一个新的虚拟机。 NOTE:无论在步骤 1 中创建什么类型的 VMDK File,在步骤 3 克隆之后它都会变成 VIXDISKLIB_DISK_VMFS_FLAT 类型的虚拟磁盘。 回滚虚拟机到指定快照时间点上的算法 找到指定快照时间点的 <vmname>-<NNN>.vmdk 重写日志。<NNN> 是一个序列号,可以通过时间戳来表示。 VixDiskLib_InitEx 初始化虚拟磁盘库 VixDiskLib_ConnectEx 连接到虚拟机 VixDiskLib_Open 打开重写日志,并得到它的父句柄。 VixDiskLib_Create 创建一个子磁盘 VixDiskLib_Attach 将子磁盘附加到指定快照时间点对应的磁盘上 读、写附加后的子虚拟磁盘。
目录 目录 前文列表 VixDiskLib 虚拟磁盘库 VixDiskLib_Open 打开 VMDK File VixDiskLib_Read 读取 VMDK File 数据 VixDiskLib_Write 写入数据到 VMDK File VixDiskLib_GetInfo 获取 VMDK File 信息 VixDiskLib_FreeInfo 释放 VMDK File 信息 VixDiskLib_Close 关闭 VMDK File 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VMware 虚拟化编程(5) — VixDiskLib 虚拟磁盘库详解之一 VixDiskLib 虚拟磁盘库 紧接上篇。 VixDiskLib_Open 打开 VMDK File 函数原型: /** * Opens a local or remote virtual disk. * @param connection [in] A valid connection. * @param path [in] VMDK file name given as absolute path * e.g. "[storage1] MailServer/SystemDisk.vmdk" * @param flags [in, optional] Bitwise or'ed combination of * VIXDISKLIB_FLAG_OPEN_UNBUFFERED * VIXDISKLIB_FLAG_OPEN_SINGLE_LINK * VIXDISKLIB_FLAG_OPEN_READ_ONLY. * @param diskHandle [out] Handle to opened disk, NULL if disk was not opened. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Open(const VixDiskLibConnection connection, const char *path, uint32 flags, VixDiskLibHandle *diskHandle); 函数调用: vixError = VixDiskLib_Open(appGlobals.connection, appGlobals.diskPath, appGlobals.openFlags, &srcHandle); 是初始化并建立了与服务器的连接之后,调用 VixDiskLib_Open 即可打开本机(寄宿磁盘)或远程(托管磁盘)的 VMDK File。 需要注意的是,如果使用了 SAN 或 HotAdd 高级传输模式来建立与服务器之间的连接的话,则虚拟机需要存在至少一个 Snapshot 才能够执行 Open VMDK File 的操作。 @param connection:实际上是在调用 VixDiskLib_Connect 时 Return 的 VixDiskLibConnection 类型对象 @param path:指定需要 Open 的 VMDK File 的路径,使用 vSphere 的通过路径格式 [datastore] virtualmachine/vmdk_file.vmdk @param flags:可以指定下列 flags: VIXDISKLIB_FLAG_OPEN_UNBUFFERED 禁用主机磁盘缓存。 VIXDISKLIB_FLAG_OPEN_SINGLE_LINK 打开当前磁盘链接,而不是开发整个磁盘链(仅托管磁盘)。 VIXDISKLIB_FLAG_OPEN_READ_ONLY只读模式打开虚拟磁盘。 @param diskHandle:若 Open SUCCESS,则返回一个虚拟磁盘对象句柄,该句柄会在后续的 读/写/克隆/获取磁盘信息/管理磁盘元数据/伸缩磁盘 Size/磁盘碎片整理 函数调用中作为实参传入。 VixDiskLib_Read 读取 VMDK File 数据 函数原型: /** * Reads a sector range. * @param diskHandle [in] Handle to an open virtual disk. * @param startSector [in] Absolute offset. * @param numSectors [in] Number of sectors to read. * @param readBuffer [out] Buffer to read into. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Read(VixDiskLibHandle diskHandle, VixDiskLibSectorType startSector, VixDiskLibSectorType numSectors, uint8 *readBuffer); 函数调用: vixError = VixDiskLib_Read(diskHandle, startSector, numSectors, &mybuffer); @param diskHandle:为调用 VixDiskLib_Open Return 的虚拟磁盘对象句柄。 @param startSector, @param numSectors:通过指定开始扇区 startSector 和扇区数量 numSectors,VixDiskLib_Read 从打开的 VMDK File 中读取一片连续的扇区数据。扇区的大小可以不同,但是在 VixDiskLib.h 头文件中已经将这个 Size 定义为 512 个字节,因为 VMDK File 的扇区大小就是 512 字节。 #define VIXDISKLIB_SECTOR_SIZE 512 @param readBuffer:是实际读取到的 VMDK File 的数据。 需要注意的是,因为每次读取的扇区 Size 通常为 512 字节,所以读取数据的完整性实际上是需要由应用程序来控制的,并不是调用一次 VixDiskLib_Read 函数就能够得到完整的 VMDK File 数据。 VixDiskLib_Write 写入数据到 VMDK File 函数原型: /** * Writes a sector range. * @param diskHandle [in] Handle to an open virtual disk. * @param startSector [in] Absolute offset. * @param numSectors [in] Number of sectors to write. * @param writeBuffer [in] Buffer to write. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Write(VixDiskLibHandle diskHandle, VixDiskLibSectorType startSector, VixDiskLibSectorType numSectors, const uint8 *writeBuffer); 函数调用: VixDiskLib_Write(diskHandle, startsector, (sizeof mybuffer) / 512, mybuffer); @param writeBuffer:该实参的长度必须是 VIXDISKLIB_SECTOR_SIZE 的整数倍字节。 VixDiskLib_GetInfo 获取 VMDK File 信息 函数原型: /** * Retrieves information about a disk. * @param diskHandle [in] Handle to an open virtual disk. * @param info [out] Disk information filled up. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_GetInfo(VixDiskLibHandle diskHandle, VixDiskLibInfo **info); 函数调用: VixError vixError = VixDiskLib_GetInfo(diskHandle, &info); @param info:返回 VMDK File 的信息。 VixDiskLib_GetInfo 获取指定 Opened VMDK File 的下列相关信息,分配并填充 VixDiskLibDiskInfo 数据结构,其中的一部分信息会与 VMDK File Metadata 的信息相同: bios capacity adapterType links blocks VixDiskLib_FreeInfo 释放 VMDK File 信息 函数原型: /** * Frees memory allocated in VixDiskLib_GetInfo. * @param info [in] Disk information to be freed. */ void VixDiskLib_FreeInfo(VixDiskLibInfo *info); 函数调用: vixError = VixDiskLib_FreeInfo(diskInfo); @param info: 为调用 VixDiskLib_GetInfo 返回的 VixDiskLibInfo 类型对象 因为 VixDiskLib_GetInfo 会在内存中分配并填充 VixDiskLibDiskInfo 数据结构,所以需要在 VixDiskLib_GetInfo 调用失败之后立即调用 VixDiskLib_FreeInfo 以释放内存空间,避免内存泄漏。 VixDiskLib_Close 关闭 VMDK File 函数原型: /** * Closes the disk. * @param diskHandle [in] Handle to an open virtual disk. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_Close(VixDiskLibHandle diskHandle); 函数调用: VixDiskLib_Close(diskHandle); 在完成对 VMDK File 的操作之后,一定要谨记关闭 VMDK File。
目录 目录 前文列表 VixDiskLib 虚拟磁盘库 虚拟磁盘数据的传输方式 Transport Methods VixDiskLib_ListTransportModes 枚举支持的传输模式 VixDiskLib_InitEx 初始化 VixDiskLib 库 VixDiskLib_ConnectEx 连接到 virtual disk library VixDiskLib_Disconnect 断开 VixDiskLib 的连接 VixDiskLib_Cleanup 断开连接之后的清理 VixDiskLib_Exit cleans up the library before exit VixDiskLib_GetErrorText 获取错误信息 VixDiskLib_FreeErrorText 释放错误描述 VMDK File 的访问认证和权限 Credentials and Privileges SSL 证书和安全 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VMware 虚拟化编程(4) — VDDK 安装 VixDiskLib 虚拟磁盘库 VDDK 实际上是基于 Virtual Disk API(虚拟磁盘接口) 来实现的,而 Virtual Disk API 也就是 VixDiskLib 虚拟磁盘库,是一组管理 VMDK File 的函数调用集合。 虚拟磁盘数据的传输方式 Transport Methods VDDK 总共提供了 5 种数据传输方式,其中前 3 种为基本传输方式,后两种为高级传输方式: 本地文件(Local File) 网络块设备 NBD(Network Block Device) 局域网的加密 NBD(NBDSSL,NBD with encryption) SAN 热添加的 SCSI(SCSI HotAdd) 而在 VDDK 1.1 以后的版本中,都能够支持应用程序通过 SAN 或者 HotAdd 等方式来直接访问存储设备(Datastore)上的 VMDK File,而不仅限于使用 LAN 或 NBD 的传输方式来访问 VMDK File 数据,从而显着提高了 I/O 性能。 VixDiskLib_ListTransportModes 枚举支持的传输模式 函数原型: /** * Get a list of transport modes known to VixDiskLib. This list is also the * default used if VixDiskLib_ConnectEx is called with transportModes set * to NULL. * * The string is a list of transport modes separated by colons. For * example: "file:san:hotadd:nbd". See VixDiskLib_ConnectEx for more details. * * @return Returns a string that is a list of plugins. The caller must not * free the string. */ const char * VixDiskLib_ListTransportModes(void); 函数调用: printf(“Transportmethods: %s\n”, VixDiskLib_ListTransportModes()); 该函数返回一个表示当前支持的传输模式列表字符串,E.G. file:san:hotadd:nbd,其中 nbd 表示 LAN 传输,nbdssl 表示 SSL 加密的 NBD 传输。san:hotadd:nbdssl:nbd 表示所有传输模式均可以使用。 VixDiskLib_InitEx 初始化 VixDiskLib 库 函数原型: typedef void (VixDiskLibGenericLogFunc)(const char *fmt, va_list args); /** * Initializes VixDiskLib. * @param majorVersion [in] Required major version number for client. * @param minorVersion [in] Required minor version number for client. * @param log [in] Callback for Log entries. * @param warn [in] Callback for warnings. * @param panic [in] Callback for panic. * @param libDir [in] Directory location where dependent libs are located. * @param configFile [in] Configuration file path in local encoding. * configuration files are of the format * name = "value" * each name/value pair on a separate line. For a detailed * description of allowed values, refer to the VixDiskLib * documentation. * * @return VIX_OK on success, suitable VIX error code otherwise. */ VixError VixDiskLib_InitEx(uint32 majorVersion, uint32 minorVersion, VixDiskLibGenericLogFunc *log, VixDiskLibGenericLogFunc *warn, VixDiskLibGenericLogFunc *panic, const char* libDir, const char* configFile); 函数调用: VixError vixErr = VixDiskLib_InitEx(majorVersion, minorVersion, &logFunc, &warnFunc, &panicFunc, *libDir, *configFile); 函数 VixDiskLib_InitEx 用于初始化 Virtual Disk Library,与 VixDiskLib_Exit 相对,都应且仅应该在「应用程序的生命周期中」被调用一次。VixDiskLib_InitEx 函数支持使用高级传输方式。 形参 majorVersion, minorVersion 表示 VDDK 的发行版本的主板本号和次版本号。例如:如果我们使用的是 VDDK 6.0 Version,那么就是 majorVersion == 6; minorVersion == 0。 PS:版本号由二至四个部分组成,主版本号、次版本号、内部版本号和修订号。主版本号和次版本号两个部分为必选。内部版本号和修订号两个部分为可选;格式为:主版本号.次版本号[.内部版本号[.修订号]]。 形参 &logFunc, &warnFunc, &panicFunc 分别为 Log、Warning、Panic 级别日志函数指针类型,若传入 NULL 实参则表示使用默认日志函数。但需要注意的是,默认日志函数式线程非安全的,如果在多线程场景下,务必需要实现自定义日志函数。 形参 *libDir 指定了 VDDK Lib 库的系统路径,按照上篇博文所提到的,在 Linux OS 中该路径应该为 /usr/lib/vmware-vix-disklib。 形参 *configFile 指定了 VDDK 配置文件的路径,通常为 /usr/etc/vddk.conf 配置文件中能够指定连接认证的方式、日志级别、传输类型、超时时间等。EXAMPLE: # temporary directory for logs etc. # 日志或一些 etc 文件的临时目录 tmpDirectory="/usr/local/vendorapp/var/vmware/temp" # log level 0 to 6 for quiet ranging to verbose # 高级传输方式(不包括NFC)的日志级别 0=Panic(failureonly),1=Error,2=Warning,3=Audit,4=Info,5=Verbose,6=Trivia vixDiskLib.transport.LogLevel=2 # disable caching to disk # 当信息被重复读取或随机访问时,缓存可以提高性能。但在备份程序中,信息通常是被连续读取,缓存实际上降低了性能。所以备份程序而言一般不建议开启。 vixDiskLib.disklib.EnableCache=0 # whether to check SSL thumbprint on Linux - has no effect # 从 Linux 连接到 VMware 虚拟机时是否检查 SSL 指纹 vixDiskLib.linuxSSL.verifyCertificates=1 # nfc.LogLevel (0 = Quiet, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug) # NFC 操作日志级别,NFC 设置应用于 NBD/NBDSSL,但不适用于 SAN 或 HotAdd。0=Quiet(minimallogging),1=Error,2=Warning,3=Info,4=Debug vixDiskLib.nfc.LogLevel=2 # network file copy options # NFC 接收操作的默认值(3分钟) vixDiskLib.nfc.AcceptTimeoutMs=180000 # NFC 请求操作的默认值(3分钟) vixDiskLib.nfc.RequestTimeoutMs=180000 # NFC 读操作的默认值(1分钟) vixDiskLib.nfc.ReadTimeoutsMs=60000 # NFC 写操作的默认值 vixDiskLib.nfc.WriteTimeoutsMs=600000 # NFC 文件系统操作的默认值(0,不定时等待) vixDiskLib.nfcFssrvr.TimeoutMs=0 # NFC 文件系统写操作的默认值(没有超时) vixDiskLib.nfcFssrvrWrite.TimeoutMs=0 # Specifiesadevicenodepath,oracommaseparatedlistof device node paths, that VDDK uses as a list of LUNs on which not to attempt VMFS file system discovery. This has the effect of making fewer device nodes available to SAN transport, and avoids I/O on specific device node paths. The special string all indicates that VDDK should use only the device node paths specified by vixDiskLib.transport.san.whitelist. # SAN 高级传输设备黑名单 vixDiskLib.transport.san.blacklist= # Specifiesadevicenodepath,oracommaseparatedlistof device node paths, that VDDK uses as a list of LUNs on which to attempt VMFS file system discovery. This has the effect of making more device nodes available to SAN transport, encouraging I/O on specific device node paths. Backup applications may create a special device node and whitelist this device node for use in addition to those found by VDDK’s device node scanner. Backup applications can also blacklist specific devices found by VDDK’s device node scanner to prevent use by SAN transport. Combining whitelist and blacklist, applications can establish a preferred device policy for backup I/O. # SAN 高级传输设备白名单 vixDiskLib.transport.san.whitelist= VixDiskLib_ConnectEx 连接到 virtual disk library 函数原型: /** * Create a transport context to access disks belonging to a * particular snapshot of a particular virtual machine. Using this * transport context will enable callers to open virtual disks using * the most efficient data acces protocol available for managed * virtual machines, hence getting better I/O performance. * * If this call is used instead of VixDiskLib_Connect, the additional * information passed in will be used in order to optimize the I/O * access path, to maximize I/O throughput. * * Note: For local virtual machines/disks, this call is equivalent * to VixDiskLib_Connect. * * @param connectParams [in] NULL if maniuplating local disks. * For remote case this includes esx hostName and * user credentials. * @param readOnly [in] Should be set to TRUE if no write access is needed * for the disks to be accessed through this connection. In * some cases, a more efficient I/O path can be used for * read-only access. * @param snapshotRef [in] A managed object reference to the specific * snapshot of the virtual machine whose disks will be * accessed with this connection. Specifying this * property is only meaningful if the vmxSpec property in * connectParams is set as well. * @param transportModes [in] An optional list of transport modes that * can be used for this connection, separated by * colons. If NULL is specified, VixDiskLib's default * setting of "file:san:hotadd:nbd" is used. If a disk is * opened through this connection, VixDiskLib will start * with the first entry of the list and attempt to use * this transport mode to gain access to the virtual * disk. If this does not work, the next item in the list * will be used until either the disk was successfully * opened or the end of the list is reached. * @param connection [out] Returned handle to a connection. * @return VIX_OK if success, suitable VIX error code otherwise. */ VixError VixDiskLib_ConnectEx(const VixDiskLibConnectParams *connectParams, Bool readOnly, const char *snapshotRef, const char *transportModes, VixDiskLibConnection *connection); 函数调用: VixDiskLibConnectParams cnxParams = {0}; if (appGlobals.isRemote) { cnxParams.vmName = vmxSpec; cnxParams.serverName = hostName; cnxParams.credType = VIXDISKLIB_CRED_UID; cnxParams.creds.uid.userName = userName; cnxParams.creds.uid.password = password; cnxParams.port = port; cnxParams.thumbPrint = thumbPrint; } VixError vixError = VixDiskLib_ConnectEx(&cnxParams, TRUE, snapshotMoref, transportModes, &connection); 在完成了 Virtual Disk Library 的初始化之后就可以调用 VixDiskLib_ConnectEx 将 Virtual Disk Library 连接到 vSphere or ESX/ESXi。其实,实际上是连接到某个具体的虚拟机。与 VixDiskLib_Disconnect 相对的,都应且仅应该在「对虚拟机所拥有的 VMDK File 操作的生命周期中」被调用。 @param connectParams:连接参数集,包含以下子参数: vmxSpec:一个 VirtualMachine moRef 虚拟机托管对象引用,E.G. moref=vm-3133。指定了连接到一个具体的虚拟机,集合参数 @param snapshotRef 就能够指定连接到一个具体的虚拟机的具体快照数据文件(VMDK)。 serverName, port: 是 vCenter os ESX/ESXi server 的 Ipaddress 和 Port,若 Port 设置为 NULL 时, VixDiskLib 使用默认的通信端口,通常是 443(HTTPS) 或者 902(VIX自动化)。注意,这里指的是数据传输端口,而不是 SOAP 请求的端口。 creds.uid.userName, creds.uid.password: 是登录账户认证信息 credType: 账户认证方式,支持下列认证方式: VIXDISKLIB_CRED_UID (用户名/密码) VIXDISKLIB_CRED_SESSIONID (HTTP Session ID) VIXDISKLIB_CRED_TICKETID (vSphere Ticket ID) VIXDISKLIB_CRED_SSPI (当前线程的认证凭据,只用于 Windows) thumbPrint: vCenter or ESX/ESXi 的 SHA1 指纹信息,可以通过下述方式获取。 root@mickeyfan-dev:~# echo | openssl s_client -connect 192.168.10.105:443 |& openssl x509 -fingerprint -noout SHA1 Fingerprint=96:20:34:47:6A:1A:15:DF:D4:BE:1D:52:A8:25:26:76:40:52:C6:BB @param readOnly: 为 TRUE 时,表示只读的访问 VMDK,通常效率会更高,而且之后调用 VixDiskLib_Open 也总是只读的,而无需理会其他的 openFlags 参数如何设置。若为 FALSE 表示可读写的方式访问 VMDK。 @param snapshotRef: Snapshot moRef,与 vmxSpec 的格式不同,这里只需要给出 Snapshot moRef 的唯一值,E.G. snapshot-47。如果在参数 @param connectParams 中给定了 vmxSpec 子参数,那么该参数只能传入,Snapshot Mof。相反,还能够提供 ESXi Mof 或 vSphere Mof 来连接到 ESX/ESXi Host 和 vCenter Server。如果希望使用 SAN, HotAdd, NBDSSL 传输方式和需要访问一个开启电源的虚拟机的话,该参数也是必须的。如果访问的 VMDK File 为托管磁盘,就必须提供该实参;反之,若 VMDK File 为寄宿磁盘则可以为 NULL。 @param transportModes: 传入一个以「:」为间隔的传输模式列表字符串,NULL 表示默认的 file:san:hotadd:nbd 传输模式。一般的,VixDiskLib 会遵循自左向右的优先级尝试使用不同的传输模式,直至成功连接或到达列表结尾为止。特别的,如果 SAN 存储可用的话,就会直接选择 SAN 传输模式、当然,你也可以仅指定一种传输模式,并且如果该模式不可用,连接也不会失败,只是在调用 VixDiskLib_Open 函数时,会自动选择使用 NDB 模式。 @param connection: 该实参是一个指针类型,会在 VixDiskLib_ConnectEx 函数调用成功后返回,成为一个 connection 对象的句柄 handle,在 Disconnection 时会被用到。 NOTE:VixDiskLib_InitEx 和 VixDiskLib_ConnectEx 函数是 VixDiskLib_Init 和 VixDiskLib_Connect 的加强版,在版本较新的 VDDK 中才有提供,前者的组合有更加丰富的功能,建议使用。 VixDiskLib_Disconnect 断开 VixDiskLib 的连接 函数原型: /** * Breaks an existing connection. * @param connection [in] Valid handle to a (local/remote) connection. * @return VIX_OK if success suitable VIX error code otherwise. */ VixError VixDiskLib_Disconnect(VixDiskLibConnection connection); 函数调用: VixDiskLib_Disconnect(connectionHandle); 调用该函数即可断开指定连接。 @param connection:为调用 VixDiskLib_ConnectEx 之后 Return 的 VixDiskLibConnection 类型对象 VixDiskLib_Cleanup 断开连接之后的清理 函数原型: /** * Perform a cleanup after an unclean shutdown of an application using * VixDiskLib. * * When using VixDiskLib_ConnectEx, some state might have not been cleaned * up if the resulting connection was not shut down cleanly. Use * VixDiskLib_Cleanup to remove this extra state. * * @param connection [in] Hostname and login credentials to connect to * a host managing virtual machines that were accessed and need * cleanup. While VixDiskLib_Cleanup can be invoked for local * connections as well, it is a no-op in that case. Also, the * vmxSpec property of connectParams should be set to NULL. * @param numCleanedUp [out] Number of virtual machines that were * successfully cleaned up. -- Can be NULL. * @param numRemaining [out] Number of virutal machines that still * require cleaning up. -- Can be NULL. * @return VIX_OK if all virtual machines were successfully cleaned * up or if no virtual machines required cleanup. VIX error * code otherwise and numRemaning can be used to check for * the number of virtual machines requiring cleanup. */ VixError VixDiskLib_Cleanup(const VixDiskLibConnectParams *connectParams, uint32 *numCleanedUp, uint32 *numRemaining); 函数调用: int numCleanedUp, numRemaining; VixError vixError = VixDiskLib_Cleanup(&cnxParams, &numCleanedUp, &numRemaining); 如果在 Disconnect 之后,虚拟机的额外状态并未正确的清除掉,调用该函数就能将这些额外的状态都清理掉。 NOTE:通过我们会在 VixDiskLib_InitEx 之前,或者 VixDiskLib_Disconnect 之后调用该函数,以清除不确定因素。但需要注意的是,在多并发或并行的场景中,调用该函数将变得非常危险。 VixDiskLib_Exit cleans up the library before exit 函数原型: /** * Cleans up VixDiskLib. */ void VixDiskLib_Exit(void); 函数调用: VixDiskLib_Exit(); 在程序退出之前,均需要调用 VixDiskLib_Exit 来清理 Lib 库所分配的资源。 VixDiskLib_GetErrorText 获取错误信息 函数原型: /** * Returns the textual description of an error. * @param err [in] A VIX error code. * @param locale [in] Language locale - not currently supported and must be NULL. * @return The error message string. This should only be deallocated * by VixDiskLib_FreeErrorText. * Returns NULL if there is an error in retrieving text. */ char * VixDiskLib_GetErrorText(VixError err, const char *locale); 函数调用: char*msg = VixDiskLib_GetErrorText(errorCode, NULL); 调用 VixDiskLib_GetErrorText 函数,能够获取 errorCode 所代表的的错误信息描述。 VixDiskLib_FreeErrorText 释放错误描述 函数原型: /** * Free the error message returned by VixDiskLib_GetErrorText. * @param errMsg [in] Message string returned by VixDiskLib_GetErrorText. * It is OK to call this function with NULL. * @return None. */ void VixDiskLib_FreeErrorText(char* errMsg); 函数调用: VixDiskLib_FreeErrorText(msg); 需要注意的是,在获取了错误描述信息之后,均需要调用该函数来释放这些信息。 @param errMsg:即为调用 VixDiskLib_GetErrorText 所获得的错误信息类型对象。 VMDK File 的访问认证和权限 Credentials and Privileges ESX/ESXi Host 使用登陆凭据信息进行访问认证,只要凭据正确,VixDiskLib 可以访问 ESX/ESXi Host 上任意 VMDK File。而 VMware vSphere 则有专属的一组权限(Privileges)机制,需要拥有正确的权限和登陆凭据,VixDiskLib 才能访问所有由 vCenter Server 管理的 ESX/ESXi Host 上任意的 VMDK File。 vCenter Server 中,备份应用用户在备份虚拟机数据时,必须具有以下权限: 虚拟机(VirtualMachine) > 配置(Configuration) > 跟踪磁盘修改(Disk Change Tracking) 虚拟机(VirtualMachine) > 支持(Provisioning) > 允许磁盘只读访问,并且允许虚拟机下载(VM download) 虚拟机(VirtualMachine) > 状态(State) > 创建快照和移除快照 备份应用中,用户必须具有以下权限: 数据存储(Datastore) > 分配空间(Allocate space) 虚拟机(VirtualMachine) > 配置(Configuration) > 添加新磁盘和移除磁盘 虚拟机(VirtualMachine) > 配置(Configuration) > 修改资源(Resource)和设置(Setting) 当备份涉及 vCenter Server 以及所有 ESX/ESXi Host 时,用户还需要具有以下权限: 全局(Global) > 禁用和启用方法(DisableMethods and EnableMethods) 全局(Global) > 牌照(License) 所有的权限都必须应用在 vCenter Server 级别,否则将会返回类似「The host is not licensed for this feature」这样的错误。 SSL 证书和安全 在 Linux 上的 VixDiskLib 的 SSL 证书验证需要提供指纹(Thumbprints)序列,vSphere 的指纹是指从一个可信源(vCenter Server or ESX/ESXi)中获得的一个哈希值(hash),并将该指纹传入 NFC Ticket 的 SSLVerifyParam 数据结构,若检查 Thumbprints PASS 则可以建立 SSL 连接。 在上述的 vddk.conf 配置文件中使用下述配置,则表示开启检查 SSL SHA1 Thumbprints。 vixDiskLib.linuxSSL.verifyCertificates= 1
目录 目录 前文列表 VDDK 安装 VDDK 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware 虚拟化编程(3) —VMware vSphere Web Service API 解析 VDDK 摘自官方文档:The Virtual Disk Development Kit (VDDK) is a collection of C/C++ libraries, code samples, utilities, and documentation to help you create and access VMware virtual disk storage. The VDDK is useful in conjunction with the vSphere API for writing backup and recovery software, or similar applications. 虚拟磁盘开发包(VDDK) 实际上是一系列的 C/C++ Lib 库以及相关的 Docs 和 Sample,开发者能够通过调用这些库函数来实现连接和管理 VMware 虚拟磁盘文件(VMDK File)。 VDDK 提供了以下功能: 读取虚拟磁盘文件的数据。 写入数据到虚拟磁盘文件。 备份虚拟机的单个指定卷或所有卷。 将备份代理连接到 vSphere,备份存储集群上的所有虚拟机。 管理虚拟磁盘文件的整合、扩展、转换、重命名、压缩文件系统镜像。 运行离线的虚拟机病毒扫描、统一补丁与数据分析。 对中毒和被破坏的离线虚拟机进行数据恢复或病毒清除。 NOTE:其中对虚拟磁盘文件的数据读写操作,除了可以通过直接访问 VMDK File 来获取虚拟机的全量数据之外。还能够应用 CBT 功能通过访问快照数据文件来获取指定时间间隔的虚拟机增量数据。 获得 VDDK:从官方 download 下载 VDDK 软件包 VMware-vix-disklib-6.0.3-4888596.x86_64.tar.gz。 VDDK 的组成:解压软件包后即可得的 vmware-vix-disklib-distrib 目录,内含了 bin64、doc、include、lib32、lib64 等子目录。 lib:包含 vixDiskLib.lib(Windows) 或者 libvixDiskLib.so(Linux) 等动态链接库(共享对象)文件,其中最重要莫过于 libvixDiskLib.so 库文件,该文件也相当于 VixDiskLib 虚拟磁盘库,VDDK 中绝大多数的虚拟磁盘操作函数都是由它提供。 include:包含一系列 C/C++ Lib 库所需的头文件,其中 vixDiskLib.h 头文件,其作为 VixDiskLib 虚拟磁盘库的声明,是引用 VixDiskLib 库函数的关键。 doc 目录:提供了 HTML 文档,以及 doc/samples/diskLib 目录下使用 C++ 实现的示例程序。 安装 VDDK Step 1:将 VDDK 解压目录放置到操作系统 Lib 库路径下 mv vmware-vix-disklib-distrib/ /usr/lib/vmware-vix-disklib/ Step 2:根据操作系统环境的不同,部分 VDDK 提供的 .so 文件可能会与操作系统自身的 .so 文件造成冲突。所以需要将与操作系统原先已经存在的同名 .so 文件移动到 removed 目录,防止安装 VDDK 后会影响到操作系统的正常运行。注意,需要移动的 .so 文件清单根据个人环境而定。 cd /usr/lib/vmware-vix-disklib/lib64 mkdir removed mv libcrypto.so.* libcurl.so.* libglib-* libgobject-* libgthread-* libssl.so.* removed/ Step 3:将 VDDK Lib 库文件的路径写入系统环境路径并刷新 echo "/usr/lib/vmware-vix-disklib/lib64" > /etc/ld.so.conf.d/vmware-vix-disklib.conf ldconfig
目录 目录 前文列表 VMware vSphere Web Services API VMware vSphere Web Services SDK vSphere WS API 中的托管对象 Managed Object 托管对象引用 Managed Object References 托管对象属性收集器 PropertyCollector 连接 vCenter 并获取 MO 最后 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 VMware 虚拟化编程(2) — 虚拟磁盘文件类型详解 VMware vSphere Web Services API VMware vSphere Web Services API (vSphere WS API),是官方提供的 ESX/ESXi Host 和 vCenter Server 开发者接口。通过调用 vSphere WS API,开发者能够实现 vSphere Web Client 上所提供的绝大部分管理操作功能,所以熟练使用 vSphere WS API 是面向 VMware 相关编程的必备技能之一。 vSphere WS API 应用了一种基于 Web 服务的编程模型,客户端会生成 Web 服务的 WSDL 网络服务描述语言请求,然后通过 SOAP 简单对象访问协议将请求封装成 XML 格式消息,最后再发送到到服务端。而在 ESX/ESXi Host 或 vCenter Server 端中,则由 vSphere 层负责应答客户端的请求,并返回 SOAP 响应。这是区别于面向对象函数调用的一种编程模型。 VMware vSphere Web Services SDK 摘自官方文档:The VMware vSphere Management SDK is a bundle that contains a set of VMware vSphere SDKs (vSphere Web Services SDK, vSphere Storage Management SDK, vSphere ESX Agent Manager SDK, SSO Client SDK and vSphere Storage Policy SDK) The vSphere Software Development Kits provide all the documentation, libraries, and code examples needed to developers to rapidly build solutions integrated with the industry’s leading virtualization platform. VMware vSphere Web Services SDK (vSphere WS SDK),是官方提供的标准开发工具,其中包含了 Web Services、Storage Management、ESX Agent Manager、SSO Client、Storage Policy 等多方面编程相关的文档、Lib 库以及示例代码。能够帮助开发者快速搭建 Python、Microsoft C#、Java、C/C++ 的应用端开发环境,并且这些示例代码大部分都是使用 Java 编写的,还单独提供了 JAX-WS 开发框架,可以说是很照顾 Java Developer了。不过奈何我是一位 Pythoner,所以我们后面的所有 Sample 均为 Python 实现。 VMware vSphere Web Services SDK Docs vSphere WS API 中的托管对象 Managed Object VMware vSphere API Reference Docs 中记录了大量的托管对象 Managed Object,这些托管对象充当着 API 信息容器的角色,开发者通过处理这些信息容器中的内容来最终实现对 vSphere 系统的操作。 每个托管对象都拥有一个唯一专属的句柄(Handle),称为托管对象引用(Managed Object Referenct),简称 moRef。开发者能够通过 moRef + PropertyCollector(获取对象详细属性信息的机制) 的方式来获取一个托管对象的详细状态信息。 下面列出的是五种基本托管对象类型,它们描述了 vSphere 系统的组织结构,其他的托管对象可以看作是这五种基本对象的详细扩展。 目录(Folder) 数据中心(Datacenter) 计算资源(ComputeResource) 资源池(ResourcePool) 虚拟机(VirtualMachine) 托管对象引用 Managed Object References 托管对象引用 moRef 实际上是一个句柄,它就像是一个信息容器的入口,而非容器本身。并且 moRef 具有唯一性,但这种唯一性是取决于你的连接方式的。当你的连接的对象为 ESX/ESXi Host 时,那么 moRef 的命名空间由 ESX/ESXi Host 来维护,这很简单;但如果你的连接对象是 vCenter,并且 vCenter 管理着 ESX/ESXi Host 集群时,moRef 的唯一性就需要由 vCenter 来确保。这就意味着即便是同一个 MO,当你的连接方式不同时,可能会获得不一样的 moRef 唯一值,从而使得后续的操作产生混乱。 官方文档中也明确指出了 A vSphere instance (vCenter or ESXi) tries to keep the moRef for a virtual machine consistent across sessions, however consistency is not guaranteed.,这里是一个坑,要求开发者应该始终贯彻同一种连接方式,并且不建议持久化 moRef。 托管对象属性收集器 PropertyCollector PropertyCollector 的功能是帮助开发者取得一个托管对象的配置状态信息。PropertyCollector 具有两个非常重要的形参 PropertySpec, ObjectSpec。PropertyCollector 返回的数据是一个 PropertyFilterUpdate 容器类,它包含一个 ObjectSet。 ObjectSpec:用于指定自何处查找所需的属性信息。 由于 vSphere 使用目录树的形式来组织配置信息,所以 ObjectSpec 必须描述如何遍历树以获取所需的信息。 PropertySpec:用于指定所需获取的属性信息列表。 连接 vCenter 并获取 MO 常见的 Python SDK 有 oslo_vmware 和 pyVmomi 两种,前者是 OpenStack Nova 项目为了纳管 VMware 资源所开发出来的基础通用库,后者则是 VMware 官方提供的 Python SDK。就个人而言会更加偏向在正式项目中使用 pyVmomi,主要原因在于其提供了非常完备的 文档 和许多实用的 Sample 实现,这些都是非常重要的技术选型考量点。 但是,为了更直观方便的辅助理解上述概念,我们这里还是选择使用封装程度较低的 oslo_vmware 结合 IPython CLI 来作为示例。 oslo_vmware 的具体使用细节,请移步《Python Module_oslo.vmware_连接 vCenter》。 Step 1: 建立连接 In [10]: from oslo_vmware import api In [11]: from oslo_vmware import vim_util In [12]: session = api.VMwareAPISession( ....: '192.168.10.105', ....: 'administrator@vsphere.local', ....: 'Root123.', ....: 1, ....: 0.1) Step 2:获取你期望的 Managed Object 其中 HostSystem 就是该托管对象的名字,我们可以自 vSphere WS API 中找到相应的 API 文档。 In [15]: hosts = session.invoke_api( ....: vim_util, ....: 'get_objects', ....: session.vim, ....: 'HostSystem', ....: 100) In [16]: hosts Out[16]: (RetrieveResult){ objects[] = (ObjectContent){ obj = (obj){ value = "host-2068" _type = "HostSystem" } propSet[] = (DynamicProperty){ name = "name" val = "192.168.10.103" }, }, } In [19]: hosts.objects[0].obj Out[19]: (obj){ value = "host-2068" _type = "HostSystem" } 这里我们获得了一个 HostSystem Managed Object 的一个句柄也就是 moRef:hosts.objects[0].obj,其中包含了唯一值 host-2068。 Step3:获取你期望的托管对象中所含有的配置状态信息。 通过 moRef 结合应用 PropertyCollector 机制,就能得到该托管对象的配置状态信息。我们可以在 API 文档中浏览该托管对象的属性项目。 In [26]: host = hosts.objects[0].obj In [27]: nets = session.invoke_api(vim_util, 'get_object_properties_dict', session.vim, host, 'config.network.portgroup') 在上述代码中,host 实参就是一个 ObjectSpec,指定了从 moRef host-2068 中查找所需要的信息。而 config.network.portgroup 实参则是 PropertySpec,指定了所需要获取的属性信息。最终获取到 ESXi/ESX Host 192.168.10.103 的网络信息如下: In [28]: nets Out[28]: {config.network.portgroup: (ArrayOfHostPortGroup){ HostPortGroup[] = (HostPortGroup){ key = "key-vim.host.PortGroup-VM Network" port[] = (HostPortGroupPort){ key = "key-vim.host.PortGroup.Port-33554439" mac[] = "00:0c:29:de:9b:d6", type = "virtualMachine" }, (HostPortGroupPort){ key = "key-vim.host.PortGroup.Port-33554441" mac[] = "00:50:56:9d:6e:09", type = "virtualMachine" }, (HostPortGroupPort){ key = "key-vim.host.PortGroup.Port-33554463" mac[] = "00:50:56:9d:4d:69", type = "virtualMachine" }, vswitch = "key-vim.host.VirtualSwitch-vSwitch0" computedPolicy = (HostNetworkPolicy){ security = (HostNetworkSecurityPolicy){ allowPromiscuous = False macChanges = True forgedTransmits = True } nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" reversePolicy = True notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkSpeed = "minimum" speed = 10 checkDuplex = False fullDuplex = False checkErrorPercent = False percentage = 0 checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic0", } } offloadPolicy = (HostNetOffloadCapabilities){ csumOffload = True tcpSegmentation = True zeroCopyXmit = True } shapingPolicy = (HostNetworkTrafficShapingPolicy){ enabled = False } } spec = (HostPortGroupSpec){ name = "VM Network" vlanId = 0 vswitchName = "vSwitch0" policy = (HostNetworkPolicy){ security = "" nicTeaming = (HostNicTeamingPolicy){ failureCriteria = "" } offloadPolicy = "" shapingPolicy = "" } } }, (HostPortGroup){ key = "key-vim.host.PortGroup-Management Network" port[] = (HostPortGroupPort){ key = "key-vim.host.PortGroup.Port-33554438" mac[] = "c8:1f:66:b9:33:32", type = "host" }, vswitch = "key-vim.host.VirtualSwitch-vSwitch0" computedPolicy = (HostNetworkPolicy){ security = (HostNetworkSecurityPolicy){ allowPromiscuous = False macChanges = True forgedTransmits = True } nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" reversePolicy = True notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkSpeed = "minimum" speed = 10 checkDuplex = False fullDuplex = False checkErrorPercent = False percentage = 0 checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic0", } } offloadPolicy = (HostNetOffloadCapabilities){ csumOffload = True tcpSegmentation = True zeroCopyXmit = True } shapingPolicy = (HostNetworkTrafficShapingPolicy){ enabled = False } } spec = (HostPortGroupSpec){ name = "Management Network" vlanId = 0 vswitchName = "vSwitch0" policy = (HostNetworkPolicy){ security = "" nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic0", } } offloadPolicy = "" shapingPolicy = "" } } }, (HostPortGroup){ key = "key-vim.host.PortGroup-iscsi01" port[] = (HostPortGroupPort){ key = "key-vim.host.PortGroup.Port-50331656" mac[] = "00:50:56:65:ca:28", type = "host" }, vswitch = "key-vim.host.VirtualSwitch-vSwitch1" computedPolicy = (HostNetworkPolicy){ security = (HostNetworkSecurityPolicy){ allowPromiscuous = False macChanges = True forgedTransmits = True } nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" reversePolicy = True notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkSpeed = "minimum" speed = 10 checkDuplex = False fullDuplex = False checkErrorPercent = False percentage = 0 checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic1", } } offloadPolicy = (HostNetOffloadCapabilities){ csumOffload = True tcpSegmentation = True zeroCopyXmit = True } shapingPolicy = (HostNetworkTrafficShapingPolicy){ enabled = False } } spec = (HostPortGroupSpec){ name = "iscsi01" vlanId = 0 vswitchName = "vSwitch1" policy = (HostNetworkPolicy){ security = "" nicTeaming = (HostNicTeamingPolicy){ failureCriteria = "" } offloadPolicy = "" shapingPolicy = "" } } }, (HostPortGroup){ key = "key-vim.host.PortGroup-iscsivspg" vswitch = "key-vim.host.VirtualSwitch-vSwitch1" computedPolicy = (HostNetworkPolicy){ security = (HostNetworkSecurityPolicy){ allowPromiscuous = False macChanges = True forgedTransmits = True } nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" reversePolicy = True notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkSpeed = "minimum" speed = 10 checkDuplex = False fullDuplex = False checkErrorPercent = False percentage = 0 checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic1", } } offloadPolicy = (HostNetOffloadCapabilities){ csumOffload = True tcpSegmentation = True zeroCopyXmit = True } shapingPolicy = (HostNetworkTrafficShapingPolicy){ enabled = False } } spec = (HostPortGroupSpec){ name = "iscsivspg" vlanId = 0 vswitchName = "vSwitch1" policy = "" } }, (HostPortGroup){ key = "key-vim.host.PortGroup-aju-test" vswitch = "key-vim.host.VirtualSwitch-vSwitch0" computedPolicy = (HostNetworkPolicy){ security = (HostNetworkSecurityPolicy){ allowPromiscuous = False macChanges = True forgedTransmits = True } nicTeaming = (HostNicTeamingPolicy){ policy = "loadbalance_srcid" reversePolicy = True notifySwitches = True rollingOrder = False failureCriteria = (HostNicFailureCriteria){ checkSpeed = "minimum" speed = 10 checkDuplex = False fullDuplex = False checkErrorPercent = False percentage = 0 checkBeacon = False } nicOrder = (HostNicOrderPolicy){ activeNic[] = "vmnic0", } } offloadPolicy = (HostNetOffloadCapabilities){ csumOffload = True tcpSegmentation = True zeroCopyXmit = True } shapingPolicy = (HostNetworkTrafficShapingPolicy){ enabled = False } } spec = (HostPortGroupSpec){ name = "aju-test" vlanId = 0 vswitchName = "vSwitch0" policy = "" } }, }} 最后 最后需要注意的是,这里使用 oslo_vmware 是为了让示例更加贴近所提到的概念。而这些概念在实际的生成中完全是可以透明的,只需要浏览 pyVmomi 所提供的文档,并熟练掌握其使用技巧,也能够很好的完成实现。
前言 在 CSDN 写博已经 2 年有余,相比一些大佬,时间不算太长。但工作再忙,我也会保持每月产出,从未间断。每天上线回复评论,勘误内容,参加活动,看看阅读量已经成为一种习惯,可以说是 CSDN 博客的忠实用户。在这里自己的文章能够得到认可、赞许,自己的付出能够收获一些荣誉是非常值得开心的事情。在我看来,这就是分享的意义所在。 但对于这一次的博客改版实在让人感到无比失望和痛心。失望在于官方的无脑作死,痛心在于核心用户的流失已经在所难免。 你要说这个锅应该谁来背? 我只能说「团战可以输,产品经理必须死。」 该博文列举这次博客改版让人无法接受的几点………好吧,其实是整个改版都是一坨狗屎。 强制统一皮肤 这里应该改为「皮肤升级后将为您带来更差劲的阅读体验,更大的广告空间!」 统一皮肤这个案子到底是谁想出来的???拉出去打三天三夜吧,简无礼、简直傲慢。你们将用户感受置于何地?你们将用户需求置于何地?你们是不是还在意淫用户会有多么多么的喜欢这个狗屎一般的皮肤界面??? 说句公道化,在现在年轻人对个性化、特异化、定制化追求越来越高的趋势下,你们不出更多的皮肤,更灵活的插件也就算了。统一皮肤,实在让人费解。 难道就为了那更大、更整齐的广告位???那你跟「 Linux 公社 」这一类的有毛区别啊? NOTE:提醒现在还没升级的朋友,千万不要升级。 劣质的皮肤界面设计 这个 UI 到底是什么鬼嘛,简直连我司实习生都比不上,CSDN 你们非得把每个产品都做得这么挫吗?美感何在?设计何在?不会是大学毕设作品直接上吧??? 我就想问问,你们自己家产品做成怎么样心里没点 13 数吗?手机端就都这样了,我也能忍受一直坚持在用。但是这个皮肤的 UI 真的…人的忍受力是有限的好吗? 没有博客荣誉 所有博主兢兢业业,辛辛苦苦,写这么多博文才换来的荣誉,你们说不要就不要,牛逼,洒脱。 荣誉才是众多博主坚持写下去的动力好么!!! 最后 最后我想说,吐槽这么多只因为爱的深沉。我这么写肯定会得罪官方的人,我本人也在官方的微信群里。我只是单纯的希望官方能够听见用户的声音,尽我自己的一点绵力。 至于语气是否刻薄了一些?对不起我就是这样的人。
目录 目录 VMDK VDDK VixDiskLib VADP VMDK VMDK(VMware’s Virtual Machine Disk Format,VMware 虚拟磁盘格式):简单来说就是存储虚拟机虚拟磁盘数据的文件格式。不过一般来说,VMDK 指代的是 VMware GuestOS File System Data Storage File,也就是 VMware 虚拟磁盘文件,简称「虚拟磁盘」。 VDDK VDDK(VMware’s Virtual Disk Development Kit,VMware 虚拟磁盘开发工具集):是 VMware 官方提供的 VMDK 开发工具包,其包含了一系列与「 VMDK 相关的 C 函数库」,e.g.: 用于管理 VMDK 的虚拟磁盘库 VixDiskLib,也被称之为虚拟磁盘接口 Virtual Disk API 用于挂载 VMDK 分区的 VixMntApi VDDK 主要关注如何有效地访问并传输 VMDK 数据,对 VMDK 的管理是其主要核心功能,常被用于开发 VMware 容灾备份相关产品。 VixDiskLib VixDiskLib 虚拟磁盘库:是一组「管理 VMDK 的函数调用集合」,目的是帮助开发者集成 VMware 平台产品的解决方案。 可以在 VDDK 安装目录下的 doc 子目录中找到 index.html 文件,使用 Web 浏览器打开即可查看 VixDiskLib 的接口参考文档。 VADP VADP(vSphere Storage Data Protection API,数据存储保护接口):是 vShpere API 的一个子集,由官方提供的、针对 vSphere 场景的容灾备份解决方案。 基于快照的 VADP 框架允许非主机(off-host)的、由中央控制的(centralized)的虚拟机备份方案。在创建虚拟机快照后,VMDK File 会变为静默状态(quiesce),此时就能够使用 VDDK 进行备份了。
目录 目录 前文列表 虚拟磁盘文件 VMDK 用户可以创建的虚拟磁盘类型 VixDiskLib 中支持的虚拟磁盘类型 虚拟机文件类型 前文列表 VMware 虚拟化编程(1) — VMDK/VDDK/VixDiskLib/VADP 概念简析 虚拟磁盘文件 VMDK 虚拟磁盘文件(VMDK File) 后缀为 .vmdk,是虚拟机的存储卷,Guest OS File System 储存在 VMDK File,而 VMDK File 又会以文件的形式储存在物理磁盘设备上。VMDK File 支持两种物理磁盘类型: 托管磁盘(Managed Disk):托管磁盘通常指的是 File System Format 为 VMFS 的物理存储设备,能够支持使用光纤、iSCSI 或 SAS 来连接到 ESX/ESXi Host 的存储网络(SAN),也能够支持网络挂载存储(NAS),甚至能够直接挂载到 ESX/ESXi Host 上。在 vCenter 体系中,VMDK File 会被储存于共享的 Datastore 之上,再由 vCenter 管理着这些存储簇(Storage Clusters),这令 vCenter 能够支持在 ESX/ESXi Host 之间迁移虚拟机而不需要移动 VMDK 文件;在 ESX/ESXi Host 体系中,VMDK File 通常存放在物理存储设备的某个 /vmfs/volumes 目录中。 寄宿磁盘(Hosted Disk):寄宿磁盘没有特定的 File System Format 要求,所谓寄宿,即适应 Host 原生的磁盘类型。在 Worksation 体系中,寄宿磁盘会适应 Host File System,而将 VMDK File 储存于本地磁盘之上。 NOTE:需要注意的是 VDDK 对两种不同类型的磁盘设备的操作函数也是有所区别的,对于托管磁盘,VDDK 应用程序可以利用高级传输接口函数通过 SAN 而不是 LAN 来执行大多数 I/O 操作,以此来提高程序性能,并保护网络带宽。 用户可以创建的虚拟磁盘类型 在创建一个虚拟磁盘时,会进行两个操作:分配空间、置零。 厚置备延迟置零(Lazy Zeroed Thick):默认的磁盘创建格式,创建磁盘时会直接从磁盘分配所需空间,但不会即时擦除磁盘上保留的数据,而是在虚拟机执行 I/O 操作时按需要将其置零。简单来说,就是立即完全分配指定的磁盘空间给虚拟机,但延迟对该磁盘空间进行清零操作. 特性:磁盘性能较好,创建时间短,适合于做池模式的虚拟桌面。 厚置备置零(Eager Zeroed Thick):创建支持群集功能(E.G. FaultTolerance)的厚磁盘格式,创建磁盘时,直接从磁盘分配空间并立即对物理设备上保留的数据置零。所以当虚拟机有 I/O 操作时,就能够直接执行。简单来说,就是立即完全分配指定的磁盘空间给虚拟机, 并立即清零磁盘空间, 所需时间较长。 特性:磁盘性能最好,创建时间长,适合于做跑运行繁重应用业务的虚拟机。 精简置备(Thin):创建磁盘时,占用磁盘的空间大小根据实际使用量计算,即用多少分多少,提前不分配空间,对磁盘保留数据不置零,且最大不超过划分磁盘的大小。简单来说,就是按实际磁盘使用量动态增长分配磁盘空间,但最大不能超过指定的最大磁盘分配空间。 特性:当有 I/O 操作时,需要先分配空间,再将空间置零,最后才能执行 I/O 操作。当有频繁 I/O 操作时,磁盘性能会有所下降,I/O 不频繁时,磁盘性能较好;创建时间短,适合于对磁盘 I/O 不频繁的业务应用虚拟机。 VixDiskLib 中支持的虚拟磁盘类型 注:VixDiskLib,即虚拟磁盘库,提供了管理虚拟磁盘的系统调用接口。 虽然用户能够创建的虚拟磁盘类型只有 3 种,但在底层程序接口中会根据不同的应用场景(E.G. vCenter、ESX/ESXi) 提供多种磁盘类型参数,开发者可能按照实际情况选择相应的虚拟磁盘类型参数。 寄宿磁盘(Hosted Disk) 单片稀疏型 VIXDISKLIB_DISK_MONOLITHIC_SPARSE:只包含一个虚拟磁盘文件并能够动态扩展的虚拟磁盘。 单片平面型 VIXDISKLIB_DISK_MONOLITHIC_FLAT:只包含一个虚拟磁盘文件,提前分配存储空间的虚拟磁盘。创建这种磁盘需要较多的时间,并占用大量空间,但是可能会提供比稀疏型磁盘更好的性能。 分片稀疏型 VIXDISKLIB_DISK_SPLIT_SPARSE:可扩展的虚拟磁盘,整个磁盘被分为多个2GB大小的关联文件。这些文件可以增大到2GB,然后在新的文件中继续扩展。这种类型可以在较老的文件系统上使用。 分片平面型 VIXDISKLIB_DISK_SPLIT_FLAT:提前分配空间的虚拟磁盘,并被分为多个大小为 2GB 的虚拟磁盘文件。这些从 2GB 开始,所以创建它们需要较长的时间,但是能够以 2GB 持续增长。 托管磁盘(Managed Disk) VMFS 平面型 VIXDISKLIB_DISK_VMFS_FLAT:提前分配空间的虚拟磁盘,在 ESX3 或更新的平台上可用,也叫做厚置备磁盘(Thick Disk)。 VMFS 稀疏型 VIXDISKLIB_DISK_VMFS_SPARSE:使用一种写时复制(Copy-on-Write, COW)机制来节省存储空间,这是虚拟机常用的快照磁盘类型。 VMFS 精简型 VIXDISKLIB_DISK_VMFS_THIN:这种类型假设需要尽可能多的空间,然后以此来扩展虚拟磁盘的大小。能够在 ESX3 以及更新的平台上使用,也叫做精简置备磁盘(Thin Disk)。。 单片流优化 VIXDISKLIB_DISK_STREAM_OPTIMIZED:单片、稀疏格式对数据流进行压缩。这种格式不支持随机读写。 虚拟机文件类型 一台 VMware 虚拟机除了包含上述提到的 VMDK File 之外,还包含了各式各样、针对各种场景的配置文件或描述文件,以及特殊文件在 API 中的参数项。 后缀 描述 API 参数 [vmname].vmx 虚拟机配置文件 [vmname].vmdk 如果选择「动态分配磁盘」设置,会创建一个可根据需要空间大小动态增长的 VMDK File。此时该文件是实际的磁盘数据文件。[vmname] 表示虚拟机的名称。在 VMFS 分区上,这是磁盘描述文件的名称。除此之外,该文件还可能保存的是该虚拟机磁盘文件的元数据。 MONOLITHIC_SPARSE [vmname]-flat.vmdk Extent description 文件,如果勾选了「立即分配磁盘空间」,虚拟磁盘文件将会提前分配所有空间,不会动态增长。第一个 VMDK File 很小(即上述的元数据文件),并指向一个大的 -flat.vmdk VMDK 文件,此时该文件保存的是虚拟机实际的虚拟磁盘数据。 MONOLITHIC_FLAT、VMFS_FLAT、VMFS_THIN [vmname]-ctk.vmdk Change Tracking File 改变追踪文件,保存自上次快照以来的所发生改变的虚拟机数据块的信息。 [vmname].vmem 虚拟机的内存页面文件,存放虚拟机运行时的内存数据,在虚拟机运行或者崩溃时被创建 .vmss 虚拟机挂起时的状态信息文件 .vmsd 虚拟机快照的元数据文件,保存了如快照名、UID(Unique Identifier)、磁盘文件名等信息。在创建快照前,其 size 为 0byte .vmtx 虚拟机模板文件 .nvram 虚拟机 bios 文件 .vswp 虚拟机交换文件 .log 虚拟机日志文件 [vmname]-s<###>.vmdk 如果只勾选「分割成 2GB 大小的文件」,虚拟磁盘文件将会在需要更多空间时才被扩展。第一个 VMDK File 很小,为元数据文件,并指向一系列的其他 VMDK 文件,它们在序号数字前都有一个 S 标志,表明是稀疏类型的(sparse)。VMDK 文件的数字依赖于所需要的磁盘大小。随着数据增长,将会在这个序列中新增更多的 VMDK File。 SPLIT_SPARSE [vmname]-f<###>.vmdk 如果勾选「立即分配磁盘空间」和「将磁盘分成 2GB 大小的文件」两个选项,那么虚拟磁盘空间将会提前分配,不会动态扩展。第一个 VMDK File 是一个元数据文件,指向一系列的其他文件,这些文件在数字序号之前都有一个 f(flat) 前缀。数字由磁盘大小决定。 SPLIT_FLAT [diskname]-<###>.vmdk 当给虚拟机创建快照时,会生成 redo-log 文件,也叫做子磁盘(child disk)或者差异链接(delta link)。快照文件中有序号数字,但是没有 f/s 前缀。针对原来的父磁盘或者更早的回写日志(即更早的快照)的数据修改,将会写进这些带编号的的 VMDK File 中。 MONOLITHIC_SPARSE、SPLIT_SPARSE [vnname]Snapshot.vmsn 虚拟机快照的状态信息文件,用于保存创建快照时虚拟机的状态,包含了指向所有 VMDK File 的信息。这个文件的大小取决于创建快照时是否选择保存内存的状态。如果保存的话,那么这个文件会比分配给这个虚拟机的内存大小还要大几兆 n/a
目录 目录 前文列表 VDDK 安装 VDDK VixDiskLib VADP 前文列表 VMware 虚拟机的虚拟磁盘编程知识点扫盲之一 VDDK 摘自官方文档:The Virtual Disk Development Kit (VDDK) is a collection of C/C++ libraries, code samples, utilities, and documentation to help you create and access VMware virtual disk storage. The VDDK is useful in conjunction with the vSphere API for writing backup and recovery software, or similar applications. 从上述内容可知,VDDK(虚拟磁盘开发包) 主要提供了 C/C++ Lib 库,让开发者能够连接并且访问 VMware 虚拟磁盘存储文件(VMDK File),是编写虚拟机数据备份和恢复的常用工具。简单来说 VDDK 就是一套 C 库及相关的 Sample 和 Docs。 VDDK 的具体功能: 读取虚拟磁盘数据。 写入虚拟磁盘。 备份虚拟机的单个指定卷或所有卷。 将备份代理连接到 vSphere,备份存储簇上的所有虚拟机。 管理虚拟磁盘的整理、扩展、转换、重命名、压缩文件系统镜像。 运行离线的虚拟机病毒扫描、统一补丁与数据分析。 对中毒和被破坏的离线虚拟机进行数据恢复或病毒清除。 其中对虚拟磁盘数据的读写操作,无疑是虚拟磁盘编程的重头戏,除了可以通过直接访问 VMDK File 来获取全量的虚拟机磁盘数据之外。并且由于虚拟机的快照数据文件同样是 VMDK File,所以 VDDK 甚至能够读写快照数据。与 CBT 功能结合更是能够实现获取指定时间点间隔的虚拟机增量数据。 获取 VDDK 的方法:可以从官方 download 下载 VDDK 软件包 VMware-vix-disklib-6.0.3-4888596.x86_64.tar.gz。 VDDK 的组成:解压软件包后得的 vmware-vix-disklib-distrib 目录,内含了 bin64、doc、include、lib32、lib64 等子目录。 include 目录:包含 C/C++ Lib 库所需的头文件,如:vixDiskLib.h、vm_basic_types.h lib 目录:包含 vixDiskLib.lib(Windows) 或者 libvixDiskLib.so(Linux) 等库文件 doc 目录:包含 HTML 参考文档,以及 doc/samples 目录下的示例程序。 安装 VDDK Step1: mv vmware-vix-disklib-distrib/ /usr/lib/vmware-vix-disklib/ Step2: cd /usr/lib/vmware-vix-disklib/lib64 mkdir removed Step3:将于系统 Lib 同名(冲突)的库文件移动到 removed 目录,防止影响到系统的正常运行,需要移动的库文件清单需要根据系统和个人环境来决定。例如: mv libcrypto.so.* libcurl.so.* libglib-* libgobject-* libgthread-* libssl.so.* removed/ Step4:将 VDDK Lib 库文件的路径写入系统环境路径并刷新 echo "/usr/lib/vmware-vix-disklib/lib64" > /etc/ld.so.conf.d/vmware-vix-disklib.conf ldconfig VixDiskLib VDDK 实际上是基于 Virtual Disk API 来实现的,Virtual Disk API(虚拟磁盘接口) 也就是 VixDiskLib,是一组管理 VMDK File 的函数调用集合,目的是帮助开发者集成 VMware 平台产品的解决方案,提供了以下主要功能函数: Create: 创建 VMDK File VixDiskLib_Create() vixError = VixDiskLib_Create(appGlobals.connection, appGlobals.diskPath, &createParams, NULL, NULL); Delete: 删除 VMDK File Convert: 转换 VMDK File 的格式 Expand: 扩大 VMDK File 的 Size Shrink: 缩小 VMDK File 的 Size Defragment: 整理磁盘碎片 Rename: 重命名 VMDK File Create redo logs: 创建虚拟机快照数据文件 允许随机读写 VMDK File 上任意位置的数据 VixDiskLib_Open() vixError = VixDiskLib_Open(appGlobals.connection, appGlobals.diskPath, appGlobals.openFlags, &srcHandle); VixDiskLib_Close() VixDiskLib_Close(srcHandle); VixDiskLib_Read() vixError = VixDiskLib_Read(srcHandle, i, j, buf); VixDiskLib_Write() vixError = VixDiskLib_Write(newDisk.Handle(), i, j, buf); 读取元数据 VixDiskLib_GetInfo() vixError = VixDiskLib_GetInfo(srcHandle, diskInfo); 连接 vSphere Storage,支持包括 SAN、HotAdd 等高级传输方式 NOTE: 使用高级传输模式,如果在恢复虚拟机时已经有一个预先存在的快照了,那么你应该先删除该快照,否则 SAN 模式恢复将失败。备份程序的运行服务器能够直接通过 FC 或者 iSCSI 和虚拟机磁盘所在的 Storage 连接。 VixDiskLib 本质上是应用了 C 的文件 I/O 系统调用(Function call semantics are patternedafter C system calls for file I/O)功能,所以开发者能够在软件中导入 VixDiskLib 来编写访问 VMDK File 的程序。 VADP VADP(vStorage APIs for DataProtection) 全称 VMware Storage APIs - Data Protection,其应用了 Virtual Disk API 和部分 vSphere API 来创建和管理 ESXi 上虚拟机的快照,并且虚拟机的支持全量和增量备份。是 VMware 官方的数据保护方案,具体的使用方法这里不做赘述。
描述 在使用 Devstack 的时候需要时常切换用户su stack,此时会触发错误: root@mickeyfan-dev:~# su stack bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied bash: /dev/null: Permission denied 一般而言,Permission denied 问题可以通过 reset 权限 chmod 666 /dev/null 来解决,但在 Ubuntu 中,系统会自动将设备的权限还原为 420 crw--w---- 1 root tty 1, 3 Aug 26 11:46 /dev/null。所以常用的方法并不能解决该问题。 解决方案 -bash: /dev/null: Permission denied 此时,你需要删除 /dev/null 文件,并重新创建它: rm -f /dev/null; mknod -m 666 /dev/null c 1 3
目录 目录 前言 VMware 虚拟机文件类型 VMware 虚拟机的快照 Quiseced Snapshot Quiseced Snapshot 的创建过程 创建快照 创建快照的执行过程及原理 删除快照 CBT CBT 执行过程 queryChangedDiskAreas 开启 CBT 前言 该篇博文是 VMware 虚拟磁盘编程的基础,也是编写 VMware 虚拟机保护与恢复程序的基础,属于相关知识点扫盲。 VMware 虚拟机文件类型 VMware 虚拟机在 EXSi 宿主机上的文件类型: 后缀 描述 .vmx 虚拟机的配置文件 .vmdk 虚拟机磁盘文件的元数据文件 flat.vmdk 虚拟机的二进制磁盘文件,是虚拟机真正磁盘数据文件 ctk.vmdk 虚拟机磁盘文件的数据块变化追踪文件,保存自从上次快照以来的所发生变化的数据块偏移量信息 .vmem 虚拟机的内存页面文件,存放虚拟机运行时的内存数据,在虚拟机运行或者崩溃时被创建 .vmss 虚拟机挂起时的状态信息文件 .vmsd 虚拟机快照的元数据文件,保存了如快照名、UID(Unique Identifier)、磁盘文件名等信息。在创建快照前,其 size 为 0byte .vmsn 虚拟机快照的状态信息文件,用于保存创建快照时虚拟机的状态。这个文件的大小取决于创建快照时是否选择保存内存的状态。如果保存的话,那么这个文件会比分配给这个虚拟机的内存大小还要大几兆 .vmtx 虚拟机模板文件 .nvram 虚拟机 bios 文件 .vswp 虚拟机交换文件 .log 虚拟机日志文件 下面着重记录 VMware 虚拟机必备的三类文件: vm_name.vmdk (配置文件):保存的是该虚拟机磁盘文件的元数据(一般包含有两类最重要的磁盘文件的基本信息),EXAMPLE: # Disk DescriptorFile version=3 encoding="UTF-8" CID=2a9d852c parentCID=ffffffff isNativeSnapshot="no" createType="vmfs" # Extent description RW 8388608 VMFS "vm_name-flat.vmdk" # Change Tracking File changeTrackPath="vm_name-ctk.vmdk" # The Disk Data Base #DDB ddb.adapterType = "lsilogic" ddb.deletable = "true" ddb.geometry.cylinders = "522" ddb.geometry.heads = "255" ddb.geometry.sectors = "63" ddb.longContentID = "01b9f98fe5883631cfc93a082a9d852c" ddb.uuid = "60 00 C2 92 fb ad ff ed-6e d3 c0 fc 62 15 05 73" ddb.virtualHWVersion = "8" vm_name-flat.vmdk (二进制文件):Extent description 文件,保存虚拟机实际的虚拟磁盘信息。 vm_name-ctk.vmdk (二进制文件):Change Tracking File 改变追踪文件,保存自从上次快照以来的虚拟机所发生变化的数据块信息。 VMware 虚拟机的快照 VMware 虚拟机快照有下列三种类型: 崩溃一致快照(crash-consistent snapshot):是 VMware 虚拟机的默认快照类型,相当于电脑突然断电时磁盘的状态,闪存中的数据会丢失。 文件系统一致快照(file-system-consistent snapshot): 快照时间点之前,虚拟机的文件系统被冻结,内存中的脏数据刷盘,快照完成后,文件系统解冻。这样的快照能够保证文件系统的一致性,即内存中的数据不会丢失。 应用一致性(application-consistent snapshot):快找时间点前,虚拟机上运行的应用被冻结,内存中应用的所有脏数据刷盘,快照完成后,应用被解冻,这样的快照能够保证指定应用的数据是完整的,但不会保证文件系统也是完全一致的。 Quiseced Snapshot 后两种快照类型,也被统称为 Quiseced Snapshot 。使用 Quiseced Snapshot 需要特别的环境配置,主要的实现方式有两种: 使用客户机操作系统内置的应用服务或 VMware 提供的一致性驱动: 版本较新的 Windows 客户机提供了 VSS(Volume Shadow Copy Service) 服务,VSS 提供了 Requester-Writer 来满足有冻结需求的应用和文件系统,在快照时间点前进行冻结和内存数据落盘并且在快照完成后进行解冻; 对于版本较老的 Windows 客户机,VMware 提供了 SYNC driver 来支持应用和文件系统一致性快照; 而在 Linux 客户机上,VMware 提供的 vmsync kernel module 仅支持文件系统一致性,而不能支持应用一致性。 脚本程序:如果是非 Windows 客户机,那么就需要编写针对指定应用的脚本,来对应用进行冻结或者解冻。 NOTE:上述列举的 VSS、SYNC driver、vmsync kernel module 和脚本,均要依赖 VMware Tools 来调用,所以即便客户机操作系统支持上述功能,仍需安装 VMware Tools 才能完美支持 Quiseced Snapshot。例如针对 VSS,VMware tools 就提供了 VSS support 功能,它是 VMware tools 和 Windows VSS 之间交互的桥梁。要创建 quiseced snapshots,VSS support 就必须被安装。 Quiseced Snapshot 的创建过程 用户发出 quiesced snapshot 创建请求给 vCenter,vCenter 再给虚拟机所在的 ESXi 的 hostd service 发出快照创建请求。 ESXi 上的 Hostd service 将快照创建请求传递给客户机内的 VMware tools。 VMware tools 以 VSS Requester 的身份通知 VSS,VSS 再通知已经被注册的文件系统和各应用的 VSS writer 执行冻结操作。 一旦完成冻结和内存数据落盘,VMware tools 就将完成结果通知 hostd service。 Hostd service 执行快照操作。 快照完成后,按照前面的顺序再对文件系统和各应用进行解冻 创建快照 VMware 虚拟机创建快照之后会生成 3 个文件: vm_name-000001.vmdk (配置文件): 虚拟机快照的元数据文件,记录了该次快照相关文件的信息,其中 000001 表示第一次快照。 # Disk DescriptorFile version=3 encoding="UTF-8" CID=a368e4cf parentCID=2a9d852c isNativeSnapshot="no" createType="vmfsSparse" parentFileNameHint="vm_name.vmdk" # Extent description RW 8388608 VMFSSPARSE "vm_name-000001-delta.vmdk" # Change Tracking File changeTrackPath="vm_name-000001-ctk.vmdk" # The Disk Data Base #DDB ddb.deletable = "true" ddb.longContentID = "6f4669ab6aec6b1b2cf5cc6ca368e4cf" vm_name-000001-delta.vmdk (二进制文件):被称为快照数据文件或者 redo-log 文件(在 VDDK 相关的术语中,子磁盘 child disk、重做日志 redo log、差异链 delta link 表示同一个意思),该文件用于保存快照时间点后虚拟机所产生的新数据,即快照数据。应用了 in-file delta technology 技术,初始大小为 16MB,会随着虚拟机数据落盘操作的增多,而按照 16MB 的大小进行增长(降低 SCSI reservation 冲突),并且该文件的大小永远不会超过 Base Disk File 的大小。 vm_name-000001-ctk.vmdk (二进制文件):改变追踪文件,保存了自从上次快照以来的虚拟磁盘文件所发生变化的数据块偏移量信息。 创建快照的执行过程及原理 从上图可以看出 VMware 虚拟机快照的特性为: VMware 虚拟机使用的是链式快照。 VMware 虚拟机的 Base Disk File 在创建快照之后,其访问权限为 OR 只读模式。 快照时间点之后新落盘的数据会写入快照数据文件中。 快照链上的任意快照文件的损坏都会导致虚拟机无法正常运行。 删除快照 从创建快照的特性中可以理解,如果希望在删除一个快照的同时保证虚拟机能够正常运行的话,那么就需要将该快照数据文件中的数据合并到父快照数据文件中,以此来保证虚拟机磁盘数据的完整性。 删除虚拟机快照一般会是以下两种情况: 待删除的虚拟机快照在快照链中:delta vmdk 中的数据会向父快照的 delta vmdk 或基础虚拟磁盘文件 base vmdk 合并,然后 delta vmdk 被删除。 待删除的虚拟机快照不在快照链中(VMware 支持独立快照):不需要合并,直接删除快照数据文件。 删除 VMware 虚拟机快照的特点: 删除快照过程包括两个异步的操作:1. 从 Snapshot Manager 中将快照删除;2. vmdk 数据合并。如果 1 成功而 2 失败,就会残留 delta vmdk 文件,这样的话就需要手工进行快照文件的合并。 删除快照可能会带来大量的数据写操作,有时候可能需要删除很长的时间,并且期间虚拟机的性能会受到负面影响。 自从 vSphere 4 Update 2 开始,优化了选择删除所有虚拟机快照的过程,不再是顺序向下一层层的合并,而是各层分别直接合并到 Base vmdk 中。 CBT CBT 是在 vSphere 4.0 版本引入的实现增量备份的功能,增量备份表示仅会备份两个时间点之间所被改变的差异数据,而这些差异数据往往就是虚拟机的快照数据。ESXi 为每个开启了 CBT 功能的虚拟机的虚拟磁盘都创建了一个 -ctk.vmdk 文件。CBT 会对磁盘带来一些性能损失,所以虚拟机的 CBT 的配置项默认为关闭状态。 CBT 的原理就是让 VMKernel 监控自上次快照以来有那些数据块被改变了,记录下这些被改变的数据块的偏移量,从而就能够获取这些被改变了的差异的数据。 CBT 执行过程 Step 1:进行全量备份,即备份第一次虚拟机快照的快照数据。 Step 2:通过 vShpere API(VirtualDisk.getBacking.getChangeId) 获取 Step 1 中所创建的快照对应的的 ChangeId。 Step 3:执行第二次快照。 Step 4:进行量备份,调用 vShpere API(queryChangedDiskAreas),传递 Step 2 中获取的 ChangeId 和 Step 3 创建的快照的快找对象作为参数,以此获得自第一次快照时间点(前端点)到第二次快照时间点(后端点)之间发生改变的数据块,并备份这些数据块。 queryChangedDiskAreas CBT 变化快获取函数 QueryChangedDiskAreas 的原型: QueryChangedDiskAreas(snapshot, deviceKey, startOffSet, changeID) - snapshot 当前的快照对象,即后端点; - deviceKey 目标虚拟磁盘的 device ID; - startOffSet 开始获取变化块的 offset; - changeID 即前端点对应的 changeId; 其输出结果类似: (117768192, 65536) (132120576, 65536) (145096704, 43122688) (265289728, 65536) (958398464, 65536) 每项的格式均为(offset,length),表示一个发生改变的数据块的偏移量。 NOTE:在没有虚拟机快照的情况下开启 CBT 配置项并调用 queryChangedDiskAreas 函数时,changeId 为的实参应为 *。 开启 CBT 关闭虚拟机 右击虚拟机,选择 「Edit Settings」 点击 「Options」 点击 「Advanced section」 下的 「General」,点击 「Configuration Parameters」,开启「Configuration Parameters」 窗口后,查找 「ctkEnabled」项,设置为 「true」,并设置每个磁盘的「ctkEnabled=true」。 开启虚拟机 设置成功后,会在虚拟机文件目录下生成 -ctk.vmdk 文件。 NOTE: VDDK API 能调用 configSpec.changeTrackingEnabled = new Boolean(true) 来动态的设置 CBT 状态,而不需要关闭虚拟机设置后才能设置。
目录 目录 Socket 套接字 套接字的原理 套接字的数据处理方式 套接字类型 Socket 标准函数 ServerSocket 标准函数 ClientSocket 标准函数 公有标准函数 Socket 编程 编程思路 Demo TCP 服务端 TCP 客户端 Socket 套接字 源 IP 地址和目的 IP 地址以及源端口号和目的端口号的组合称为套接字,是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,解决网络上两台主机之间的进程通信问题。简单的说就是通信双方的一种约定,用套接字中的相关函数来完成通信过程。其用于标识客户端请求的服务器和服务,是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远地主机的 IP 地址、远地进程的协议端口(Socket = IP address + TCP/UDP + port)。应用层(HTTP)和传输层(TCP/UDP)就可以通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。 套接字的原理 Socket 可以看成在两个程序进行通讯连接中的一个端点,是连接应用程序和网络驱动程序的桥梁,Socket 在应用程序中创建,通过绑定与网络驱动建立关系。此后,应用程序送给 Socket 的数据,由 Socket 交给网络驱动程序向网络上发送出去。计算机从网络上收到与该 Socket 绑定 IP 地址和端口号相关的数据后,由网络驱动程序交给 Socket,应用程序便可从该 Socket 中提取接收到的数据,网络应用程序就是这样通过 Socket 进行数据的发送与接收的。要通过 Internet 进行通信,至少需要一对套接字,其中一个运行在客户端,称之为 ClientSocket,另一个运行于服务器端面,称为 ServerSocket。根据连接启动的方式以及本地要连接的目标,套接字之间的连接过程可以分为三个步骤: 服务器监听:是指服务端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。 客户端请求:是由客户端的套接字提出连接请求,要连接的目标是服务器端套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器套接字的地址和端口号,然后再向服务器端套接字提出连接请求。 连接确认:连接确认是当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的信息发送给客户端,一旦客户端确认了此连接,连接即可建立。而服务器端继续处于监听状态,继续接收其他客户端的连接请求。 套接字的数据处理方式 同步模式:同步模式的特点是在通过 Socket 进行连接、接收、发送数据时,客户机和服务器在接收到对方响应前会处于阻塞状态,即一直等到收到对方请求才继续执行下面的语句。可见,同步模式只适用于数据处理不太多的场合。当程序执行的任务很多时,长时间的等待可能会让用户无法忍受,但同步模式的好处在于应用接口的访问有着更好的稳定性。 异步模式:异步模式的特点是在通过 Socket 进行连接、接收、发送操作时,客户机或服务器不会处于阻塞方式,而是利用 callback 机制进行连接、接收、发送处理,这样就可以在调用发送或接收的方法后直接返回,并继续执行下面的程序。可见,异步套接字特别适用于进行大量数据处理的场合,使用 s.setblocking(0) 启用异步模式。 套接字类型 套接字类型 描述 socket.AF_UNIX 只能够用于单一的 Unix 系统进程间通信,同一台机器进程间通信 socket.AF_INET 服务器之间网络通信,Internet 进程间通信 socket.AF_INET6 IPv6 socket.SOCK_STREAM 流式 socket, for TCP,流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即 TCP(The Transmission Control Protocol) 协议。 socket.SOCK_DGRAM 数据报式 socket, for UDP,数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用 UDP(User Datagram Protocol) 协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。 socket.SOCK_RAW 原始套接字,普通的套接字无法处理 ICMP、IGMP 等网络报文,而 SOCK_RAW 可以。其次,SOCK_RAW 也可以处理特殊的 IPv4 报文。此外,利用原始套接字,可以通过 IP_HDRINCL 套接字选项由用户构造 IP 头。原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如 IP、 ICMP 协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为 RAW SOCKET 可以自如地控制 Windows 下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过 RAW SOCKET 来接收发向本机的 ICMP、IGMP 协议包,或者接收 TCP/IP 栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的 IP 包。网络监听技术很大程度上依赖于 SOCKET_RAW socket.SOCK_SEQPACKET 可靠的连续数据包服务 创建TCP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 创建UDP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) UDP 和 TCP 的区别: tcp是可靠的、面向连接的、尽力传输的协议,而udp是不可靠 的、面向非连接的、不尽力传输的协议。但是不可靠不代表它没有用,udp有自己的应用场景,语音和视频几乎都在使用udp协议,它的不可靠只是相对于 tcp来说的,但是它的好处就是效率,高效在某些场景要比可靠性重要。这就涉及trade-off了,也就是权衡,需要根据你的应用权衡利弊,然后进行选择。 NOTE: 1)TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的,每次发送要指定是发给谁。 2)服务端与客户端不能直接发送列表,元组,字典。需要字符串化 repr(data)。 Socket 标准函数 ServerSocket 标准函数 Socket 函数 描述 s.bind(address) 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址. s.listen(backlog) 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求。 s.accept() 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。socket 进入“waiting”状态。客户请求连接时,方法建立连接并返回服务器。accept方法返回一个含有两个元素的元组(connection,address)。第一个元素connection是新的socket对象,服务器必须通过它与客户通信;第二个元素 address是客户的Internet地址。 ClientSocket 标准函数 Socket 函数 描述 s.connect(address) 连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 s.connect_ex(adddress) 功能与connect(address)相同,但是成功返回0,失败返回errno的值。 公有标准函数 Socket 函数 描述 s.recv(bufsize[,flag]) 接受 TCP 套接字的数据。数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。recv 方法在接收数据时会进入「blocked」状态,如果发送的数据量超过了 recv 所允许的,数据会被截短。多余的数据将缓冲于接收端。以后调用 recv 时,多余的数据会从缓冲区删除(以及自上次调用 recv 以来,客户可能发送的其它任何数据)。 s.send(string[,flag]) 发送 TCP 数据。将 String 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 String 的字节大小。 s.sendall(string[,flag]) 完整发送 TCP 数据。将 String 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。 s.recvfrom(bufsize[,flag]) 接受 UDP 套接字的数据。与 recv() 类似,但返回值是(data, address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。 s.sendto(string[,flag],address) 发送 UDP 数据。将数据发送到套接字,address 是形式为 (ipaddr, port) 的元组,指定远程地址。返回值是发送的字节数。 s.close() 关闭套接字。 s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr, port)。 s.getsockname() 返回套接字自己的地址。通常是一个元组 (ipaddr, port) s.setsockopt(level,optname,value) 设置给定套接字选项的值。 s.getsockopt(level,optname[,buflen]) 返回套接字选项的值。 s.settimeout(timeout) 设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。值为 None 表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如: connect()) s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。 s.fileno() 返回套接字的文件描述符。 s.setblocking(flag) 如果 flag 为 0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。 s.makefile() 创建一个与该套接字相关连的文件 Socket 编程 编程思路 TCP 服务端: 创建套接字,绑定套接字到本地 IP 与端口 开始监听连接 进入循环,不断接受客户端的连接请求 然后接收传来的数据,并发送给对方数据 传输完毕后,关闭套接字 TCP 客户端: 创建套接字,连接远端地址 连接后发送数据和接收数据 传输完毕后,关闭套接字 Demo TCP 服务端 Step 1:创建套接字,如果创建 socket 函数失败,会抛出一个 socket.error 的异常,需要捕获。 try: # create an AF_INET, STREAM socket (TCP) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, msg: print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1] sys.exit() Step 2:绑定 socket HOST = '' # Symbolic name meaning all available interfaces PORT = 8888 # Arbitrary non-privileged port ADDR = (HOST, RORT) try: s.bind(ADDR) except socket.error , msg: print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] sys.exit() Step 3: 监听连接 s.listen(5) Step 4:接收请求 # wait to accept a connection - blocking call conn, addr = s.accept() Step 5:发送响应 # now keep talking with the client data = conn.recv(1024) conn.sendall(data) Step 6:关闭 socket s.close() NOTE1:将上面的服务器程序改造成一直运行,最简单的办法是将 accept 放到一个循环中,那么就可以一直接收连接了。 # now keep talking with the client while True: #wait to accept a connection - blocking call conn, addr = s.accept() print 'Connected with ' + addr[0] + ':' + str(addr[1]) data = conn.recv(1024) reply = 'OK...' + data if not data: break conn.sendall(reply) conn.close() NOTE2:多个 Client 可以随时建立连接,而且每个客户端可以跟服务器进行多次通信,将处理的程序与主程序的接收连接分开。一种方法可以使用线程来实现,主服务程序接收连接,创建一个线程来处理该连接的通信,然后服务器回到接收其他连接的逻辑上来。 定义线程体函数 # Function for handling connections. This will be used to create threads def client_thread(conn): # Sending message to connected client conn.send('Welcome to the server. Type something and hit enter\n') # send only takes string # infinite loop so that function do not terminate and thread do not end. while True: # Receiving from client data = conn.recv(1024) reply = 'OK...' + data if not data: break conn.sendall(reply) # came out of loop conn.close() 创建多线程 # now keep talking with the client while True: # wait to accept a connection - blocking call conn, addr = s.accept() print 'Connected with ' + addr[0] + ':' + str(addr[1]) # start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function. start_new_thread(client_thread ,(conn,)) s.close() TCP 客户端 Step 1:创建套接字,如果创建 socket 函数失败,会抛出一个 socket.error 的异常,需要捕获。 try: # create an AF_INET, STREAM socket (TCP) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, msg: print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1] sys.exit() Step 2: 连接 remote_hostname = 'www.google.com' try: # 获得远程主机的 IP 地址 remote_ip = socket.gethostbyname(remote_hostname) except socket.gaierror: #could not resolve print 'Hostname could not be resolved. Exiting' sys.exit() s.connect((remote_ip , port)) Step 3:发送请求 try : #Set the whole string s.sendall(message) except socket.error: #Send failed print 'Send failed' sys.exit() Step 4:接收响应 reply = s.recv(4096) Step 5:关闭 socket s.close()
目录 目录 方式一 方式二 方式一 思路:以 QCOW2 格式来备份和恢复被保护的 KVM 虚拟机 Step1:centos7_0(base qcow2) 以 qcow2 格式写入到 iSCSI 设备 root@h3cas-e306:/vms/images# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running root@h3cas-e306:/vms/images# virsh domblklist centos7 Target Source ------------------------------------------------ vda /vms/images/centos7_0 hda /vms/isos/CentOS-7-x86_64-Minimal-1511.iso root@h3cas-e306:/vms/images# qemu-img convert -f qcow2 /vms/images/centos7_0 -O qcow2 /dev/sdd root@h3cas-e306:/vms/images# qemu-img info /dev/sdd image: /dev/sdd file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 0 cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false Step2:尝试使用 iSCSI 设备手动启动 KVM 虚拟机 # 创建 XML 文件 root@h3cas-e306:/etc/libvirt/qemu# vim centos7_q_0.xml <domain type='kvm'> <name>centos7_q_0</name> <uuid>d668e699-22a4-464b-99c4-1ffdcd6ad4e1</uuid> <osha>0</osha> <timesync>0</timesync> <automem>0</automem> <memory unit='KiB'>4194304</memory> <currentMemory unit='KiB'>4194304</currentMemory> <memorySlots>10</memorySlots> <maxMemory unit='KiB'>34359738368</maxMemory> <blkiotune> <weight>300</weight> </blkiotune> <memtune> <priority>1</priority> </memtune> <memoryBacking> <locked>0</locked> </memoryBacking> <vcpu placement='static' current='2'>24</vcpu> <cputune> <shares>512</shares> <period>1000000</period> <quota>-1</quota> </cputune> <os> <type arch='x86_64' machine='pc-i440fx-2.1'>hvm</type> <system>linux</system> <boot dev='hd'/> <boot dev='cdrom'/> </os> <features> <acpi/> <apic/> <pae/> </features> <cpu> <topology sockets='24' cores='1' threads='1'/> <numa> <cell cpus='0-1' memory='4194304'/> </numa> <gurantee unit='MHz'>0</gurantee> </cpu> <clock offset='utc'/> <on_poweroff>destroy</on_poweroff> <on_reboot>restart</on_reboot> <on_crash>restart</on_crash> <pm> <suspend-to-mem enabled='no'/> <suspend-to-disk enabled='no'/> </pm> <tools upgrade='auto'/> <devices> <emulator>/usr/bin/kvm</emulator> <disk type='file' device='disk'> <driver name='qemu' type='qcow2' cache='directsync' io='native'/> <source file='/dev/sdd'/> <target dev='vda' bus='virtio'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/> </disk> <disk type='file' device='cdrom'> <driver name='qemu' type='raw' cache='none'/> <target dev='hdc' bus='ide'/> <readonly/> <address type='drive' controller='0' bus='1' target='0' unit='0'/> </disk> <controller type='usb' index='0'> <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/> </controller> <controller type='pci' index='0' model='pci-root'/> <controller type='ide' index='0'> <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/> </controller> <controller type='virtio-serial' index='0'> <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> </controller> <serial type='pty'> <target port='0'/> </serial> <console type='pty'> <target type='serial' port='0'/> </console> <channel type='unix'> <source mode='bind' path='/var/lib/libvirt/qemu/centos7.agent'/> <target type='virtio' name='org.qemu.guest_agent.0'/> <address type='virtio-serial' controller='0' bus='0' port='1'/> </channel> <input type='tablet' bus='usb'/> <input type='mouse' bus='ps2'/> <graphics type='vnc' port='-1' autoport='yes' listen='0.0.0.0'> <listen type='address' address='0.0.0.0'/> </graphics> <video> <model type='cirrus' vram='9216' heads='1'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> </video> <memballoon model='virtio'> <address type='pci' domain='0x0000' bus='0x00' slot='0x09' function='0x0'/> </memballoon> </devices> root@h3cas-e306:/etc/libvirt/qemu# virsh define centos7_q_0.xml Domain centos7_q_0 defined from centos7_q_0.xml root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running - centos7_q_0 shut off root@h3cas-e306:/etc/libvirt/qemu# virsh start centos7_q_0 Domain centos7_q_0 started root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running 34 centos7_q_0 running Step3:对 centos7_0 做快照得到增量快照数据 centos7_1(increment qcow2) root@h3cas-e306:/vms/images# virsh snapshot-create-as --domain centos7 snap01 snap01-desc --disk-only --diskspec vda,snapshot=external,file=/vms/images/centos7_1 --atomic Domain snapshot snap01 created root@h3cas-e306:/vms/images# l centos7_0 centos7_1 root@h3cas-e306:/vms/images# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 772K cluster_size: 262144 backing file: /vms/images/centos7_0 backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/vms/images# virsh domblklist centos7 Target Source ------------------------------------------------ vda /vms/images/centos7_1 hda /vms/isos/CentOS-7-x86_64-Minimal-1511.iso NOTE: 现在虚拟机的数据会写入到 centos7_1 中, centos7_0 理论上应该是只读的. Step4:Copy centos7_1 的副本到备份目录下 root@h3cas-e306:/vms/images# cp centos7_1 /kvm_backup/ root@h3cas-e306:/vms/images# ll /kvm_backup/ total 2572 drwxr-xr-x 2 root root 4096 Apr 18 23:39 ./ drwxr-xr-x 33 root root 4096 Apr 18 23:22 ../ -rw------- 1 root root 2883584 Apr 18 23:39 centos7_1 Step5:rebase 并 commit centos7_1 到 iSCSI 设备中 root@h3cas-e306:/etc/libvirt/qemu# virsh destroy 34 Domain 34 destroyed root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running - centos7_q_0 shut off root@h3cas-e306:/vms/images# cd /kvm_backup/ root@h3cas-e306:/kvm_backup# ls centos7_1 root@h3cas-e306:/kvm_backup# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 2.5M cluster_size: 262144 backing file: /vms/images/centos7_0 backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img info /dev/sdd image: /dev/sdd file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 0 cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img rebase -b /dev/sdd -F qcow2 centos7_1 root@h3cas-e306:/kvm_backup# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 15M cluster_size: 262144 backing file: /dev/sdd backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img info /dev/sdd image: /dev/sdd file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 0 cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img commit -f qcow2 centos7_1 Image committed. root@h3cas-e306:/kvm_backup# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 15M cluster_size: 262144 backing file: /dev/sdd backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img info /dev/sdd image: /dev/sdd file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 0 cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false Step6:使用 commit 后的 iSCSI 设备启动虚拟机 root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running - centos7_q_0 shut off root@h3cas-e306:/etc/libvirt/qemu# virsh start centos7_q_0 Domain centos7_q_0 started root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 33 centos7 running 35 centos7_q_0 running NOTE: 虚拟机启动成功, 证明 QCOW2 格式的虚拟机增量快照文件是能够合并到虚拟机 Base 数据文件中的。 方式二 思路:以 RAW 格式来备份和恢复被保护的 KVM 虚拟机 Step1:centos7(base qcow2) 以 raw 格式写入到 iSCSI 设备 root@h3cas-e306:/vms/images# qemu-img info centos7 image: centos7 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 1.0G cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/vms/images# qemu-img convert -f qcow2 /vms/images/centos7 -O raw /dev/sdd root@h3cas-e306:/vms/images# qemu-img info /dev/sdd image: /dev/sdd file format: raw virtual size: 23G (24696061952 bytes) disk size: 0 Step2:使用 iSCSI 设备手动启动 KVM 虚拟机 root@h3cas-e306:/etc/libvirt/qemu# virsh define centos7_r_0.xml Domain centos7_r_0 defined from centos7_r_0.xml root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 36 centos7 running - centos7_q_0 shut off - centos7_r_0 shut off root@h3cas-e306:/etc/libvirt/qemu# virsh start centos7_r_0 Domain centos7_r_0 started root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 36 centos7 running 37 centos7_r_0 running - centos7_q_0 shut off Step3:对 centos7 做快照得到增量快照数据 centos7_1(increment qcow2) root@h3cas-e306:/vms/images# virsh snapshot-create-as --domain centos7 snap01 snap01-desc --disk-only --diskspec vda,snapshot=external,file=/vms/images/centos7_1 --atomic Domain snapshot snap01 created root@h3cas-e306:/vms/images# ls centos7 centos7_1 root@h3cas-e306:/vms/images# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 2.0M cluster_size: 262144 backing file: /vms/images/centos7 backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false Step4: Copy centos7_1 的副本到备份目录下 root@h3cas-e306:/vms/images# cp centos7_1 /kvm_backup/ root@h3cas-e306:/vms/images# cd /kvm_backup/ root@h3cas-e306:/kvm_backup# ls centos7_1 root@h3cas-e306:/kvm_backup# qemu-img info /kvm_backup/centos7_1 image: /kvm_backup/centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 2.5M cluster_size: 262144 backing file: /vms/images/centos7 backing file format: qcow2 Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img info /dev/sdd image: /dev/sdd file format: raw virtual size: 23G (24696061952 bytes) disk size: 0 Step5:以 raw 的格式 rebase 并 commit centos7_1 到 iSCSI 设备中 root@h3cas-e306:/kvm_backup# qemu-img rebase -b /dev/sdd -F raw centos7_1 root@h3cas-e306:/kvm_backup# qemu-img info centos7_1 image: centos7_1 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 25M cluster_size: 262144 backing file: /dev/sdd backing file format: raw Format specific information: compat: 1.1 lazy refcounts: false root@h3cas-e306:/kvm_backup# qemu-img info /dev/sdd image: /dev/sdd file format: raw virtual size: 23G (24696061952 bytes) disk size: 0 root@h3cas-e306:/kvm_backup# qemu-img commit -f qcow2 centos7_1 Image committed. root@h3cas-e306:~# qemu-img info /dev/sdd image: /dev/sdd file format: raw virtual size: 23G (24696061952 bytes) disk size: 0 Step6: 使用 commit 后的 iSCSI 设备再次启动虚拟机 root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 38 centos7 running - centos7_q_0 shut off - centos7_r_0 shut off root@h3cas-e306:/etc/libvirt/qemu# virsh start centos7_r_0 Domain centos7_r_0 started root@h3cas-e306:/etc/libvirt/qemu# virsh list --all Id Name State ---------------------------------------------------- 38 centos7 running 39 centos7_r_0 running - centos7_q_0 shut off NOTE:尚未测试虚拟机在备份和恢复的过程中虚拟机的应用业务是否被中断。
目录 目录 vSS vSSPG vSphere SDK 中相关的网络对象 创建 vSS PortGroup vSS & vSSPG vSS(Standard vSwitch 标准交换机) 为在同一 ESXi/ESX 或不同 ESXi/ESX 上 VirtualMachine 之间的连接, 也可以让 VirtualMachine 连接到外部网络. vSSPG(Standard vSwitch PortGroup 标准交换机端口组) 是 vSphere 的基本网络对象模型, VirtualMachine 会通过一个 PortGroup 来访问网络. 使用 oslo.vmware 模块结合 vSphere API 来创建 vSSPG 是一个相对综合的调用过程, 在实现的过程中能让我们理解 vSphere SDK 定义的相关概念模型, 以及熟悉 VMware vSphere API Reference Docs. vSphere SDK 中相关的网络对象 vSphere Web Services SDK 包括下列对象和方法来管理网络配置: HostNetworkSystem(Managed Object): 代表了主机的网路配置, 拥有检索和改变网络配置的方法. 所以我们能够使用 HostNetworkSystem MO 对象来访问和控制 ESX/ESXi 的网络概念模型. HostNetworkConfig(Data Object): 用于规划 ESXi Host 详细的网络配置. 创建 vSS PortGroup 首先我们已经知道创建标准交换机端口组需要使用 HostNetowrkSystem MO 的 AddPortGroup 方法, 所以调用该方法的前提就是能够获取到HostNetowrkSystem MO. 但遗憾的是我们无法通过 oslo.vmware 模块提供的 get_objects invoke_api 来直接获取, 这就需要我们费些心思去找到其上级且可使用 get_objects invoke_api 来获取的 MO. NOTE: 下述多数截图出自于 VMware vSphere API Reference Docs. 从下图可知 HostNetworkSystem MO 的上级 Property 是 HostConfigManager, 那么我们首先进入到 HostConfigManager 的 Docs. 然后从下图可知 HostConfigManager 是一个 Data object, 我们仍无法通过 get_object invoke_api 的方式来获取这个对象, 所以继续进入 HostConfigManager 对象的上级 Property. 直到现在我们看到了非常熟悉的 HostSystem MO, 这是一个非常常用的 MO, 并且支持 get_object invoke_api 的获取方式, 所以从 HostSystem 开始, 我们就能够其属性, 直到得到 HostNetowrkSystem MO 为止. Step 1: 连接到 vCenter 或 ESXi/ESX from oslo_vmware import api from oslo_vmware import vim_util # Create the vmware session session = api.VMwareAPISession( '200.21.102.7', 'root', 'vmware', 1, 0.1) Step 2: 获取 HostSystem MO hosts = session.invoke_api( vim_util, 'get_objects', session.vim, 'HostSystem', 100) # 我们随意选取一个 ESXi Host, 并且打印其 Object host_obj = hosts.objects[0].obj In [17]: hosts.objects[0].obj Out[17]: (obj){ value = "host-10" _type = "HostSystem" } Step 3: 从下图可以看出 HostSystem 的 Properties configManager 就是 HostConfigManager 对象, HostConfigManager 的 Properties networkSystem 就是 HostNetworkSystem 对象. (HostSystem ==> HostConfigManager) (HostConfigManager ==> HostNetworkSystem) # 获取 HostNetworkSystem MO, 并打印其 Value host_network_system_val = session.invoke_api( vim_util, 'get_object_properties_dict', session.vim, host_obj, 'configManager.networkSystem') In [24]: host_network_system_val Out[24]: {configManager.networkSystem: (val){ value = "networkSystem-10" _type = "HostNetworkSystem" }} Step 4: 在得到了 host_network_system_val 对象之后, 我们就能够通过 session.invoke_api 来调用该对象的 AddPortGroup 方法了. 但仍有一个前提, 就是我们需要为 AddPortGroup 准备好需要传入的实参 portgrp(HostPortGroupSpec). 再如下图, 我们得知 HostPortGroupSpec 是一个 Data Object, 所以我们能够通过 session.vim.client.factory.create 方法来获取该对象的数据结构. # 获取 HostPortGroupSpec 对象的数据结构, 并打印 client_factory = session.vim.client.factory vswitch_port_group_spec = client_factory.create('ns0:HostPortGroupSpec') In [26]: vswitch_port_group_spec Out[26]: (HostPortGroupSpec){ dynamicType = None dynamicProperty[] = <empty> name = None vlanId = None vswitchName = None policy = (HostNetworkPolicy){ dynamicType = None dynamicProperty[] = <empty> security = (HostNetworkSecurityPolicy){ dynamicType = None dynamicProperty[] = <empty> allowPromiscuous = None macChanges = None forgedTransmits = None } nicTeaming = (HostNicTeamingPolicy){ dynamicType = None dynamicProperty[] = <empty> policy = None reversePolicy = None notifySwitches = None rollingOrder = None failureCriteria = (HostNicFailureCriteria){ dynamicType = None dynamicProperty[] = <empty> checkSpeed = None speed = None checkDuplex = None fullDuplex = None checkErrorPercent = None percentage = None checkBeacon = None } nicOrder = (HostNicOrderPolicy){ dynamicType = None dynamicProperty[] = <empty> activeNic[] = <empty> standbyNic[] = <empty> } } offloadPolicy = (HostNetOffloadCapabilities){ dynamicType = None dynamicProperty[] = <empty> csumOffload = None tcpSegmentation = None zeroCopyXmit = None } shapingPolicy = (HostNetworkTrafficShapingPolicy){ dynamicType = None dynamicProperty[] = <empty> enabled = None averageBandwidth = None peakBandwidth = None burstSize = None } } } Step 5: 通过设置 HostPortGroupSpec 的项目值来达到设置 PortGroup 属性的目的 一般我们需要关注的项目值在 Docs 中都有明确的指示: # 其中 policy 项的值有必须是一个 HostNetworkPolicy Data Object # 所以我们需要使用与获取 HostPortGroupSpec 相同的方式来获取 HostNetworkPolicy 对象的数据结构. policy = client_factory.create('ns0:HostNetworkPolicy') nicteaming = client_factory.create('ns0:HostNicTeamingPolicy') nicteaming.notifySwitches = True policy.nicTeaming = nicteaming port_group_name = 'fanguiju-test' vswitch_name = 'vSwitch0' vlan_id = '0' vswitch_port_group_spec.policy = policy vswitch_port_group_spec.name = port_group_name vswitch_port_group_spec.vswitchName = vswitch_name vswitch_port_group_spec.vlanId = int(vlan_id) 直到现在, 我们终于准备好了 AddPortGroup 方法的实参, 接下来就能够调用这个方法实现标准交换机端口组的创建. Step 6: 调用 AddPortGroup 方法 session.invoke_api(session.vim, 'AddPortGroup', host_network_system_val['configManager.networkSystem'], portgrp=vswitch_port_group_spec) # session.invoke_api() 的使用方法: # - 第二个参数是我们要调用的目标方法: AddPortGroup # - 第三个参数是我们要调用的目标方法 AddPortGroup 的属主 MO: HostNetowrkSystem # - 第四个~第 n 个参数是传递给目标方法 AddPortGroup 的实参 Step 7: 验证结果
目录 目录 测试环境 Nova 配置OpenStack 纳管 vCenter 虚拟机 Glance 配置OpenStack 纳管 vCenter 镜像 Cinder 配置OpenStack 纳管 vCenter 块设备 Ceilometer 配置 测试 测试环境 OpenStack Liberty: 双节点(192.168.1.1/192.168.1.2), 后续内容中分别以 node1/node2 表示双节点. vCenter 环境: VMware vCenter Server Application(192.168.1.100) 账号: root/vmware vCenter 层级结构: vCenter ==> Datacenter-1(datastore1) ==> Cluster-1 ==> Host(192.168.1.3) Nova 配置(OpenStack 纳管 vCenter 虚拟机) 使用 vCenter 驱动配置. 参考文档 现阶段而言, OpenStack 对 vCenter 虚拟机的管理包括 虚拟机的创建/删除/启停/休眠/唤醒/挂起/迁移 等. 但一些高级的 vCenter 功能如 DRS/DAS 等仍然不被支持. node1: vim /etc/nova/nova.conf [DEFAULT] ... force_config_drive=True ... config_drive_format=iso9660 compute_driver=vmwareapi.VMwareVCDriver [vmware] host_ip=192.168.1.100 host_username=root host_password=vmware cluster_name=Cluster-1 datastore_regex=datastore1 wsdl_location=https://192.168.1.100/sdk/vimService.wsdl insecure=True node2: vim /etc/nova/nova.conf [DEFAULT] ... force_config_drive=True config_drive_format=iso9660 ... compute_driver=vmwareapi.VMwareVCDriver [vmware] host_ip=192.168.1.100 host_username=root host_password=vmware cluster_name=Cluster-1 datastore_regex=datastore1 wsdl_location=https://192.168.1.100/sdk/vimService.wsdl insecure=True Glance 配置(OpenStack 纳管 vCenter 镜像) 参考文档 实际上在接入 vCenter 之后, Glance 的镜像仍能够存放在本地, 但当 vCenter 需要使用这个镜像来启动一个虚拟机时, 首先会将本地的镜像文件上传到 vCenter Datastore 之后才能开始创建, 相当耗时, 所以建议将镜像文件都上传到 Datastore 中. OpenStack 对 vCenter 镜像的管理包括: 上传/下载 等功能. node1: vim /etc/glance/glance-api.conf [DEFAULT] ... known_stores = vmware default_store = vsphere [glance_store] stores = file,http,vmware vmware_server_host = 192.168.1.100 vmware_server_username = root vmware_server_password = vmware vmware_datastore_name = datastore1 vmware_datacenter_path = Datacenter-1 vmware_datastores = Datacenter-1:datastore1 vmware_task_poll_interval = 5 vmware_store_image_dir = /openstack_glance vmware_api_insecure = True Cinder 配置(OpenStack 纳管 vCenter 块设备) 参考文档 其实这里与其说是 OpenStack 纳管 vCenter 块设备 不如说是 OpenStack Cinder 为 vCenter 提供块设备功能. 换句话来说, vCenter 能够使用 Cinder 提供的 Volumes 来创建虚拟机和作为存储使用. 除此之外, Cinder 仍能保持对块存储的管理功能, 如: Volumes 的 创建/删除/快照/挂载/卸载 等. 有以下两点需要注意: Volumes 的挂载与卸载操作需要关闭虚拟机电源 Cinder 不支持 Volumes 的备份功能 node1: vim /etc/cinder/cinder.conf [DEFAULT] ... default_volume_type = vmware enabled_backends = vmware [vmware] vmware_insecure=True vmware_host_ip=192.168.1.100 vmware_host_username=root vmware_host_password=vmware vmware_volume_folder=Volumes vmware_wsdl_location=https://192.168.1.100/sdk/vimService.wsdl volume_driver=cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver vmware_insecure = True node2: vim /etc/cinder/cinder.conf [DEFAULT] ... default_volume_type = vmware enabled_backends = vmware [vmware] vmware_insecure = true vmware_host_ip=192.168.1.100 vmware_host_username=root vmware_host_password=vmware vmware_wsdl_location=https://192.168.1.100/sdk/vimService.wsdl volume_driver = cinder.volume.drivers.vmware.vmdk.VMwareVcVmdkDriver vmware_insecure = True NOTE: 需要创建名为 vmware 的类型 cinder type-create vmware Ceilometer 配置 参考文档 VMware 暴露的监控最小采集频率是 300s, OpenStack Ceilometer 目前仅提供了对虚拟机的监控能力, 但能够通过其他的 vSphere API 来拿到更多 Datacenter/Cluster/Host/VirtualMachine 的 runtime information, 基本上包含了 CPU/Mem/DiskIO/网络流量 四个常规的监控对象. node1: vim /etc/ceilometer/ceilometer.conf [DEFAULT] ... hypervisor_inspector=vsphere [node] virt_inspector=vmware [vmware] host_ip=192.168.1.100 host_username=root host_password=vmware wsdl_location=https://192.168.1.100/sdk/vimService.wsdl insecure=True [service_credentials] # 如果是多region,请更改下面配置项 #os_region_name = <RegionName> node2: vim /etc/ceilometer/ceilometer.conf [DEFAULT] ... hypervisor_inspector=vsphere [node] virt_inspector=vmware [vmware] host_ip=192.168.1.100 host_username=root host_password=vmware wsdl_location=https://192.168.1.100/sdk/vimService.wsdl insecure=True [service_credentials] # 如果是多region,请更改下面配置项 #os_region_name = <RegionName> 测试 Step1: 重启相关服务 # node1 systemctl restart openstack-nova-compute.service openstack-nova-api.service openstack-nova-consoleauth.service openstack-nova-scheduler.service openstack-nova-conductor.service openstack-nova-novncproxy.service systemctl restart openstack-glance-api.service systemctl restart openstack-cinder-api.service openstack-cinder-volume.service systemctl restart openstack-ceilometer-collector.service openstack-ceilometer-api.service # node2 systemctl restart openstack-nova-compute.service openstack-nova-api.service systemctl restart openstack-cinder-api.service openstack-cinder-volume.service systemctl restart openstack-ceilometer-collector.service openstack-ceilometer-api.service Step2: 尝试上传镜像文件, 在上传镜像的时候, 我们需要注意以下两点: NOTE 1: 首先需要知道镜像文件的「disktype 和 adaptertype」, 使用指令 head -20 <vmdk_image_file> 就能够查看相关类型. [root@seed ~]# head -20 cirros-0.3.2-i386-disk.vmdk KDMV�9�� # Disk DescriptorFile version=1 CID=5350c0c0 parentCID=ffffffff createType="monolithicSparse" # Extent description RW 80325 SPARSE "cirros-0.3.2-i386-disk.vmdk" # The Disk Data Base #DDB ddb.virtualHWVersion = "4" ddb.geometry.cylinders = "79" ddb.geometry.heads = "16" ddb.geometry.sectors = "63" ddb.adapterType = "ide" # adaptertype == ide ��������� NOTE 2: 如果使用指令 qemu-img 将 qcow2/img 格式的镜像文件转换成 vmdk 格式镜像文件时, vmware_disktype 通常为 sparse, 而 vmware_adaptertype 通常为 ide, 所以相应的可以使用下面的镜像上传指令: glance image-create \ --name fanguiju-test-2 \ --container-format bare \ --disk-format vmdk \ --property vmware_disktype="sparse" \ --property vmware_adaptertype="ide" \ < cirros-0.3.2-i386-disk.vmdk +--------------------+----------------------------------------------------------------------------------+ | Property | Value | +--------------------+----------------------------------------------------------------------------------+ | checksum | 3fd19141ff969dcb4926b610769e5ba4 | | container_format | bare | | created_at | 2017-04-19T08:27:25Z | | direct_url | vsphere://192.168.1.100/folder/openstack_glance/70d581d4-1829-45c1-ac6c- | | | ac1755da1650?dcPath=Datacenter-1&dsName=datastore1 | | disk_format | vmdk | | id | 70d581d4-1829-45c1-ac6c-ac1755da1650 | | locations | [{"url": "vsphere://192.168.1.100/folder/openstack_glance/70d581d4-1829-45c1 | | | -ac6c-ac1755da1650?dcPath=Datacenter-1&dsName=datastore1", "metadata": {}}] | | min_disk | 0 | | min_ram | 0 | | name | fanguiju-test | | owner | 1774b99ae7374d9b948fb4146fbf49fb | | protected | False | | size | 17104896 | | status | active | | tags | [] | | updated_at | 2017-04-19T08:27:28Z | | virtual_size | None | | visibility | private | | vmware_adaptertype | ide | | vmware_disktype | sparse | +--------------------+----------------------------------------------------------------------------------+ PS: 建议使用测试镜像文件. NOTE 3: 如果上传镜像文件失败, 并且通过 glance/api.log 看出为 glance_store 相关的 ERROR, 可以尝试升级 glance_store 的版本. L版管理vmware需要升级 glance-store 的步骤: [root@seed yum.repos.d]# rpm -ivh https://repos.fedorapeople.org/repos/openstack/openstack-mitaka/rdo-release-mitaka-5.noarch.rpm [root@seed yum.repos.d]# vim rdo-release.repo [root@seed yum.repos.d]# yum makecache [root@seed yum.repos.d]# yum update python-glance-store Datastore 创建了 openstack_glance 目录: Step 3: 启动虚拟机 vCenter 任务流: 在 vCenter 中启动成功: Step4: 创建 Cinder volumes 后能在 vCenter「虚拟机与模板」界面看到对应的 Volumes, 然后就可以使用这个 Volumes 来创建虚拟机或作为存储使用了.
目录 目录 扩展阅读 RAW QCOW2 QEMU-COW 2 QCOW2 Header QCOW2 的 COW 特性 QCOW2 的快照 qemu-img 的基本使用 RAW 与 QCOW2 的区别 扩展阅读 The QCOW2 Image Format 为什么用ls和du显示出来的文件大小有差别? Qcow2镜像格式解析 ROW/COW 快照技术原理解析 RAW KVM 虚拟化中使用的镜像格式通常为 RAW 和 QCOW2 两种格式. RAW 的原意是「未被加工的」, 所以 RAW 格式镜像文件又被称为 原始镜像 或 裸设备镜像, 从这些称谓可以看出, RAW 格式镜像文件能够直接当作一个块设备, 以供 GuestOS 使用. 也就是说 KVM 的 GuestOS 可以直接从 RAW 镜像中启动, 就如 HostOS 直接从硬盘中启动一般. 块设备: IO 设备中的一类, 将信息存储在固定大小的块中, 并且每个块都有自己的地址, 常用的块设备有硬盘. 因为 RAW 镜像文件赤裸裸的特性带来了下列好处: 使用 dd 指令创建一个 File 就能够模拟 RAW 镜像文件 性能较 QCOW2 要更高 支持裸设备的原生特性, 例如: 直接挂载 能够随意转换格式, 甚至作为其他两种格式转换时的中间格式 能够使用 dd 指令来追加 RAW 镜像文件的空间 相对的, RAW 镜像文件也具有一个非常大的缺陷, 就是不支持快照. 所以才有了后来 QCOW 和 QCOW2 的发展. QCOW2 (QEMU-COW 2) (摘自官方文档)QEMU copy-on-write format with a range of special features, including the ability to take multiple snapshots, smaller images on filesystems that don’t support sparse files, optional AES encryption, and optional zlib compression QEMU-COW 镜像文件具有一系列特性, 支持包括 多重快照(能够创建基于之前镜像的新镜像, 速度更快), 占用更小的存储空间(不支持稀疏特性, 不会预先分配指定 Size 的存储空间), 可选的 AES 加密方式, 可选的 zlib 压缩方式 等功能. QCOW2 镜像格式是 KVM-QEMU 支持的磁盘镜像格式之一, 其表现形式为在一个系统文件中模拟一个具有一定 Size 的块设备. QCOW2 Header 每个 QCOW2 镜像文件都会以一个格式固定的 Header 开始. typedef struct QCowHeader { uint32_t magic; uint32_t version; # Version number 版本号 (valid values are 2 and 3) uint64_t backing_file_offset; # backing_file_offset: 表示 backing file 文件绝对路径的字符串相对于 QCOW2 镜像文件起始位置的偏移量 uint32_t backing_file_size; # 因为上述的 backing file 文件绝对路径的字符串不是以'\0'结束的 # 所以需要通过 backing_file_size 来指出该字符串的长度 # 如果当前 Header 的镜像是一个 COW 镜像, 则存在 backing file 文件, 否则没有 uint32_t cluster_bits; # cluster_bits: cluster 的位数, 表示 cluster 的大小 # 1 << cluster_bits (1 向左移 cluster_bits 位得到的就是 cluster 的大小) # 一般是 512 byte <= cluster_bits <= 2 MB uint64_t size; /* in bytes */ # size: 镜像文件以块设备呈现时的 Size(byte) uint32_t crypt_method; # crypt_method: 1 表示开启采用了 AES 加密;0 表示没有加密 uint32_t l1_size; # L1 table(1 级索引) 可用的 8 字节项个数 uint64_t l1_table_offset; # L1 table 相对于镜像文件在存储中起始位置的偏移量, 需要与 cluster 对齐 uint64_t refcount_table_offset; # refcount table(引用计数表) 相对于镜像文件在存储中起始位置的偏移量, 需要与 cluster 对齐 uint32_t refcount_table_clusters; # refcount table 占用多少个 cluster uint32_t nb_snapshots; # 镜像文件中所包含的快照数量 uint64_t snapshots_offset; # snapshot table 相对于镜像文件在存储中起始位置的偏移量, 需要与 cluster 对齐 } QCowHeader; NOTE 1: 上述所有的偏移量数值, 都是为了帮助 QCOW2 镜像文件定位相应的 metadata table. NOTE 2: QCOW2 镜像文件格式的块设备数据都被储存在一个个 cluster 中, 而 QCOW2 Header 也是保存在一个 cluster 中 NOTE 3: L1 和 L2 tables 结合使用能够实现将磁盘镜像地址映射到镜像文件偏移 NOTE 4: 每一个 cluster 都有一个引用计数值, 当引用计数值为 0 时, 该 cluster 能够被删除 QCOW2 的 COW 特性 一个 QCOW2 镜像能够用于保存其它 QCOW2 镜像(模板镜像)的变化, 这样是为了能够保证原有镜像的内容不被修改, 这就是所谓的增量镜像. 增量镜像看着就像是一个独立的镜像文件, 其所有数据都是从模板镜像获取的. 仅当增量镜像中 clusters 的内容与模板镜像对应的 clusters 不一样时, 这些 clusters 才会被保存到增量镜像中. 当要从增量镜像中读取一个 cluster 时, QEMU 会先检查这个 cluster 在增量镜像中有没有被分配新的数据(被改变了). 如果没有, 则会去读模板镜像中的对应位置. QCOW2 的快照 从原理的层面上来说, 一个增量镜像可以近似的当作一个快照, 因为增量镜像相对于模板镜像而言, 就是模板镜像的一个快照. 可以通过创建多个增量镜像来实现创建多个模板镜像的快照, 每一个快照(增量镜像)都引用同一个模板镜像. 需要注意的是, 因为模板镜像不能够修改, 所以必须保持为 read only, 而增量镜像则为可读写. 但需要注意的是增量镜像并不是真正的 QCOW2 镜像快照, 因为「真快照」是存在于一个镜像文件里的. qemu-img 的基本使用 列举一些常用的 qemu-img 指令. 创建 QCOW2 镜像文件: qemu-img create # 指定容量为 4G fanguiju@fanguiju:~$ qemu-img create -f qcow2 test.qcow2 4G Formatting 'test.qcow2', fmt=qcow2 size=4294967296 encryption=off cluster_size=65536 lazy_refcounts=off 查看 QCOW2 镜像文件信息: qemu-img info fanguiju@fanguiju:~$ qemu-img info test.qcow2 image: test.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 Format specific information: compat: 1.1 lazy refcounts: false 创建 QCOW2 的快照: qemu-img snapshot fanguiju@fanguiju:~$ qemu-img snapshot -c snap1 test.qcow2 # 再次查看 QCOW2 镜像文件信息, 可以看见 Snapshot list fanguiju@fanguiju:~$ qemu-img info test.qcow2 image: test.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 204K cluster_size: 65536 Snapshot list: ID TAG VM SIZE DATE VM CLOCK 1 snap1 0 2017-04-07 17:14:24 00:00:00.000 Format specific information: compat: 1.1 lazy refcounts: false 再次创建 QCOW2 的快照2: fanguiju@fanguiju:~$ qemu-img snapshot -c snap2 test.qcow2 fanguiju@fanguiju:~$ qemu-img info test.qcow2 image: test.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 212K cluster_size: 65536 Snapshot list: ID TAG VM SIZE DATE VM CLOCK 1 snap1 0 2017-04-07 17:14:24 00:00:00.000 2 snap2 0 2017-04-07 17:17:54 00:00:00.000 Format specific information: compat: 1.1 lazy refcounts: false 删除 QCOW2 镜像文件的快照: fanguiju@fanguiju:~$ qemu-img snapshot -d snap1 test.qcow2 fanguiju@fanguiju:~$ qemu-img info test.qcow2 image: test.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 208K cluster_size: 65536 Snapshot list: ID TAG VM SIZE DATE VM CLOCK 2 snap2 0 2017-04-07 17:17:54 00:00:00.000 Format specific information: compat: 1.1 lazy refcounts: false 恢复 QCOW2 镜像文件的快照: fanguiju@fanguiju:~$ qemu-img snapshot -a snap2 test.qcow2 创建基于 QCOW2 镜像文件的镜像 创建一个基于镜像 1(test.qcow2) 的镜像 2(test_1.qcow2), test.qcow2 成为了 test_1.qcow2 的 backing file. 对 test_1.qcow2 所作的 I/O 操作都不会影响到 test.qcow2. test.qcow2 仍能够作为其他镜像的 backing file. 前提是 test.qcow2 不被修改. NOTE: 如果作为 backing file 的镜像文件被修改了, 那么会影响到所有基于它的镜像文件. fanguiju@fanguiju:~$ qemu-img create -b test.qcow2 -f qcow2 test_1.qcow2 Formatting 'test_1.qcow2', fmt=qcow2 size=4294967296 backing_file='test.qcow2' encryption=off cluster_size=65536 lazy_refcounts=off # 可以看出镜像 test_1.qcow2 的 backing file 是 test.qcow2 fanguiju@fanguiju:~$ qemu-img info test_1.qcow2 image: test_1.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 backing file: test.qcow2 Format specific information: compat: 1.1 lazy refcounts: false * 让 QCOW2 镜像文件脱离 backing file*: qemu-img convert convert 用于转换镜像文件的格式, 当源镜像和目的镜像的格式均为 qcow2 时, 就相当于将源镜像的当前状态复制到目标镜像. 同时也因为被转换出来的目的镜像不会包含任何源镜像的快照, 所以目的镜像能够摆脱 backing file. # 将 test_1.qcow2 的状态复制到 test_1-merge.qcow2 fanguiju@fanguiju:~$ qemu-img convert -p -f qcow2 test_1.qcow2 -O qcow2 test_1-merge.qcow2 (100.00/100%) fanguiju@fanguiju:~$ qemu-img info test_1.qcow2 image: test_1.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 backing file: test.qcow2 Format specific information: compat: 1.1 fanguiju@fanguiju:~$ qemu-img info test_1-merge.qcow2 image: test_1-merge.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 Format specific information: compat: 1.1 lazy refcounts: false 更改 QCOW2 镜像文件的 backing file fanguiju@fanguiju:~$ qemu-img create -f qcow2 base.qcow2 1G Formatting 'base.qcow2', fmt=qcow2 size=1073741824 encryption=off cluster_size=65536 lazy_refcounts=off fanguiju@fanguiju:~$ qemu-img info test_1.qcow2 image: test_1.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 backing file: test.qcow2 Format specific information: compat: 1.1 lazy refcounts: false fanguiju@fanguiju:~$ qemu-img rebase test_1.qcow2 -b base.qcow2 fanguiju@fanguiju:~$ qemu-img info test_1.qcow2 image: test_1.qcow2 file format: qcow2 virtual size: 4.0G (4294967296 bytes) disk size: 196K cluster_size: 65536 backing file: base.qcow2 Format specific information: compat: 1.1 lazy refcounts: false QCOW2 镜像文件转换成 RAW 格式: fanguiju@fanguiju:~$ qemu-img convert test.qcow2 -O raw test.img RAW 与 QCOW2 的区别 两者的区别之一就是是否具有稀疏(Sparse File)特性. (关于 Sparse File 和 holes 的详细介绍, 请查阅扩展阅读.) RAW 支持 Sparse File, 其内部块中含有若干的 holes. 这些 holes 会被 HostOS Filesystems 管理. 如果我们创建了一个 RAW(20G) 镜像文件, 使用 ls 指令查看该文件大小为 20G, 但如果使用 du 指令来查看其大小时就会变得很小, 这是因为 RAW(20G) 文件中存在若干的 holes. EXAMPLE: $> qemu-img info rhel6_0.img image: rhel6_0.img file format: raw virtual size: 20G (21474836480 bytes) disk size: 1.0G $> ls -lh rhel6_0.img -rw-r--r-- 1 root root 20G Apr 7 23:15 rhel6_0.img $> du -h rhel6_0.img 1.1G rhel6_0.img 相对的, 如果创建一个同为 20G 的 QCOW2 镜像文件, 无论是使用 ls 还是 du 指令查看到的文件 Size 都应该是一致的. 这说明 QCOW2 镜像文件除了含有作为一个块设备所需要的数据信息之外, 其自身还包含了内部块分配信息的记录. EXAMPLE: $> qemu-img info rhel6_0 image: rhel6_0 file format: qcow2 virtual size: 20G (21474836480 bytes) disk size: 1.0G cluster_size: 262144 Format specific information: compat: 1.1 lazy refcounts: false $> ls -lh rhel6_0 -rw-r--r-- 1 root root 1.1G Apr 6 19:26 rhel6_0 $> du -h rhel6_0 1.1G rhel6_0 需要注意的是: 虽然 Sparse File 特性会导致镜像文件的内部块中存在 holes, 但实际上 holes 是不会占用存储空间的, 无论是 RAW(Support Sparse File) 还是 QCOW2(Don’t Support Sparse Files), 两者的磁盘利用率相等, 因为物理硬盘的块数量是固定的, 不会受到 holes 的影响. 虽说 holes 不会影响最终的磁盘使用率, 不过 holes 能够引起某些应用进程的「误解」. 例如: 上面已经举例的 ls 指令, 除此之外, 在 scp RAW 镜像文件时, 会消耗更大的网络 I/O. 同样的, tar RAW 镜像文件时也会消耗更长的时间和 CPU. 这也算是 RAW 的一大缺点了, 一般的解决方法就是将 RAW 转换为 QCOW2 之后再进行压缩或传输. 当然, Sparse File 也是具有其优势的: 「The advantage of sparse files is that storage is only allocated when actually needed: disk space is saved, and large files can be created even if there is insufficient free space on the file system.」 Sparse FIle 的优势在于: 存储只有在实际需要时, 空间才会被分配. 存储的实际空间被保留了起来, 所以即使在文件系统上显示以及没有足够的可用空间时, 仍然可以创建大文件. 除了 Sparse File 特性的区别之外, 使用 RAW 启动的虚拟机会比 QCOW2 启动的虚拟机 I/O 效率更高一些(25%), 所以如果追求性能的话建议选用 RAW 格式.
目录 目录 前言 对公司而言 标准化流程 最佳实践 对自己而言 前言 个人札记, 写下对 写文档 这件事情的理解, 欢迎讨论. 对公司而言 文档系统是 标准化流程 和 最佳实践 的温床. 我们不仅是在编写文档, 更是在打造一个属于公司的文档系统. 标准化流程 在大部分企业的发展进程中, 都需要孵化出属于自己的一套 SOP(标准化操作流程), 完善的 SOP 最终会覆盖到企业所有业务流程. 例如: 开发团队业务中的 标准化开发流程, 标准化开发环境, 标准化代码编写, 标准化注释与代码文档生成 等. 为什么需要标准化? 这个问题类似于 为什么需要规范的代码风格? 规范代码风格的实践意义在于让团队的合作更加无缝, 让代码的承接更加流畅. 而 SOP 蕴含了更大的意义, 其能带来诸如: 降低公司规模扩大所带来的阻塞感, 让公司的管理细节更加透明, 由体制和流程来调度员工, 降低对人的依赖 等好处. 总的来说, 就是打造一个中心, 让所有人都围着它转, 这样才能够提高向心力和执行力. 公司的每一位成员都是标准的制定者, 同时也标准的执行者. 如何实现 SOP? 由外部引入 好处: 有参考案例 坏处: 兼容度不确定 从内部总结 好处: 契合自身实际 坏处: 需要长时间的积累 显然, 后者所总结出来的 SOP 会更加健壮, 而总结的载体就是文档系统. 最佳实践 最佳实践可以理解为最优的问题解决方案, 是在不断实践的基础上作出的经验性结论. 如何让每一次实践都更具价值? 共享: 让所有人都能收获经验值 集思广益: 问题的抛出应该要像打渔撒网一般, 更广的受面意味着更多的可能, 网聚人的力量. “实践-总结” 的循环迭代: 这同样需要一个载体去承托实践的思路, 并在此基础上不断打磨, 优化. 所以, 文档系统能够让闪现的灵光得以持久化保存, 而不让它消散在讨论声之中. 对自己而言 培养自身表达能力和严谨逻辑思维的方法论. 写文档和写代码本质上是一样的, 都是语言的组织和思想的传递行为. 有科学数据表明, 善于写文档的程序员所写的代码会更加优雅. 因为 结构层次是否分明? 措辞选词是否精准? 科学逻辑是否严谨? 概念定义是否高度抽象? 这些优秀论文的标准, 也同样是优秀代码的标准. 在我们写代码的大多数时间里, 其实都是潜在意识在控制自己, 而我们的主观主观意识. 例如: 使用 print 语句还是 print() 函数? 使用 not in 还是 in ? 使用 for-else 还是 for-跳出缩进 ? 使用 if in 还是 if not in ? 当在使用上述两者都能够实现相同 结果 的情况下, 你会如何选择? 很可能是由潜在意识, 也就是我们的个人习惯来决定的. 但对于经验丰富的程序员而言, 在选择的时候就可能会考虑到 Python2 和 Python3 的兼容性的问题, 可读性的问题, Pythonic 的问题, 逻辑性完整的问题. 所以: 潜在意识 > 主观意识 ==> 菜鸟 主观意识 > 潜在意识 ==> 老炮 写文档相比于写代码更能培养上述的技术素养, 因为前者不会受到字符串的视觉扰乱和英文语境的阻碍, 更能专注于 思路的完善和扩展. 所以, 当你认为自身的表达能力有所欠缺时, 开始写点东西吧.
目录 目录 前言 KVM QEMU KVM 与 QEMU qemu-kvm Libvirt Libvirt 在 OpenStack 中的应用 前言 如果是刚开始接触虚拟机技术的话, 对上述的概念肯定会有所混淆, 傻傻的分不清. 尤其在看虚拟化技术文档时导致理解能力下降, 所以在开始学习虚拟化技术之前对这些概念有一个整体的认识和清晰的理解, 就显得很有必要了. KVM KVM(Kernel-basedVirtual Machine 基于内核的虚拟机), 狭义 KVM 指的是一个嵌入到 Linux kernel 中的虚拟化功能模块, 该模块在利用 Linux kernel 所提供的部分操作系统能力(如: 任务调度/内存管理/硬件设备交互)的基础上, 再为其加入了虚拟化能力, 使得 Linux kernel 具有了 转化 为 Hypervisor(虚拟化管理软件) 的条件. NOTE: 上述语句中使用了 转化 一词, 而非 变成 或 成为. 但一般所说的 KVM 是广义 KVM, 即: 一套 Linux 全虚拟化解决方案 , 先了解一下更多的 KVM 细节: KVM 内核模块本身只能提供 CPU 和内存的虚拟化 KVM 包含一个提供给 CPU 的 底层虚拟化可加载核心模块 kvm.ko(kvm-intel.ko/kvm-AMD.ko) KVM 需要在具备 Intel VT 或 AMD-V 功能的 x86 平台上运行, 所以 KVM 也被称之为 硬件辅助的全虚拟化实现. 由于 KVM 内核模块本身只能提供 CPU 和内存的虚拟化, 所以 KVM 需要一些额外的虚拟化技术组件来为虚拟机提供诸如 网卡/ IO 总线/显卡 等硬件的虚拟化实现. 最终变成了我们所使用到的 Linux 虚拟化解决方案. QEMU QEMU(Quick Emulator) 是一个广泛使用的开源计算机 仿真器和虚拟机. QEMU 作为一个独立 Hypervisor(不同于 KVM 需要嵌入到 kernel), 能在应用程序的层面上运行虚拟机. 同时也支持兼容 Xen/KVM 模式下的虚拟化, 并且当 QEMU 运行的虚拟机架构与物理机架构相同时, 建议使用 KVM 模式下的 QEMU, 此时 QEMU 可以利用 kqemu 加速器, 为物理机和虚拟机提供更好的性能. 当 QEMU 作为仿真器时, QEMU 通过动态转化技术(模拟)为 GuestOS 模拟出 CPU 和其他硬件资源, 让 GuestOS 认为自身直接与硬件交互. QEMU 会将这些交互指令转译给真正的物理硬件之后, 再由物理硬件执行相应的操作. 由于 GuestOS 的指令都需要经过 QEMU 的模拟, 因而相比于虚拟机来说性能较差. 当 QEMU 作为一个虚拟机时, QEMU 能够通过直接使用物理机的系统资源, 使虚拟机能够获得接近于物理机的性能表现. KVM 与 QEMU KVM 作为 Linux 的内核模块, 需要被加载后, 才能进一步通过其他工具的辅助以实现虚拟机的创建. 但需要注意的是, KVM 作为运行于 CPU 内核态 的内核模块, 用户是无法直接对其进行操作的. 还必须提供一个运行于 CPU 用户态 的对接程序来提供给用户使用. 而这个对接程序, KVM 的开发者选择了已经成型的开源虚拟化软件 QEMU. KVM 开发者在对 QEMU 稍加改造之后, QEMU 可以通过 KVM 对外暴露的 /dev/kvm 接口来进行调用, 官方提供的 KVM 下载有 QEMU 和 KVM 两大部分, 包含了 KVM 模块、QEMU 工具以及二者的合集 qemu-kvm 三个文件. qemu-kvm 在 Linux 全虚拟化解决方案 中, KVM 负责提供 CPU 虚拟化和内存虚拟化, 但是 KVM 对于一些计算机硬件设备还是无法进行完美的虚拟(如: 网卡/磁盘/ IO 设备…). 于是就引入了 QEMU, QEMU 负责提供硬件设备的虚拟化, 以此弥补来 KVM 的缺陷. 同时, 为了提高 QEMU 虚拟出来的虚拟硬件设备性能, 于是产生了 pass through 半虚拟化设备virtio_blk/virtio_net. KVM + QEMU 才能实现真正意义上虚拟化. 而 qemu-kvm 就是将两者整合到了一起的媒介. qemu-kvm 通过 ioctl 调用 KVM 的 /dev/kvm 接口, 将 KVM 内核模块相关的 CPU 指令传递到内核模块中执行. 在 RHEL6/CentOS6 系统中, qemu-kvm 存放在 /usr/libexec 目录下, 由于 PATH 环境变量默认不包含此目录, 所以用户无法直接使用 qemu-kvm. 这样做是为了防止 QEMU 替代了 KVM 作为物理机 Hypervisor 的角色. 如果希望使用 QEMU 虚拟机, 可以通过将 /usr/libexec/qemu-kvm 链接为 /usr/bin/qemu 来实现. Libvirt Libvirt 是目前使用最为广泛的异构虚拟化管理工具及 API, 其具有一个 Libvirtd Daemon, 可供本地或远程的 virsh 调用. libvirt 由 应用程序编程接口库、libvirtd 守护进程、virsh CLI 组成. 其中 libvirtd 守护进程负责调度管理虚拟机, 而且这个守护进程可以分为 root 权限的 libvirtd和普通用户权限的 libvirtd 两种. 前者权限更大, 可以虚拟出物理主机的各种设备. 开启 root 权限 libvirtd: sudo libvirtd --daemon Domain:虚拟机的一个运行实例 Hypervisor:指的就是虚拟化管理程序 Libvirt 本质上是一些被提供的库函数(C语言), 用于管理物理机的虚拟机. 它引用了面向驱动的架构设计, 对所有的虚拟化技术都提供了相应的驱动和统一的接口, 所以 Libvirt 支持异构的虚拟化技术. 同时 Libvirt 提供了多种语言的编程接口, 可以通过程序调用这些接口来实现对虚拟机的操作. 由此可见, Libvirt 具有非常强的可扩展性, OpenStack 就与该库联系得相当密切. 在 KVM 中, 可以使用 virsh CLI 来调用 libvirtd, libvirtd 再通过调用 qemu-kvm 来操作虚拟机. Libvirt的控制方式: 本地控制管理: Management Application 和 Domain 在同一个 Node 上. (左图是没有应用 Libvirt 的虚拟化架构) 远程控制管理: Management Application 和 Domain 不再同一个 Node 上. 该模式使用了运行于远程节点上的 libvirtd 守护进程, 当在其他节点上安装 libvirt 时 Management Application 会自动启动, 并且会自动确定本地虚拟机的监控程序和为虚拟机安装驱动程序, Management Application 通过一种通用协议从本地 libvirt 连接到远程 libvirtd。 Libvirt 在 OpenStack 中的应用 OpenStack 原生使用 KVM 虚拟化技术, 以 KVM 作为最底层的 Hypervisor, KVM 用于虚拟化 CPU 和内存, 但 KVM 缺少对 网卡/磁盘/显卡 等周边 I/O 设备的虚拟化能力, 所以需要 qemu-kvm 的支持, 它构建于 KVM 内核模块之上为 KVM 虚拟机提供完整的硬件设备虚拟化能力. OpenStack 不会直接控制 qemu-kvm, 而是使用 libvirt 作为与 qemu-kvm 之间的中间件. libvirt 具有跨虚拟化平台能力, 可以控制 VMware/Virtualbox/Xen 等多种虚拟化实现. 所以为了让 OpenStack 具有虚拟化平台异构能录, OpenStack 没有直接调用 qemu-kvm, 而是引入了异构层 libvirt. 除此之外, libvirt 还提供了诸如 pool/vo l管理等高级的功能.
目录 目录 前文列表 扩展阅读 osloconfig argparse cfgpy class Opt class ConfigOpts CONF 对象的单例模式 前文列表 OpenStack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 OpenStack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata OpenStack 实现技术分解 (3) 开发工具 — VIM & dotfiles OpenStack 实现技术分解 (4) 通用技术 — TaskFlow OpenStack 实现技术分解 (5) 应用开发 — 使用 OpenStackClients 进行二次开发 OpenStack 实现技术分解 (6) 通用库 — oslo_log 扩展阅读 oslo.config — oslo.config 3.23.1.dev1 documentation argparse - 命令行选项与参数解析(译) Openstack Oslo.config 学习 oslo.config (摘自官方文档) An OpenStack library for parsing configuration options from the command line and configuration files. oslo.config 是一个 OpenStack 通用库, 用于解析并加载来自于 CLI 或配置文件中所提供的参数项或配置项. oslo.config 属于 OpenStack 中复用率最高的通用库之一, 在网上能够轻易的找到其使用方法与示例, 并且在 oslo.config 的源码中(oslo_config/cfg.py)也给出了丧心病狂的 443 行使用文档以及大量的注释, 其详尽程度简直前所未见. 所以本篇不再赘述 oslo.config 的使用方法, 主要介绍并记录 oslo.config 的源码实现, 使用技巧及其 单例特性. 本质上, oslo.config 是对 Python’s standard argparse library 标准库的封装, 主要解决了下列两个问题: 如何将配置文件中的配置项与 CLI 中的参数项进行了统一管理? 如何处理配置文件中与 CLI 中同名选项的优先级? argparse 在阅读 oslo.config 源码之前, 首先要对 argparse 标准库有一定的了解. 准确来说我们需要对 argparse 定义的概念对象以及 argparse 的使用流程有所了解, 因为 oslo.config 仍然沿用了这些概念对象. parser(解析器): 使用 argparse 的第一步就是创建一个解析器对象, 并由解析器对象来完成指令行选项清单的预设与解析. import argparse # 创建解析器对象 parser parser = argparse.ArgumentParser(add_help=True, description='This is a demo of argparse library.') # 调用 argparse.ArgumentParser.add_argument method 预设指令行选项清单 # 只有在为解析器预设了指令行选项清单后, 解析器才能判别并接收解析指定的指令行选项 parser.add_argument("--show", "-s", desc="show", action="store", help="show message", default="You Know") NOTE 1: 当参数 add_help=True 时, 会自动生成帮助手册. 即隐式的添加了 -h 选项 NOTE 2: argparse.ArgumentParser.add_argument method 的原型为 add_argument(self, *args, **kwargs), 其中 args 元组冗余形参用于接收指令行选项的使用样式等信息, kwargs 字典冗余形参用于接收指令行选项对应的 dest/action/help/default 等功能定义. action(动作): 如上例所示, 在向解析器添加指令行选项清单时, 需要指定在解析该指令行选项时触发的动作(action), 常用的 action 有 store/store_const/append/version 等, 具体含义请浏览扩展阅读. namespace(命名空间): 预设完指令行选项清单之后, 就可以从 CLI 接收选项输入并对其进行解析, 这需要调用 argparse.ArgumentParser.parse_args method 来实现. 该 method 的实参缺省从 sys.argv[1:](CLI 输入) 中获取, 但你也可以传递自己定义的参数列表, E.G.argparse.ArgumentParser.parse_args(['-s', 'Helloword']). 该 method 返回的对象就是 namespace 对象, namespace 对象中就以属性的形式保存了指令行选项解析后的值. 所以可以通过 namespace 对象来检索相应选项在 CLI 中指定的 values. nsp = parser.parse_args(sys.argv[1:]) EXAMPLE: import sys import argparse parser = argparse.ArgumentParser(add_help=True, description='This is a demo of argparse library.') parser.add_argument('--show', '-s', action="store", help="show message", default="You Know") nsp = parser.parse_args(sys.argv[1:]) print(nsp.show) print('=' * 20) print nsp Output: $ python test.py You Know ==================== Namespace(show='You Know') $ python test.py -h usage: test.py [-h] [--show SHOW] This is a demo of argparse library. optional arguments: -h, --help show this help message and exit --show SHOW, -s SHOW show message $ python test.py -s usage: test.py [-h] [--show SHOW] test.py: error: argument --show/-s: expected one argument $ python test.py -s 'Hello world' Hello world ==================== Namespace(show='Hello world') $ python test.py --show 'Hello world' Hello world ==================== Namespace(show='Hello world') 上述是 argparse 标准库的一个简单应用, 主要为了介绍其定义的概念对象, 更复杂的应用方式请浏览扩展阅读. 注意: 上文中反复使用了 指令行选项 这个词语, 是为了与后文中的 options 作出区分. 后文中 options 均为配置文件中的配置项与 CLI 中的参数项的统称, 因为 cfg 模块中大量应用了 OOP 的编程思想. cfg.py oslo_config.cfg 是 oslo.config 库的核心模块, 3274 行的代码实现了 oslo.config 提供的绝大部分功能. 其中最重要的类实现就是 Opt 和 ConfigOpts. class Opt 回到第一个问题: 如何将配置文件中的配置项与 CLI 中的参数项进行了统一管理? 答案就是 Opt 抽象类以及其衍生出的子类. class Opt(object): """Base class for all configuration options. The only required parameter is the option's name. However, it is common to also supply a default and help string for all options. 从 Opt document 可以看出, Opt 是所有 configuration options 的基类, 衍生出了 IntOpt(Opt)/FloatOpt(Opt)/StrOpt(Opt)/... 等不同类型的 options 类, 对应了如下 option types: ==================================== ====== Type Option ==================================== ====== :class:`oslo_config.types.String` - :class:`oslo_config.cfg.StrOpt` - :class:`oslo_config.cfg.SubCommandOpt` :class:`oslo_config.types.Boolean` :class:`oslo_config.cfg.BoolOpt` :class:`oslo_config.types.Integer` :class:`oslo_config.cfg.IntOpt` :class:`oslo_config.types.Float` :class:`oslo_config.cfg.FloatOpt` :class:`oslo_config.types.Port` :class:`oslo_config.cfg.PortOpt` :class:`oslo_config.types.List` :class:`oslo_config.cfg.ListOpt` :class:`oslo_config.types.Dict` :class:`oslo_config.cfg.DictOpt` :class:`oslo_config.types.IPAddress` :class:`oslo_config.cfg.IPOpt` :class:`oslo_config.types.Hostname` :class:`oslo_config.cfg.HostnameOpt` :class:`oslo_config.types.URI` :class:`oslo_config.cfg.URIOpt` ==================================== ====== 而且需要注意的是, Opt 还衍生出了 _ConfigFileOpt(Opt) 与 _ConfigDirOpt(Opt). 两者都实现了一个内部类 ConfigDirAction(argparse.Action) 与 ConfigFileAction(argparse.Action). class ConfigFileAction(argparse.Action): """An argparse action for --config-file. As each --config-file option is encountered, this action adds the value to the config_file attribute on the _Namespace object but also parses the configuration file and stores the values found also in the _Namespace object. """ 正如 ConfigFileAction document 所示, 这是一个 CLI 参数项 --config-file 的 argparse action, 这个 action 同时会解析 CLI 参数项 --config-file 并以 config_file 属性的形式添加到 namespace 对象中. 并且还会将 --config-file 选项指定的配置文件中的配置项解析后也以属性的形式添加到 namespace 中. 同理, --config-dir 也是一样的. 所以看到这里, 大家应该可以体会到, oslo.config 正是通过对 Opt 的封装与衍生出不同类型的 sub-Opt 子类来实现将 CLI 中的参数项 和 配置文件中的配置项 以 options 对象统一起来. 而且 Opt 中有几个 method 是比较重要的: def _add_to_cli(self, parser, group=None) def _add_to_cli(self, parser, group=None): """Makes the option available in the command line interface. This is the method ConfigOpts uses to add the opt to the CLI interface as appropriate for the opt type. Some opt types may extend this method, others may just extend the helper methods it uses. :param parser: the CLI option parser :param group: an optional OptGroup object """ container = self._get_argparse_container(parser, group) kwargs = self._get_argparse_kwargs(group) prefix = self._get_argparse_prefix('', group.name if group else None) deprecated_names = [] for opt in self.deprecated_opts: deprecated_name = self._get_deprecated_cli_name(opt.name, opt.group) if deprecated_name is not None: deprecated_names.append(deprecated_name) self._add_to_argparse(parser, container, self.name, self.short, kwargs, prefix, self.positional, deprecated_names) 该 method 接收一个 parser(the CLI option parser), 主要用于 Makes the option available in the command line interface. 生成可用于 CLI 的 options. 类比上文中 argparse 的应用过程, 该 method 就是做的事情就是为 argparse.ArgumentParser.add_argument 准备好用于预设 options 的 args 和 kwargs 实参. NOTE 1: Opt._add_to_cli method 语句块中的 container(container: an argparse._ArgumentGroup object) 对象才是 argparse 的 parser. NOTE 2: 在该 method 语句块中调用的 Opt._add_to_argparse 才是真正将 args 和 kwargs 传入到 argparse 的解析器对象中的实现. NOTE 3: 最后可以通过 Opt._get_from_namespace 来从命名空间中获取 options 的值. class ConfigOpts 第二个问题: 如何处理配置文件中与 CLI 中同名选项的优先级? 答案就是: ConfigOpts class ConfigOpts(collections.Mapping): """Config options which may be set on the command line or in config files. ConfigOpts is a configuration option manager with APIs for registering option schemas, grouping options, parsing option values and retrieving the values of options. It has built-in support for :oslo.config:option:`config_file` and :oslo.config:option:`config_dir` options. """ 从 ConfigOpts document 中的「ConfigOpts is a configuration option manager with APIs for registering option schemas, grouping options, parsing option values and retrieving the values of options.」可以看出, class ConfigOpts 是一个提供了 注册 option 与 option group, 解析 options, 检索 options 值 的管理接口. 所以同样由 ConfigOpts 来负责处理配置文件中与 CLI 中同名选项的优先级. 先来看 oslo.config 在程序中的使用例子, EXAMPLE: from config import cfg opts = [ cfg.StrOpt('bind_host', default='0.0.0.0'), ] cli_opts = [ cfg.IntOpt('bind_port', default=9292), ] CONF = cfg.CONF CONF.register_opts(opts) # 此时没有注册任何 CLI 参数项或配置文件配置项, 仅仅注册了一些动态 options CONF.register_cli_opts(cli_opts) # 注册 CLI 参数项 print CONF.bind_host print CONF.bind_port CONF(args=sys.argv[1:], default_config_files=['./test.conf']) # 接收 CLI 参数项与配置文件配置项 print CONF.bind_host print CONF.bind_port Output 1: 没有传递 CLI 参数项, 并且 test.conf 没有写入配置项 $ python test.py 0.0.0.0 9292 0.0.0.0 9292 可以看见当没有指定任何配置文件或 CLI 选项时, 只解析了动态定义的 options Output 2: 没有传递 CLI 参数项, 但有写入 test.conf 配置项 bind_host/bind_port $ python test.py 0.0.0.0 9292 192.168.0.1 55553 配置文件中的配置项会覆盖动态定义的 options. Output 3: 有传递 CLI 参数项, 也有写入 test.conf 配置项 bind_host/bind_port $ python test.py --config-file ./test.conf --bind_port 9090 0.0.0.0 9292 192.168.0.1 9090 CLI 参数项会覆盖配置文件中的配置项. 所以有两点是我们在使用 oslo.config 时需要注意的: 优先级: CLI 参数项 > 配置文件配置项 > 动态定义的 options 如果需要接收并解析最新的 CLI 参数项时, 需要实现 CLI 注册语句 CONF.register_cli_opts(cli_opts) 并且 传入 CLI 参数项值 args=sys.argv[1:] CONF 对象的单例模式 大家如果有使用 osls.config 的经历, 一定会有一个疑问: 为什么在不同模块中添加的 options 值能够通过 CONF 对象实现共用? 答案是: 其实没有什么共用一说, 因为在整个应用程序中的 CONF 对象都可能是同一个对象. 因为实例化 CONF 对象的 ConfigOpt 类应用了由 import 语句支持的单例模式. 先看一个例子, EXAMPLE: vim test.py class ConfigOpts(object): def foo(self): pass CONF = ConfigOpts() vim test_1.py from test import CONF print __name__, CONF, id(CONF) vim test_2.py from test import CONF import test_1 print __name__, CONF, id(CONF) Output: $ python test_2.py test_1 <test.ConfigOpts object at 0x7fd347eec710> 140545421657872 __main__ <test.ConfigOpts object at 0x7fd347eec710> 140545421657872 可以看见在模块 test_1 和 test_2 中生成的 CONF 对象在内存中是同一个对象, 这是 Python 的特性决定的 作为 Python 的模块是天然的单例模式. 只要多个模块之间有互相 import 的实现, 那么这个模块之间所生成的类实例化对象就是同一个内存对象. 再回头看看 cfg 模块中的 CONF 对象是如何实例化的: CONF = ConfigOpts() 这条语句是顶格代码, 所以当在程序中执行 from oslo_config import cfg 的时候, 就会实例化 class ConfigOpts 的实例对象 CONF, 满足单例模式的实现. 所以程序员在使用 oslo.config 的过程中, 会感到非常的便利, 因为整个程序中的 CONF 都可能是同一个 CONF, 那么程序之中的 options 传递就会显得那么的轻松与优雅.
目录 目录 前文列表 扩展阅读 日志级别 oslolog 初始化设置 DEMO oslolog 的相关配置项 oslolog 的日志级别 oslolog 的使用技巧 推荐使用 LOGdebug 的地方 推荐使用 LOGinfo 的地方 推荐使用 LOGexception 的地方 推荐使用 LOGerror 的地方 推荐使用 LOGcretical 的地方 前文列表 OpenStack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 OpenStack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata OpenStack 实现技术分解 (3) 开发工具 — VIM & dotfiles OpenStack 实现技术分解 (4) 通用技术 — TaskFlow OpenStack 实现技术分解 (5) 应用开发 — 使用 OpenStackClients 进行二次开发 扩展阅读 Usage — oslo.log 3.21.1.dev1 documentation oslo.log – Oslo Logging Library 日志级别 在记录日志时, 需要将日志消息关联一个级别, 系统默认提供了 6 个级别,它们分别是: critical > error > warning > info > debug > notset 级别越高打印的日志越少,反之亦然: DEBUG: 打印全级别日志( notset 等同于 debug) INFO: 打印 info/warning/error/critical 级别日志 WARNING: 打印 warning/error/critical 级别日志 ERROR: 打印 error/critical 级别日志 CRITICAL: 打印 critical 级别 一般而言, 在程序的开发阶段会打印大量的日志, 此时的日志级别应为 DEBUG. 直到程序稳定后, 为了提高程序的执行效率, 需要打印日志应该相对减少. 根据实际情况, 可能会删减 DEBUG 级别的日志代码. 此时, 程序员更关心的是跟踪用户的动作, 例如: 用户对于核心数据的修改动作就非常有必要记录在案, 所以可以选择 INFO 及以上级别的日志, 其中最重要的信息就应该选择 CRITICAL 级别了. 需要注意的是, 异常捕获代码块(try-except)中都应该使用 ERROR/EXCEPTION(由 oslo_log 提供的异常栈级别) 级别的日志, 以便定位具体的错误原因. oslo.log (摘自官方文档)The oslo.log (logging) configuration library provides standardized configuration for all openstack projects. It also provides custom formatters, handlers and support for context specific logging (like resource id’s etc). oslo.log(logging) 库为所有的 OpenStack 项目提供了标准的日志处理方式, 它还提供能自定义日志格式以及各种 handlers, 同时也支持指定上下文对象的日志, 例如: resource_id 的日志等等. 实际上 oslo_log 是基于 Python’s standard logging library 标准库的封装, 在整体使用的简易性上得到了提升. 初始化设置 DEMO from oslo_config import cfg from oslo_log import log as logging LOG = logging.getLogger(__name__) CONF = cfg.CONF DOMAIN = "demo" logging.register_options(CONF) logging.setup(CONF, DOMAIN) # Oslo Logging uses INFO as default LOG.info("Oslo Logging") LOG.warning("Oslo Logging") LOG.error("Oslo Logging") 显然的, 为了充分利用 oslo_conf 的跨文件作用域特性, oslo_log 一般会与 oslo_conf 结合使用, 由 oslo_conf 为 oslo_log 提供项目的日志配置项参数值. 对于 oslo_log 而言, 配置项引入是一个非常重要且具有实践价值的设计理念. oslo.log 的相关配置项 各个配置项的含义在注释中也作了清晰的介绍: ################ # From oslo.log ################ # If set to true, the logging level will be set to DEBUG instead of the default # INFO level. (boolean value) # Note: This option can be changed without restarting. debug = true # DEPRECATED: If set to false, the logging level will be set to WARNING instead # of the default INFO level. (boolean value) # This option is deprecated for removal. # Its value may be silently ignored in the future. #verbose = true # The name of a logging configuration file. This file is appended to any # existing logging configuration files. For details about logging configuration # files, see the Python logging module documentation. Note that when logging # configuration files are used then all logging configuration is set in the # configuration file and other logging configuration options are ignored (for # example, logging_context_format_string). (string value) # Note: This option can be changed without restarting. # Deprecated group/name - [DEFAULT]/log_config #log_config_append = <None> # Defines the format string for %%(asctime)s in log records. Default: # %(default)s . This option is ignored if log_config_append is set. (string # value) #log_date_format = %Y-%m-%d %H:%M:%S # (Optional) Name of log file to send logging output to. If no default is set, # logging will go to stderr as defined by use_stderr. This option is ignored if # log_config_append is set. (string value) # Deprecated group/name - [DEFAULT]/logfile #log_file = <None> # (Optional) The base directory used for relative log_file paths. This option # is ignored if log_config_append is set. (string value) # Deprecated group/name - [DEFAULT]/logdir #log_dir = <None> # Uses logging handler designed to watch file system. When log file is moved or # removed this handler will open a new log file with specified path # instantaneously. It makes sense only if log_file option is specified and # Linux platform is used. This option is ignored if log_config_append is set. # (boolean value) #watch_log_file = false # Use syslog for logging. Existing syslog format is DEPRECATED and will be # changed later to honor RFC5424. This option is ignored if log_config_append # is set. (boolean value) #use_syslog = false # Syslog facility to receive log lines. This option is ignored if # log_config_append is set. (string value) #syslog_log_facility = LOG_USER # Log output to standard error. This option is ignored if log_config_append is # set. (boolean value) #use_stderr = true # Format string to use for log messages with context. (string value) #logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s # Format string to use for log messages when context is undefined. (string # value) #logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s # Additional data to append to log message when logging level for the message # is DEBUG. (string value) #logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d # Prefix each line of exception output with this format. (string value) #logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s # Defines the format string for %(user_identity)s that is used in # logging_context_format_string. (string value) #logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s # List of package logging levels in logger=LEVEL pairs. This option is ignored # if log_config_append is set. (list value) #default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,dogpile.core.dogpile=INFO # Enables or disables publication of error events. (boolean value) #publish_errors = false # The format for an instance that is passed with the log message. (string # value) #instance_format = "[instance: %(uuid)s] " # The format for an instance UUID that is passed with the log message. (string # value) #instance_uuid_format = "[instance: %(uuid)s] " # Enables or disables fatal status of deprecations. (boolean value) #fatal_deprecations = false 对于有 Python logging 标准库使用经验的程序员而言, 应该很容易的就能体会到 oslo_log 对 logging 进行封装后带来的好处. 以往使用 logging 时需要在代码中显式定义的状态信息(e.g. 日志级别/打印格式/handler 等), 现在都能够通过 oslo_log 配置文件的配置项参数值来简化定义. 有效的避免了在程序中写出许多无谓的代码, 而且通过阅读配置文件能够更清晰的理解当前程序的日志状态. 一般来说我们需要关心的 oslo_log 配置项有以下几个: debug = true: 是否将日志级别设定为 DEBUG verbose = true: 将日志级别设定为 INFO(true) 还是 WARNING(false) log_date_format = %Y-%m-%d %H:%M:%S: 日志的日期格式 log_file = <None>: 日志文件名称 log_dir = <None>: 日志文件路径 use_stderr = true: 是否让日志以 system standard error 管道输出 logging_context_format_string: 日志内容格式 default_log_levels: 相关程序的默认日志级别 instance_format: instance 的日志打印格式, 所谓的上下文对象日志 oslo_log 支持通过调用 logging 的 register_options() method 来实现配置项的注册: CONF = cfg.CONF def prepare(): # Required step to register common, logging and generic configuration # variables logging.register_options(CONF) register_options method 会帮助我们将上述配置文件中的配置项以默认值注册到 CONF 对象中: logging.register_options(CONF) >>> CONF.items() [('default_log_levels', ['amqp=WARN', 'amqplib=WARN', 'boto=WARN', 'qpid=WARN', 'sqlalchemy=WARN', 'suds=INFO', 'oslo.messaging=INFO', 'iso8601=WARN', 'requests.packages.urllib3.connectionpool=WARN', 'urllib3.connectionpool=WARN', 'websocket=WARN', 'requests.packages.urllib3.util.retry=WARN', 'urllib3.util.retry=WARN', 'keystonemiddleware=WARN', 'routes.middleware=WARN', 'stevedore=WARN', 'taskflow=WARN', 'keystoneauth=WARN', 'oslo.cache=INFO', 'dogpile.core.dogpile=INFO']), ('verbose', True), ('watch_log_file', False), ('logging_default_format_string', '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s'), ('use_stderr', False), ('log_date_format', '%Y-%m-%d %H:%M:%S'), ('rate_limit_burst', 0), ('logging_context_format_string', '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s'), ('instance_format', '[instance: %(uuid)s] '), ('use_syslog', False), ('log_dir', None), ('publish_errors', False), ('logging_debug_format_suffix', '%(funcName)s %(pathname)s:%(lineno)d'), ('logging_exception_prefix', '%(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s'), ('syslog_log_facility', 'LOG_USER'), ('instance_uuid_format', '[instance: %(uuid)s] '), ('log_config_append', None), ('rate_limit_except_level', 'CRITICAL'), ('rate_limit_interval', 0), ('debug', False), ('log_file', None), ('logging_user_identity_format', '%(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s')] 所以我们在使用 oslo_log 时无需在程序代码中重复定义这些 options. 能够非常方便的通过修改配置文件中配置项的值, 来实现对 oslo_log 的管理. NOTE: 如果在执行了 logging.register_options(CONF) 之后, 又在程序中重复定义了这些重名配置项的话, 会触发异常: slo_config.cfg.DuplicateOptError: duplicate option: debug 在注册了配置项之后, 还可以通过调用 logging 的 setup() method 来指定该 logger 的作用域, 支持在一个程序中存在多个不同 domain 的 logger: DOMAIN = 'demo' def prepare(): # Required setup based on configuration and domain logging.setup(CONF, DOMAIN) 同时, oslo_log 也支持使用 logging 的 set_defaults method 来动态修改日志级别: # Optional step to set new defaults if necessary for # * logging_context_format_string # * default_log_levels extra_log_level_defaults = [ 'dogpile=INFO', 'routes=INFO' ] logging.set_defaults( default_log_levels=logging.get_default_log_levels() + extra_log_level_defaults) 上述的 3 个 method 让程序中日志的使用变得更加灵活. oslo.log 的日志级别 oslo.log 常用的 5 个日志级及其对应的 function: from oslo_log import log as logging LOG = logging.getLogger(__name__) LOG.debug("Some key is %s", "key") LOG.info("Create something %s start", "test") LOG.exception("Failed to create something %(something)s as error %(err)s", {"something": "something", "err": "err"}) LOG.error("Failed to create something") LOG.cretical("Fatal error as %s", "fatal") oslo.log 的使用技巧 一般而言, 我会建议程序中的日志宜多不宜少, 但在上文也有提到, 大量的日志会对程序运行效率造成影响, 所以理解如何精准的在程序中的不同场景使用不同级别的日志就显得很有必要了. 推荐使用 LOG.debug 的地方 LOG.debug 一般用于对实参数据体精确度较为敏感的 function/method 语句块的首行, 让开发者能够快速的判断是否得到了预期实参. 所以这里需要打印出 module_name & function_name/method_name & 实参值. EXAMPLE 1: REST API controller method 中接收到的 request info: def index(self, req): LOG.debug("Get server list with parameter %s,", req.GET()) ... EXAMPLE 2: 一些通过逻辑比较复杂的流程而得到的参数 ... # 执行一次复杂的排序操作 sort_result = _complex_sort(arr) LOG.debug("Front list %(fro)s change to %(now)s after complex_sort.", {'fro': arr, 'now': sort_result}) ... 推荐使用 LOG.info 的地方 LOG.info 一般用于标记业务逻辑步骤, 让运维/开发人员都能够快速判断程序流到了哪一个步, 所以这里需要打印 module_name & step_description & 业务对象的唯一标示 EXAMPLE: 当执行一个所需步骤大于等于二的程序流程时, 需要打印程序流开始到结束的过程 ... LOG.info("Create server %s start.", server.id) ... LOG.info("Prepare image for server %s.", server.id) ... LOG.info("Prepare network for server %s", server.id) ... LOG.info("Create server %s successful", server.id) ... 推荐使用 LOG.exception 的地方 LOG.exception/LOG.error 一般用于 try-except 异常捕获语句块, 如果捕获到的是某一个准确的异常可以使用 LOG.error, 如果捕获的是某一类异常或全部异常时可以使用 LOG.exception 来打印出异常栈. EXAMPLE: 在暴露的 RPC API 或者 HTTP API 方法中加入 exception 级别日志 def index(self, req): ... try: self.server_api.get_all_servers() except Exception as err: LOG.exception("Failed to get servers as error %s", six.text_type(err)) raise exc.HTTPInnevalError() ... 推荐使用 LOG.error 的地方 EXAMPLE: 同上, 但如果程序流没有再上一层的调用时, 也可以使用LOG.error, 因为已经不需要打印异常栈了. try: # do something ... except Exception as err: LOG.error("Failed to do something as error %s", six.text_type(err)) raise 推荐使用 LOG.cretical 的地方 LOG.cretical 一般用于当错误可能会导致程序进程崩溃等极其严重的逻辑块中 try: # 执行一些危险操作 ... except Exception as err: LOG.cretical("Failed to do something as error %s", six.text_type(err)) raise
目录 目录 前文列表 参考阅读 前言 OpenStackClients 使用 OpenStackClients 获取 project_client object 的 demo 调用 project_client object 实例方法实现对 project 操作的 demo 最后 前文列表 OpenStack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 OpenStack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata OpenStack 实现技术分解 (3) 开发工具 — VIM & dotfiles OpenStack 实现技术分解 (4) 通用技术 — TaskFlow 参考阅读 Openstack API 类型 & REST 风格 OpenStackClients Python bindings to the OpenStack Identity API Python Bindings for the OpenStack Images API Python bindings to the OpenStack Nova API Cinder Python API Python bindings to the OpenStack Networking API 前言 OpenStack 为用户提供了三种操作方式, Web界面/CLI/RESTAPI, 实际上前两者是对 RESTAPI 做了两种不同形式的包装, 使用户可以通过网页或者指令行的方式来调用 RESTAPI 接口. 本篇博文主要记录了 使用 OpenStackClients (OSC 命令行客户端) 项目所提供了Python Bindings API 来进行二次开发的技巧, 以及实现一个启动虚拟机并部署 Workpass+MySQL 自动化脚本的 Demo. 源码详见 GitHub: openstackclient-api-demo 在介绍 OpenStackClients 之前, 我们可以尝试直接使用 curl 指令来查看一个 tenant 所含有的虚拟机列表. Step 1: 获取账户 admin 的身份验证 Temporary token curl -k -X 'POST' -v http://200.21.18.2:5000/v2.0/tokens -d '{"auth":{"passwordCredentials":{"username": "admin", "password":"fanguiju"}}}' -H 'Content-type: application/json' | python -mjson.tool Response: { "access": { "metadata": { "is_admin": 0, "roles": [] }, "serviceCatalog": [], "token": { "audit_ids": [ "AOMhHXq_Qx2Nz41RVoUy7g" ], "expires": "2017-03-19T05:41:20Z", "id": "16ae22b6c36f4ebc97938f51b7d0631b", "issued_at": "2017-03-19T04:41:20.039145" }, "user": { "id": "135b2cb86962401c82044fd4ca9daae4", "name": "admin", "roles": [], "roles_links": [], "username": "admin" } } } 获取到 Temporary token: 16ae22b6c36f4ebc97938f51b7d0631b, 表示我们的账户信息通过了验证流程. Step 2: 使用 Temporary token 来获取 tenants list curl -X 'GET' -H "X-Auth-Token:16ae22b6c36f4ebc97938f51b7d0631b" -v http://200.21.18.2:5000/v2.0/tenants | python -mjson.tool Response: { "tenants": [ { "description": "", "enabled": true, "id": "6c4e4d58cb9d4451b36e774b348e8813", "name": "admin" }, { "description": "", "enabled": true, "id": "ad9a69f3da8f4aa280389fcdf855aeb5", "name": "demo" } ], "tenants_links": [] } 可以看出 admin 账户含有 admin tenant 和 demo tenant. Step 3: 获取 admin tenant 的 token 信息 curl -k -X 'POST' -v http://200.21.18.2:5000/v2.0/tokens -d '{"auth":{"passwordCredentials":{"username": "admin", "password":"fanguiju"},"tenantId":"6c4e4d58cb9d4451b36e774b348e8813"}}' -H 'Content-type: application/json' | python -mjson.tool Response: { "access": { "metadata": { "is_admin": 0, "roles": [ "14a6da35e3ef4e47a540c6608aa00ca7" ] }, "serviceCatalog": [ { "endpoints": [ { "adminURL": "http://200.21.18.2:8774/v2.1/6c4e4d58cb9d4451b36e774b348e8813", "id": "705f599f3bae42ceb4a70616d9663ad8", "internalURL": "http://200.21.18.2:8774/v2.1/6c4e4d58cb9d4451b36e774b348e8813", "publicURL": "http://200.21.18.2:8774/v2.1/6c4e4d58cb9d4451b36e774b348e8813", "region": "RegionOne" } ], "endpoints_links": [], "name": "nova", "type": "compute" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:8760/v1.1/6c4e4d58cb9d4451b36e774b348e8813", "id": "39ceecd18b754c9495834d0155fe91bf", "internalURL": "http://200.21.18.2:8760/v1.1/6c4e4d58cb9d4451b36e774b348e8813", "publicURL": "http://200.21.18.2:8760/v1.1/6c4e4d58cb9d4451b36e774b348e8813", "region": "RegionOne" } ], "endpoints_links": [], "name": "egis", "type": "recovery" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:8776/v2/6c4e4d58cb9d4451b36e774b348e8813", "id": "218769a91d0943ff8db44887645ec0ff", "internalURL": "http://200.21.18.2:8776/v2/6c4e4d58cb9d4451b36e774b348e8813", "publicURL": "http://200.21.18.2:8776/v2/6c4e4d58cb9d4451b36e774b348e8813", "region": "RegionOne" } ], "endpoints_links": [], "name": "cinderv2", "type": "volumev2" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:9292", "id": "7f2f8036b0194ea0bd5231710b2cddf4", "internalURL": "http://200.21.18.2:9292", "publicURL": "http://200.21.18.2:9292", "region": "RegionOne" } ], "endpoints_links": [], "name": "glance", "type": "image" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813", "id": "054567bc62ce4b4fbdbdcd7c3a23748e", "internalURL": "http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813", "publicURL": "http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813", "region": "RegionOne" } ], "endpoints_links": [], "name": "nova_legacy", "type": "compute_legacy" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:8776/v1/6c4e4d58cb9d4451b36e774b348e8813", "id": "2eefe27748774693b635bf48f486f225", "internalURL": "http://200.21.18.2:8776/v1/6c4e4d58cb9d4451b36e774b348e8813", "publicURL": "http://200.21.18.2:8776/v1/6c4e4d58cb9d4451b36e774b348e8813", "region": "RegionOne" } ], "endpoints_links": [], "name": "cinder", "type": "volume" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:8773/", "id": "4d8f727748924cdf9d23591bad2bbd19", "internalURL": "http://200.21.18.2:8773/", "publicURL": "http://200.21.18.2:8773/", "region": "RegionOne" } ], "endpoints_links": [], "name": "ec2", "type": "ec2" }, { "endpoints": [ { "adminURL": "http://200.21.18.2:35357/v2.0", "id": "16e2a0df7fa64c8cbcdb5936e23b19cc", "internalURL": "http://200.21.18.2:5000/v2.0", "publicURL": "http://200.21.18.2:5000/v2.0", "region": "RegionOne" } ], "endpoints_links": [], "name": "keystone", "type": "identity" } ], "token": { "audit_ids": [ "4zrwvCd7TySk7jJKuO4G1Q" ], "expires": "2017-03-19T05:48:41Z", "id": "74e396f8202b481a9cbd95b319a4314b", "issued_at": "2017-03-19T04:48:42.002243", "tenant": { "description": "", "enabled": true, "id": "6c4e4d58cb9d4451b36e774b348e8813", "name": "admin" } }, "user": { "id": "135b2cb86962401c82044fd4ca9daae4", "name": "admin", "roles": [ { "name": "admin" } ], "roles_links": [], "username": "admin" } } } 需要注意的是, 这一步骤所获取的 Tenant token 是区别于 Temporary token 的, Temporary token 作为临时 token 是为了实现多租户的场景所提供的鉴权条件(外部鉴权). 而 Tenant token 才是联系不同 OpenStack Project 间的认证通行证(内部鉴权). 从这一步骤可以看出想要获取 Tenant token 就需要同时向 Keystone 提供账户信息和 tenant_id, 此时用户不仅得到了 Tenant token 还获取了相应的 endpoints list. 并且用户能够通过 endpints list 进一步的去访问注册在 Keystone 中的其他 OpenStack 组件. Step 4: 使用 Tenant token 和 tenant_id 获取 admin tenant 的虚拟机列表 curl -v -H "X-Auth-Token:74e396f8202b481a9cbd95b319a4314b" http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813/servers Response: { "servers": [ { "id": "138ecea2-1656-46bd-aefd-39449e11c356", "links": [ { "href": "http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813/servers/138ecea2-1656-46bd-aefd-39449e11c356", "rel": "self" }, { "href": "http://200.21.18.2:8774/6c4e4d58cb9d4451b36e774b348e8813/servers/138ecea2-1656-46bd-aefd-39449e11c356", "rel": "bookmark" } ], "name": "aju_test_dvs" }, { "id": "42da5d12-a470-4193-8410-0209c04f333a", "links": [ { "href": "http://200.21.18.2:8774/v2/6c4e4d58cb9d4451b36e774b348e8813/servers/42da5d12-a470-4193-8410-0209c04f333a", "rel": "self" }, { "href": "http://200.21.18.2:8774/6c4e4d58cb9d4451b36e774b348e8813/servers/42da5d12-a470-4193-8410-0209c04f333a", "rel": "bookmark" } ], "name": "TestVMwareInterface" } ] } 最终, 我们从 Response 中得到了 admin tenant 所具有的两台虚拟机的信息. 完整的 RESTAPI 请求流程图片如下: 显然, 使用 curl 请求 RESTAPI 的方式过于繁复, 不能满足用户对 OpenStack 多方位的应用需求(e.g. 实现 OpenStack 的自动化操作脚本). 对此, OpenStack 为用户提供了更高级别的 RESTAPI 调用封装 — OpenStackClients OpenStackClients (摘自 OpenStackClients 官方文档)Each OpenStack project has a related client project that includes Python API bindings and a CLI. 每一个 OpenStack 项目都具有一个包含了 Python API bindings 和 CLI 相关的 client 项目. 如图: 有图可见, OpenStackClients 项目主要实现了将 OpenStack 计算(Compute)、身份识别(Keystone)、镜像(Glance)、网络(Neutron)、对象存储(Swift)和卷存储(Cinder) 等核心组件所提供出来的 REST API 整合封装为具有统一指令结构的 CLI. 简而言之, 就是 OpenStackClients 项目使得用户能够通过 CLI 的形式调用以上组件提供的 REST API, 从而实现操作. 并且我们也可以从代码的层面直接导入 OpenStackClients, 更加便于开发者对 OpenStack 功能模块的调用. 使用 OpenStackClients 获取 project_client object 的 demo vim openstack_clients.py #!/usr/bin/env python #encoding=utf8 from openstackclient.identity.client import identity_client_v2 from keystoneclient import session as identity_session import glanceclient import novaclient.client as novaclient import cinderclient.client as cinderclient # 定义 project_client version NOVA_CLI_VER = 2 GLANCE_CLI_VER = 2 CINDER_CLI_VER = 2 class OpenstackClients(object): """Clients generator of openstack.""" def __init__(self, auth_url, username, password, tenant_name): ### Identity authentication via keystone v2 # An authentication plugin to authenticate the session with. # 通过身份验证信息获取 keystone 的 auth object # Keystoneclient v2 的详细使用介绍请浏览 https://docs.openstack.org/developer/python-keystoneclient/using-api-v2.html auth = identity_client_v2.v2_auth.Password( auth_url=auth_url, # http://200.21.18.3:35357/v2.0/ username=username, # admin password=password, # fanguiju tenant_name=tenant_name) # admin try: # 通过 auth object 获取 Keystone 的 session object self.session = identity_session.Session(auth=auth) except Exception as err: raise # Return a token as provided by the auth plugin. # 通过 session object 获取 Tenant token self.token = self.session.get_token() def get_glance_client(self, interface='public'): """Get the glance-client object.""" # Get an endpoint as provided by the auth plugin. # 默认获取 glance project 的 public endpoint glance_endpoint = self.session.get_endpoint(service_type="image", interface=interface) # Client for the OpenStack Images API. # 通过 glance endpoint 和 token 获取 glance_client object # 然后就可以使用 glance_client 调用其实例方法来实现对 glance project 的操作了 # glanceclient v2 所提供的实例方法列表请浏览 https://docs.openstack.org/developer/python-glanceclient/ref/v2/images.html glance_client = glanceclient.Client(GLANCE_CLI_VER, endpoint=glance_endpoint, token=self.token) return glance_client def get_nova_client(self): """Get the nova-client object.""" # Initialize client object based on given version. Don't need endpoint. # 也可以 不指定 endpoint 的类型, 仅使用 session object 来获取 nove_client # novaclient v2 的实例方法列表请浏览 https://docs.openstack.org/developer/python-novaclient/api.html#usage nova_client = novaclient.Client(NOVA_CLI_VER, session=self.session) return nova_client def get_cinder_client(self, interface='public'): """Get the cinder-client object.""" cinder_endpoint = self.session.get_endpoint(service_type='volume', interface=interface) # cinder_client v2 的实例方法列表请查看 https://docs.openstack.org/developer/python-cinderclient/ cinder_client = cinderclient.Client(CINDER_CLI_VER, session=self.session) return cinder_client 调用 project_client object 实例方法实现对 project 操作的 demo vim auto_dep.py #!/usr/bin/env python #encoding=utf8 import os from os import path import time import openstack_clients as os_cli # FIXME(Fan Guiju): Using oslo_config and logging AUTH_URL = 'http://200.21.18.3:35357/v2.0/' USERNAME = 'admin' PASSWORD = 'fanguiju' PROJECT_NAME = 'admin' DISK_FORMAT = 'qcow2' IMAGE_NAME = 'ubuntu_server_1404_x64' IMAGE_PATH = path.join(path.curdir, 'images', '.'.join([IMAGE_NAME, DISK_FORMAT])) MIN_DISK_SIZE_GB = 20 KEYPAIR_NAME = 'jmilkfan-keypair' KEYPAIT_PUB_PATH = '/home/stack/.ssh/id_rsa.pub' DB_NAME = 'blog' DB_USER = 'wordpress' DB_PASS = 'fanguiju' DB_BACKUP_SIZE = 5 DB_VOL_NAME = 'mysql-volume' DB_INSTANCE_NAME = 'AUTO-DEP-DB' MOUNT_POINT = '/dev/vdb' BLOG_INSTANCE_NAME = 'AUTO-DEP-BLOG' TIMEOUT = 60 class AutoDep(object): def __init__(self, auth_url, username, password, tenant_name): # 实例化上述的 openstack_client.OpenstackClients 的对象 openstack_clients = os_cli.OpenstackClients( auth_url, username, password, tenant_name) # 通过 openstack_clients 的实例方法获取 project_client 对象 self._glance = openstack_clients.get_glance_client() self._nova = openstack_clients.get_nova_client() self._cinder = openstack_clients.get_cinder_client() def _wait_for_done(self, objs, target_obj_name): """Wait for action done.""" count = 0 while count <= TIMEOUT: for obj in objs.list(): if obj.name == target_obj_name: return time.sleep(3) count += 3 raise def upload_image_to_glance(self): images = self._glance.images.list() for image in images: if image.name == IMAGE_NAME: return image # 调用 glanceclient.images.create method 创建一个 image object. new_image = self._glance.images.create(name=IMAGE_NAME, disk_format=DISK_FORMAT, container_format='bare', min_disk=MIN_DISK_SIZE_GB, visibility='public') # Open image file with read+binary. # 调用 glanceclient.images.upload method 上传一个 image self._glance.images.upload(new_image.id, open(IMAGE_PATH, 'rb')) self._wait_for_done(objs=self._glance.images, target_obj_name=IMAGE_NAME) image = self._glance.images.get(new_image.id) return image def create_volume(self): # 调用 cinderclient.volumes.list method 获取 volumes 的列表 volumes = self._cinder.volumes.list() for volume in volumes: if volume.name == DB_VOL_NAME: return volume # cinderclient.v2.volumes:VolumeManager # 调用 minderclient.volumes.create method 创建一个 volume new_volume = self._cinder.volumes.create( size=DB_BACKUP_SIZE, name=DB_VOL_NAME, volume_type='lvmdriver-1', availability_zone='nova', description='backup volume of mysql server.') if new_volume: return new_volume else: raise def get_flavor_id(self): # 调用 novaclient.flavors.list method 获取所有 flavors 的列表 flavors = self._nova.flavors.list() for flavor in flavors: if flavor.disk == MIN_DISK_SIZE_GB: return flavor.id def _get_ssh_pub_key(self): if not path.exists(KEYPAIT_PUB_PATH): raise return open(KEYPAIT_PUB_PATH, 'rb').read() def import_keypair_to_nova(self): # 调用 novaclient.keypairs.list method 获取 keypairs 的列表 keypairs = self._nova.keypairs.list() for keypair in keypairs: if keypair.name == KEYPAIR_NAME: return None keypair_pub = self._get_ssh_pub_key() # 调用 nova client.keypairs.create method 创建 keypair self._nova.keypairs.create(KEYPAIR_NAME, public_key=keypair_pub) def nova_boot(self, image, volume): flavor_id = self.get_flavor_id() self.import_keypair_to_nova() db_instance = False # 调用 novaclient.servers.list method 获取 servers 的列表 servers = self._nova.servers.list() server_names = [] for server in servers: server_names.append(server.name) if server.name == DB_INSTANCE_NAME: db_instance = server if not db_instance: # Create the mysql server db_script_path = path.join(path.curdir, 'scripts/db_server.txt') db_script = open(db_script_path, 'r').read() db_script = db_script.format(DB_NAME, DB_USER, DB_PASS) # 通过 nova client.servers.create method 创建一个 server # 这里因为希望创建 server 并对其进行预设置, 所以使用了 userdata 参数 # userdata 参数会接收一个 script 文件, 并在 server 第一次启动的时候执行 db_instance = self._nova.servers.create( # FIXME(Fan Guiju): Using the params `block_device_mapping` to attach the volume. DB_INSTANCE_NAME, image.id, flavor_id, key_name=KEYPAIR_NAME, userdata=db_script) # 通过 novaclient.server.get method 和 server_id 来获取单个 server 的详细信息 if not self._nova.server.get(db_instance.id): self._wait_for_done(objs=self._nova.servers, target_obj_name=DB_INSTANCE_NAME) # Attach the mysql-vol to mysql server, device type is `vd`. # 通过 cinderclient.volumes.attach method 挂在一个 volume 到 server 上 # mountpoint 参数执行了挂载到 server 的设备路径, e.g. /dev/vdb self._cinder.volumes.attach(volume=volume, instance_uuid=db_instance.id, mountpoint=MOUNT_POINT) time.sleep(5) if BLOG_INSTANCE_NAME not in server_names: # Create the wordpress blog server # Nova-Network db_instance_ip = self._nova.servers.\ get(db_instance.id).networks['private'][0] blog_script_path = path.join(path.curdir, 'scripts/blog_server.txt') blog_script = open(blog_script_path, 'r').read() blog_script = blog_script.format(DB_NAME, DB_USER, DB_PASS, db_instance_ip) self._nova.servers.create(BLOG_INSTANCE_NAME, image.id, flavor_id, key_name=KEYPAIR_NAME, userdata=blog_script) self._wait_for_done(objs=self._nova.servers, target_obj_name=BLOG_INSTANCE_NAME) servers = self._nova.servers.list(search_opts={'all_tenants': True}) return servers def main(): """FIXME(Fan Guiju): Operation manual.""" os.environ['LANG'] = 'en_US.UTF8' deploy = AutoDep(auth_url=AUTH_URL, username=USERNAME, password=PASSWORD, tenant_name=PROJECT_NAME) image = deploy.upload_image_to_glance() volume = deploy.create_volume() deploy.nova_boot(image, volume) if __name__ == '__main__': main() 最后 上面给出了一个自动化运行 OpenStack Project 功能模块的脚本, 但实际上, 我们能够使用 OpenStackClients 进行更加复杂的工作, 例如: 自定义一个新的 OpenStack Project, 并使之与 OpenStack 的原生 Project 进行互动, 这才是真正意义上的二次开发.
目录 目录 前文列表 扩展阅读 简介 基本概念 实现样例 最后 前文列表 Openstack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 Openstack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata Openstack 实现技术分解 (3) 开发工具 — VIM & dotfiles 扩展阅读 TaskFlow 代码库 TaskFlow 文档 简介 TaskFlow is a Python library that helps to make task execution easy, consistent and reliable. A library to do [jobs, tasks, flows] in a highly available, easy to understand and declarative manner (and more!) to be used with OpenStack and other projects. 简而言之, TaskFlow 能够控制应用程序中的长流程业务逻辑任务的暂停、重启、恢复以及回滚, 主要用于保证长流程任务执行的可靠性和一致性。 主要应用场景有如 Cinder 的 create volume 这般复杂、冗长、容易失败, 却又要求保持数据与环境一致的业务逻辑. 从 create volume 流程图看, Cinder 在 create_volume.py(cinder/volume/flows/manager/create_volume.py) 模块中定义了大量的 Tasks class 来组成 TaskFlow: OnFailureRescheduleTask ExtractVolumeRefTask ExtractVolumeSpecTask NotifyVolumeActionTask CreateVolumeFromSpecTask CreateVolumeOnFinishTask 如果在执行任务流的过程中失败了, TaskFlow 的回滚机制能够让程序流和执行环境回滚到初始状态, 并且可以重新开始执行. 总的来说, TaskFlow 非常适合于 面向任务 的应用场景. 基本概念 Atom: An atom is the smallest unit in TaskFlow which acts as the base for other classes Atom: Atom 是 TaskFlow 的最小单位, 其他的所有类, 包括 Task 类都是 Atom 类的子类. Task: A task (derived from an atom) is a unit of work that can have an execute & rollback sequence associated with it (they are nearly analogous to functions). Task: task 是拥有执行和回滚功能额最小工作单元. 在 Task 类中开发者能够自定义 execute(执行) 和 revert(回滚) method. Flow: Linear/Unordered/Graph Flow: 在 TaskFlow 中使用 flow(流) 来关联各个 Task, 并且规定这些 Task 之间的执行和回滚顺序. flow 中不仅能够包含 task 还能够嵌套 flow. 常见类型有以下几种: Linear(linear_flow.Flow): 线性流, 该类型 flow 中的 task/flow 按照加入的顺序来依次执行, 按照加入的倒序依次回滚. Unordered(unordered_flow.Flow): 无序流, 该类型 flow 中的 task/flow 可能按照任意的顺序来执行和回滚. Graph(graph_flow.Flow): 图流, 该类型 flow 中的 task/flow 按照显式指定的依赖关系或通过其间的 provides/requires 属性的隐含依赖关系来执行和回滚. Retry: A retry (derived from an atom) is a special unit of work that handles errors, controls flow execution and can (for example) retry other atoms with other parameters if needed. Retry: Retry 是一个控制当错误发生时, 如何进行重试的特殊工作单元, 而且当你需要的时候还能够以其他参数来重试执行别的 Atom 子类. 常见类型: AlwaysRevert: 错误发生时, 回滚子流 AlwaysRevertAll: 错误发生时, 回滚所有流 Times: 错误发生时, 重试子流 ForEach: 错误发生时, 为子流中的 Atom 提供一个新的值, 然后重试, 直到成功或 retry 中定义的值用完为止. ParameterizedForEach: 错误发生时, 从后台存储(由 store 参数提供)中获取重试的值, 然后重试, 直到成功或后台存储中的值用完为止. Engine: Engines are what really runs your atoms. Engine: Engines 才是真正运行 Atoms 的对象, 用于 load(载入) 一个 flow, 然后驱动这个 flow 中的 task/flow 开始运行. 我们可以通过 engine_conf 参数来指定不同的 engine 类型. 常见的 engine 类型如下: serial: 所有的 task 都在调用了 engine.run 的线程中运行. parallel: task 可以被调度到不同的线程中运行. worker-based: task 可以被调度到不同的 woker 中运行. 实现样例 源码请浏览 Github #!/usr/bin/env python #filename: tasks.py import taskflow.engines from taskflow.patterns import linear_flow as lt from taskflow import task from taskflow.types import failure as task_failed class CallJim(task.Task): default_provides = set(['jim_new_number']) def execute(self, jim_number, *args, **kwargs): print "Calling Jim %s." % jim_number print '=' * 10 jim_new_number = jim_number + 'new' return {'jim_new_number': jim_new_number} def revert(self, result, *args, **kwargs): if isinstance(result, task_failed.Failure): print "jim result" return None jim_new_number = result['jim_new_number'] print "Calling jim %s and apologizing." % jim_new_number class CallJoe(task.Task): default_provides = set(['joe_new_number', 'jim_new_number']) def execute(self, joe_number, jim_new_number, *args, **kwargs): print "Calling jim %s." % jim_new_number print "Calling Joe %s." % joe_number print '=' * 10 joe_new_number = joe_number + 'new' return {'jim_new_number': jim_new_number, 'joe_new_number': joe_new_number} def revert(self, result, *args, **kwargs): if isinstance(result, task_failed.Failure): print "joe result" return None jim_new_number = result['jim_new_number'] joe_new_number = result['joe_new_number'] print "Calling joe %s and apologizing." % joe_new_number class CallJmilkFan(task.Task): default_provides = set(['new_numbers']) def execute(self, jim_new_number, joe_new_number, jmilkfan_number, *args, **kwargs): print "Calling jim %s" % jim_new_number print "Calling joe %s" % joe_new_number print "Calling jmilkfan %s" % jmilkfan_number print '=' * 10 jmilkfan_new_number = jmilkfan_number + 'new' raise ValueError('Error') new_numbers = {'jim_new_number': jim_new_number, 'joe_new_number': joe_new_number, 'jmilkfan_new_number': jmilkfan_new_number} return {'new_numbers': new_numbers} def revert(self, result, *args, **kwargs): if isinstance(result, task_failed.Failure): print "jmilkfan result" return None jim_new_number = result['jim_new_number'] joe_new_number = result['joe_new_number'] jmilkfan_new_number = result['jmilkfan_new_number'] print "Calling jmilkfan %s and apologizing." % jmilkfan_new_number def get_flow(flow, numbers): flow_name = flow flow_api = lt.Flow(flow_name) flow_api.add(CallJim(), CallJoe(), CallJmilkFan()) return taskflow.engines.load(flow_api, engine_conf={'engine': 'serial'}, store=numbers) def main(): numbers = {'jim_number': '1'*6, 'joe_number': '2'*6, 'jmilkfan_number': '3'*6} try: flow_engine = get_flow(flow='taskflow-demo', numbers=numbers) flow_engine.run() except Exception: print "TaskFlow Failed!" raise new_numbers = flow_engine.storage.fetch('new_numbers') if __name__ == '__main__': main() Output: fanguiju@fanguiju:~/project/my-code-repertory/TaskFlow-demo$ python tasks.py Calling Jim 111111. ========== Calling jim 111111new. Calling Joe 222222. ========== Calling jim 111111new Calling joe 222222new Calling jmilkfan 333333 ========== jmilkfan result Calling joe 222222new and apologizing. Calling jim 111111new and apologizing. TaskFlow Failed! Traceback (most recent call last): File "tasks.py", line 114, in <module> main() File "tasks.py", line 105, in main flow_engine.run() File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/engine.py", line 159, in run for _state in self.run_iter(): File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/engine.py", line 223, in run_iter failure.Failure.reraise_if_any(it) File "/usr/local/lib/python2.7/dist-packages/taskflow/types/failure.py", line 292, in reraise_if_any failures[0].reraise() File "/usr/local/lib/python2.7/dist-packages/taskflow/types/failure.py", line 299, in reraise six.reraise(*self._exc_info) File "/usr/local/lib/python2.7/dist-packages/taskflow/engines/action_engine/executor.py", line 82, in _execute_task result = task.execute(**arguments) File "tasks.py", line 66, in execute raise ValueError('Error') ValueError: Error NOTE 1: 在 function get_flow 中使用 linear_flow.Flow 生成一个 TaskFlow(线性任务流) 对象 flow_api , 再通过flow_api.add method 添加要 顺序执行且倒序回滚 的 Task class(CallJim/CallJom/CallJmilkFan). NOTE 2: 使用 taskflow.engines.load method 来加载 TaskFlow(flow_api)对象/后台存储数据(store)/ engine配置 等信息并生成 Task Engine 对象. NOTE 3: 最后调用 Task Engine 对象的 flow_engine.run method 来开始执行该任务流. NOTE 4: 后台存储 store 的数据在该任务流中被所有 Task class 共享, 并且以 Task class 中的 execute method 的形参作为对接入口. e.g. 上述实现的 store 后台存储中含有 {jim_number: '1'*6}, 那么 CallJim 的 execute method 就可以通过形参 jim_number 来获取 '1'*6 的值. NOTE 5: Task class 的属性 default_provides 用于声明在执行过程中新添到后台存储的元素的名称, 其相应的值会自动的从 execute method 返回值中匹配获取, 最终存储后台存储. e.g. CallJim 的属性 default_provides = set(['jim_new_number']) 其中 jim_new_number 的值会从 execute method 的返回 return {'jim_new_number': jim_new_number} 中获取. NOTE 6: provides 的实现能够有效的帮助传递 Task class 之间在执行时产生的新属性对象. 将上一个 Task 的结果传递给后一个 Task 使用. 最后 当实现的 TaskFlow 中包含了多个 Task(的确可能存在只有一个 Task 的 TaskFlow) 时, 有两点是需要注意的: 在使用线性流类型的 TaskFlow 时, Task class revert method 回滚的应该是上一个 Task class execute method 的业务. e.g. BTask.revert 应该回滚 ATask.execute, 因为只有在 ATask.execute 成功执行的前提之下才有 revert 的价值. 所以在 revert method 的定义中需要实现语句 if isinstance(result, task_failed.Failure): return None. 当一个 Task 的 execute method 执行失败时, 那么 revert method 接收的 result 实参就是 taskflow.types.failure.Failure 的实例对象. 尽量让每个 Task class 都仅处理一件事情, 这是为了让每一次回滚都足够精准. e.g. 尽管创建虚拟机和开启虚拟机都同属于对虚拟机的操作, 但是我们仍然应该将两者各自定义一个 Task class. 假如启动虚拟机失败时, 我们只需再次重试启动虚拟机, 而无须再次重复创建虚拟机.
目录 目录 HTTP 协议 HTTP 协议工作原理 HTTP Request 请求行 Request Header HTTP Response 状态行 Response Header Body HTTP 协议 HTTP(Hyper Text Transfer Protocol 超文本传输协议), 是基于 TCP/IP 通信协议来实现数据传递的应用层协议. 用于 www 万维网服务器(Server-Side) 与 本地浏览器(Client-Side) 之间传输超文本的传输协议. 又因为 TCP/IP 协议是一个端到端的面向连接的协议, 所谓的端到端可以理解为进程到进程之间的连接, 所以 HTTP 协议在开始传输数据之前, 首先需要建立一个 TCP 连接, 而 TCP 连接的过程需要 三次握手. 在 TCP 三次握手之后, 成功建立了 TCP 连接, 此后 HTTP 协议就可以进行数据传输了. HTTP 协议工作原理 Step 1: Client-Side 与 Server-Side 建立一个 TCP 套接字连接. Step 2:Client-Side 通过 TCP 套接字向 Sever-Side 发送 HTTP Request(请求报文). Step 3: Server-Side 接收并解析 HTTP Request 之后执行事物并返回 HTTP Response. Step 4: 释放 TCP 连接, 若 connection mode 为 close, 则 Server-Side 主动关闭 TCP 连接, Client-Side 被动关闭连接, 最后释放 TCP 连接. 若 connection mode 为 keepalive, 则该连接会保持一段时间, 在该时间内 Server-Side 可以继续接收请求. HTTP Request HTTP Request 请求行 + Request_Header + Body 组成: 请求行 用于说明请求类型, 要访问的资源以及所使用的HTTP版本. 格式: Method Request-URI HTTP-Version <CR><LF> Method: HTTP Method Request-URI: 统一资源标识符 HTTP-Version: 表示请求的HTTP协议版本 : 表示回车和换行符(\r\n), 请求行必须由换行符结尾 其中 HTTP Method 有下列几种类型: GET: (获取) 请求获取 Request-URI 标识的资源 POST: (创建) 请求在 Request-URI 标识的资源添加新的数据 PUT: (更新) 请求向 Request-URI 标识的资源上传其最新内容 DELETE: (删除) 请求删除 Request-URI 标识的资源 HEAD: 请求获取 Request-URI 标识的资源的 Response-Header TRACE: 请求服务器回送请求信息, 一般用于测试或诊断 OPTIONS: 请求获取服务器的性能参数, 或者查询与资源相关的选项 CONNECT: 保留将来使用 因为这些 HTTP 协议提供了多种 Method, 所以 HTTP 协议除了作为传输协议之外, 还被作为应用协议. Request Header Request Header(请求报头) 是 HTTP Header 的其中一种类型, 用于指定服务器接受的附加信息, 由由若干个请求报头域键值对组成, 报头域的格式为 报头域名: 值 . 下面列出常用的请求报头域: Host: 指定服务器的主机和端口号信息, 发送请求时, 该请求报头域是必需的 Authorization: 请求服务器鉴权, 如果服务器的响应代码为 401 未授权, 那么可以发送一个含有 Authorization 请求报头域的请求, 要求服务器对客户端进行鉴权验证. Accept: 指定客户端接受的响应信息数据类型, E.G. 'Accept': 'application/json' 指定接受 JSON 格式数据 Accept-Charset: 指定客户端接受的响应信息字符集类型, E.G. Accept-Charset:iso-8859-1,gb2312,utf8 Accept-Encoding: 指定客户端接受的内容压缩类型, E.G. Accept-Encoding:gzip.deflate Accept-Language: 指定客户端接受的自然语言类型, E.G. eg:Accept-Language:zh-cn User-Agent: 将客户端操作系统、浏览器和其它本地属性传入服务器 Cache-Control:指定请求和响应遵循的缓存机制 Connection: 指定 TCP 连接模式 Cookie: 最重要的请求头之一, 将 cookie 发送给服务器 EXAMPLE: GET /562f25980001b1b106000338.jpg HTTP/1.1 Host img.mukewang.com User-Agent Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 Accept image/webp,image/*,*/*;q=0.8 Referer http://www.imooc.com/ Accept-Encoding gzip, deflate, sdch Accept-Language zh-CN,zh;q=0.8 HTTP Response HTTP Response 由 状态行 + Response_Header + Body 组成: 状态行 格式: HTTP-Version Status-Code Reason-Phrase <CR><LF> HTTP-Version: 服务器 HTTP 协议版本 Status-Code: 服务器发回的响应状态码 Reason-Phrase: 状态码的文本描述 : 状态行也必须以换行符结尾 其中由服务器响应的状态码分为 5 大类型: 1xx: (指示信息) 表示请求已接收,继续处理 2xx: (成功) 表示请求已被成功接收、理解、接受 3xx: (重定向) 要完成请求必须进行更进一步的操作 4xx: (客户端错误) 请求有语法错误或请求无法实现 5xx: (服务器端错误) 服务器未能实现合法的请求 更多状态码 Response Header 用来指定客户端接收的的附加信息. Server: 包含了服务器处理请求的软件环境信息 Allow: 服务器支持哪些 HTTP Method Set-Cookie:最重要的响应报头域之一, 用于把 cookie 发送到客户端, 每一个写入 cookie 都会生成一个 Set-Cookie, E.G. Set-Cookie: sc=4c31523a; path=/; domain=.acookie.taobao.com Location: 用于重定向到一个新的位置, 包含新的URL地址 EXAMPLE 1: EXAMPLE 2: HTTP/1.0 200 OK Content-Type: text/plain Content-Length: 137582 Expires: Thu, 05 Dec 1997 16:00:00 GMT Last-Modified: Wed, 5 August 1996 15:55:28 GMT Server: Apache 0.84 <html> <body>Hello World</body> </html> Body 在客户端发送 Request 或服务器响应 Response 时都可以传输一个 Body(实体), 其由 实体报头域 + 实体正文[可选]组成, 其中实体报头域用于定义了实体正文. 常用的实体报头域有下列几种类型: Content-Type: 指定了发送给接收者的实体正文的媒体格式类型(MIME type), E.G. 'Content-Type': 'application/json'/Content-Type:text/html;charset=GB2312 Content-Length: 指定了实体正文的长度, 以十进制数字表示 Content-Encoding: 指定了附加在实体正文上的附加内容的压缩类型, E.G. Content-Encoding:gzip Content-Language: 描述了资源所使用的自然语言 Expires: 指定了响应过期的日期和时间, 以此更新缓存数据, E.G. Expires:Thu,15 Sep 2006 16:23:12 GMT Last-Modified: 描述了资源的最后修改日期和时间 NOTE: 因为无论 Request 或者 Response 都可能发送 Body, 所以上述的实体报头域是通用的.
目录 目录 cookie session token cookie Web Application 一般以 HTTP 协议作为传输协议, 但 HTTP 协议是无状态的. 也就是说 server-side 与 client-side 一旦数据交换完毕后,两者之间的连接就会被关闭. client-side 再次发送请求时, 需要建立新的连接, 这就意味着 server-side 和 client-side 两者之间无法通过 HTTP 的连接来实现 会话跟踪. 显然, 这是不合理的, 因为这样无法保证完成一次 Web Application 业务流程中所产生的若干次 请求/响应 操作的原子性, 从而会导致业务逻辑混乱. cookie 就是为了解决这一问题所引入的 会话跟踪机制. 实现原理: 由 server-side 为 client-side 发放一张通行证, 并以此来认证 client-side 的身份(出于安全性的考虑, 这张通行证一般是临时的). 而这些通行证就是 cookie, 本质上 cookie 就是一小段文本信息, 里面包含了有如 session_id/login-status/token 等认证相关数据. session 基于 session 的用户认证借助于请求体对象 req 中的 session 数据来完成. 实现原理: 当 client-side 请求 server-side 并通过身份认证后, server-side 就会生成并保存身份认证相关的 session 数据, 并将对应的 sesssion_id 写入 cookie 然后再响应到 client-side, client-side 会把 cookie 文件保存在本地. 此后, client-side 的所有请求都会附带该 session_id, 以确定 server-side 是否存在对应的 session 数据以及检验 session 数据中的 login-status. 如果存在且 login-status 为 True, 则证明 client-side 是被信任的, 无须再次认证身份. 否则, 需要重新进入身份认证流程. 缺点: server-side 保存 session 数据会增加运维和存储开销 因为一个 session_id 只能被保存有对应 session 数据的 server-side 完成认证, 所以在拥有多台 server-side 集群架构的场景中会降低其扩展性. 如果原生 App 不具备 cookie 功能模块, 就会加大其接入 session 认证后端的难度. 简而言之, session 有如用户信息档案表, 里面包含了用户的认证信息和登录状态等信息. 而 cookie 就是用户通行证. token token(令牌) 由 uid+time+sign[+固定参数] 组成: uid: 用户唯一身份标识 time: 当前时间的时间戳 sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接 固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库 由其组成可以看出, token 的认证方式类似于临时的证书签名, 并且是一种 server-side 无状态的认证方式, 非常适合于 REST API 的场景. 所谓无状态就是 server-side 并不会保存身份认证相关的数据, token 只被保存在 client-side 中的 cookie 或 localstorage(数据库). 实现原理: 当 client-side 发送请求 server-side 并完成身份认证后, server-side 会生成但不保存一个 token, 而是将 token 以 cookie 或其他形式响应给 client-side. 此后 client-side 发送请求时都会在 Request-Header 中附带 token, server-side 收到 token 后再分发给其他的 身份认证服务 负责处理认证相关的业务. E.G. Openstack 中的 Keystone 项目会为整个云平台提供身份认证服务. 缺点: 因为 token 一般都是 hash/encrypt 的字符串, 所以会额外附加 加密/解密 的性能开销 有些加密方式同样存在安全隐患
目录 目录 前文列表 H3C CAS CVK Cloud Virtualization Kernel 虚拟化内核平台 CVMCloud Virtualization Manager 虚拟化管理系统 CICCloud Intelligence Center 云业务管理中心 SSVUser Self-Service Portal 用户自助服务门户 服务器类型 安装文档 前文列表 H3C CAS 介绍 & 基本概念 H3C CAS H3C CAS(Cloud Automation System) 云计算管理平台是H3C公司推出的构建云计算基础架构的资源管理平台, 它为数据中心云计算基础架构提供虚拟化管理解决方案, 实现对数据中心云计算环境的集中管理和控制. CVK (Cloud Virtualization Kernel 虚拟化内核平台) 运行在基础设施层和上层客户操作系统之间的虚拟化内核软件, 针对上层客户操作系统对底层硬件资源的访问, CVK用于屏蔽底层异构硬件之间的差异性, 消除上层客户操作系统对硬件设备以及驱动的依赖. 类似于 RHEV-H 端, 作为 VMM Hypervisor 层, 工作在 Host 之上. CVM(Cloud Virtualization Manager 虚拟化管理系统) 主要实现对数据中心内的 计算/网络/存储 等硬件资源的软件虚拟化管理, 对上层应用提供自动化服务. 安装CVM后, 可将计算、网络、存储进行虚拟化集中统一管理, 并通过集群的高可靠性和动态资源调度功能、虚拟机的容灾与备份功能来确保数据中心业务的连续性. 类似于 RHEV-M 端, 作为 CVK 的集中管理和调度平台. CIC(Cloud Intelligence Center 云业务管理中心) 由一系列云基础业务模块组成, 将基础架构资源 计算/存储/网络 及其相关策略整合成虚拟数据中心资源池, 以组织(虚拟数据中心)的形式对外提供服务, 并允许用户按需消费这些资源, 从而构建安全的多租户混合云. 同时支持开放式的 REST API(Application Programming Interface,应用程序编程接口),确保云平台之间的互操作性. 是云平台应用业务的调度层, 可以通过 REST API 来调度其功能模块. SSV(User Self-Service Portal 用户自助服务门户) 通过云业务电子流的方式申请用户所需的云资源, 构建以业务为中心的”IT即服务”模型. 借助自助服务门户, 用户可以便捷地按需申请云主机、云硬盘、云网络等资源, 并通过远程连接协议(RDP或VNC)访问自己专属的远程桌面. 服务器类型 类型 具体用途 需安装的CAS组件 业务服务器 实施服务器虚拟化的物理载体,支撑数据中心中虚拟机的运行,虚拟机上承载了所有业务。 CVK 虚拟化管理服务器 实现对数据中心内的计算、存储和网络等硬件资源的软件虚拟化管理。 CVK/CVM 云业务管理服务器 整合虚拟数据中心资源池,以组织的形式对外提供服务,允许用户按需申请虚拟资源,构建安全的多租户混合云。 CVK/CIC/SSV 若只需实现服务器虚拟化管理的功能,则需要安装业务服务器和虚拟化管理服务器。 若需实现云业务管理的功能,则需要安装业务服务器、虚拟化管理服务器和云业务管理服务器。 虚拟化管理服务器和云业务管理服务器可以安装在同一台物理主机上,也可以安装在不同物理主机上,用户可以根据实际网络环境的硬件资源来进行部署。 安装文档 H3C CAS Software 已经继承到了 Ubuntu server 中, 所以安装 CAS 的过程非常简单, 无异于安装一个操作系统. 准备工作: 到官网下载安装镜像文件 H3C_CAS-E0306.rar, 内含 H3C_CAS-E0306.iso. 使用 iso 文件制作启动光盘 部署过程请下载并查阅 H3C_CAS安装指导V1.0.docx
目录 目录 基本概念 H3C CAS 中的虚拟机 虚拟机中的虚拟设备 虚拟 CPU 的 3 种工作模式 虚拟网卡的 3 种类型 虚拟磁盘的三种类型 虚拟机辅助工具 CAS Tools 虚拟机外的虚拟设备 虚拟交换机 管理虚拟机 基本概念 H3C CAS(云计算管理平台): 是面向数据中心推出的 虚拟化和云计算 管理软件. 包含了下列 4 大组件: SSV(用户自助服务门户): 构建以业务为中心的 “IT即服务” 模型. CIC(云业务管理中心): 将基础架构资源及其相关策略整合成数据中心资源池并按需计费, 是一个多租户的混合云架构. CVK(云虚拟化内核): 物理硬件之上的虚拟化 Kernel, 消除了上层虚拟机对硬件和驱动的依赖, 提高兼容性. CVM(云虚拟化管理系统): 对 DC 中含括的 计算/网络/存储 等硬件资源的软件虚拟化管理. H3C CAS 中的虚拟机 虚拟机中的虚拟设备 虚拟 CPU 的 3 种工作模式 custom: qemu 虚拟出的 CPU, CAS 缺省使用该模式, CPU 缺省为 qemu64 类型, 兼容性好, 但因支持的指令少,导致某些性能低(E.G. aes 加密等). host-model: 启动虚拟机前, 从 qemu 能够模拟出的 CPU 类型中选择一个和主机 CPU 最接近的型号, 迁移兼容性较差, amd 和 intel 之间不能迁移. host-passthrough: 直接透传主机 CPU 型号和大部分功能给虚拟机, 迁移兼容性差, 只有在 CPU 特性完全相同的主机间才可以迁移, 同一厂商的不同代 CPU 间也不能迁移. 虚拟网卡的 3 种类型 普通网卡 Intel1000 网卡 VirtIO 网卡: 是 CVK 虚拟化内核的软驱网卡, 可以提高虚拟机的网卡性能. Kernel 版本 2.6.25 以上的 Linux 系统默认安装了 VirtIO 驱动, 而 Windows 系统需要使用 CASTools 来安装 VirtIO 驱动. 虚拟磁盘的三种类型 IDE 磁盘 USB 磁盘: USB 磁盘使用 USB 协议, 磁盘效率低下. SCSI 磁盘: Windows 系统默认没有 SCSI 磁盘驱动, 需要授权购买, 因此不建议使用 SCSI 磁盘. VirtIO 磁盘: Virtio 磁盘是经过优化的磁盘类型, 提高了磁盘的 I/O 性能, 其 I/O 性能优于 IDE. 虚拟机辅助工具 CAS Tools CAS Tools 作为 CAS CVM 的扩展, 能够获取虚拟机的 CPU使用情况/内存使用情况/操作系统类型 等信息. CAS Tools 包含在 CAS 安装光盘中, 安装完 CAS 后, 自动将 castools.iso 拷贝到 /vms/isos/路径下. 该工具实现对虚拟机的控制与状态监控, 需要在虚拟机上额外安装该工具程序, 其在 Windows 中表现为一个 service, 在 Linux 中表现为一个 daemon, 与 CVM 之间通过 libvirtd 进行通信. 虚拟机外的虚拟设备 虚拟交换机 用于完成虚拟机与外部网络之间、虚拟机与虚拟机之间的流量交换. 同一虚拟交换机可绑定多个不同的物理主机网卡做聚合,可自主选择负载均衡或者主备模式. 管理虚拟机 批量操作管理虚拟机 修改单个虚拟机: 能够修改 CPU/内存/网络/存储/网卡/Boot设备/加载驱动/USB/光驱 链接克隆(一级软只读磁盘/二三级可读写硬磁盘): 链接克隆指的是对源虚拟机克隆时, 生成空白的磁盘文件, 并通过指针指向源虚拟机磁盘文件, 使用增量磁盘数据机制. 链接克隆虚拟机的克隆: 对链接克隆产生的虚拟机进行克隆操作, 能够选择快速克隆或完全克隆. 链接克隆虚拟机的模板操作: 对链接克隆产生的虚拟机克隆/转换为模板操作时, 会把一级、二级、三级磁盘镜像文件进行合并. 虚拟机模板: 克隆为模板: 原虚拟机不变,生成单独的模板文件 转换为模板: 转换后原虚拟机不再存在 虚拟机备份: 全量备份 增量备份: 备份上一次备份之后发生变化的数据 差异备份: 备份上一次全量备份后发生变化的数据 虚拟机快照: 快照配置文件: 记录原虚拟机所有配置及快照时间 快照内容: 存放在原虚拟机的磁盘文件内 虚拟机迁移: 能够选择迁移主机或数据存储 虚拟机规则: 虚拟机规则是集群中虚拟机 运行位置 的一种策略配置
目录 目录 前文列表 扩展阅读 前言 插件管理 Vundle 主题 Solarized 浏览项目目录结构 Nerdtree Symbol 窗口 Tagbar 文件模糊查询 CtrlP 代码补全 YouCompleteMe 语法检查 Syntastic 通用配置 dotfiles 前文列表 Openstack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 Openstack 实现技术分解 (2) 虚拟机初始化工具 — Cloud-Init & metadata & userdata 扩展阅读 跟我一起学习VIM - vim插件合集 很全面的vimrc配置技巧 VIM set 指令 前言 VIM is the God of editors, EMACS is God’s editor, 这是一句非常经典的话, 可以看出 VIM 在 editors 圈的地位. 首先需要声明的是, 本人不参与任何 IDE 战争, IDE 的本质追求是提高开发效率, 能够称心如意撸代码就是你最好的选择. 但就 Openstack 开发而言, 我仍会极力推荐使用 VIM, 因为绝大多数的 Openstack 线上生产环境是极其严酷的, 不会纵容你安装和使用重量级 IDE. 那么如何能够快速搭建或者说同步自己的 VIM 编程环境到其他机器上呢? VIM + dotfiles 就是最佳的组合. 在正文之前先放张 VIM 的快捷键一览图, 大家不妨打印出来贴在自己工位上 : ) 插件管理 Vundle Vundle is short for Vim bundle and is a Vim plugin manager. 现在所统计的 VIM 扩展插件多达 4900 多种, 基本上能够很好的满足开发者们各种各样奇葩的要求. 同时, 如何友好的将这些插件应用到自己的开发环境中成为了刚需求. Vundle 就是为此而生的一个 VIM 插件管理工具. 在介绍如何使用 Vundle 之前, 还需要了解一个文件 .vimrc . .vimrc 是 VIM 的配置文件, 绝对路径为 ~/.vimrc, 是整个 VIM 的灵魂, 拥有非常强大的自定义能力. Vundle 首先会读取 .vimrc 中以关键字 Plugin 开始的语句, 这条语句的值实际上就是插件项目在 Github 上的名称, 然后再实现对插件的 安装/卸载/更新 . Set up Vundle by manual: git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim NOTE: 当然你也可以通过修改 .vimrc 来实现自动安装 Vundle Configure Plugins: 这里给出 Vundle 的官方配置样例 set nocompatible " 关闭兼容 vi 模式, 必须 filetype off " 必须 " 指定 Vundle 的源码路径 " set the runtime path to include Vundle and initialize set rtp+=~/.vim/bundle/Vundle.vim call vundle#begin() " alternatively, pass a path where Vundle should install plugins "call vundle#begin('~/some/path/here') " let Vundle manage Vundle 安装 Vundle 插件 Plugin 'VundleVim/Vundle.vim' " 下列列出了你所希望管理的 VIM 插件的样例 " The following are examples of different formats supported. " Keep Plugin commands between vundle#begin/end. " plugin on GitHub repo Plugin 'tpope/vim-fugitive' " plugin from http://vim-scripts.org/vim/scripts.html Plugin 'L9' " Git plugin not hosted on GitHub Plugin 'git://git.wincent.com/command-t.git' " git repos on your local machine (i.e. when working on your own plugin) Plugin 'file:///home/gmarik/path/to/plugin' " The sparkup vim script is in a subdirectory of this repo called vim. " Pass the path to set the runtimepath properly. Plugin 'rstacruz/sparkup', {'rtp': 'vim/'} " Install L9 and avoid a Naming conflict if you've already installed a " different version somewhere else. Plugin 'ascenator/L9', {'name': 'newL9'} " All of your Plugins must be added before the following line call vundle#end() " 必须 filetype plugin indent on " 开启插件, 必须 " To ignore plugin indent changes, instead use: "filetype plugin on " 插件管理类型 " Brief help " :PluginList - lists configured plugins " :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate " :PluginSearch foo - searches for foo; append `!` to refresh local cache " :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal " 查看帮助手册 " see :h vundle for more details or wiki for FAQ " Put your non-Plugin stuff after this line NOTE: 可以看出在语句 call vundle#begin() 和 call vundle#end() 之间就定义了需要安装的插件列表. Install Plugins: 在定义好需要安装的插件列表之后, 只需要执行下面的指令就可以自动的完成所有插件的安装. vim +PluginInstall +qall 当然了, 在安装这些插件之前, 我们首先需要知道那些插件是做什么用的, 是否适合自己. 下面继续推荐几个常用的 VIM 插件, 不妨在之后再进行安装. 主题 Solarized Solarized 具有阴阳(light/dark)两种风格鲜明的主题和灵活的自定义配色能力, 是最受欢迎的主题插件之一. 安装它只需要对 .vimrc 进行如下编辑: Installation: ... " 添加 Solarized 主题插件 Plugin 'altercation/vim-colors-solarized' ... " Solarized 配置 " Solarized ================================================= syntax enable set background=dark " 使用阴主题 let g:solarized_termcolors=16 let g:solarized_visibility='high' let g:solarized_contrast='high' try colorscheme solarized " 设定配色方案 catch /^Vim\%((\a\+)\)\=:E185/ endtry NOTE 1: 上文已经提到了, 表示插件的 Plugin 'altercation/vim-colors-solarized' 配置语句必须放在call vundle#begin() 和 call vundle#end() 之间, 下面所有的插件同理, 所以不在赘述. NOTE 2: 这里使用了阴主题 dark, 阳主题的值为 light. 微调你喜欢的 Terminal 配色 效果: 浏览项目目录结构 Nerdtree Nerdtree 提供的项目目录结构浏览功能, 极大的加强了开发者对整个项目目录结构的辨识和把控. Installation: ... Plugin 'scrooloose/nerdtree' ... " NERD Tree ================================================= let NERDChristmasTree=0 let NERDTreeWinSize=35 let NERDTreeChDirMode=2 let NERDTreeIgnore=['\~$', '\.pyc$', '\.swp$'] let NERDTreeShowBookmarks=1 let NERDTreeWinPos="left" " Automatically open a NERDTree if no files where specified autocmd vimenter * if !argc() | NERDTree | endif " Close vim if the only window left open is a NERDTree autocmd bufenter * if (winnr("$") == 1 && exists("b:NERDTreeType") && b:NERDTreeType == "primary") | q | endif " Open a NERDTree nmap <F2> :NERDTreeToggle<CR> NOTE 1: 当关闭最后一个文件界面时会同时退出 Nerd tree, 避免多输入一个 :q NOTE 2: 设置了快捷键 <F2> 来 Open/Close Nerd tree 效果: Symbol 窗口 Tagbar Symbol 窗口列出了当前文件中的 宏/全局变量/函数/类 的信息, 使用光标选择就能够跳转相应源代码的位置, 非常便捷. Installation: ... Plugin 'majutsushi/tagbar' ... " Tagbar ================================================= let g:tagbar_width=35 let g:tagbar_autofocus=1 nmap <F3> :TagbarToggle<CR> NOTE: 这里使用了快捷键 <F3> 来 Open/Close Tagbar 安装 ctags 因各人环境不同, 可能需要手动安装 ctags sudo apt-get install exuberant-ctags 效果 文件模糊查询 CtrlP CtrlP 文件模糊查询插件, 又一大杀器, 让你在项目的文件海中自由穿梭. Installation: ... Plugin 'kien/ctrlp.vim' ... " Ctrlp ================================================= set wildignore+=*/tmp/*,*.so,*.swp,*.zip,*.png,*.jpg,*.jpeg,*.gif " Ignore for MacOSX/Linux let g:ctrlp_custom_ignore = { \ 'dir': '\v[\/]\.(git|hg|svn|rvm)$', \ 'file': '\v\.(exe|so|dll|zip|tar|tar.gz|pyc)$', \ } let g:ctrlp_match_window = 'bottom,order:btt,min:1,max:10,results:20' let g:ctrlp_max_height = 30 "let g:ctrlp_user_command = [ " \ '.git', 'cd %s && git ls-files . -co --exclude-standard', " \ 'find %s -type f' " \ ] if executable('ag') " Use Ag over Grep set grepprg=ag\ --nogroup\ --nocolor " Use ag in CtrlP for listing files. let g:ctrlp_user_command = 'ag %s -l --nocolor -g ""' " Ag is fast enough that CtrlP doesn't need to cache let g:ctrlp_use_caching = 0 endif let g:ctrlp_working_path_mode=0 let g:ctrlp_match_window_bottom=1 let g:ctrlp_max_height=15 let g:ctrlp_match_window_reversed=0 let g:ctrlp_mruf_max=500 let g:ctrlp_follow_symlinks=1 let g:ctrlp_map = '<leader>p' let g:ctrlp_cmd = 'CtrlP' nmap <leader>f :CtrlPMRU<CR> NOTE 1: 这里使用了 ag 搜索来代替 find 指令搜索, 更加高效. NOTE 2: 设置了 leader+f 快捷键来 Open/Close CtrlP NOTE 3: leader 键类似于 Home 键, 是组合快捷键的基础, 一般设置为 , 号, 后文会给出该键的设置方法. 效果 代码补全 YouCompleteMe 代码补全必备插件. Installation: ... Plugin 'Valloric/YouCompleteMe' ... " YouCompleteMe ================================================= let g:ycm_autoclose_preview_window_after_completion=1 NOTE 1: 完成补全之后自动关闭预览窗口 语法检查 Syntastic Installation: ... Plugin 'scrooloose/syntastic' ... " Syntastic ================================================= " configure syntastic syntax checking to check on open as well as save let g:syntastic_check_on_open=1 let g:syntastic_html_tidy_ignore_errors=[" proprietary attribute \"ng-"] let g:syntastic_always_populate_loc_list = 1 let g:syntastic_auto_loc_list = 1 let g:syntastic_check_on_wq = 0 set statusline+=%#warningmsg# set statusline+=%{SyntasticStatuslineFlag()} set statusline+=%* 官方效果图 通用配置 VIM 的通用配置数不胜数, 这里列出常见的一些作为参考. " General Config ================================================= set nocompatible " be iMproved, required filetype off " required set number " 显示行号 set ruler " 打开状态栏标尺 set backspace=indent,eol,start " Allow backspace in insert mode set fileencodings=utf-8,gbk " Set encoding of files set history=1000 " Number of things to remember in history set showcmd " Show incomplete cmds down the bottom set showmode " Show current mode down the bottom set showmatch " 输入 )/} 时,光标会暂时的回到相匹配的 (/{ set gcr=a:blinkon0 " Disable cursor blink set novisualbell " No sounds set noerrorbells " No noise set autoread " Reload files changed outside vim set laststatus=2 " 显示状态栏 set statusline+=%{fugitive#statusline()} " Git Hotness set list listchars=tab:>.,trail:. " Display tabs and trailing spaces visually set linebreak " Wrap lines at convenient points set nobackup set nowb set tabstop=4 set shiftwidth=4 set textwidth=80 " Make it obvious where 80 characters is highlight ColorColumn ctermbg=gray set colorcolumn=80 set numberwidth=4 set fileformat=unix set expandtab set t_Co=256 set list "set ignorecase set incsearch " 输入搜索内容时就显示搜索结果 au WinLeave * set nocursorline nocursorcolumn " Highlight current line au WinEnter * set cursorline cursorcolumn set cursorline cursorcolumn " 突出当前行和列 " Persistent Undo set undodir=~/.vim/backups set undofile " Search Options set incsearch " Find the next match as we type the search set hlsearch " 搜索时高亮显示被找到的文本 set viminfo='100,f1 " Save up to 100 marks, enable capital marks " Indentation set autoindent set smartindent " 开启新行时使用智能自动缩进 set smarttab set shiftwidth=4 " 设定 << 和 >> 命令移动时的宽度为 4 set softtabstop=4 " 使得按退格键时可以一次删掉 4 个空格 set tabstop=4 " 设定 tab 长度为 4 set expandtab " Folds set foldmethod=indent " Fold based on indent set foldnestmax=3 " Deepest fold is 3 levels set nofoldenable " Dont fold by default " Leader setting let mapleader = "," " Rebind <Leader> key " Syntax Highlight syntax on " Run commands that require an interactive shell nnoremap <Leader>r :RunInInteractiveShell<space> 最终效果 NOTE: 完整的 .vimrc 文件非常长, 感兴趣的小伙伴请移步到 JMilkFan’s Github dotfiles dotfiles(点文件) 顾名思义就是文件名前缀带 . 的文件, 因为这类文件在 Linux 中一般为与系统环境相关的隐藏文件(EG. .vimrc/.bashrc/.profile/.bash_profile), 所以在一定程度上 ditfiles 代表了 Linux 系统环境的个性化配置. 简而言之就是, 如果在另外一台计算机中同步了这些 dotfils 就能拥有与你自己的计算机一致的环境设置. 而且 dotfiles + Github 就能够实现只要有网络, 那么所有的计算机都能够变成自己熟悉且习惯的样子. 工作原理: 收集相关的 “dotfiles” 将这些 “dotfiles” 都放置到同一个目录 dotfiles 中 将 dotfiles 目录上传到 Github 或者任意网络存储设备上 在另外一台计算机上拉下 dotfiles 目录, 并以软链接的方式将 dotfiles 目录中对应的 “dotfiles”文件链接到系统中相应路径中 EXAMPLE: git clone dotfiles jmilkfan@JmilkFan-Devstack:~$ git clone https://github.com/JmilkFan/dotfiles.git 建立软链接 jmilkfan@JmilkFan-Devstack:~$ ln -s dotfiles/.vimrc ~/.vimrc 安装插件 vim +PluginInstall +qall 安装完之后就能够愉快的撸代码了 : ) NOTE: 这里只是一个仅含有 .vimrc 文件的 dotfiles, 实际上会含有更多的文件, 那么就需要使用到 Bash 来为我们快速的建立软链接了.
目录 目录 前文列表 扩展阅读 系统环境 前言 Cloud-init Cloud-init 的配置文件 metadata userdata metadata 和 userdata 的区别 metadata 的服务机制 ConfigDrive Metadata RESTful 前文列表 Openstack 实现技术分解 (1) 开发环境 — Devstack 部署案例详解 扩展阅读 Documentation — Cloud-Init 0.7.9 documentation 系统环境 Devstack-M Ubuntu TLS 14.04 前言 Cloud-Init + metadata + userdata 是一套初始化定制云平台虚拟机的解决方案, 最主要解决了下列功能需求: 能够自动化的完成对云平台虚拟机的初始设置, EG. set-hostname/set-ipv4/set-disk-size/upgrade/exec-script 等等 支持云平台与虚拟机的通信, 以此来获取虚拟机的具体信息 简单来说就是能够 注入/获取 虚拟机的信息, 并以此衍生出对虚拟机的初始化定制能力. 其生产价值类似于无人值守技术, 避免了单独为每一台虚拟机进行人工初始化的繁琐. Cloud-init Everything about cloud-init, a set of python scripts and utilities to make your cloud images be all they can be! Cloud-Init 是一组 Python Script 的集合, 是一个能够定制 Cloud Images 的实用工具. 所以 Cloud-init 一般会被包含在用于启动云平台虚拟机的 Images 文件中, 并且使用该镜像启动虚拟机时, Cloud-init 应该是自启动的, 因为其工作在虚拟机的启动过程中, 对虚拟机进行定制化的初始配置. 安装 Cloud-init 方法非常简单, 基本上常规的系统发行版都有原生的软件源, EG. ubuntu 安装: sudo apt-get install cloud-init NOTE: Cloud-init 安装在虚拟机中, 然后再将该虚拟机制作成有如 qcow2 格式的 Image 文件. 那么, 第一个问题: Cloud-init 是怎么定制虚拟机配置的呢? 答案就是 Cloud-init 的配置文件 cloud.cfg. Cloud-init 的配置文件 一般我们也只需要关心 Cloud-init 配置文件的定义, /etc/cloud/cloud.cfg: stack@fanguiju-dev:~/devstack$ cat /etc/cloud/cloud.cfg | grep -v ^# | grep -v ^$ users: - default disable_root: true preserve_hostname: false cloud_init_modules: - migrator - seed_random - bootcmd - write-files - growpart - resizefs - set_hostname - update_hostname - update_etc_hosts - ca-certs - rsyslog - users-groups - ssh cloud_config_modules: - emit_upstart - disk_setup - mounts - ssh-import-id - locale - set-passwords - grub-dpkg - apt-pipelining - apt-configure - package-update-upgrade-install - landscape - timezone - puppet - chef - salt-minion - mcollective - disable-ec2-metadata - runcmd - byobu cloud_final_modules: - rightscale_userdata - scripts-vendor - scripts-per-once - scripts-per-boot - scripts-per-instance - scripts-user - ssh-authkey-fingerprints - keys-to-console - phone-home - final-message - power-state-change Cloud-Init 根据配置文件的内容, 来定制虚拟机配置, 其中最主要配置项的就是下列三个模块列表: cloud_init_modules cloud_config_modules cloud_final_modules 在虚拟机启动时, 会顺序的根据模块列表中含有的各个模块的变量值来对其进行配置, EG. 模块列表 cloud_init_modules 中包含的模块 update_etc_hosts (/usr/lib/python2.7/dist-packages/cloudinit/config/cc\_update\_etc\_hosts.py). 从该模块的代码可以看出其能够配置虚拟机的 hostname/fqdn/manage_etc_hosts 等信息. Cloud-Init 首先会尝试从配置文件 /etc/cloud/cloud.cfg 读取变量 hostname/fqdn/manage_etc_hosts 的值, 如果没有定义, 则尝试从其他的数据源中获取并实现配置. EG. Openstack 可以通过 Metadata 来获取 hostname 等变量值. NOTE 1: 除此之外, Cloud-Init 还会按照上述模块列表的顺序来进行配置, 这是因为有些模块的执行对虚拟机操作系统当前的状态是有要求的, 后面模块的配置可能需要前面模块的配置做支撑. NOTE 2: 而且, 模块列表中的模块具有多种运行模式: per-once: 仅执行一次, 在执行完毕之后会在 sem 目录中创建一个信号文件, 防止在下次启动虚拟机时重复执行. per-always: 每次启动都会执行 per-instance: 每一个虚拟机都会执行 EG. cloud_final_modules: - scripts-per-once - scripts-per-boot - scripts-per-instance 配置文件 cloud.cfg 更相信的用法请查阅官网, 一般而言, 默认的就够用了. 第二个问题: Cloud-init 定制虚拟机操作系统配置时, 配置项目的值, 从哪里获取? 答案就是 metadata/userdata metadata & userdata metadata 是一个数据源, 在 Openstack 中是由 nova-api service 提供的, 一般我们会在虚拟机中通过IP 169.254.169.254 来获取. 选择一个版本 选择一个配置项目 显然, Cloud-init 能够通过访问这些 URL 来获取其所需要的信息, 然后再进行配置. 但是需要说明的一点是 169.254.169.254 这个 IP 实际是不存在的, 本质上提供 metadata 的是 nova-api service, 所以通常都需要设定防火墙 DNAT 将 169.254.169.254 映射到 nova-api-service-ip:port 这个 IP. metadata 和 userdata 的区别 其实 userdata 与 metadata 本质上都是提供配置信息的数据源, 使用了相同的信息注入机制, 只是两者代表了不同的信息类型而已: metadata 主要提供了虚拟机的常用属性, EG. hostname/network/SSH/…, 其以 key/value 的形式进行注入, 所以非常适合应用到 REST 的场景中. userdata 主要提供了 Shell 相关的 CLI 和 Script 等, 其通过文件的方式进行注入, 支持多种文件格式(EG. gzip/Bash/cloud-init/…). 所以, 两者的区别仅在于虚拟机在获取到信息后, 对两者的处理方式不尽相同而已. 第三个问题: metadata 和 userdata 含有的配置信息是怎么被注入到虚拟机中的? 答案就是 ConfigDrive/RESTful API metadata 的服务机制 ConfigDrive 手动指定使用 ConfigDrive: nova boot --config-drive=true ... 启动虚拟机时, 使用 --config-drive=true 就是使用 ConfigDrive 机制来注入 metadata 信息. 修改配置文件默认使用 ConfigDrive: vim /etc/nova/nova.conf [DEFAULT] ... force_config_drive = True ConfigDrive 机制: OpenStack 会将 metadata 信息写入虚拟机的特殊设备中, 然后在虚拟机启动时, 会将该设备挂载到虚拟机上并由 Cloud-init 读取内含的 metadata 信息, 从而实现信息注入. 例如, 初始化定制 Openstack 默认支持的 Libvirt 虚拟机配置时, OpenStack 就会将 metadata 写入虚拟机的 vdisk 文件中, 并将 vdisk 指定为 cdrom 设备. 我们启动一个测试用的 Libvirt 虚拟机, 其 id 为 30ba8cc0-b2f9-4e38-9a27-6bfa9d82f5f2. 然后找到该虚拟机的 XML 文件, 其中含有以下配置内容: vim /opt/stack/data/nova/instances/30ba8cc0-b2f9-4e38-9a27-6bfa9d82f5f2/libvirt.xml <disk type="file" device="cdrom"> <driver name="qemu" type="raw" cache="none"/> <source file="/opt/stack/data/nova/instances/30ba8cc0-b2f9-4e38-9a27-6bfa9d82f5f2/disk.config"/> <target bus="ide" dev="hdd"/> </disk> 所以, 这里的 cdrom 设备就是以 ConfigDrive 方式进行 metadata 信息注入所使用到的特殊设备. 但是需要注意的是: 显然, 不同的底层 hypervisor 支撑, 其所挂载的设备类型也不尽相同. 在虚拟机中查看 metadata 信息: ubuntu@auto-dep-db:~$ sudo mount /dev/disk/by-label/config-2 /mnt/ mount: block device /dev/sr0 is write-protected, mounting read-only ubuntu@auto-dep-db:~$ cd /mnt/ ubuntu@auto-dep-db:/mnt$ ls ec2 openstack ubuntu@auto-dep-db:/mnt$ cd openstack/ ubuntu@auto-dep-db:/mnt/openstack$ ls 2012-08-10 2013-04-04 2013-10-17 2015-10-15 latest ubuntu@auto-dep-db:/mnt/openstack$ cd 2015-10-15/ ubuntu@auto-dep-db:/mnt/openstack/2015-10-15$ ls meta_data.json network_data.json user_data vendor_data.json ubuntu@auto-dep-db:/mnt/openstack/2015-10-15$ vim user_data 其中 user_data 文件就是我们在创建虚拟机时, 指定需要执行的脚本文件. Metadata RESTful Openstack 中的虚拟机也可以通过 RESTful API 来获取 metadata 信息, 提供该服务的组件为 nova-api-metadata service + neutron-metadata-agent + neutron-ns-metadata-proxy. 注意, 如果在 Nova-Network 网络模式中后两个服务是不存在也不需要的. Nova-api-metadata: 负责接收并处理虚拟机发出的 REST API 请求(EG.curl 169.254.169.254), 从 HTTP Request Header 中能够获得获得虚拟机 id, 继而从 database 中读取虚拟机的 metadata 信息并返回结果给虚拟机. Neutron-metadata-agent: 负责将自身节点中的虚拟机发出的 metadata 请求转发到运行 nova-api-metadata 服务的节点中, neutron-metadata-agent 会将虚拟机 id 和 project id 添加到 HTTP Request Header, 最后由 nova-api-metadata 会根据这些信息到 database 中获取 metadata 并返回结果给虚拟机. Neutron-ns-metadata-proxy: 为了解决 Node 中的物理网段和 Project 中的虚拟网段重复的问题, OpenStack 引入了 network namespace 的概念, 每个 namespace 都是独立的, 其包含了各自拥有的 Route 和 DHCP Server. 由于虚拟机的 metadata 请求都是以 Route 和 DHCP Server 作为网络出口的, 所以需要通过 neutron-ns-metadata-proxy 来打通不同的 namespace, 让该请求在不同的 namespace 间跳转, 其实现原理是利用了在 Unix domain socket 基础之上的 HTTP 技术, 并在 HTTP Request Header 中添加 X-Neutron-Router-ID 和 X-Neutron-Network-ID 字段信息, 使得 neutron-metadata-agent 能够定位发出请求的虚拟机并获取其 id. Instance 发送 metadata 请求被发送至 network namespace 再由 namespace 中的 neutron-ns-metadata-proxy service(添加 router-id/network-id 到请求头) 通过 unix domian socket for IPC 技术转发给 neutron-metadata-agent 在 neutron-metadata-agent 中, 其会根据请求头中的 router-id/network-id/ip/port , 来获取并添加 instance-id/tenant-id 到请求头中 然后由 neutron-metadata-agent 将请求被转发给 nova-api-metadata, 并且利用请求头中的 instance-id/tenant-id 从数据库中获取虚拟机的 metadata 最终原路返回 metadata 到虚拟机中 NOTE: 上面已经提到过了如果虚拟机希望访问 169.254.169.254 首先需要在 Node 上设置 DNET: sudo iptables -t nat -A PREROUTING -d 169.254.169.254/32 -p tcp -m multiport --dport 80 -j DNAT --to-destination <nova_api_server_ip>:8775
目录 目录 问题描述 问题解决 最后 问题描述 Cinder 的僵尸卷一般是因为操作不当导致分配的卷无法正常使用且无法正常分离或删除. 问题解决 解决僵尸卷问题的思路类似解决 Linux 系统中的僵尸进程, 需要手动的通过修改数据库和执行 CLI 来实现. 使用 CLI 定位僵尸卷的基本信息 最主要的是要获取其 id, 然后到数据库中查看其详细信息. stack@fanguiju-dev:~$ openstack volume list +--------------------------------------+--------------+--------+------+-----------------------------------------------------------+ | ID | Display Name | Status | Size | Attached to | +--------------------------------------+--------------+--------+------+-----------------------------------------------------------+ | 0d71a98a-0d2c-4b73-9886-a98005d9f969 | mysql-vol | in-use | 5 | Attached to e1fd229f-413e-49dd-a741-1bbffa7f249c on /mnt | +--------------------------------------+--------------+--------+------+-----------------------------------------------------------+ 可以看出这次的问题的原因是因为使用 openstackclient 挂载卷到 Instance 的时候指定了错误设配(/mnt), 应该指定挂载设备为(/dev/vdX). 而且使用 CLI 的 –force 也无法强制删除: stack@fanguiju-dev:~$ openstack volume delete 0d71a98a-0d2c-4b73-9886-a98005d9f969 --force Invalid volume: Volume must not be migrating, attached, belong to a consistency group or have snapshots. (HTTP 400) (Request-ID: req-8ec31dac-591a-4895-942d-3e4998c5407d) 手动的修改 cinder 数据库的表 volumes 的 status 字段为 deleted 在挂载完卷之后该记录的字段 status 的值为 in-use. *************************** 2. row *************************** created_at: 2017-01-18 14:59:38 updated_at: 2017-01-18 16:12:09 deleted_at: NULL deleted: 0 id: 0d71a98a-0d2c-4b73-9886-a98005d9f969 ec2_id: NULL user_id: b03df4585b7d41cca635ec341217404d project_id: d0f2734c0cd3421eaab8e7d3da5b61d1 host: fanguiju-dev@lvmdriver-1#lvmdriver-1 size: 5 availability_zone: nova status: in-use attach_status: attached scheduled_at: 2017-01-18 14:59:39 launched_at: 2017-01-18 14:59:40 terminated_at: NULL display_name: mysql-vol display_description: backup volume of mysql server. provider_location: 200.21.18.30:3260,2 iqn.2010-10.org.openstack:volume-0d71a98a-0d2c-4b73-9886-a98005d9f969 1 provider_auth: CHAP xvATFN4UYj3ueGXZBW5P aTsMksaXX6KSsw8n snapshot_id: NULL volume_type_id: 2fac34b8-f25c-490c-b0bb-6989d4778432 source_volid: NULL bootable: 0 provider_geometry: NULL _name_id: NULL encryption_key_id: NULL migration_status: NULL replication_status: disabled replication_extended_status: NULL replication_driver_data: NULL consistencygroup_id: NULL provider_id: NULL multiattach: 0 previous_status: NULL 手动修改其 status 字段值: mysql> update volumes set status='deleted' where id='0d71a98a-0d2c-4b73-9886-a98005d9f969'; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 执行指令删除卷 同样的, 在删除之前先要查看现在逻辑卷的使用情况. stack@fanguiju-dev:~$ sudo vgs VG #PV #LV #SN Attr VSize VFree cinder-volumes-default 1 0 0 wz--n- 50.00g 50.00g cinder-volumes-lvmdriver-1 1 1 0 wz--n- 50.00g 45.00g ubuntu-dev-fanguiju-vg 1 2 0 wz--n- 99.76g 20.00m stack@fanguiju-dev:~$ sudo lvs LV VG Attr LSize Pool Origin Data% Move Log Copy% Convert volume-0d71a98a-0d2c-4b73-9886-a98005d9f969 cinder-volumes-lvmdriver-1 -wi-a---- 5.00g root ubuntu-dev-fanguiju-vg -wi-ao--- 95.74g swap_1 ubuntu-dev-fanguiju-vg -wi-ao--- 4.00g 通过 volume 的 id(0d71a98a-0d2c-4b73-9886-a98005d9f969) 我们不能判断出现问题的 lv 就是 /dev/cinder-volumes-lvmdriver-1/volume-0d71a98a-0d2c-4b73-9886-a98005d9f969. 但是在删除该 lv 之前, 我们需要保证该 lv 没有被任何的进程占用. stack@fanguiju-dev:~$ lsof /dev/cinder-volumes-lvmdriver-1/volume-0d71a98a-0d2c-4b73-9886-a98005d9f969 使用 lsof 指令可以获取使用该设备的进程列表, 如果被占用的话, 需要使用 kill 指令来将这些进程杀掉. 之后再将该 lv remove 掉. stack@fanguiju-dev:~$ sudo lvremove /dev/cinder-volumes-lvmdriver-1/volume-0d71a98a-0d2c-4b73-9886-a98005d9f969 Do you really want to remove and DISCARD active logical volume volume-0d71a98a-0d2c-4b73-9886-a98005d9f969? [y/n]: y Logical volume "volume-0d71a98a-0d2c-4b73-9886-a98005d9f969" successfully removed 因为该僵尸卷在数据库中记录的状态已经修改为了 deleted, 所以 dashboard 不会读取该记录, 而且又使用了 lvremove 来将该逻辑卷删除了, 所以也算彻底的解决了这个问题. 最后 最后记录一下, 如果使用 cinderclient 包提供的 cinderclient.v2.volumes:VolumeManager.attach 来挂载卷的话, 其参数 mountpoint 的含义是指定该卷挂载到 Instance 的那一个设备文件上, 而不是指定挂载目录路径.
目录 目录 代码布局 缩进 最大行宽 空行 模块导入 字符串 表达式和语句中的空格 注释 命名规则 编程建议 代码布局 缩进 每级缩进用4个空格。 括号中使用垂直隐式缩进或使用悬挂缩进。 EXAMPLE: # (垂直隐式缩进)对准左括号 foo = long_function_name(var_one, var_two, var_three, var_four) # (悬挂缩进) 一般情况只需多一层缩进 foo = long_function_name( var_one, var_two, var_three, var_four) # (悬挂缩进) 但下面情况, 需再加多一层缩进, 和后续的语句块区分开来 def long_function_name( var_one, var_two, var_three, var_four): print(var_one) # 右括号回退 my_list = [ 1, 2, 3, 4, 5, 6, ] result = some_function_that_takes_arguments( 'a', 'b', 'c', 'd', 'e', 'f', ) 错误示范: # 不使用垂直对齐时,第一行不能有参数。 foo = long_function_name(var_one, var_two, var_three, var_four) # 参数的悬挂缩进和后续代码块缩进不能区别。 def long_function_name( var_one, var_two, var_three, var_four): print(var_one) # 右括号不回退,不推荐 my_list = [ 1, 2, 3, 4, 5, 6, ] result = some_function_that_takes_arguments( 'a', 'b', 'c', 'd', 'e', 'f', ) 最大行宽 每行最大行宽不超过 79 个字符 一般续行可使用反斜杠 括号内续行不需要使用反斜杠 EXAMPLE: # 无括号续行, 利用反斜杠 with open('/path/to/some/file/you/want/to/read') as file_1, \ open('/path/to/some/file/being/written', 'w') as file_2: file_2.write(file_1.read()) # 括号内续行, 尽量在运算符后再续行 class Rectangle(Blob): def __init__(self, width, height, color='black', emphasis=None, highlight=0): if (width == 0 and height == 0 and color == 'red' and emphasis == 'strong' or highlight > 100): raise ValueError("sorry, you lose") if width == 0 and height == 0 and (color == 'red' or emphasis is None): raise ValueError("I don't think so -- values are %s, %s" % (width, height)) 空行 两行空行用于分割顶层函数和类的定义 单个空行用于分割类定义中的方法 EXAMPLE: # 类的方法定义用单个空行分割,两行空行分割顶层函数和类的定义。 class A(object): def method1(): pass def method2(): pass def method3(): pass 模块导入 导入的每个模块应该单独成行 导入顺序如下: (各模块类型导入之间要有空行分割,各组里面的模块的顺序按模块首字母自上而下升序排列) 标准库 相关的第三方库 本地库 EXAMPLE: # 按模块首字母排序导入, 依此递推 import active import adidas import create 错误示例: # 一行导入多模块 import sys, os, knife # 不按首字母导入 import create import active import beyond 字符串 单引号和双引号作用是一样的,但必须保证成对存在,不能夹杂使用. (建议句子使用双引号, 单词使用单引号, 但不强制.) EXAMPLE: # 单引号和双引号效果一样 name = 'JmilkFan' name = "Hey Guys!" 表达式和语句中的空格 括号里边避免空格 EXAMPLE: spam(ham[1], {eggs: 2}) 错误示例: spam( ham[ 1 ], { eggs: 2 } ) 逗号,冒号,分号之前避免空格 EXAMPLE: if x == 4: print x, y; x, y = y, x 错误示例: if x == 4 : print x , y ; x , y = y , x 函数调用的左括号之前不能有空格 EXAMPLE: spam(1) dct['key'] = lst[index] 错误示例: spam (1) dct ['key'] = lst [index] 赋值等操作符前后不能因为对齐而添加多个空格 EXAMPLE: x = 1 y = 2 long_variable = 3 错误示例: x = 1 y = 2 long_variable = 3 二元运算符两边放置一个空格 涉及 = 的复合操作符 ( += , -=等) 比较操作符 ( == , < , > , != , <> , <= , >= , in , not in , is , is not ) 逻辑操作符( and , or , not ) EXAMPLE: a = b a or b # 括号内的操作符不需要空格 name = get_name(age, sex=None, city=Beijing) 注释 注释块 注释块通常应用在代码前,并和代码有同样的缩进。每行以 ‘# ’ 开头, 而且#后面有单个空格。 EXAMPLE: # Have to define the param `args(List)`, # otherwise will be capture the CLI option when execute `python manage.py server`. # oslo_config: (args if args is not None else sys.argv[1:]) CONF(args=[], default_config_files=[CONFIG_FILE]) 单行注释(应避免无谓的注释) EXAMPLE: x = x + 1 # Compensate for border 文档字符串 EXAMPLE: # 多行文档, 首行首字母大写,结尾的 """ 应该单独成行 """Return a foobang Optional plotz says to frobnicate the bizbaz first. """ # 单行的文档, 结尾的 """ 在同一行。 """Return a foobang""" 命名规则 包和模块名: 包和模块名应该简短,全部用小写字母, 多字母之间可以使用单下划线连接。 类名: 遵循驼峰命名 class MyClass(object): pass 全局变量名: 全局变量名应尽量只在模块内部使用, 对可能使用语句 from moduleName import variableName 而被导入的模块,应采用 __all__ 机制来防止全局变量被别的模块导入, 或者在全局变量名开头加一个前置下划线. EXAMPLE: _name = 'name' 函数名 函数名应该为全部小写的凹驼峰规则。 EXAMPLE: vcenter_connection = '' 常量名 常量全部使用大写字母的凹驼峰规则来表示, 通常在模块顶格定义 EXAMPLE: MAX_OVERFLOW = '' TOTAL = 1 方法名和实例变量 非公开方法和实例变量开头使用前置下划线 有时候可能会为了避免与子类命名冲突,采用两个前置下划线 需要注意的是: 若 class Foo 的属性名为 __a, 该属性是不能以 Foo.__a 的方式访问的(执著的用户还是可以通过Foo._Foo__a 来访问), 所以通常双前置下划线仅被用来避免与基类的属性发生命名冲突。 编程建议 None 的比较用 is 或 is not,而不要用 == 用 is not 代替 not … is, 前者的可读性更好 EXAMPLE: # Yes if foo is not None # No if not foo is None 使用函数定义关键字 def 代替 lambda 赋值给标识符, 这样更适合于回调和字符串表示 # Yes def f(x): return 2*x # No f = lambda x: 2*x 异常类应该继承自Exception,而不是 BaseException Python 2 中用raise ValueError('message') 代替 raise ValueError, 'message' (考虑兼容python3和续行的方便性) 捕获异常时尽量指明具体异常, 尽量不用 except Exception, 应该捕获 出了什么问题,而不是 问题发生 EXAMPLE: # Yes (捕获具体异常) try: import platform_specific_module except ImportError: platform_specific_module = None # No (不要全局捕获) try: import platform_specific_module except: platform_specific_module = None try/except 子句中的代码要尽可能的少, 以免屏蔽掉其他的错误 EXAMPLE: # Yes try: value = collection[key] except KeyError: return key_not_found(key) else: return handle_value(value) # No try: return handle_value(collection[key]) except KeyError: # 可能会捕捉到 handle_value()中的 KeyError, 而不是 collection 的 return key_not_found(key) 函数或者方法在没有返回值时要明确返回 None # Yes def foo(): return None # No def foo(): return 使用字符串方法而不是 string 模块 python 2.0 以后字符串方法总是更快,而且与 Unicode 字符串使用了相同的 API 使用使用 .startswith() 和 .endswith() 代替字符串切片来检查前缀和后缀 startswith() 和 endswith 更简洁,利于减少错误 EXAMPLE: # Yes if foo.startswith('bar'): # No if foo[:3] == 'bar': 使用 isinstance() 代替对象类型的比较 EXAMPLE: # Yes if isinstance(obj, int): # No if type(obj) is type(1): 空序列类型对象的 bool 为 False: # Yes if not seq: pass if seq: pass # No if len(seq): pass if not len(seq): pass 不要用 == 进行 bool 比较 # Yes if greeting: pass # No if greeting == True pass if greeting is True: # Worse pass
目录 目录 资料 手动配置 ESXi 主机挂载 NFS 的最大值 资料 官方 KB 地址 手动配置 ESXi 主机挂载 NFS 的最大值 Open: 清单 ==> ESXi 主机 ==> 配置 ==> 高级配置 Set 1: NFS ==> NFS.MaxVolumes 限制 vSphere ESXi/ESX 主机可同时挂载的 NFS 数据存储数量。 ESXi/ESX 3.x:将 NFS.MaxVolumes 设置为 32 ESXi/ESX 4.x:将 NFS.MaxVolumes 设置为 64 ESXi 5.0/5.1/5.5:将 NFS.MaxVolumes 设置为 256 ESXi 6.0:将 NFS.MaxVolumes 设置为 256 Set 2: Net ==> Net.TcpipHeapSize 为管理 VMkernel TCP/IP 网络连接而分配的堆内存量(单位为 MB)。增加 NFS 数据存储数量时,请同时增加默认堆内存量。 ESXi/ESX 3.x:将 Net.TcpipHeapSize 设置为 30 ESXi/ESX 4.x:将 Net.TcpipHeapSize 设置为 32 ESXi 5.0/5.1/5.5:将 Net.TcpipHeapSize 设置为 32 ESXi 6.0:将 Net.TcpipHeapSize 设置为 32 Set 3: Net ==> Net.TcpipHeapMax 为管理 VMkernel TCP/IP 网络连接而分配的最大堆内存量(单位为 MB)。增加 NFS 数据存储数量时,请同时将最大堆内存量增大至相关 ESXi/ESX 主机版本规定的最大值。 ESXi/ESX 3.x:将 Net.TcpipHeapMax 设置为 120 ESXi/ESX 4.x:将 Net.TcpipHeapMax 设置为 128 ESXi 5.0/5.1:将 Net.TcpipHeapMax 设置为 128 ESXi 5.5:将 Net.TcpipHeapMax 设置为 512 ESXi 6.0:将 Net.TcpipHeapMax 设置为 1536 更改 Net.TcpipHeapSize 和/或 Net.TcpipHeapMax 后,需要重新引导主机才能使更改生效。
目录 目录 前文列表 第一阶段结语 打 Tag 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (28) — 使用 Flask-Assets 压缩 CSS/JS 提升网页加载速度 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一 用 Flask 来写个轻博客 (33) — 使用 Flask-RESTful 来构建 RESTful API 之二 用 Flask 来写个轻博客 (34) — 使用 Flask-RESTful 来构建 RESTful API 之三 用 Flask 来写个轻博客 (35) — 使用 Flask-RESTful 来构建 RESTful API 之四 用 Flask 来写个轻博客 (36) — 使用 Flask-RESTful 来构建 RESTful API 之五 第一阶段结语 从 2016/11/13 至今 2017/01/02 刚好 50 天, <<用 Flask 来写个轻博客>> 系列博文的第一阶段也就算告一段落了. 总计 36 篇博文 主要参考书籍 <<深入理解 Flask>> 93 个 commits (获取全部代码请点击 Github) 涉及应用了下列 Flask Extensions: Flask-Admin==1.4.2 Flask-Assets==0.12 Flask-Bcrypt==0.7.1 Flask-Cache==0.13.1 Flask-Celery-Helper==1.1.0 Flask-DebugToolbar==0.10.0 Flask-Login==0.4.0 Flask-Mail==0.9.1 Flask-Migrate==2.0.1 Flask-OAuth==0.12 Flask-OpenID==1.2.5 Flask-Principal==0.4.0 Flask-RESTful==0.3.5 Flask-Script==2.0.5 Flask-SQLAlchemy==2.1 Flask-WTF==0.13.1 接下来会为 jmilkfansblog 项目打上第一个 Tag, 版本为 0.0.1 . 之所以说是第一阶段, 说明该项目还远没有完成, 但是往后的开发性质会发生改变. 就现今而言, 整个 blog 项目可以勉强称之为是以上线为主导生产项目, 而往后却会以实验目的为主导. 我会将其变成实践 Openstack 技术要点的实验项目, 将庞大的 Openstack 解体迁移到其上, 其实使用 “硬塞” 这一个词会更加准确. 因为之间并不会考虑最优解决, 是以学习记录为主的实践过程. 现在为止大部分代码都是后端业务逻辑的实现, 前端的页面展示可以说是惨不忍睹, 实在碍于个人水平有限. 但是基本的架构和常使用到的 Flask 知识点基本已经记录了在系列博文中, 感兴趣的朋友可以在此之上继续完成开发. 接下来的日子里 << 用 Flask 来写个轻博客 >> 系列会被更名为 << Openstack 实现技术分解 >>, 而且我想更新的速度会慢下许多. 总而言之, 感谢所有帮助我 Fix bug 的小伙伴们 Hope you enjoy : ) ~ 打 Tag git tag -a 0.0.1 -m "JmilkFan's blog v0.0.1 Python-Flask stage done" git push -u origin master git push -u origin --tag
目录 目录 前文列表 PUT 请求 DELETE 请求 测试 对一条已经存在的 posts 记录进行 update 操作 删除一条记录 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一 用 Flask 来写个轻博客 (33) — 使用 Flask-RESTful 来构建 RESTful API 之二 用 Flask 来写个轻博客 (34) — 使用 Flask-RESTful 来构建 RESTful API 之三 用 Flask 来写个轻博客 (35) — 使用 Flask-RESTful 来构建 RESTful API 之四 PUT 请求 紧接前 4 篇, 继续完成 PUT 和 DELETE 请求的实现. PUT 请求对应的是资源类的 put() 方法, 表示更新操作. 所以仍然需要先定义 post_put 解析器 vim jmilkfansblog/controllers/flask_restful/parsers.py post_put_parser = reqparse.RequestParser() post_put_parser.add_argument( 'title', type=str) post_put_parser.add_argument( 'text', type=str) post_put_parser.add_argument( 'tags', type=str, action='append') post_put_parser.add_argument( 'token', type=str, required=True, help='Auth Token is required to update the posts.') NOTE: 从 post_put_parser 解析器可以看见, 我们仅允许传入最多 4 个参数, 其中也一定不能缺少 token 以保证数据的安全性. 定义资源类 PostApi 的 put() 更新方法 vim jmilkfansblog/controllers/flask_restful/posts.py ... def put(self, post_id=None): """Will be execute when receive the HTTP Request Methos `PUT`.""" if not post_id: abort(400) post = Post.query.filter_by(id=post_id).first() if not post: abort(404) args = parsers.post_put_parser.parse_args() user = User.verify_auth_token(args['token']) if not user: abort(401) if user != post.user: abort(403) if args['title']: post.title = args['title'] if args['text']: post.text = args['text'] if args['tags']: for item in args['tags']: tag = Tag.query.filter_by(name=item).first() if tag: post.tags.append(tag) else: new_tag = Tag() new_tag.name = item post.tags.append(new_tag) db.session.add(post) db.session.commit() return (post.id, 201) ... NOTE: put() 方法和 post() 方法很相似, 所以这里不在赘叙. DELETE 请求 DELETE 请求的实现是最简单的, 一般而言, 不需要返回任何的数据, 只需要返回正确的 HTTP status_int 就可以了. vim jmilkfansblog/controllers/flask_restful/posts.py def delete(self, post_id=None): """Will be execute when receive the HTTP Request Method `DELETE`.""" if not post_id: abort(400) post = Post.query.filter_by(id=post_id).first() if not post: abort(404) args = parsers.post_delete_parser.parse_args(strict=True) user = User.verify_auth_token(args['token']) if user != post.user: abort(403) # Will be delete relationship record with posts_tags too. # But you have to ensure the number of record equal with len(post.tags) db.session.delete(post) db.session.commit() return "", 204 测试 对一条已经存在的 posts 记录进行 update 操作 mysql> select * from posts where id='1746b650-bcab-436b-82ab-7411e252b576'; +--------------------------------------+-----------+-------+--------------+--------------------------------------+ | id | title | text | publish_date | user_id | +--------------------------------------+-----------+-------+--------------+--------------------------------------+ | 1746b650-bcab-436b-82ab-7411e252b576 | Just Test | Hello | NULL | 65cb9792-b876-49e7-b2c5-46468624199e | +--------------------------------------+-----------+-------+--------------+--------------------------------------+ 1 row in set (0.00 sec) 获取 Token jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "username=<username>" -d "password=<password>" http://localhost:8089/api/auth { "token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM2MjExNywiaWF0IjoxNDgzMzYxNTE3fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.2r7f-SZJS2U8Zafqyl7oUYPfFGilDJemVwImPuIHxd0" } PUT 请求 jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -X PUT -d "title=Just Test PUT" -d "text=Hello Guys" -d "tags=Python" -d "tags=Flask" -d "token=eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM2MjExNywiaWF0IjoxNDgzMzYxNTE3fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.2r7f-SZJS2U8Zafqyl7oUYPfFGilDJemVwImPuIHxd0" http://localhost:8089/api/posts/1746b650-bcab-436b-82ab-7411e252b576 "1746b650-bcab-436b-82ab-7411e252b576" 结果 mysql> select * from posts where id='1746b650-bcab-436b-82ab-7411e252b576'; +--------------------------------------+---------------+------------+--------------+--------------------------------------+ | id | title | text | publish_date | user_id | +--------------------------------------+---------------+------------+--------------+--------------------------------------+ | 1746b650-bcab-436b-82ab-7411e252b576 | Just Test PUT | Hello Guys | NULL | 65cb9792-b876-49e7-b2c5-46468624199e | +--------------------------------------+---------------+------------+--------------+--------------------------------------+ 1 row in set (0.00 sec) 删除一条记录 jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -X DELETE -d "token=eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM2MjkzMywiaWF0IjoxNDgzMzYyMzMzfQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.B8ISY5Fb6AD_PyrJxNVQNYuxXUMrEioB-rlhtyvYcxU" http://localhost:8089/api/posts/1746b650-bcab-436b-82ab-7411e252b576 结果 mysql> select * from posts where id='1746b650-bcab-436b-82ab-7411e252b576'; Empty set (0.00 sec)
目录 目录 前文列表 POST 请求 身份认证 测试 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一 用 Flask 来写个轻博客 (33) — 使用 Flask-RESTful 来构建 RESTful API 之二 用 Flask 来写个轻博客 (34) — 使用 Flask-RESTful 来构建 RESTful API 之三 POST 请求 前三篇博文介绍了如果实现 GET 请求, 接下来继续实现 POST 创建数据请求. 执行 Create 操作, 就肯定少不了要向服务端传入数据. 所以第一步当然就是定义解析器了. vim jmilkfansblog/controllers/flask_restful/parsers.py from flask.ext.restful import reqparse post_post_parser = reqparse.RequestParser() post_post_parser.add_argument( 'title', type=str, required=True, help='Title is required!') post_post_parser.add_argument( 'text', type=str, required=True, help='Text is required!') post_post_parser.add_argument( 'tags', type=str, action='append') post_post_parser.add_argument( 'token', type=str, required=True, help='Auth Token is required to create posts.') NOTE 1: add_argument 的关键字参数 action='append' 指定了传入的参数会转换为以字典为元素的列表数据类型. 这是为了便于创建 post.tags 对象. NOTE 2: 定义 token 参数是为了后期的身份认证做准备 在资源来 PostApi 中实现 post()方法 vim jmilkfansblog/controllers/flask_restful/posts.py class PostApi(Resource): """Restful API of posts resource.""" ... def post(self, post_id=None): """Can be execute when receive HTTP Method `POST`. """ if post_id: abort(400) else: args = parsers.post_post_parser.parse_args(strict=True) new_post = Post() new_post.title = args['title'] new_post.date = datetime.datetime.now() new_post.text = args['text'] new_post.user = user if args['tags']: for item in args['tags']: tag = Tag.query.filter_by(name=item).first() # If the tag already exist, append. if tag: new_post.tags.append(tag) # If the tag not exist, create the new one. # Will be write into DB with session do. else: new_tag = Tag() new_tag.name = item new_post.tags.append(new_tag) db.session.add(new_post) db.session.commit() return (new_post.id, 201) NOTE 1: post() 返回了一个 Tuple 类型对象, 第二个元素会作为 Response Hander 中的 HTTP status_int 状态码. 身份认证 需要注意的是, 对外开发的 RESTful API 一定要非常注重安全, 所有从外部对数据库的写入操作请求都必须进行身份认证. 身份认证的功能我们仍然可以由 Flask-Login 来支持, 但很明显的, 这并不符合 REST 的无状态约束. 所以我们在这里引入 Token 的概念, 外部请求如果希望通过 RESTful API 执行写数据库操作时, 必须携带用户登录信息, 通过身份认证之后, 再由服务端发放一段时间内有效的 Token. 身份认证在整个项目中也应当作为一种资源来定义. 首先还是定义 auth 解析器来接受用户信息 vim jmilkfansblog/controllers/flask_restful/parsers.py ######################################################## # User's HTTP Request Parser ######################################################## user_post_parser = reqparse.RequestParser() user_post_parser.add_argument( 'username', type=str, required=True, help='Username is required!') user_post_parser.add_argument( 'password', type=str, required=True, help='Password is required!') NOTE : auth 的解析器只需要用户名和密码两个参数. 创建认证资源 auth 的资源类 vim jmilkfansblog/controllers/flask_restful/auth.py from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask import abort, current_app from flask.ext.restful import Resource from jmilkfansblog.controllers.flask_restful import parsers from jmilkfansblog.db.sqlalchemy.models import User class AuthApi(Resource): """Restful api of Auth.""" def post(self): """Can be execute when receive HTTP Method `POST`.""" args = parsers.user_post_parser.parse_args() user = User.query.filter_by(username=args['username']).first() # Check the args['password'] whether as same as user.password. if user.check_password(args['password']): # serializer object will be saved the token period of time. serializer = Serializer( current_app.config['SECRET_KEY'], expires_in=600) return {'token': serializer.dumps({'id': user.id})} else: abort(401) NOTE 1: Token 使用 Python 内建的 itsdangerous 库来实现, itsdangerous.TimedJSONWebSignatureSerializer() 的第一个参数需要传入 app 对象的私钥, 该私钥在之前的 Flask-WTForm 已经定义在 config.py 中了. 第二个参数定义了 Token 的有效时间. NOTE 2: 我们使用 post() 方法来实现用户身份验证和发放 Token 定义资源 auth 的路由 vim jmilkfansblog/__init__.py def create_app(object_name): ... restful_api.add_resource( AuthApi, '/api/auth', endpoint='restful_api_auth') restful_api.init_app(app) 为 User 对象实现 Token 验证方法 需要注意的是: 这个 Token 应该是有时限的, 如果永久生效则会非常危险. 所以我们还需要对有时限的 Token 进行校验, 确定其没有失效. vim jmilkfansblog/models.py ... class User(db.Model): """Represents Proected users.""" ... @staticmethod @cache.memoize(60) def verify_auth_token(token): """Validate the token whether is night.""" serializer = Serializer( current_app.config['SECRET_KEY']) try: # serializer object already has tokens in itself and wait for # compare with token from HTTP Request /api/posts Method `POST`. data = serializer.loads(token) except SignatureExpired: return None except BadSignature: return None user = User.query.filter_by(id=data['id']).first() return user NOTE: 使用 itsdangerous.TimedJSONWebSignatureSerializer.loads() 来进行 Token 验证, 如果验证失败的话, 我们直接返回 None 现在我们拥有了生成 Token 和验证 Token 的支撑, 最后我们在 PostApi.post() 中加入 Token 验证机制. vim jmilkfansblog/controllers/flask_restful/posts.py def post(self, post_id=None): """Can be execute when receive HTTP Method `POST`. """ if post_id: abort(400) else: args = parsers.post_post_parser.parse_args(strict=True) # Validate the user identity via token(/api/auth POST). # Will be create the post(/api/posts POST), if pass with validate token. user = User.verify_auth_token(args['token']) if not user: abort(401) ... 测试 不加 Token 的 POST 请求 (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "title=Just Test POST" -d "text=Hello world" -d "tags=Python" http://localhost:8089/api/posts { "message": { "token": "Auth Token is required to create posts." } } 创建 Token (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "username=<username>" -d "password=<password>" http://localhost:8089/api/auth{ "token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM1MjkwNSwiaWF0IjoxNDgzMzUyMzA1fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.hYpczUEZUalgzutyyIViheBd_jnnCmegvp4sazHIEoA" } 加 Token 的 POST 请求 (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl -d "title=Just Test" -d "text=Hello" -d "tags=Python" -d "token=eyJhbGciOiJIUzI1NiIsImV4cCI6MTQ4MzM1MjkwNSwiaWF0IjoxNDgzMzUyMzA1fQ.eyJpZCI6IjY1Y2I5NzkyLWI4NzYtNDllNy1iMmM1LTQ2NDY4NjI0MTk5ZSJ9.hYpczUEZUalgzutyyIViheBd_jnnCmegvp4sazHIEoA" http://localhost:8089/api/posts "1746b650-bcab-436b-82ab-7411e252b576" 数据库记录 : mysql> select * from posts; +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+ | id | title | text | publish_date | user_id | +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+ | 1746b650-bcab-436b-82ab-7411e252b576 | Just Test | Hello | NULL | 65cb9792-b876-49e7-b2c5-46468624199e | | 1af8f334-c9ac-4eba-bdca-4dda597aba70 | 333333333 | <p>22222</p> | 2016-12-17 22:39:16 | 65cb9792-b876-49e7-b2c5-46468624199e | | 29bab6a0-6a0f-48f1-a088-6c271cebe906 | 222222 | <p>222222</p> | 2016-12-27 22:35:00 | 65cb9792-b876-49e7-b2c5-46468624199e | | 9c25d00e-49a7-4369-ac83-c0aca046ba73 | Just Test | Hello | NULL | 65cb9792-b876-49e7-b2c5-46468624199e | +--------------------------------------+-----------+-----------------+---------------------+--------------------------------------+
目录 目录 前文列表 应用请求中的参数实现 API 分页 测试 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一 用 Flask 来写个轻博客 (33) — 使用 Flask-RESTful 来构建 RESTful API 之二 应用请求中的参数实现 API 分页 API 也需要实现分页功能, 以此降低 API 对数据库的压力. Flask-RESTful 提供了一种叫做解析器的功能, 用于查找和解析请求中所携带的参数. 而且还可以规定必备的参数和类型. 实现解析器模块 vim jmilkfansblog/controllers/flask_restful/parsers.py from flask.ext.restful import reqparse post_get_parser = reqparse.RequestParser() post_get_parser.add_argument( 'page', type=int, location=['json', 'args', 'headers'], required=False) post_get_parser.add_argument( 'user', type=str, location=['json', 'args', 'headers']) NOTE 1: 命名规则为 resourceName_functionName_parser NOTE 2: add_argument() 函数的参数列表: (1). page 定义参数名称 (2). type=int 定义参数类型 (3). location=['json', 'args', 'headers'] 搜索参数的位置列表 (4). required=False 是否为必须的参数 除此之外, 还能够定义非常多的关键字参数, 具体请参照官方文档. NOTE 3: 可以定义多个参数 应用自定义的解析器 直接应用到资源类的实例方法中 vim jmilkfansblog/controllers/flask_restful/posts.py from jmilkfansblog.models import db, User, Post, Tag from jmilkfansblog.controllers.flask_restful import parsers from flask import abort ... class PostApi(Resource): """Restful API of posts resource.""" @marshal_with(post_fields) def get(self, post_id=None): """Can be execute when receive HTTP Method `GET`. Will be return the Dict object as post_fields. """ if post_id: post = Post.query.filter_by(id=post_id).first() if not post: abort(404) return post else: args = parsers.post_get_parser.parse_args() page = args['page'] or 1 # Return the posts with user. if args['user']: user = User.query.filter_by(username=args['user']).first() if not user: abort(404) posts = user.posts.order_by( Post.publish_date.desc()).paginate(page, 30) # Return the posts. else: posts = Post.query.order_by( Post.publish_date.desc()).paginate(page, 30) return posts.items 分页的原理在之前的博文中已经介绍过了, 这里不在重复. NOTE 1: 这里实现了解析器 reqparse 从 URL 参数或者 HTTP Header 中找到 user/page 参数, 并返回对应的 Model 分页对象. 测试 使用 curl 工具进行测试, 根据个人环境可能需要安装. 获取所有 posts (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl http://localhost:8089/api/posts [ { "author": "jmilkfan", "id": "29bab6a0-6a0f-48f1-a088-6c271cebe906", "publish_date": "2016-12-27T22:35:00", "tags": [ { "id": "720ae67d-87ed-4b6e-8207-d8f1d7b6509e", "name": "Flask" } ], "text": "222222\r\n", "title": "222222" }, { "author": "jmilkfan", "id": "1af8f334-c9ac-4eba-bdca-4dda597aba70", "publish_date": "2016-12-17T22:39:16", "tags": [ { "id": "6e1e1f94-8076-430f-9597-097a68754ca8", "name": "Python" } ], "text": "22222\r\n", "title": "333333333" } ] 获取单一 post (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl http://localhost:8089/api/posts/1af8f334-c9ac-4eba-bdca-4dda597aba70 { "author": "jmilkfan", "id": "1af8f334-c9ac-4eba-bdca-4dda597aba70", "publish_date": "2016-12-17T22:39:16", "tags": [ { "id": "6e1e1f94-8076-430f-9597-097a68754ca8", "name": "Python" } ], "text": "22222\r\n", "title": "333333333" } 传入正确的参数 (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl http://localhost:8089/api/posts?page=1 [ { "author": "jmilkfan", "id": "29bab6a0-6a0f-48f1-a088-6c271cebe906", "publish_date": "2016-12-27T22:35:00", "tags": [ { "id": "720ae67d-87ed-4b6e-8207-d8f1d7b6509e", "name": "Flask" } ], "text": "222222\r\n", "title": "222222" }, { "author": "jmilkfan", "id": "1af8f334-c9ac-4eba-bdca-4dda597aba70", "publish_date": "2016-12-17T22:39:16", "tags": [ { "id": "6e1e1f94-8076-430f-9597-097a68754ca8", "name": "Python" } ], "text": "22222\r\n", "title": "333333333" } ] 传入错误的参数 (env) jmilkfan@JmilkFan-Devstack:/opt/JmilkFan-s-Blog$ curl http://localhost:8089/api/posts?user='aasdasdasd' { "message": "The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. You have requested this URI [/api/posts] but did you mean /api/posts or /api/posts/<string:post_id> or /admin/post/ ?" }
目录 目录 前文列表 扩展阅读 构建 RESTful Flask API 定义资源路由 格式化输出 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 用 Flask 来写个轻博客 (32) — 使用 Flask-RESTful 来构建 RESTful API 之一 扩展阅读 使用 Flask 设计 RESTful APIs 快速入门 — Flask-RESTful 0.3.1 documentation Python爬虫常用之HtmlParser 构建 RESTful Flask API 为什么要构建 RESTful API ? 对于一个 blog application 而言, 其实完全可以不用到 restful api 也能满足日常所需. 加入 restful api 的唯一目标就是加强该项目的可扩展性, 为后期所要实现的诸如: 博客迁移/数据备份/功能扩展 提供统一且可靠的接口. 定义资源路由 首先我们要有一个大概的需求, 如果希望通过 HTTP 请求来完成对服务端资源的操作, 我们需要解决那些问题? 1. 首先要定位到该资源 2. 告诉服务端我要对该资源做那种操作 3. 前提还可能需要满足身份鉴权(这个需求, 我们后期再实现) 安装 Flask-RESTful pip install Flask-Restful pip freeze > requirements.txt 初始化 restful_api 对象 vim jmilkfansblog/extensions.py from flask.ext.restful import Api ... #### Create the Flask-Restful's instance restful_api = Api() 实现 PostApi 资源类 我们将 posts 博客文章定义为一类资源, 只有定义了资源并且对外公开后, 才能被外部所调用. vim jmilkfansblog/controllers/flask_restful/posts.py from flask.ext.restful import Resource class PostApi(Resource): """Restful API of posts resource.""" def get(self, post_id=None): """Can be execute when receive HTTP Method `GET`. Will be return the Dict object as post_fields. """ return {'hello': 'world'} NOTE 1: jmilkfansblog/controllers/flask_restful 会作为一个包, 所以要记得创建 __init__.py 文件, 否则无法作为导入路径. NOTE 2: 每个 REST 资源类都需要继承 flask_restful 的 Resource 类. 其所有的子类都可以通过定义同名实例函数来将该函数绑定到 HTTP Methods 中. EG. GET <==> get(), 放接受定位到资源的 HTTP GET 方法时, 就会执行该资源类的实例函数 get() . 将 restful_api 对象注册到 app 对象中 vim jmilkfansblog/__init__.py from jmilkfansblog.extensions import restful_api from jmilkfansblog.controllers.flask_restful.posts import PostApi ... def create_app(object_name): ... #### Init the Flask-Restful via app object # Define the route of restful_api restful_api.add_resource( PostApi, '/api/posts') restful_api.init_app(app) NOTE 1: 在 restful_api.add_resource() 指定了资源类 PostApi 所对应的资源名称为 posts, 访问路由为 /api/posts, 这样才完成了对一个资源的完整定义. NOTE 2: 同时再结合 PostApi 中的 get() 会自动的适配到 HTTP GET 方法, 这样就解决了我们之前所提出的 2 个问题. 现在我们引入一个新的问题, 通过上述定义的 get() 方法我们基本可以获取到数据库 posts 表中的所有记录(当然现在还没有连接数据库操作), 那么如果我只需要获取其中的某一条指定的记录呢? 这里需要在请求中指定 id 来完成单一的定位, 或者也可以传递一个 filters 来过滤若干条满足要求的数据记录. 为资源 posts 添加多条路由 vim jmilkfansblog/__init__.py def create_app(object_name): ... #### Init the Flask-Restful via app object # Define the route of restful_api restful_api.add_resource( PostApi, '/api/posts', '/api/posts/<string:post_id>', endpoint='restful_api_post') NOTE: add_resource() 允许为同一个资源类绑定多条路由, '/api/posts/<string:post_id>' 表示可以访问 posts 这一类资源中某一个 post_id 一致的资源对象. 为 get() 方法添加 post_id 形参数 vim jmilkfansblog/controllers/flask_restful/posts.py class PostApi(Resource): """Restful API of posts resource.""" def get(self, post_id=None): """Can be execute when receive HTTP Method `GET`. Will be return the Dict object as post_fields. """ if post_id: return {'post_id': post_id} return {'hello': 'world'} 格式化输出 在上一篇博文中提到, REST 约束要求我们使用一致的数据包装形式来进行响应, 所以我们需要实现一致的格式化功能. 本项目使用最常见的 JSON 格式. Flask-Restful 的格式化输出, 首先需要定义出一个类似模板的 Dict 类型对象 其 keys 是资源对应的 Model 对象所拥有且需要输出的字段名, values 则声明了该字段的值以何种类型转换并输出. 然后把该字典模板传给装饰器 @marshal_with 并装饰到所有资源类中需要返回数据到客户端的实例方法中. 如此之后,实例方法在返回数据之前都会按照该模板将数据进行格式化转换. 注意: 字典模板的 keys 最好与 models 模块中定义的字段名相同, 否则无法自动完成字典模板与 Model 对象的匹配. vim jmilkfansblog/controllers/flask_restful/posts.py from flask.ext.restful import Resource, fields, marshal_with from jmilkfansblog.controllers.flask_restful import fields as jf_fields ... # String format output of tag nested_tag_fields = { 'id': fields.String(), 'name': fields.String()} # String format output of post post_fields = { 'author': fields.String(attribute=lambda x: x.user.username), 'title': fields.String(), 'text': jf_fields.HTMLField(), 'tags': fields.List(fields.Nested(nested_tag_fields)), 'publish_date': fields.DateTime(dt_format='iso8601')} class PostApi(Resource): """Restful API of posts resource.""" @marshal_with(post_fields) def get(self, post_id=None): """Can be execute when receive HTTP Method `GET`. Will be return the Dict object as post_fields. """ if post_id: return {'post_id': post_id} return {'hello': 'world'} NOTE 1: 这里需要使用到 flask_restful.fields, 其提供了绝大多数常用的格式类型定义, 具体格式类型列表可以查看官方文档. 当然, 我们也可以自定义一些格式类型, 例如 jf_fields.HTMLField() NOTE 2: tags 和 author 字段并不存在与 posts 表中, 返回该字段是为了遵守 REST 的约束之一, RESTful API 返回的数据应该尽量满足客户端的需求. 所以我们一般会将表与表之前含有关联关系的字段都一同返回. 格式类型 List 可以接受另外一个格式化输出字典模板对象. 类似于 字典内嵌套字典的格式. 自定义 fields 类型 因为 posts 表中的 text 字段内容是一系列的 HTML 字符串(由 CKEditor 产生), 这些 HTML 字符串是不允许被 RESTful API 返回的, 因为要满足 REST 的约束之一, 服务端不参与用户界面表现层的业务逻辑(即 HTML 代码), 所以我们需要将该字段值中的 HTML 标签过滤掉. vim jmilkfansblog/controllers/flask_restful/fields.py from HTMLParser import HTMLParser from flask.ext.restful import fields class HTMLField(fields.Raw): """Define a new fields for filter the HTML tags string.""" def format(self, value): return strip_tags(str(value)) class HTMLStripper(HTMLParser): """HTML Parser of Stripper.""" def __init__(self): self.reset() self.fed = [] def handle_data(self, data_object): self.fed.append(data_object) def get_data(self): return ''.join(self.fed) def strip_tags(html): """Filter the tags string of HTML for data object of Restful api.""" stripper = HTMLStripper() stripper.feed(html) return stripper.get_data() NOTE 1: 在 fields 模块中通过继承了 flask_restful.fields.Raw 类, 实现了新的格式类型 HTMLField . NOTE 2: 使用 HTTPParser 来实现 HTML 解析, 重载 handle_data 方法用于将 HTML 标签之间的文本内容合并.
目录 目录 前文列表 扩展阅读 RESTful API REST 原则 无状态原则 面向资源 RESTful API 的优势 REST 约束 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 用 Flask 来写个轻博客 (31) — 使用 Flask-Admin 实现 FileSystem 管理 扩展阅读 cookie 和 session 的区别详解 RESTful API REST(Representational State Transfer):是一种软件架构的设计风格,而不是一种标准。主要用于 C/S 架构的软件设计,也能很好的支持 B/S 架构,为软件设计提供了一组原则和约束条件,但这些原则和约束的条件均不具有标准性。所以 REST 可以理解为是一组没有严格标准的架构约束条件和设计原则。而满足 REST 风格的应用或设计,就是 RESTful. API: 为外部提供了可以访问应用程序内容数据资源和业务资源的接口, 并且这个接口应该是统一的, 不依赖于硬件平台/软件操作系统/编程语言的. REST 原则 无状态原则 REST 具有通过 HTTP/HTTPS(无状态传输协议) 直接传输数据(XML/JSON)的特性, 所以 C/S 或 B/S 之间的交互请求是无状态的. 相对的, 在 <<用 Flask 来写个轻博客>> 系列博文中 Flask-Login 的实现, 其需要通过 session(Server) 和 cookie(Browser/client) 结合来实现在服务器端或客户端中保存用户的身份和登录状态, 以此来实现不同的用户在不同的页面中可以具有不同访问权限的效果. REST 会使用另外的方式来这个目的, 例如 Openstack 中的 Keystone 认证中间件. 面向资源 资源以 URI(统一资源标识符) 的形式向外部公开, 以 URL(统一资源定位器) 的形式来访问获取. 大概而言, 服务器端 Application 的所有概念都可以定义为一种资源. 使用 URL/URI 能够更好的契合 HTTP/HTTPS 协议, 结合 HTTP/HTTP 协议的内置 Methods(GET/POST/PUT/DELETE) 可能非常方便的实现对资源的 CRUD 数据操作, 除此之外还能够自定义出不同的 action 方法并将其绑定到不同的 URL 中来扩展请求的类型, 实现了不仅限于资源的业务逻辑操作. EG. Openstack Nova 中的资源 server, 其含有的 ../os-start, ../os-stop 等 URL, 对应了 启动/关闭 虚拟机的业务操作. 总的来说, 每个资源都使用唯一的 URI 标识, 而资源的 URL 就是 HTTP 方法调用的目标. URI: protocol://hostname[:port]/path 定义了某一类资源 URL: protocol://hostname[:port]/path/[;parameters][?query]#fragment 定义了某一个具体的资源单位 所以 URL 一般都理解为 URI 的子集. RESTful API 的优势 基于状态与基于无状态 service 在分布式系统中的区别与理解: (因水平问题, 并不严谨, 仅为当下的理解, 欢迎指正) 前者, 客户端的 Cookie 中保存了 session_id, 如果在服务器中该 session_id 对应的 Session 仍然存在的话将会由该 Session 来继续维护用户请求的状态等公共信息. 但问题是, 如果在这个过程中该服务器宕机的话, 则需要重新在另外一台服务器中新建一个 Session 并与客户端建立再次连接并生成 Cookie, 显然在分布式系统中这个过程是比较无谓. 这就是基于状态的 Web service 的分布式系统的弊端, 所有的服务器都需要维护用户请求状态. 在这个前提下, 提出了类似于 LDAP(用户信息集中管理服务器) 这样的状态集中管理机制, 由单独的一台或一类服务器或服务来为整个分布式系统提供身份鉴权功能模块. 那么其先决条件当然就是, 所有的用户请求都必须是无状态的, 不再有所有的服务器都维护着用户请求状态等公共信息的情况发生. 而且还需要引入别的实现方式来满足这一方面的需求, 在 Openstack 中就引入了 Keystone 身份认证服务. 这样做的好处还在于改善了分布式系统的可靠性以及可伸缩性. 所以 RESTful API 的优势在于: 解耦了客户端和服务器 高可靠 易扩展 不依赖与平台和语言 简单轻量 REST 约束 注意: 这里的约束是针对这次的 blog 项目所设计的, 并部一定完全设用于所有应用场景, 需要结合实际进行改进, 所以不应满足与使用, 而是要在不断的实践中加深对 REST 的理解和累积经验. 客户端和服务端关心的业务是完全分离的, 前者不能处理数据的持久化, 后者不能处理与用户页面表现层相关的逻辑. 在 blog application 中, 整个项目都会作为服务端. 服务端是无状态的, 请求与响应的过程中服务端不会保留任何的 session 等状态信息. 处理请求所需要的信息都会被存储在客户端中, 或者被每一次单独的请求所携带. Flask Application 接受的每一个请求都可以携带保存在客户端中的 cookie 数据, 再交由服务端解析并以此判断权限等信息, 但不保留. 服务端公布的所有资源都必须保证统一的接口形似, 即 : 接口必须是基于资源的 不直接返回数据库中的数据而是包装成为 JSON 或 XML 等形式后返回 所有资源都应用一致的包装形式 服务端返回的数据应该足够完整能够满足客户端的业务需求 允许中间层的存在, EG. 能够支持使用 负载均衡/代理/缓存 等中间层服务 返回正确的 HTTP/HTTPS 状态码
目录 目录 前文列表 扩展阅读 编写 FileSystem Admin 页面 Flask-Admin 的权限安全 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能 扩展阅读 [译]Flask-Admin中文入门教程 Flask-admin使用经验技巧总结 编写 FileSystem Admin 页面 所谓的 FileSystem Admin 功能, 就是哪呢钢构通过后台管理页面查看并修改 blog 项目中, 或自定义的文件目录内容. 使用 Flask-Admin 最后一种视图类型 FileAdmin 来实现. 从效果图中可以看见, 其中包括了 上传/创建目录/删除/重命名 等功能. 继承 FileAdmin 视图类 vim jmilkfansblog/controllers/admin.py 最简单的实现方法, 只需要继承了 FileAdmin 类, 并且将子视图类添加到 flask_admin 对象中就可以了. class CustomFileAdmin(FileAdmin): """File System admin.""" pass 添加到 flask_admin 对象 该对象在之前的博文中已经定义过了 vim jmilkfansblog/__init__.py from jmilkfansblog/controllers import admin #### Init the Flask-Admin via app object flask_admin.init_app(app) ... # Register and define path of File System for Flask-Admin flask_admin.add_view( admin.CustomFileAdmin( os.path.join(os.path.dirname(__file__), 'static'), '/static', name='Static Files')) NOTE: CustomFileAdmin 继承了 FileAdmin 的构造器, os.path.join(os.path.dirname(__file__), 'static') 的值为 你需要管理的文件目录在系统中的全路径. name 参数指定了在管理页面显示 Label 的名字. Flask-Admin 的权限安全 一般的我们都不希望网站后台被匿名访问, 权限的设置是最基本的要求. 我们应该之前使用了 Flask-Login 来实现整个 blog 项目的权限功能模块, 所以我们直接引入到 Flask-Admin 就可以了. 设置 CustomView 的限制 vim jmilkfansblog/controllers/admin.py from flask.ext.login import login_required, current_user from jmilkfansblog.extensions import admin_permission ... class CustomView(BaseView): """View function of Flask-Admin for Custom page.""" @expose('/') @login_required @admin_permission.require(http_exception=403) def index(self): return self.render('admin/custom.html') @expose('/second_page') @login_required @admin_permission.require(http_exception=403) def second_page(self): return self.render('admin/second_page.html') NOTE: 路由的访问限制我们直接使用 Flask-Login 提供的装饰器 @login-required 和之前定义好的 RoleNeed 对象来实现. 现在, 只有管理员用户处于登录状态时, 才能访问这个 URL . 限制 ModelAdmin 和 FileAdmin class CustomModelView(ModelView): """View function of Flask-Admin for Models page.""" def is_accessible(self): """Setup the access permission for CustomModelView.""" # callable function `User.is_authenticated()`. # FIXME(JMilkFan): Using function is_authenticated() return current_user.is_authenticated() and\ admin_permission.can() class PostView(CustomModelView): """View function of Flask-Admin for Post create/edit Page includedin Models page""" # Using the CKTextAreaField to replace the Field name is `test` form_overrides = dict(text=CKTextAreaField) # Using Search box column_searchable_list = ('text', 'title') # Using Add Filter box column_filters = ('publish_date',) # Custom the template for PostView # Using js Editor of CKeditor create_template = 'admin/post_edit.html' edit_template = 'admin/post_edit.html' class CustomFileAdmin(FileAdmin): """File System admin.""" def is_accessible(self): """Setup the access permission for CustomFileAdmin.""" # callable function `User.is_authenticated()`. return current_user.is_authenticated() and\ admin_permission.can() NOTE 1: 设置 ModelAdmin 和 FileAdmin 子类的访问权限, 需要为它们定义一个 is_accessible() function, 并且返回的值必须为 Bool 类型对象, 至于权限的设置模式由我们自己定义.
自己的总结, 格式撩乱, 想到什么写什么. 年终总结就像吃饭喝酒, 吃饭不一定要喝酒才能吃饱, 但酒能让这顿饭充满回味. 长进的地方, 今年自己确实更加成熟了, 改善的地方有几点: 说话没那么粗浅了, 尤其在对行业和生活的理解上, 说明今年没少花脑细胞. 这也算是我一直以来仅有的优点之一. 身边的朋友也更愿意和我交谈一些问题, 自己也希望让每一次谈话都变得有营养而非重复, 实在不希望成为复读机, 即便是同一个问题, 也希望在不同的朋友身上得到不同的观点. 做人稍稳重了, 能更加克制自己. 少了几分自我表现, 多了几分倾听他人, 也许这就是用户意识加强了. 让少数人喜欢你是很容易的, 性格再恶劣的人也有几个朋友, 但想要更多的人喜欢你, 那就需要更好的克制自己, 显然后者更难. 懂得为他人付出, 但也更铁石心肠. 这应该是价值观和为处世的方法论慢慢成型了, 有了自己评判标准. 更懂得人情世故了, 开始承担家庭的责任和参与亲朋的琐碎. 这是最大的改变之一, 让我意识到自己从一个消费者到生产者身份的转变. 技术的成长, 毕竟转型了, 新东西铺天盖地的涌过来, 但是不能着急啊, Hold 住啊, 别流于表面, 按照自己的节奏坚持. 知道了何谓虚名, 自己的头衔, 称谓. 很可能是他人出于商务和利益上操作的结果, 不要被蒙骗捧杀. 理智的看待自己, 认清定位. 以上是一些点滴的成长, 同样也伴随着问题: 开始难以相信别人, 尤其是老板的蛋糕尤为难以接受. 慢慢的能够区分事情和话语的真假, 这让我比较苦恼, 容易聪明反被聪明误. 希望自己能够简简单单就好, 慢慢的有点理解何为难得糊涂, 希望找回单纯的自己. 这个问题只能在接下来的日子对生活有了更高的感悟后才能解决吧. 开始变得没有热情, 尤其是工作的热情, 这是今天坐地铁的时候突然意识的问题, 自己越来越喜欢私下做一些小的项目, 很有激情风雨不断. 但工作上的任务却有交差就行的想法. 这个问题很严重, 是时候应该把自己的项目先停一停了, 让重心回到工作, 没必要因为小的成就而失去大的收获. 身体差了, 怕挤公交地铁, 人多的地方, 应该算是一种心理疾病了, 这并非夸大, 有会死掉的错觉. 自己也变得更加怕死, 总以为自己有什么毛病. 可能就是所谓的亚健康, 压力大和生活不规律导致. 写一份作息时间表吧, 希望有用. 但是现在让我从健康是事业中权衡, 还是追求事业更占上峰吧, 实在难以独善其身. 有点无可奈何. 至少希望生活作息能先规律起来. 变得更依赖他人, 这是自制力差的体现了. 要找到自己解决问题的方法论才行啊, 不然层次无法提升. 更容易不服气和嫉妒他人, 其实就是被捧杀了, 也是自己的性格缺陷. 要看清自己, 多宽容他人. 导致以上的变化, 最大的原因是工作的调整, 今年从技术讲师 ⇒ 市场负责 ⇒ 又回到开发. 见识的事情和人是多了, 接受新东西的心态也越来越宽松了, 但精力确实分散了. 一年转了三个岗位, 难免精力不足, 力不从心. 做市场的时间虽然不长, 但感触很深, 经历了一些不被搬上桌面的行业污点和人情冷暖. 有如: 利益分配不均导致的人事震动, 同事间拉帮结派巩固地位, 企业和客户之间的私下交易欺骗投资, 酒桌上的称兄道弟反口一句傻逼. 到底都是为了争取自身的利益, 问题是这些利益从哪里来? 其实是从最无辜的人手上剥夺, 消费了那些不知情者的梦想和希望. 有句话是: 真相之掌握在少数人手中, 我的理解为, 一说是只有少数人是知情者, 二说是真相本就是这些人制造出来的假象. 那段时间自己的良心确实受到了挑战, 尤其在明白了自己的业绩是从那些憧憬未来人手中夺取时, 而且这些人还可能只是校园里学生, 那些愿意相信你的人. 表面功夫做的很好, 但 “客户就是傻逼” 这一句话, 彻底让我死了继续下去的心, 即便这份工作确能为我带来优厚的待遇和更好的生活. 其实但我并不谴责这份工作, 因为想要出头确实要狠心, 尤其作为一名管理或领导的时候. 最终我还是回到了技术岗位, 这里是最单纯的地方, 只有 True or False. 然后, 有个问题特地记录一下, 在做技术的时候一定要提醒自己保持情商. 情商低的技术人员, 实在无力吐槽. 希望自己能多从他人的角度思考, 克制情绪, 表达清晰. 接下来是转型的过程, 无非集齐几个要素: 愿意收留自己的企业, 过度学习, 能装孙子. 第一点: 可遇也可求, 平时积累的人品绝对能祝你一臂之力. 涨薪或以一个自己可以接受的薪资转型, 是最好不过的, 但是毕竟转型穷三年嘛, 自己的选的路跪着也得走完. 第二点: 不要认为工作用不到的技术就不学不了解, 不深入是可以理解的, 但起码的了解和实践还是要的. 找几个不同方向的朋友定期组织分享会. 可以快速看清一件新鲜事物或技术的模样. 第三点: 以前你也许是 Leader 甚至 dalao, 但到了新的方向这些帽子就得放得下. 总的来说, 多交朋友, 多学习, 看清实际. 我一直坚持一个观点, 技术并不是程序员的全部, 因为在程序员之前你是作为一个人而被社会所认知的. 新的一年继续加油吧, 希望能解决现有的问题. 现在还年轻, 如果不能过上富足的生活, 但起码希望精神能先富足起来, 这样的话相信生活也会富足起来的. 最后就是明年的期许和提醒了, 目标就不说了, 憋足这股劲儿. 坚持初心走开发, 把代码写好 在救助之前, 再想一想, 再尝试一下, 把问题的困难程度分类, 设定解决时间. 总结出方法论 把感情照顾好, 多关心, 所花时间给爱你的人 保重身体, 保重, 保重 多看看世界, 开阔自己的心境
目录 目录 前文列表 扩展阅读 实现文章管理功能 实现效果 前文列表 用 Flask 来写个轻博客 (1) — 创建项目 用 Flask 来写个轻博客 (2) — Hello World! 用 Flask 来写个轻博客 (3) — (M)VC_连接 MySQL 和 SQLAlchemy 用 Flask 来写个轻博客 (4) — (M)VC_创建数据模型和表 用 Flask 来写个轻博客 (5) — (M)VC_SQLAlchemy 的 CRUD 详解 用 Flask 来写个轻博客 (6) — (M)VC_models 的关系(one to many) 用 Flask 来写个轻博客 (7) — (M)VC_models 的关系(many to many) 用 Flask 来写个轻博客 (8) — (M)VC_Alembic 管理数据库结构的升级和降级 用 Flask 来写个轻博客 (9) — M(V)C_Jinja 语法基础快速概览 用 Flask 来写个轻博客 (10) — M(V)C_Jinja 常用过滤器与 Flask 特殊变量及方法 用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数 用 Flask 来写个轻博客 (12) — M(V)C_编写和继承 Jinja 模板 用 Flask 来写个轻博客 (13) — M(V)C_WTForms 服务端表单检验 用 Flask 来写个轻博客 (14) — M(V)C_实现项目首页的模板 用 Flask 来写个轻博客 (15) — M(V)C_实现博文页面评论表单 用 Flask 来写个轻博客 (16) — MV(C)_Flask Blueprint 蓝图 用 Flask 来写个轻博客 (17) — MV(C)_应用蓝图来重构项目 用 Flask 来写个轻博客 (18) — 使用工厂模式来生成应用对象 用 Flask 来写个轻博客 (19) — 以 Bcrypt 密文存储账户信息与实现用户登陆表单 用 Flask 来写个轻博客 (20) — 实现注册表单与应用 reCAPTCHA 来实现验证码 用 Flask 来写个轻博客 (21) — 结合 reCAPTCHA 验证码实现用户注册与登录 用 Flask 来写个轻博客 (22) — 实现博客文章的添加和编辑页面 用 Flask 来写个轻博客 (23) — 应用 OAuth 来实现 Facebook 第三方登录 用 Flask 来写个轻博客 (24) — 使用 Flask-Login 来保护应用安全 用 Flask 来写个轻博客 (25) — 使用 Flask-Principal 实现角色权限功能 用 Flask 来写个轻博客 (26) — 使用 Flask-Celery-Helper 实现异步任务 用 Flask 来写个轻博客 (27) — 使用 Flask-Cache 实现网页缓存加速 用 Flask 来写个轻博客 (29) — 使用 Flask-Admin 实现后台管理 SQLAlchemy 扩展阅读 Flask项目集成富文本编辑器CKeditor 实现文章管理功能 该功能是在 ModelView 提供 CRUD 管理的基础上实现, 但是仍然存在一个问题, 就是 Flask-Admin 默认使用的文本编辑字段为 textarea, 这无法满足博客文章的样式需求. 所以我们这次来看看怎样替换一个默认的字段类型. 创建一个新的字段类型 vim jmilkfansblog/forms.py from wtforms import ( widgets, StringField, TextField, TextAreaField, PasswordField, BooleanField, ValidationError ) class CKTextAreaField(TextAreaField): """Create a new Field type.""" # Add a new widget `CKTextAreaField` inherit from TextAreaField. widget = CKTextAreaWidget() class CKTextAreaWidget(widgets.TextArea): """CKeditor form for Flask-Admin.""" def __call__(self, field, **kwargs): """Define callable type(class).""" # Add a new class property ckeditor: `<input class=ckeditor ...>` kwargs.setdefault('class_', 'ckeditor') return super(CKTextAreaWidget, self).__call__(field, **kwargs) NOTE 1: 新建的 CKTextAreaField 字段类型继承了 TextAreaField, 唯一的区别在于, CKTextAreaField 增加了一个 widget 的小部件, widget 的意义在于为该字段的 HTML 标签增加一个 class EG. <input class=ckedior ...>. NOTE 2: widget 是由 class CKTestAreaWidget 返回的, 该类作为的唯一一件事情就是将 HTML 标签中的 class 的值设定为 ckedior, EG. <textarea name="editor" id="editor" class="ckeditor" rows="10" cols="80"> 这样的话, Ckeditor 就会将原默认的 TextArea(editor) 给替换掉. 定义一个 PostView 类 vim jmilkfansblog/controllers/admin.py class PostView(CustomModelView): """View function of Flask-Admin for Post create/edit Page includedin Models page""" # Using the CKTextAreaField to replace the Field name is `test` form_overrides = dict(text=CKTextAreaField) # Using Search box column_searchable_list = ('text', 'title') # Using Add Filter box column_filters = ('publish_date',) # Custom the template for PostView # Using js Editor of CKeditor create_template = 'admin/post_edit.html' edit_template = 'admin/post_edit.html' NOTE 1: form_overrides 指定使用新的字段类型 CKTextAreaField 替换原来的 TextAreaField. NOTE 2: column_searchable_list 指定一个搜索框, 和搜索的范围为 post.text/post.title NOTE 3: column_filters 指定一个过滤器, 筛选更加精确的值 NOTE 4: create_template/edit_template 指定自定义模板文件 创建自定义模板 vim jmilkfansblog/templates/admin/post_edit.html {% extends 'admin/model/edit.html' %} {% block tail %} {{ super() }} <script src="//cdn.ckeditor.com/4.4.7/standard/ckeditor.js"></script> {% endblock %} NOTE 1: 该自定义模板继承了 admin/model/edit.html, 但却替换了 {% block tail %} 为一个 CKEditor. 实现效果 文章列表 创建/编辑文章
2022年03月