<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; IaaS</title>
	<atom:link href="https://blog.gmem.cc/category/work/cloud/iaas/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Terraform快速参考</title>
		<link>https://blog.gmem.cc/terraform</link>
		<comments>https://blog.gmem.cc/terraform#comments</comments>
		<pubDate>Wed, 20 Oct 2021 02:15:51 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[IaaS]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=38551</guid>
		<description><![CDATA[<p>简介 Terraform用于实现基础设施即代码（infrastructure as code）—— 通过代码（配置文件）来描述基础设施的拓扑结构，并确保云上资源和此结构完全对应。Terraform有三个版本，我们主要关注Terraform CLI。 Terraform CLI主要包含以下组件： 命令行前端 Terraform Language（以下简称TL，衍生自HashiCorp配置语言HCL）编写的、描述基础设施拓扑结构的配置文件。配置文件的组织方式是模块。本文使用术语“配置”（Configuration）来表示一整套描述基础设施的Terraform配置文件 针对各种云服务商的驱动（Provider），实现云资源的创建、更新和删除 云上资源不单单包括基础的IaaS资源，还可以是DNS条目、SaaS资源。事实上，通过开发Provider，你可以用Terraform管理任何资源。 Terraform会检查配置文件，并生成执行计划。计划描述了那些资源需要被创建、修改或删除，以及这些资源之间的依赖关系。Terraform会尽可能并行的对资源进行变更。当你更新了配置文件后，Terraform会生成增量的执行计划。 命令行 安装命令行 直接到https://www.terraform.io/downloads.html下载，存放到$PATH下即可。 基本特性 切换工作目录 使用选项 [crayon-69e2a559242d1488343462-i/] Shell自动完成 使用[crayon-69e2a559242d7282401664-i/]安装自动完成脚本，使用[crayon-69e2a559242d9401270932-i/]删除自动完成脚本。 <a class="read-more" href="https://blog.gmem.cc/terraform">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/terraform">Terraform快速参考</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1" style="padding-left: 30px;"><span class="graybg">简介</span></div>
<p>Terraform用于实现基础设施即代码（infrastructure as code）—— <span style="background-color: #c0c0c0;">通过代码（配置文件）来描述基础设施的拓扑结构</span>，并确保云上资源和此结构完全对应。Terraform有三个版本，我们主要关注Terraform CLI。</p>
<p>Terraform CLI主要包含以下组件：</p>
<ol>
<li>命令行前端</li>
<li>Terraform Language（以下简称TL，衍生自HashiCorp配置语言HCL）编写的、描述基础设施拓扑结构的配置文件。配置文件的组织方式是模块。本文使用术语“配置”（Configuration）来表示一整套描述基础设施的Terraform配置文件</li>
<li>针对各种云服务商的驱动（Provider），实现云资源的创建、更新和删除</li>
</ol>
<p>云上资源不单单包括基础的IaaS资源，还可以是DNS条目、SaaS资源。事实上，<span style="background-color: #c0c0c0;">通过开发Provider，你可以用Terraform管理任何资源</span>。</p>
<p>Terraform会检查配置文件，并生成执行计划。<span style="background-color: #c0c0c0;">计划描述了那些资源需要被创建、修改或删除，以及这些资源之间的依赖关系</span>。Terraform会<span style="background-color: #c0c0c0;">尽可能并行</span>的对资源进行变更。当你更新了配置文件后，Terraform会生成增量的执行计划。</p>
<div class="blog_h1"><span class="graybg">命令行</span></div>
<div class="blog_h2"><span class="graybg">安装命令行</span></div>
<p>直接到<a href="https://www.terraform.io/downloads.html">https://www.terraform.io/downloads.html</a>下载，存放到$PATH下即可。</p>
<div class="blog_h2"><span class="graybg">基本特性</span></div>
<div class="blog_h3"><span class="graybg">切换工作目录</span></div>
<p>使用选项 <pre class="crayon-plain-tag">-chdir=DIR</pre></p>
<div class="blog_h3"><span class="graybg">Shell自动完成</span></div>
<p>使用<pre class="crayon-plain-tag">terraform -install-autocomplete</pre>安装自动完成脚本，使用<pre class="crayon-plain-tag">terraform -uninstall-autocomplete</pre>删除自动完成脚本。</p>
<div class="blog_h2"><span class="graybg">资源地址</span></div>
<p>很多子命令接受资源地址参数，下面是一些例子：</p>
<pre class="crayon-plain-tag"># 资源类型.资源名
aws_instance.foo
# 资源类型.资源列表名[索引]
aws_instance.bar[1]
# 子模块foo的子模块bar中的
module.foo.module.bar.aws_instance.baz</pre>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>配置文件的路径可以通过环境变量<pre class="crayon-plain-tag">TF_CLI_CONFIG_FILE</pre>设置。非Windows系统中，<pre class="crayon-plain-tag">$HOME/.terraformrc</pre>为默认配置文件路径。配置文件语法类似于TF文件：</p>
<pre class="crayon-plain-tag"># provider缓存目录
plugin_cache_dir   = "$HOME/.terraform.d/plugin-cache"
# 
disable_checkpoint = true

# 存放凭证信息，包括模块仓库、支持远程操作的系统的凭证
credentials "app.terraform.io" {
  token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}

# 改变默认安装逻辑
provider_installation {
  # 为example.com提供本地文件系统镜像，这样安装example.com/*/*的provider时就不会去网络上请求
  # 默认路径是：
  # ~/.terraform.d/plugins/${host_name}/${namespace}/${type}/${version}/${target}
  # 例如：
  # ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.3.1/linux_amd64/terraform-provider-hashicups_v0.3.1
  filesystem_mirror {
    path    = "/usr/share/terraform/providers"
    include = ["example.com/*/*"]
  }
  direct {
    exclude = ["example.com/*/*"]
  }
  
  # Terraform会在terraform init的时候，校验Provider的版本和checksum。Provider从Registry或者本地
  # 目录下载Provider。当我们开发Provider的时候，常常需要方便的测试临时Provider版本，这种Provider还
  # 没有关联版本号，也没有在Registry中注册Chencksum
  # 为了简化开发，可以配置dev_overrides，它能覆盖所有配置的安装方法
  dev_overrides {
    "hashicorp.com/edu/hashicups-pf" = "$(go env GOBIN)"
  }
}</pre>
<div class="blog_h2"><span class="graybg">init</span></div>
<p>配置工作目录，为使用其它命令做好准备。</p>
<p>Terraform命令需要在一个编写了Terraform配置文件的目录（配置根目录）下执行，它会在此目录下存储设置、缓存插件/模块，以及（默认使用Local后端时）存储状态数据。此目录必须进行初始化。</p>
<p>初始化后，会生成以下额外目录/文件：</p>
<p style="padding-left: 30px;">.terraform目录，用于缓存provider和模块<br />如果使用Local后端，保存状态的terraform.tfstate文件。如果使用多工作区，则是terraform.tfstate.d目录。</p>
<p>对配置的某些变更，需要<span style="background-color: #c0c0c0;">重新运行初始化，包括provider需求的变更、模块源/版本约束的变更、后端配置的变更</span>。需要重新初始化时，其它命令可能会无法执行并提示你进行初始化。</p>
<p>命令<pre class="crayon-plain-tag">terraform get</pre>可以仅仅下载依赖的模块，而不执行其它init子任务。</p>
<p>运行 <pre class="crayon-plain-tag">terraform init -upgrade</pre>会强制拉取最新的、匹配约束的版本并更新依赖锁文件。</p>
<div class="blog_h2"><span class="graybg">validate</span></div>
<p>校验配置是否合法。</p>
<div class="blog_h2"><span class="graybg">plan</span></div>
<p>显示执行计划，即当前配置将请求（结合state）哪些变更。Terraform的核心功能时创建、修改、删除基础设施对象，使基础设施的状态和当前配置匹配。当我们说运行Terraform时，主要是指plan/apply/destroy这几个命令。</p>
<p>terraform plan命令评估当前配置，确定其声明的所有资源的期望状态。然后比较此期望状态和真实基础设施的当前状态。它<span style="background-color: #c0c0c0;">使用state来确定哪些真实基础设施对象和声明资源的对应关系，并且使用provider的API查询每个资源的当前状态</span>。当确定到达期望状态需要执行哪些变更后，Terraform将其打印到控制台，它并不会执行任何实际的变更操作。</p>
<div class="blog_h3"><span class="graybg">保存计划</span></div>
<p>terraform plan命令得到的计划可以保存起来，并被后续的terraform apply使用：</p>
<pre class="crayon-plain-tag">terraform plan -out=FILE </pre>
<div class="blog_h3"><span class="graybg">计划模式</span></div>
<p>plan命令支持两种备选的工作模式：</p>
<ol>
<li>销毁模式：创建一个计划，其目标是销毁所有当前存在于配置中的远程对象，留下一个空白的state。对应选项<pre class="crayon-plain-tag">-destroy</pre></li>
<li>仅刷新模式：创建一个计划，其目标仅仅是<span style="background-color: #c0c0c0;">更新state和根模块的输出值</span>，以便<span style="background-color: #c0c0c0;">和从Terraform之外对基础设施对象的变更匹配</span>。对应选项<pre class="crayon-plain-tag">-refresh-only</pre></li>
</ol>
<div class="blog_h3"><span class="graybg">指定输入变量</span></div>
<p>使用选项<pre class="crayon-plain-tag">-var 'NAME=VALUE'</pre>可以指定输入变量，该选项可以使用多次。</p>
<p>使用选项<pre class="crayon-plain-tag">-var-file=FILENAME</pre>可以从文件读取输入变量，某些文件会自动读取，参考<a href="#input-vars">输入变量</a>一节。</p>
<div class="blog_h3"><span class="graybg">并发度</span></div>
<p>选项<pre class="crayon-plain-tag">-parallelism=n</pre>限制操作最大并行度，默认10。</p>
<div class="blog_h3"><span class="graybg">其它选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-refresh=false</td>
<td>
<p>默认情况下，Terraform在检查配置变更之前，会将state和远程基础设施进行同步。此选项禁用此行为</p>
<p>该选项可能会提升性能，因为减少了远程API请求数量。但是可能会无法识别某些Terraform外部对基础设施资源的变更</p>
</td>
</tr>
<tr>
<td>-replace=ADDRESS</td>
<td>
<p>提示Terraform去计划替换掉具有指定ADDRESS的单个资源。对于0.15.2+可用，老版本可以使用terraform taint命令代替</p>
<p>ADDRESS就是针对某个资源实例的引用表达式，例如<pre class="crayon-plain-tag">aws_instance.example[0]</pre></p>
</td>
</tr>
<tr>
<td>-target=ADDRESS</td>
<td>提示Terraform仅仅针对指定ADDRESS的资源（以及它依赖的资源）指定执行计划</td>
</tr>
<tr>
<td>-input=false</td>
<td>禁止Terraform交互式的提示用户提供根模块的输入变量，对于批处理方式运行Terraform很重要</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">apply</span></div>
<p>应用执行计划，创建、更新设施对象。</p>
<p>apply会做plan的任何事情，并在其基础上，直接执行变更操作。默认情况下，apply即席的执行一次plan，你也可以直接使用已保存的plan</p>
<p>命令格式： <pre class="crayon-plain-tag">terraform apply [options] [plan file]</pre></p>
<div class="blog_h3"><span class="graybg">自动确认</span></div>
<p>选项<pre class="crayon-plain-tag">-auto-approve</pre>可以自动确认并执行所需操作，不需要人工确认。</p>
<div class="blog_h3"><span class="graybg">使用已有计划</span></div>
<p>如果指定plan file参数，则读取先前保存的计划并执行。</p>
<div class="blog_h3"><span class="graybg">计划模式</span></div>
<p>支持plan命令中关于计划模式的选项。</p>
<div class="blog_h3"><span class="graybg">其它选项</span></div>
<p>-input=false、-parallelism=n等选项含义和plan命令相同。特有选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-lock-timeout=DURATION</td>
<td>对状态加锁的最大时间</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">destroy</span></div>
<p>删除先前创建的基础设施对象。</p>
<p>当前配置（+工作区）管理的所有资源都会被删除，destroy会使用状态数据确定哪些资源需要删除。</p>
<div class="blog_h2"><span class="graybg">console</span></div>
<p>在交互式命令中估算Terraform表达式。</p>
<div class="blog_h2"><span class="graybg">fmt</span></div>
<p>格式化配置文件</p>
<div class="blog_h2"><span class="graybg">force-unlock</span></div>
<p>强制解除当前工作区的状态锁。当其它terraform进程锁定状态后，没有正常解锁时使用。如果其它进程仍然在运作，可能导致状态不一致。</p>
<div class="blog_h2"><span class="graybg">get</span></div>
<p>安装或升级远程Terraform模块。格式：<pre class="crayon-plain-tag">terraform get [options] PATH</pre>。</p>
<p>选项：</p>
<p style="padding-left: 30px;">-update 检查已经下载的模块的新版本，如果存在匹配约束的新版本则更新<br />-no-color 禁用彩色输出</p>
<div class="blog_h2"><span class="graybg">graph</span></div>
<p>生成操作中包含步骤的图形化表示。</p>
<div class="blog_h2"><span class="graybg">import</span></div>
<p>导入现有的基础设施对象，让其关联到配置中的资源定义。</p>
<div class="blog_h2"><span class="graybg">login</span></div>
<p>获取并保存远程服务（例如模块私服）的登录凭证。</p>
<div class="blog_h2"><span class="graybg">logout</span></div>
<p>删除远程服务（例如模块私服）的登录凭证。</p>
<div class="blog_h2"><span class="graybg">output</span></div>
<p>显示根模块的输出值。格式：<pre class="crayon-plain-tag">terraform output [options] [NAME]</pre></p>
<div class="blog_h2"><span class="graybg">providers</span></div>
<p>显示此模块依赖的providers</p>
<div class="blog_h2"><span class="graybg">refresh</span></div>
<p>更新状态，使其和远程基础设施匹配。</p>
<div class="blog_h2"><span class="graybg">show</span></div>
<p>显示当前状态或保存的执行计划。</p>
<div class="blog_h2"><span class="graybg">taint</span></div>
<p>将资源实例标记为“非功能完备（fully functional）”的。</p>
<p>所谓<span style="background-color: #c0c0c0;">非功能完备，通常意味着资源创建过程出现问题，存在部分失败</span>。此外taint子命令也可以强制将资源标记为非功能完备。</p>
<p>因为上述两种途径，进入tainted状态的资源，<span style="background-color: #c0c0c0;">不会立即影响基础设施对象。但是在下一次的plan中，会销毁并重新创建对应基础设施对象</span>。</p>
<p>命令格式：<pre class="crayon-plain-tag">terraform [global options] taint [options] &lt;address&gt;</pre></p>
<div class="blog_h2"><span class="graybg">untaint</span></div>
<p>解除资源的tainted状态。</p>
<div class="blog_h2"><span class="graybg">workspace</span></div>
<p>管理和切换工作区。</p>
<div class="blog_h1"><span class="graybg">TL语言</span></div>
<div class="blog_h2"><span class="graybg">块</span></div>
<p>配置文件由若干块（Block）组成，块的语法如下：</p>
<pre class="crayon-plain-tag"># Block header, which identifies a block
&lt;BLOCK TYPE&gt; "&lt;BLOCK LABEL&gt;" "&lt;BLOCK LABEL&gt;" "..." {
  # Block body
  &lt;IDENTIFIER&gt; = &lt;EXPRESSION&gt; # Argument
}</pre>
<p>块是一个容器，它的作用取决于块的类型。块常常用来描述某个资源的配置。</p>
<p>取决于块的类型，<span style="background-color: #c0c0c0;">标签的数量可以是0-N个</span>。对于resource块，标签数量为两个。某些特殊的块，可能支持任意数量的标签。某些内嵌的块，例如<span style="color: #1d1e23;">network_interface，则不支持标签。</span></p>
<p>块体中可以<span style="background-color: #c0c0c0;">包含若干参数（Argument），或者其它内嵌的块</span>。参数用于将一个表达式分配到一个标识符，常常对应某个资源的一条属性。表达式可以是字面值，或者引用其它的值，正是这种引用让Terraform能够识别资源依赖关系。</p>
<p>直接位于配置文件最外层的块，叫做顶级块（Top-level Block），<span style="background-color: #c0c0c0;">Terraform支持有限种类的顶级块</span>。大部分Terraform特性，例如resource，基于顶级块实现。</p>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">resource "aws_vpc" "main" {
  cidr_block = var.base_cidr_block
}</pre>
<div class="blog_h2"><span class="graybg">参数</span></div>
<p>在HCL语言中，Argument被称作Attribute。但是在TL中，Attribute术语另有它用。例如<span style="background-color: #c0c0c0;">各种resource都具有一个名为id的属性，它可以被参数表达式饮用，但是不能被赋值（因而不是参数）</span>。</p>
<p>参数其实一个赋值表达式，它将一个值分配给一个名称。</p>
<p>上下文决定了哪些参数可用，其类型是什么。不同的资源（由resource标签识别）支持不同的参数集。</p>
<div class="blog_h2"><span class="graybg">标识符</span></div>
<p>参数名、块类型名、大部分Terraform特有结构的名字（例如resource名，即其第二标签），都是标识符。</p>
<p>标识符由字母、数字、-、_组成，第一个字符不能是数字。</p>
<div class="blog_h2"><span class="graybg">注释</span></div>
<p><pre class="crayon-plain-tag">#</pre>或者<pre class="crayon-plain-tag">//</pre>开头的是单行注释。<pre class="crayon-plain-tag">/**/</pre>作为多行注释的边界。 </p>
<div class="blog_h2"><span class="graybg">数据类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">string</td>
<td>
<p style="text-align: left;">Unicode字符序列，基本形式 <pre class="crayon-plain-tag">"hello"</pre></p>
</td>
</tr>
<tr>
<td class=" blog_h3">number</td>
<td>数字，形式<pre class="crayon-plain-tag">6.02</pre></td>
</tr>
<tr>
<td class=" blog_h3">bool</td>
<td><pre class="crayon-plain-tag">true</pre>或<pre class="crayon-plain-tag">false</pre></td>
</tr>
<tr>
<td class=" blog_h3">list/tuple</td>
<td style="text-align: left;">一系列的值，形式<span style="color: #1d1e23;"><pre class="crayon-plain-tag">["us-west-1a", "us-west-1c"]</pre></span></td>
</tr>
<tr>
<td class=" blog_h3">map/object</td>
<td style="text-align: left;">键值对，形式<pre class="crayon-plain-tag">{name = "Mabel", age = 52}</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">空值</span></div>
<p style="text-align: left;">空值使用<pre class="crayon-plain-tag">null</pre>表示。</p>
<div class="blog_h2"><span class="graybg">字符串和模板</span></div>
<div class="blog_h3"><span class="graybg">转义字符</span></div>
<p>转义字符：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">\n</pre> 换行<br /><pre class="crayon-plain-tag">\r</pre> 回车<br /><pre class="crayon-plain-tag">\t</pre> 制表<br /><pre class="crayon-plain-tag">\"</pre> 引号<br /><pre class="crayon-plain-tag">\\</pre> 反斜杠<br /><pre class="crayon-plain-tag">\uNNNN</pre> Unicode字符<br /><pre class="crayon-plain-tag">\UNNNNNNNN</pre> Unicode字符</p>
<p>注意，在Heredoc中反斜杠不用于转义，可以使用：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">$${</pre> 字符串插值标记${<br /><pre class="crayon-plain-tag">%%{</pre> 模板指令标记%{</p>
<div class="blog_h3"><span class="graybg">Heredoc</span></div>
<p>支持Unix的Heredoc风格的字符串：</p>
<pre class="crayon-plain-tag">block {
  value = &lt;&lt;EOT
hello
world
EOT
}</pre>
<p>Heredoc还支持缩进编写：</p>
<pre class="crayon-plain-tag">block {
  value = &lt;&lt;-EOT
  hello
    world
  EOT
}</pre>
<div class="blog_h3"><span class="graybg">JSON和YAML</span></div>
<p>要将对象转换为JSON或YAML，可以调用函数：</p>
<pre class="crayon-plain-tag">example = jsonencode({
    a = 1
    b = "hello"
})</pre>
<div class="blog_h3"><span class="graybg">字符串模板</span></div>
<p>不管在普通字符串格式，还是Heredoc中，都可以使用字符串模板。模板需要包围在<pre class="crayon-plain-tag">${</pre> 或<pre class="crayon-plain-tag">%{}</pre>中：</p>
<pre class="crayon-plain-tag"># ${ ... } 中包含的是表达式
"Hello, ${var.name}!"


# %{ ... } 则定义了一条模板指令，可以用于实现条件分支或循环

# %{if &lt;BOOL&gt;}/%{else}/%{endif} 条件分支
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"

# %{for &lt;NAME&gt; in &lt;COLLECTION&gt;} / %{endfor} 循环
&lt;&lt;EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT</pre>
<div class="blog_h3"><span class="graybg">空白符去除 </span></div>
<p style="text-align: left;">可以在模板指令开始或结尾添加<pre class="crayon-plain-tag">~</pre>，用于去除前导或后缀的空白符：</p>
<pre class="crayon-plain-tag">&lt;&lt;EOT
%{ for ip in aws_instance.example.*.private_ip ~} # 这后面的换行符被~去除
server ${ip}  # 这后面的换行符被保留
%{ endfor ~}
EOT</pre>
<div class="blog_h2"><span class="graybg">引用</span></div>
<div class="blog_h3"><span class="graybg">资源</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">&lt;RESOURCE TYPE&gt;.&lt;NAME&gt;</pre>引用指定类型、名称的资源。 其值可能是：</p>
<ol>
<li>如果资源没有count/for_each参数，那么值是一个object，可以访问资源的属性</li>
<li>如果资源使用count参数，那么值是list</li>
<li>如果资源使用for_each参数，那么值是map</li>
</ol>
<div class="blog_h3"><span class="graybg">输入变量</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">var.&lt;NAME&gt;</pre>引用指定名称的输入变量。如果变量使用type参数限定其类型，对此引用进行赋值时，Terraform会自动进行必要的类型转换。</p>
<div class="blog_h3"><span class="graybg">本地值</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">local.&lt;NAME&gt;</pre>引用指定名称的本地值。本地值可以引用其它本地值，<span style="background-color: #c0c0c0;">甚至是在同一个local块中，唯一的限制就是不得出现循环依赖</span>。</p>
<div class="blog_h3"><span class="graybg">子模块输出</span></div>
<p><pre class="crayon-plain-tag">module.&lt;MODULE NAME&gt;</pre>引用指定子模块声明的结果输出。如果module块（表示对目标模块的调用）没有count/for_each参数，则其值是一个object，可以直接访问某个输出值：<pre class="crayon-plain-tag">module.&lt;MODULE NAME&gt;.&lt;OUTPUT NAME&gt;</pre></p>
<p style="text-align: left;">如果module块使用了for_each，则引用的值是一个map，其key是for_each中的每个key，其值是子模块输出。</p>
<p style="text-align: left;">如果module块使用了count，则引用的值是一个list，其元素是子模块输出。</p>
<div class="blog_h3"><span class="graybg">数据源</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">data.&lt;DATA TYPE&gt;.&lt;NAME&gt;</pre>引用指定类型的数据资源，取决于数据资源是否使用count/foreach，其值可能是object/list/map。</p>
<div class="blog_h3"><span class="graybg">文件系统信息</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">path.module</pre> 表达式所在模块的路径。</p>
<p style="text-align: left;"><pre class="crayon-plain-tag">path.root</pre>表示当前配置根模块的路径。</p>
<p style="text-align: left;"><pre class="crayon-plain-tag">path.cwd</pre> 表示当前工作模块的路径，默认情况下和path.root一样。但是Terraform CLI可以指定不同的工作目录。</p>
<div class="blog_h3"><span class="graybg">工作区信息</span></div>
<p style="text-align: left;"><pre class="crayon-plain-tag">terraform.workspace</pre>是当前选择的工作区的名称。</p>
<div class="blog_h3"><span class="graybg">块内特殊引用</span></div>
<p style="text-align: left;">在特定的块内部，在特定的上下文下（例如使用count/for_each的情况下），可以引用一些特殊值：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">count.index</pre> 使用count原参数时，表示当前索引<br /><pre class="crayon-plain-tag">each.key</pre> / <pre class="crayon-plain-tag">each.value</pre> 使用for_each原参数时，表示当前迭代的键值<br /><pre class="crayon-plain-tag">self</pre>可以在provisioner和connection块中使用，表示当前上下文资源</p>
<div class="blog_h2"><span class="graybg">操作符</span></div>
<p style="text-align: left;">逻辑操作符：<pre class="crayon-plain-tag">!</pre> <pre class="crayon-plain-tag">&amp;&amp;</pre> <pre class="crayon-plain-tag">||</pre><br />算数操作符：<pre class="crayon-plain-tag">*</pre> <pre class="crayon-plain-tag">/</pre> <pre class="crayon-plain-tag">%</pre> <pre class="crayon-plain-tag">+</pre> <pre class="crayon-plain-tag">-</pre><br />比较操作符：<pre class="crayon-plain-tag">&gt;, &gt;=, &lt;, &lt;= ==, !=</pre></p>
<p style="color: #000000;">函数调用：</p>
<pre class="crayon-plain-tag">&lt;FUNCTION NAME&gt;(&lt;ARGUMENT 1&gt;, &lt;ARGUMENT 2&gt;)

# 支持参数展开
min([55, 2453, 2]...)</pre>
<div class="blog_h2"><span class="graybg">条件表达式</span></div>
<pre class="crayon-plain-tag">condition ? true_val : false_val

var.a != "" ? var.a : "default-a"</pre>
<div class="blog_h2"><span class="graybg">for表达式</span></div>
<p>使用for表达式可以通过转换一种复杂类型输出，生成另一个复杂类型结果。<span style="background-color: #c0c0c0;">输入中的每个元素，可以对应结果的0-1个元素</span>。任何表达式可以用于转换，下面是使用upper函数将列表转换为大写：</p>
<pre class="crayon-plain-tag">[for s in var.list : upper(s)]</pre>
<div class="blog_h3"><span class="graybg">输入类型</span></div>
<p>作为for表达式的输入的类型可以是list / set / tuple / map / object。可以为for声明两个临时符号，前一个表示index或key：</p>
<pre class="crayon-plain-tag">[for k, v in var.map : length(k) + length(v)]</pre>
<div class="blog_h3"><span class="graybg">结果类型 </span></div>
<p>结果的类型取决于包围for表达式的定界符：</p>
<p style="padding-left: 30px;">[] 表示生成的结果是元组<br />{} 表示生成的结果是object，你必须使用<pre class="crayon-plain-tag">=&gt;</pre>符号：<pre class="crayon-plain-tag">{for s in var.list : s =&gt; upper(s)}</pre></p>
<div class="blog_h3"><span class="graybg">输入过滤</span></div>
<p style="text-align: left;">包含一个可选的if子句可以对输入元素进行过滤：<pre class="crayon-plain-tag">[for s in var.list : upper(s) if s != ""]</pre></p>
<p>示例：</p>
<pre class="crayon-plain-tag">variable "users" {
  type = map(object({
    is_admin = boolean
  }))
}

locals {
  admin_users = {
    for name, user in var.users : name =&gt; user
    if user.is_admin
  }
}</pre>
<div class="blog_h3"><span class="graybg">元素顺序</span></div>
<p>for表达式可能将无序类型（map/object/set）转换为有序类型（list/tuple）：</p>
<p style="padding-left: 30px;">对于map/object，键/属性名的字典序，决定结果的顺序<br />对于set，如果值是字符串，则使用其字典序。如果值是其它类型，则结果的顺序可能随着Terraform的版本改变</p>
<div class="blog_h3"><span class="graybg">结果分组</span></div>
<p style="text-align: left;">如果输出是对象，通常要求键的唯一性。Terraform支持分组模式，允许键重复。要激活此模式，在表达式结尾添加<pre class="crayon-plain-tag">...</pre>： </p>
<pre class="crayon-plain-tag">variable "users" {
  type = map(object({
    role = string
  }))
}

locals {
  users_by_role = {
    for name, user in var.users : user.role =&gt; name...
  }
}</pre>
<div class="blog_h2"><span class="graybg">dynamic块</span></div>
<p>对于顶级块，表达式仅能在给参数赋值的时候，用在右侧。某些情况下，我们需要在特定条件下，<span style="background-color: #c0c0c0;">重复、启用/禁用某个子块</span>，表达式就没法实现了，此时可以利用dynamic块。</p>
<p>dynamic块可以遍历一个列表，为每个元素生成一个块：</p>
<pre class="crayon-plain-tag">resource "aws_elastic_beanstalk_environment" "tfenvtest" {
  name                = "tf-test-name"
  application         = "${aws_elastic_beanstalk_application.tftest.name}"
  solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"

  # 对于每个设置，生成一个setting子块。dynamic块的标签，对应生成的块类型
  dynamic "setting" {
    # 迭代对象
    for_each = var.settings
    # 子块的标签，可选
    labels: []
    # 子块的体（参数）
    content {
      namespace = setting.value["namespace"] # 块类型.key对应映射的键或者列表的索引，.value对应当前迭代的值
      name = setting.value["name"]
      value = setting.value["value"]
    }
  }
}</pre>
<p>注意dynamic块仅可能为resource、data、provider、provisioner块生成参数（子块），不能用于生成源参数块，例如lifecycle。</p>
<div class="blog_h3"><span class="graybg">多级嵌套</span></div>
<p>允许dynamic块的多级嵌套。</p>
<div class="blog_h2"><span class="graybg">splat表达式</span></div>
<p>splat表达式提供了更简单语法，在某些情况下代替for表达式：</p>
<pre class="crayon-plain-tag">[for o in var.list : o.id]
# 等价于
var.list[*].id

[for o in var.list : o.interfaces[0].name]
# 等价于
var.list[*].interfaces[0].name</pre>
<p>注意splat表达式仅仅用于列表，不能用于map/object</p>
<p>splat表达式还有个特殊用途，将单值转换为列表：</p>
<pre class="crayon-plain-tag">for_each = var.website[*]</pre>
<div class="blog_h2"><span class="graybg">类型约束</span></div>
<p>module/provider的作者可以利用类型约束来校验用户输入。Terraform的类型系统比较强大，比如他不但可以限制类型为map，还可以规定其中有哪些键，每个键的值是什么类型。</p>
<div class="blog_h3"><span class="graybg">复杂类型</span></div>
<p>复杂类型可以将多个值组合为单个值，复杂类型由类型构造器（<span style="color: #1d1e23;">type constructor）定义，复杂类型分为两类：</span></p>
<ol>
<li>集合类型：组合相似（类型）的值</li>
<li>结构类型：组合可能不同的值</li>
</ol>
<div class="blog_h3"><span class="graybg">集合类型</span></div>
<p>集合类型包括map/list/set等，我们可以限制集合的成员类型：</p>
<pre class="crayon-plain-tag"># 字符串列表
list(string)
# 数字列表
list(number)

# 任意元素列表，等价于list
list(any)</pre>
<div class="blog_h3"><span class="graybg">结构类型</span></div>
<p>结构类型包括object和tuple。</p>
<p>object定义了多个命名的属性，以及属性的类型：</p>
<pre class="crayon-plain-tag">object( { &lt;KEY&gt; = &lt;TYPE&gt;, &lt;KEY&gt; = &lt;TYPE&gt;, ... } )

# 示例
object({ name=string, age=number })

# 下面是符合此类型的实例
{
  name = "John"
  age  = 52
}</pre>
<p>元组则是定义了限定元素个数、每个元素类型的列表： </p>
<pre class="crayon-plain-tag">tuple([string, number, bool])
# 下面是符合此类型的实例
["a", 15, true]</pre>
<div class="blog_h3"><span class="graybg">复杂类型转换 </span></div>
<p>大部分情况下，<span style="background-color: #c0c0c0;">相似的集合类型和结构类型的行为类似</span>。Terraform文档某些时候也不去区分，这是由于以下类型转换行为：</p>
<ol>
<li>可能的情况下，Terraform会自动在相似复杂类型之间进行转换：
<ol>
<li>object和map是相似的，只要map包含object的schema所要求的键集合，即可自动转换。多余的键在转换过程中被丢弃，这意味着map - object - mapl两重转换可能丢失信息</li>
<li>tuple和list是相似的，但是转换仅仅在list元素数量恰好满足tuple的schema时发生</li>
<li>set和tuple/list是相似的：
<ol>
<li>当list/tuple转换为set，重复的值会被丢弃，元素顺序消失</li>
<li>当set转换为list/tuple，元素的顺序是任意的，一个例外是set(string)，将会按照元素字典序生成list/tuple</li>
</ol>
</li>
</ol>
</li>
<li>可能的情况下，Terraform会自动转换复杂类型的元素的类型，如果元素是复杂类型，则递归的处理</li>
</ol>
<p>每当提供的值，和要求的值类型不一致时，自动转换都会发生。</p>
<p>module/provider的作者应该注意不同类型的差别，特别是在限制输入方面能力的不同。</p>
<div class="blog_h3"><span class="graybg">动态类型any</span></div>
<p>特殊关键字any用做尚未决定的类型的占位符，其本身并非一个类型。当解释类型约束的时候，Terraform会尝试寻找单个的实际类型，替换any关键字，并满足约束。</p>
<p>例如，对于list(any)这一类型约束，对于给定的值["a", "b", "c"]其实际类型是tuple([string, string, string])，当将该值赋值给list(any)变量时，Terraform分析过程如下：</p>
<ol>
<li>tuple和list是相似类型，因此应用上问的tuple-list转换规则</li>
<li>tuple的元素类型是string，满足any约束，因此将其替换，结果类型是list(string)</li>
</ol>
<div class="blog_h3"><span class="graybg">可选object属性</span></div>
<p>从0.14开始，支持这一实验特性。可以在object类型约束中，标注某个属性为可选的： </p>
<pre class="crayon-plain-tag">variable "with_optional_attribute" {
  type = object({
    a = string           # 必须属性
    b = optional(string) # 可选属性
  })
}</pre>
<p>默认情况下，对于必须属性来说，如果类型转换时的源（例如map）不具备对应的键，会导致报错。</p>
<div class="blog_h2"><span class="graybg">版本约束</span></div>
<p>版本约束是一个特殊的字符串值，在引用module、使用provider时，或者通过terraform块的required_version时，需要用到版本约束：</p>
<pre class="crayon-plain-tag"># 版本范围区间
version = "&gt;= 1.2.0, &lt; 2.0.0"

# 操作符
=   等价于无操作符，限定特定版本
!=  排除特定版本
&gt; &gt;= &lt; &lt;= 限制版本范围
~&gt;  允许最右侧的版本号片段的变化</pre>
<div class="blog_h1"><span class="graybg">函数</span></div>
<p>所有可用函数：<a href="https://www.terraform.io/docs/language/functions/index.html">https://www.terraform.io/docs/language/functions/index.html</a></p>
<div class="blog_h1"><span class="graybg">资源(resource)</span></div>
<p>资源是TL语言中最重要的元素，由resource块定义。正如其名字所示，资源声明了某种云上基础设施的规格，这些基础设施可以是虚拟机、虚拟网络、DNS记录，等等。</p>
<p>数据源是一种特殊的资源，由data块定义。</p>
<div class="blog_h2"><span class="graybg">行为</span></div>
<p>当你第一次为某个资源编写配置时，它值存在于配置文件中，尚未代表云上的某个真实基础设施对象。通过应用Terraform配置，触发创建/更新/销毁等操作，实现云上环境和配置文件的匹配。</p>
<div class="blog_h3"><span class="graybg">生命周期</span></div>
<p>当一个新的资源被创建后，对应真实基础设施对象的标识符被保存到Terraform的State中。这个标识符作为后续更新/删除的依据。对于State中已经存在关联的标识符的那些Resource块，Terraform会比较真实基础设施对象和Resource参数的区别，并在必要的时候更新对象。</p>
<p>概括起来说，当Terraform配置被应用时：</p>
<ol>
<li>创建存在于配置文件中，但是在State中没有关联真实基础设施对象的资源</li>
<li>销毁存在于State中，但是不存在于配置文件的资源</li>
<li>更新参数发生变化的资源</li>
<li><span style="background-color: #c0c0c0;">删除、重新创建参数发生变化，但是不能原地（in-palce）更新的资源</span>。不能更新通常是因为云API的限制，例如腾讯云VPC的CIDR不支持修改</li>
</ol>
<p>以上4点，适用于所有资源类型。但是需要注意，底层发生的事情，取决于Provider，Terraform只是去调用Provider的相应接口。</p>
<div class="blog_h3"><span class="graybg">访问资源属性</span></div>
<p>在相同模块中，你可以在表达式中访问某个资源的属性，语法<pre class="crayon-plain-tag">&lt;RESOURCE TYPE&gt;.&lt;NAME&gt;.&lt;ATTRIBUTE&gt;</pre></p>
<p>除了配置文件中列出的资源参数之外，资源还提供一些<span style="background-color: #c0c0c0;">只读的属性</span>。属性表示了一些提供云API拉取到的信息。这些信息通畅需要在资源创建后才可获知，例如随机生成的资源ID vpc-d8o3c8vq</p>
<p>很多Provider还<span style="background-color: #c0c0c0;">提供特殊的数据源（data）资源，这种特殊的资源仅仅用来查询信息</span>。</p>
<div class="blog_h3"><span class="graybg">资源依赖关系</span></div>
<p>某些资源必须在另外一些资源之后创建，例如CVM必须在其所属Subnet创建后才能创建，这意味着某些资源存在依赖关系。</p>
<p>Terraform能够自动分析资源依赖关系，其分析依据就是资源的参数的值表达式。<span style="background-color: #c0c0c0;">如果表达式中引用了另一个资源，则当前资源依赖于该资源</span>。</p>
<p>对于无法通过配置文件分析的隐含依赖，需要你手工配置<pre class="crayon-plain-tag">depends_on</pre>元参数。</p>
<p>Terraform会自动并行处理没有依赖关系的资源。</p>
<div class="blog_h3"><span class="graybg">Local-Only资源</span></div>
<p>这类特殊的资源不会对应某个云上基础设施对象，而是仅<span style="background-color: #c0c0c0;">仅存在于Terraform本地State中</span>。Local-Only资源用于一些中间计算过程，包括生成随机ID、创建私钥等。</p>
<p>Local-Only资源的<span style="background-color: #c0c0c0;">行为和普通资源一致</span>，只是其结果数据仅仅存在于State中，删除时也仅仅是从State中移除对应数据。</p>
<div class="blog_h2"><span class="graybg">语法</span></div>
<pre class="crayon-plain-tag">resource "resource_type" "local_name" {
  # arguments...
}</pre>
<p>两个标签分别代表资源的类型和本地名称。</p>
<p>资源类型提示正在描述的是那种云上基础设施，资源类型决定可用的参数集。<span style="background-color: #c0c0c0;">本地名称用于在模块的其它地方饮用该资源，此外没有意义</span>。资源类型+本地名称是资源的唯一标识，必须在模块范围内唯一。</p>
<div class="blog_h2"><span class="graybg">Provider</span></div>
<p>每一种资源都由一个Provider来实现。<span style="background-color: #c0c0c0;">Provider是Terraform的插件，它提供若干资源类型</span>。通常一个云服务商提供提供一个Provider。初始化工作目录时Terraform能够自动从Terraform仓库下载大部分所需的Provider。</p>
<p>模块需要知道，利用哪些Provider才能管理所有的资源。此外Provider还需要经过配置才能工作，例如设置访问云API的凭证。这些配置由根模块负责。</p>
<div class="blog_h2"><span class="graybg">元参数</span></div>
<p>元参数可以用于任何资源类型。</p>
<div class="blog_h3"><span class="graybg">depends_on</span></div>
<p>该元参数用于处理隐含的资源/模块依赖，这些依赖无法通过分析Terraform配置文件得到。从0.13版本开始，该元参数可用于模块。之前的版本仅仅用于资源。</p>
<p>depends_on的值是一个列表，其元素具必须是其它资源的引用，不支持任意表达式。</p>
<p>depends_on应当仅仅用作最后手段，避免滥用。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">resource "aws_iam_role" "example" {
  name = "example"
  assume_role_policy = "..."
}

# 这个策略允许运行在EC2中的实例访问S3 API
resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}


resource "aws_iam_instance_profile" "example" {
  # 这是可以自动分析出的依赖
  role = aws_iam_role.example.name
}


resource "aws_instance" "example" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # 这是可以自动分析出的依赖，包括传递性依赖
  iam_instance_profile = aws_iam_instance_profile.example

  # 如果这个实例中的程序需要访问S3接口，我们需要用元参数显式的声明依赖
  # 从而分配策略
  depends_on = [
    aws_iam_role_policy.example,
  ]
} </pre>
<div class="blog_h3"><span class="graybg">count</span></div>
<p>默认情况下，一个resource块代表单个云上基础设施对象。如果你想用一个resource块生成多个类似的资源，可以用count或for_each参数。</p>
<p>设置了此元参数的上下文中，可以访问名为<pre class="crayon-plain-tag">count</pre>的变量，它具有属性<pre class="crayon-plain-tag">index</pre>，为从0开始计数的资源实例索引。 示例：</p>
<pre class="crayon-plain-tag">resource "aws_instance" "server" {
  # 创建4个类似的实例
  count = 4

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  tags = {
    #              实例的索引作为tag的一部分
    Name = "Server ${count.index}"
  }
}</pre>
<p>在当前模块的其它地方，可以用这样的语法访问如上资源实例：</p>
<pre class="crayon-plain-tag">&lt;RESOURCE_TYPE&gt;.&lt;NAME&gt;[&lt;INDEX&gt;]

aws_instance.server[0]</pre>
<div class="blog_h3"><span class="graybg">for_each</span></div>
<p>如果资源的规格几乎完全一致，可以用count，否则，需要使用更加灵活的for_each元参数。</p>
<p><span style="background-color: #c0c0c0;">for_each的值必须是一个映射或set(string)</span>，你可以在上下文中访问<pre class="crayon-plain-tag">each</pre>对象， 它具有<pre class="crayon-plain-tag">key</pre>和<pre class="crayon-plain-tag">value</pre>两个属性，如果for_each的值是<span style="background-color: #c0c0c0;">集合，则key和value相等</span>。示例：</p>
<pre class="crayon-plain-tag">resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  # 对于每个键值对都会生成azurerm_resource_group资源
  name     = each.key
  location = each.value
}


resource "aws_iam_user" "the-accounts" {
  # 数组转换为集合
  for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
}</pre>
<p>调用子模块时使用for_each的示例：</p>
<pre class="crayon-plain-tag"># 父模块 my_buckets.tf
module "bucket" {
  for_each = toset(["assets", "media"])
  # 调用publish_bucket子模块
  source   = "./publish_bucket"
  # 将键赋值给子模块的name变量
  name     = "${each.key}_bucket"
}

# 子模块          文件名
# publish_bucket/bucket-and-cloudfront.tf

# 声明模块的输入参数
variable "name" {} 

resource "aws_s3_bucket" "example" {
  # 访问变量
  bucket = var.name
  # ...
}</pre>
<p>关于for_each的值的元素，有如下限制：</p>
<ol>
<li><span style="background-color: #c0c0c0;">键必须是确定的值</span>，如果应用配置前值无法确定会报错。例如，你不能引用CVM的ID，因为这个ID必须在配置应用之后才可知</li>
<li>如果<span style="background-color: #c0c0c0;">键</span>是函数调用，则此函数<span style="background-color: #c0c0c0;">不能是impure的（非幂等）</span>，impure函数包括uuid/bcrypt/timestamp等</li>
<li><span style="background-color: #c0c0c0;">敏感（Sensitive）值不能用做键值</span>。需要注意，大部分函数，在接受敏感值参数后，返回值仍然是敏感的</li>
</ol>
<p>当使用集合的时候，你必须明确的将值转换为集合。例如通过<pre class="crayon-plain-tag">toset</pre>函数将列表、元组转换为集合。使用<pre class="crayon-plain-tag">flatten</pre>函数可以将多层次的嵌套结构转换为列表。</p>
<p>for_each所在块定义的资源A，可以赋值给另一个资源B的for_each参数。这时，<span style="background-color: #c0c0c0;">B那个for_each的键值对，值是资源的完整（创建或更新过的）实例</span>。这种用法叫<span style="background-color: #c0c0c0;">chaining</span>：</p>
<pre class="crayon-plain-tag">variable "vpcs" {
  # 这里定义了map类型的变量，并且限定了map具有的键
  type = map(object({
    cidr_block = string
  }))
}


# 创建多个VPC资源
resource "aws_vpc" "example" {
  for_each = var.vpcs
  cidr_block = each.value.cidr_block
}
# 上述资源作为下面那个for_each的值

# 创建对应数量的网关资源
resource "aws_internet_gateway" "example" {
  # 为每个VPC创建一个网关
  #          资源作为值
  for_each = aws_vpc.example

  # 映射的值，在这里是完整的VPC对象
  vpc_id = each.value.id
}


# 输出所有VPC ID
output "vpc_ids" {
  value = {
    for k, v in aws_vpc.example : k =&gt; v.id
  }

  # 显式依赖网关资源，确保网关创建后，输出才可用
  depends_on = [aws_internet_gateway.example]
}</pre>
<p>引用for_each创建的资源的实例时，使用如下语法： </p>
<pre class="crayon-plain-tag">&lt;TYPE&gt;.&lt;NAME&gt;[&lt;KEY&gt;]

azurerm_resource_group.rg["a_group"]</pre>
<div class="blog_h3"><span class="graybg">provider</span></div>
<p>这个参数用于指定<span style="background-color: #c0c0c0;">使用的provider配置</span>。可以覆盖Terraform的<span style="background-color: #c0c0c0;">默认行为：将资源类型名的第一段（下划线分隔）作为provider的本地名称。同时使用provider的默认配置</span>。例如资源类型google_compute_instance默认识别为google这个provider。</p>
<p>每个provider可以提供多个配置，<span style="background-color: #c0c0c0;">配置常常用于管理多区域服务</span>中的某个特定Region。每个provider可以有一个默认配置。</p>
<p>该参数的值必须是不被引号包围的<pre class="crayon-plain-tag">&lt;PROVIDER&gt;.&lt;ALIAS&gt;</pre>。使用该参数你可以显式的指定provider及其配置：</p>
<pre class="crayon-plain-tag"># 默认配置
provider "google" {
  region = "us-central1"
}

# 备选配置
provider "google" {
  alias  = "europe"
  region = "europe-west1"
}

resource "google_compute_instance" "example" {
  # 通过使用该参数选择备选配置
  provider = google.europe

  # ...
}</pre>
<p>注意：<span style="background-color: #c0c0c0;">资源总是隐含对它的provider的依赖</span>，这确保创建资源前provider被配置好。 </p>
<div class="blog_h3"><span class="graybg">lifecycle</span></div>
<p>该参数可以对资源的生命周期进行控制。示例：</p>
<pre class="crayon-plain-tag">resource "azurerm_resource_group" "example" {
  # ...
  lifecycle {
    create_before_destroy = true
  }
}</pre>
<p>lifecycle作为一个块，只能出现在resources块内。可用参数包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>create_before_destroy</td>
<td>
<p>默认情况下， 当Terraform无法进行in-place更新时，会删除并重新创建资源</p>
<p>该参数可以修改此行为：先创建新资源，然后删除旧资源</p>
</td>
</tr>
<tr>
<td>prevent_destroy</td>
<td>如果设置为true，并且配置会导致基础设施中某个对象被删除，则报错</td>
</tr>
<tr>
<td>ignore_changes</td>
<td>
<p>默认情况下，Terraform会对比真实基础设施中对象和当前配置文件中的所有字段，任何字段的不一致都会引发更新操作</p>
<p>该参数指定，在Terraform评估是否需要更新时，资源的哪些字段被忽略</p>
<p>如果设置为特殊值<pre class="crayon-plain-tag">all</pre>，则Terraform不会进行任何更新操作</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">timeouts参数</span></div>
<p>某些资源类型提供了特殊的<pre class="crayon-plain-tag">timeouts</pre>内嵌块，用于指定多长时间后认定操作失败：</p>
<pre class="crayon-plain-tag">resource /* ... */ {
  # ...
  timeouts {
    create = "60m"
    update = "30s"
    delete = "2h"
  }
}</pre>
<div class="blog_h2"><span class="graybg">资源特定参数</span></div>
<p>资源的绝大部分参数由资源类型决定。需要翻阅Provider的文档了解哪些参数可用。</p>
<p>对于大部分公共的、托管在Terraform仓库的Provider来说，其文档可以直接在<a href="https://registry.terraform.io/browse/providers">仓库网站</a>上获得。</p>
<div class="blog_h2"><span class="graybg">Provisioner</span></div>
<p>对于一些无法使用Terraform声明式模型来表达的某些行为，可以使用<span style="background-color: #c0c0c0;">Provisioner作为最后（总是不推荐的）手段</span>。</p>
<p>使用Provisioner会引入复杂性和不确定性：</p>
<ol>
<li>Terraform无法将Provisioner执行的动作，作为计划的一部分。因为Provisioner理论上可以做任何事情</li>
<li>Provisioner通常需要使用更多细节信息，例如直接访问服务器的网络、使用Terraform的登录凭证</li>
</ol>
<div class="blog_h3"><span class="graybg">self对象</span></div>
<p>Provisioner块不能用名字访问其所在的上下文资源，你必须使用<pre class="crayon-plain-tag">self</pre>对象。这个对象就指代对应的资源，你可以访问它的参数和属性。</p>
<div class="blog_h3"><span class="graybg">when参数</span></div>
<p>该参数指定何时（create/update/destroy）运行Provisioner，下面是一个仅仅在删除资源时才执行的Provisioner：</p>
<pre class="crayon-plain-tag">resource "aws_instance" "web" {
  provisioner "local-exec" {
    # 在实际删除前调用
    when    = destroy
    command = "echo 'Destroy-time provisioner'"
  }
}</pre>
<p>默认情况下，Provisioner在资源创建的时候调用，在更新/删除时不会调用。最<span style="background-color: #c0c0c0;">常见的用法是使用Provisioner来初始化系统</span>。如果Provisioner失败，则资源被标记为tainted，并且会在下一次<pre class="crayon-plain-tag">terraform apply</pre>时删除、重新创建。</p>
<div class="blog_h3"><span class="graybg">on_failure参数</span></div>
<p>定制Provisioner失败时的行为：</p>
<ol>
<li><pre class="crayon-plain-tag">continue</pre> 忽略错误</li>
<li><pre class="crayon-plain-tag">fail</pre> 默认行为，导致配置应用立即失败，如果正在创建资源，则taint该资源</li>
</ol>
<div class="blog_h3"><span class="graybg">连接设置</span></div>
<p>大部分Provisioner要求通过SSH或WinRM来访问远程资源。你可以在<pre class="crayon-plain-tag">connection</pre>块中声明如何连接。connection块可以内嵌在以下位置：</p>
<ol>
<li>resource，对资源的所有Provisioner生效</li>
<li>provisioner，仅仅对当前Provisioner生效</li>
</ol>
<p>在connection块中，你也可以使用<pre class="crayon-plain-tag">self</pre>来访问包含它的resource，这一点类似于provisioner</p>
<p>示例：</p>
<pre class="crayon-plain-tag">provisioner "file" {
  # Linux
  connection {
    type     = "ssh"
    user     = "root"
    password = "${var.root_password}"
    host     = "${var.host}"
  }
}

provisioner "file" {
  # Windows
  connection {
    type     = "winrm"
    user     = "Administrator"
    password = "${var.admin_password}"
    host     = "${var.host}"
  }
}</pre>
<p>关于如何通过证书进行身份验证，如何通过堡垒机连接，参考<a href="https://www.terraform.io/docs/language/resources/provisioners/connection.html">官方文档</a>。 </p>
<div class="blog_h3"><span class="graybg">null_resource</span></div>
<p>如果你希望运行一个Provisioner，但是不想在任何真实的资源的上下文下运行，可以使用这种特殊的资源。</p>
<pre class="crayon-plain-tag">resource "aws_instance" "cluster" {
  count = 3

  # ...
}

resource "null_resource" "cluster" {
  # Provisioner的触发时机
  triggers = {
    # cluster中任何实例改变会触发Provisioner的重新执行
    #                                  这种通配符语法会得到一个列表
    cluster_instance_ids = "${join(",", aws_instance.cluster.*.id)}"
  }

  # 仅仅连接到第一个实例
  connection {
    #        该函数访问列表的特定元素
    host = "${element(aws_instance.cluster.*.public_ip, 0)}"
  }

  provisioner "remote-exec" {
    # 执行命令
    inline = [
      "bootstrap-cluster.sh ${join(" ", aws_instance.cluster.*.private_ip)}",
    ]
  }
}</pre>
<div class="blog_h3"><span class="graybg">通用Provisioners</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">Provisioner</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>file</td>
<td>
<p>从运行Terraform的机器，复制文件或目录到新创建的资源（通常是虚拟机）。示例：</p>
<pre class="crayon-plain-tag">resource "aws_instance" "web" {
  # ...

  # 文件到文件
  provisioner "file" {
    source      = "conf/myapp.conf"
    destination = "/etc/myapp.conf"
  }

  # 字符串到文件
  provisioner "file" {
    content     = "ami used: ${self.ami}"
    destination = "/tmp/file.log"
  }

  # 目录到目录，拷贝后生成/etc/configs.d目录
  provisioner "file" {
    source      = "conf/configs.d"
    destination = "/etc"
  }

  # Windows下的路径语法
  provisioner "file" {
    # apps/app1/下的所有文件拷贝到D:/IIS/webapp1下
    source      = "apps/app1/"
    destination = "D:/IIS/webapp1"
  }
}</pre>
<p>关于整个目录的拷贝，需要注意：</p>
<ol>
<li>如果连接type是ssh，则目标目录必须已经存在。你可能需要使用remote-exec预先创建好目录</li>
<li>原路径以/结尾，则拷贝目录下的所有文件，而非目录本身</li>
</ol>
</td>
</tr>
<tr>
<td>local-exec</td>
<td>
<p>在资源创建之后，调用一个本地（运行Terraform的机器）可执行程序</p>
<p>注意：即使是在资源创建之后，但是不保证sshd这样的服务已经可用了。因此不要尝试在local-exec中调用ssh命令登录到资源</p>
<pre class="crayon-plain-tag">resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    command = "echo ${self.private_ip} &gt;&gt; private_ips.txt"
    # 可选的工作目录
    working_dir = "/root"
    # 可选的解释器
    interpreter = [ "/bin/bash", "-c" ]
    # 可选的环境变量
    environment = {
      FOO = "bar"
    }
  }
}</pre>
</td>
</tr>
<tr>
<td>remote-exec</td>
<td>
<p>在资源创建之后，登录到新创建的资源，执行命令
<pre class="crayon-plain-tag">resource "aws_instance" "web" {
  provisioner "remote-exec" {
    # 命令列表，逐个执行
    inline = [
      "puppet apply",
      "consul join ${aws_instance.web.private_ip}",
    ]
    # 单个脚本路径
    script = ""
    # 多个脚本路径
    scripts = []
  }
} </pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">数据资源(data)</span></div>
<p>数据资源，由data块来描述，让Terraform从数据源读取信息。Data是一种特殊的Resource，也是由Provider来提供。<span style="background-color: #c0c0c0;">Data只支持读取操作</span>，相对的，<span style="background-color: #c0c0c0;">普通的Resource支持增删改查操作，普通资源也叫受管（Manged）资源，一般简称Resource</span>。
<p>数据资源要求Terraform去特定数据源（第1标签）读取数据，并将结果导出为本地名称（第2标签）：</p>
<pre class="crayon-plain-tag"># 读取匹配参数的aws_ami类型的数据源，并存放到example
data "aws_ami" "example" {
  # 这些参数是查询条件(query constraints)，具体哪些可用取决于数据源类型
  most_recent = true

  owners = ["self"]
  tags = {
    Name   = "app-server"
    Tested = "true"
  }
}</pre>
<p>两个标签组合起来是data在模块中的唯一标识。</p>
<div class="blog_h2"><span class="graybg">行为</span></div>
<div class="blog_h3"><span class="graybg">生命周期</span></div>
<p>如果数据资源的查询约束（参数）都是<span style="background-color: #c0c0c0;">常量值，或者是已知的变量值</span>，那么<span style="background-color: #c0c0c0;">数据资源的状态会在Terraform的Refresh阶段更新，这个阶段在创建执行计划之前</span>。</p>
<p>查询约束可能引用了某些值，这些值在应用配置文件之前无法获知（例如资源ID）。<span style="background-color: #c0c0c0;">这种情况下，读取数据源的操作将推迟到Apply之后</span>。在Data读取完成之前，所有对它结果（导出名称）的引用都是不可用的。</p>
<div class="blog_h3"><span class="graybg">Local-Only数据源</span></div>
<p>大部分数据源都对应了某种云上基础设施，需要通过云API远程的读取。</p>
<p>某些特殊数据源仅仅供Terraform自己使用，例如Hashicorp的Provider template，它提供的template_file数据源，用于在本地渲染模板文件。这类数据源叫Local-Only数据源，其行为和一般数据源没有区别。</p>
<div class="blog_h3"><span class="graybg">资源依赖关系</span></div>
<p>数据资源的依赖解析行为，和受管资源一致。</p>
<div class="blog_h2"><span class="graybg">元参数</span></div>
<p>数据资源支持的元参数，和受管资源一致。</p>
<div class="blog_h1"><span class="graybg">变量和输出</span></div>
<p>和传统编程语言对比，模块类似于函数，输入变量类似于函数参数，输出值类似于返回值，本地值则类似于函数内的局部变量。</p>
<div class="blog_h2"><span class="graybg"><a id="input-vars"></a>输入变量</span></div>
<p>输入变量作为<span style="background-color: #c0c0c0;">Terraform模块的参数</span>，从而实现模块的参数化、可跨多个配置复用。</p>
<p>对于定义在<span style="background-color: #c0c0c0;">根模块中的变量，其值可以从Terraform CLI选项传入</span>。对于<span style="background-color: #c0c0c0;">子模块中定义的变量，则必须通过module块传入其值</span>。</p>
<div class="blog_h3"><span class="graybg">声明语法</span></div>
<pre class="crayon-plain-tag"># 声明一个字符串类型的输入变量
#        变量名必须在模块范围内唯一
#        不得用做变量名：ource, version, providers, count, for_each, lifecycle, depends_on, locals
variable "image_id" {
  # 类型
  type = string
  # 描述
  description = ""
  # 校验规则
  validation {
    # 如果为true则校验成功
    condition = bool-expr
    # 校验失败时的消息
    error_message = ""
  }
  # 是否敏感，敏感信息不会现在Terraform UI上输出
  sensitive = false
}

# 声明一个字符串的列表，并给出默认值
variable "availability_zone_names" {
  type    = list(string)
  default = ["us-west-1a"]
}

# 声明一个对象的列表，并给出默认值
variable "docker_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 8300
      external = 8300
      protocol = "tcp"
    }
  ]
}</pre>
<div class="blog_h3"><span class="graybg">类型</span></div>
<p>支持的简单类型：<pre class="crayon-plain-tag">string</pre>、<pre class="crayon-plain-tag">number</pre>、<pre class="crayon-plain-tag">bool</pre></p>
<p>支持的容器类型：</p>
<ol>
<li>列表：<pre class="crayon-plain-tag">list(&lt;TYPE&gt;)</pre></li>
<li>集合：<pre class="crayon-plain-tag">set(&lt;TYPE&gt;)</pre></li>
<li>映射：<pre class="crayon-plain-tag">map(&lt;TYPE&gt;)</pre></li>
<li>对象：<pre class="crayon-plain-tag">object({&lt;ATTR NAME&gt; = &lt;TYPE&gt;, ... })</pre></li>
<li>元组：<pre class="crayon-plain-tag">tuple([&lt;TYPE&gt;, ...])</pre></li>
</ol>
<p>关键字<pre class="crayon-plain-tag">any</pre>表示，任何类型都允许。</p>
<div class="blog_h3"><span class="graybg">默认值</span></div>
<p>如果同时指定了类型和默认值，则提供的默认值必须可以转换为类型。</p>
<div class="blog_h3"><span class="graybg">校验规则</span></div>
<p>validation是一个块，其中condition是一个bool表达式：</p>
<pre class="crayon-plain-tag">variable "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    #               can函数将错误转换为false
    #                  regex函数在找不到匹配的时候会失败
    condition     = can(regex("^ami-", var.image_id))
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}</pre>
<div class="blog_h3"><span class="graybg">引用输入变量</span></div>
<p><span style="background-color: #c0c0c0;">在声明输入变量的模块</span>中，可以使用<pre class="crayon-plain-tag">var.&lt;NAME&gt;</pre>引用输入变量的值：</p>
<pre class="crayon-plain-tag">resource "aws_instance" "example" {
  instance_type = "t2.micro"
  ami           = var.image_id
}</pre>
<div class="blog_h3"><span class="graybg">给根模块变量赋值</span></div>
<p>要给根模块中定义的变量赋值，有以下几种方式：</p>
<ol>
<li>使用<pre class="crayon-plain-tag">-var</pre>命令行选项，可以多次使用，每次赋值一个变量，示例：<br />
<pre class="crayon-plain-tag">terraform apply -var="image_id=ami-abc123"
terraform apply -var='image_id_list=["ami-abc123","ami-def456"]' -var="instance_type=t2.micro"
terraform apply -var='image_id_map={"us-east-1":"ami-abc123","us-east-2":"ami-def456"}' </pre>
</li>
<li>作为环境变量传入，示例：<br />
<pre class="crayon-plain-tag"># 环境变量需要TF_VAR_前缀
export TF_VAR_image_id=ami-abc123</pre>
</li>
<li>使用<pre class="crayon-plain-tag">.tfvars</pre>文件，此文件可以自动载入或者通过命令行选项显式载入，示例：<br />
<pre class="crayon-plain-tag">image_id = "ami-abc123"
availability_zone_names = [
  "us-east-1a",
  "us-west-1c",
]</pre><br />
<pre class="crayon-plain-tag">terraform apply -var-file="testing.tfvars"</pre></p>
<p>注意以下文件可以自动识别并载入：</p>
<ol>
<li>名为<pre class="crayon-plain-tag">terraform.tfvars</pre>或<pre class="crayon-plain-tag">terraform.tfvars.json</pre>的文件</li>
<li>以<pre class="crayon-plain-tag">.auto.tfvars</pre>或<pre class="crayon-plain-tag">.auto.tfvars.json</pre>结尾的文件</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">变量值优先级</span></div>
<p>如果通过多种方式给变量赋值，则优先级高的生效。优先级顺序<span style="background-color: #c0c0c0;">从低到高</span>：</p>
<ol>
<li>环境变量文件</li>
<li>terraform.tfvars</li>
<li>terraform.tfvars.json</li>
<li>*.auto.tfvars和*.auto.tfvars.json文件，多个文件，按字典序，后面的优先级高</li>
<li>-var或-var-file命令行选项，多个选项，后面的优先级高</li>
</ol>
<div class="blog_h2"><span class="graybg">输出值</span></div>
<p>输出值是模块的“返回值”，具有以下用途：</p>
<ol>
<li>子模块使用输出值将它创建的<span style="background-color: #c0c0c0;">资源的属性的子集暴露给父模块</span></li>
<li>根模块可以利用输出值，将一些<span style="background-color: #c0c0c0;">信息在terraform apply之后打印到控制台</span>上</li>
<li>当使用远程状态（remote state）时，根模块的输出可以<span style="background-color: #c0c0c0;">被其它配置通过 terraform_remote_state 数据源捕获到</span></li>
</ol>
<div class="blog_h3"><span class="graybg">声明语法</span></div>
<pre class="crayon-plain-tag"># 输出的名字
output "instance_ip_addr" {
  # 值
  value = aws_instance.server.private_ip
  # 描述
  description = ""
  # 是否敏感
  sensitive = ""
  # 依赖
  depends_on = []
}</pre>
<div class="blog_h3"><span class="graybg">访问子模块输出</span></div>
<p>子模块的输出值，通过这样的表达式访问：</p>
<pre class="crayon-plain-tag">module.&lt;MODULE NAME&gt;.&lt;OUTPUT NAME&gt;

# 访问子模块web_server的输出值instance_ip_addr
module.web_server.instance_ip_addr</pre>
<div class="blog_h3"><span class="graybg">输出值的依赖</span></div>
<p>使用depends_on参数可以明确指定输出值依赖什么：</p>
<pre class="crayon-plain-tag">output "instance_ip_addr" {
  value       = aws_instance.server.private_ip
  depends_on = [
    aws_security_group_rule.local_access,
  ]
}</pre>
<div class="blog_h2"><span class="graybg">本地值</span></div>
<p>在一个模块内部，将表达式分配给一个名称。你可以同时声明多个本地值：</p>
<pre class="crayon-plain-tag">locals {
  service_name = "forum"
  instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
  common_tags = {
    Service = local.service_name
    Owner   = local.owner
  }
}</pre>
<p>引用本地值时，使用表达式： <pre class="crayon-plain-tag">local.&lt;NAME&gt;</pre></p>
<div class="blog_h1"><span class="graybg">模块(modules)</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>一套完整的配置文件（Configuration，简称配置），由1个根目录和若干文件/子目录组成。配置文件的扩展名为<pre class="crayon-plain-tag">.tf</pre>，此外HCL语言还提供了基于JSON的变体，这种配置文件的扩展名为<pre class="crayon-plain-tag">.tf.json</pre>。配置文件的编码方式为UTF-8，使用Unix风格的换行符。</p>
<p>所谓模块（Module）是存放在一个目录中的配置文件的集合。子目录中的文件不属于模块，不会自动包含到配置中。</p>
<p>将块存放在不同配置文件中仅仅是方便人类的阅读，和Terraform的行为无关。Terraform总是会评估模块中的所有文件，并将它们合并为单一的文档来看待。</p>
<div class="blog_h3"><span class="graybg">根模块</span></div>
<p>Terraform命令总是在单个根模块的上下文中运行，根模块的目录通常作为命令的工作目录。此根模块会调用其它模块，这种调用关系会递归的发生，从而形成一个子模块树结构。</p>
<div class="blog_h3"><span class="graybg">子模块</span></div>
<p>一个模块（通常是根模块）可以调用其它模块，从而将这些模块中定义的资源包含到配置中。当一个模块被其它模块调用时，它的角色是子模块。</p>
<p>子模块可以被同一个配置调用多次，不同模块也可以同时调用一个子模块。</p>
<div class="blog_h3"><span class="graybg">发布的模块</span></div>
<p>引用子模块时，除了可以从本地文件系统获取，还可以从公共/私有仓库下载。</p>
<p>Terraform Registry托管了大量公共模块，用于管理各种各样的基础设施，这些模块可以被免费使用。Terraform能够自动下载这些模块的正确版本。Terraform Cloud / Enterprise版本都包含一个私服模块可以托管组织私有的模块。</p>
<div class="blog_h3"><span class="graybg">覆盖文件</span></div>
<p>使用名为<pre class="crayon-plain-tag">override.tf</pre>或<pre class="crayon-plain-tag">override.tf.json</pre>，或者后缀为<pre class="crayon-plain-tag">_override.tf</pre>或<pre class="crayon-plain-tag">_override.tf.json</pre>的文件，可以覆盖既有配置的某一部分。这些文件叫做覆盖文件。</p>
<p>加载配置时，Terraform最初会跳过覆盖文件。加载完成功文件后，会按照字典序处理覆盖文件。对于覆盖文件中的每个顶级块，Terraform会尝试寻找已经存在的，定义在常规文件中的对应块，并将块的内容进行合并。内容合并规则如下：</p>
<ol>
<li>覆盖文件中的顶级块、普通配置文件中的顶级块，对应关系通过块头（块类型+标签）识别，相同块头的块被合并</li>
<li>顶级块中的参数被替换</li>
<li>顶级块中的内嵌块被替换，不会递归的进行合并</li>
<li>多个覆盖文件覆盖了同一个块的定义时，按覆盖文件名的字典序依次合并</li>
</ol>
<p>此外，对于resource / data块，有如下特殊规则：</p>
<ol>
<li>内嵌的lifecycle块不是简单的直接替换。假设覆盖文件仅仅设置了lifecycle的create_before_destroy属性，原始配置中任何ignore_changes参数保持原样</li>
<li>对于内嵌的provisioner块，原始配置中的（不管有几个）provisioner块直接被忽略</li>
<li>原始配置中的内嵌connection块被完全覆盖</li>
<li>元参数（meta-argument）depends_on不能出现在覆盖文件中</li>
</ol>
<p>对于variable（变量）块，有如下特殊规则：</p>
<ol>
<li>如果原始块定义了default参数（默认值）并且覆盖块修改了变量的type，则Terraform尝试将default转换为新的type，如果转换无法自动完成则报错</li>
<li>如果覆盖块修改了default，那么其值必须匹配原始块中的type</li>
</ol>
<p>不建议过多的使用覆盖文件，这会降低配置的可读性。</p>
<p>对于output块，有如下特殊规则：</p>
<ol>
<li>元参数depends_on不能出现在覆盖文件中</li>
</ol>
<p>对于local块，有如下特殊规则：</p>
<ol>
<li>每个local块定义（或修改）了若干具有名字的值，覆盖时使用value-by-value的方式，至于值在何处定义不影响</li>
</ol>
<p>对于terraform块，有如下特殊规则：</p>
<ol>
<li>required_providers的值，按element-by-element的方式进行覆盖。这样，覆盖块可以仅仅调整单个provider的配置，而不影响其他providers</li>
<li>覆盖required_version、required_providers时，被覆盖的元素被整个替换</li>
</ol>
<p>下面是原始文件+覆盖文件的示例：</p>
<pre class="crayon-plain-tag"># example.tf
resource "aws_instance" "web" {
  instance_type = "t2.micro"
  ami           = "ami-408c7f28"
}

# override.tf
resource "aws_instance" "web" {
  ami = "foo"
}</pre>
<div class="blog_h2"><span class="graybg">使用</span></div>
<div class="blog_h3"><span class="graybg">调用子模块</span></div>
<p>所谓调用，就是将特定的值传递给子模块作为输入变量，从而将子模块中的配置包含进来。</p>
<pre class="crayon-plain-tag">module "servers" {
  # 源，必须，指定子模块的位置
  source = "./app-cluster"
  # 版本，如果模块位于仓库中，建议制定
  version = "0.0.5"
  
  # 支持一些元参数
  
  # 大部分其它参数，都是为子模块提供输入变量
  servers = 5
}</pre>
<div class="blog_h2"><span class="graybg">元参数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">元参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">count</td>
<td>用于创建多个模块实例，参考资源(resources)的元参数</td>
</tr>
<tr>
<td class=" blog_h3">for_each</td>
<td>用于创建多个模块实例，参考资源(resources)的元参数</td>
</tr>
<tr>
<td class=" blog_h3">providers</td>
<td>
<p>将Provider配置传递给子模块：</p>
<pre class="crayon-plain-tag">provider "aws" {
  alias  = "usw2"
  region = "us-west-2"
}

module "example" {
  source    = "./example"
  providers = {
    # 配置名称由provider块的第1标签（+可选的alias参数）构成
    # 键是子模块中的Provider配置名称
    # 值是父模块中的Provider配置名称
    aws = aws.usw2
  }
}</pre>
<p>如果子模块没有定义任何Provider alias，则该元参数是可选的。不指定该元参数时，子模块会从父模块继承所有默认Provider配置，所谓<span style="background-color: #c0c0c0;">默认配置就是没有alias的provider块所定义的配置</span></p>
</td>
</tr>
<tr>
<td class=" blog_h3">depends_on</td>
<td>
<p>显式指定整个模块对特定目标的依赖，参考资源(resources)的元参数</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="module-source"></a>源</span></div>
<p>module块的source参数指定了从何处下载子模块的代码。</p>
<p>terraform init中有一个步骤，专门负责模块的安装。它支持从本地路径、Terraform Registry、GitHub、一般性Git仓库、HTTP URL、S3桶等多种来源下载模块。</p>
<div class="blog_h3"><span class="graybg">本地路径</span></div>
<p>对于紧密相关的模块，例如为了减少代码重复，从单一模块拆分得到的，建议使用本地路径方式存储。</p>
<pre class="crayon-plain-tag">module "consul" {
  #        必须以./或../开头，提示这是一个本地模块
  source = "./consul"
}</pre>
<p>本地路径的源具有一个特殊的地方，它没有“安装”这个步骤。</p>
<div class="blog_h3"><span class="graybg">Terraform Registry</span></div>
<p>对于希望公开分享的模块，可以存放在这个Terraform仓库中。这种仓库的源地址格式为<pre class="crayon-plain-tag">&lt;HOSTNAME&gt;/&lt;NAMESPACE&gt;/&lt;NAME&gt;/&lt;PROVIDER&gt;</pre>。示例： </p>
<pre class="crayon-plain-tag"># 这个模块创建一个Consul服务
module "consul" {
  # 为AWS提供的consule模块，使用Terraform公共仓库时HOSTNAME可以省略
  source = "hashicorp/consul/aws"
  version = "0.1.0"
}</pre>
<p>为了访问私有仓库，你可能需要在CLI配置中添加访问令牌。</p>
<div class="blog_h3"><span class="graybg">GitHub</span></div>
<p>如果source以github.com开头，则Terraform会尝试到GitHub拉取模块源码：</p>
<pre class="crayon-plain-tag">module "consul" {
  # 通过HTTPS
  source = "github.com/hashicorp/example"
}

module "consul" {
  # 通过SSH
  source = "git@github.com:hashicorp/example.git"
}</pre>
<div class="blog_h3"><span class="graybg">Git</span></div>
<p>如果source以<pre class="crayon-plain-tag">git::</pre>开头，则Terraform认为模块托管在一般性的Git服务器中：</p>
<pre class="crayon-plain-tag">module "vpc" {
  source = "git::https://example.com/vpc.git"
}

module "storage" {
  source = "git::ssh://username@example.com/storage.git"
}</pre>
<p>Terraform使用git clone命令下载模块，因此本地机器的任何Git配置都可用，包括凭证信息。</p>
<p>默认情况下，使用Git仓库的HEAD，要选择其它修订版，使用ref参数：</p>
<pre class="crayon-plain-tag">module "vpc" {
  source = "git::https://example.com/vpc.git?ref=v1.2.0"
}</pre>
<p>任何可以作为git checkout参数的值，都可以作为ref参数。</p>
<div class="blog_h3"><span class="graybg">HTTP</span></div>
<p>如果source指定为一个普通的URL，那么Terraform会：</p>
<ol>
<li>附加GET参数<pre class="crayon-plain-tag">terraform-get=1</pre>，请求那个URL</li>
<li>如果得到2xx应答，那么尝试从以下位置读取模块实际地址：
<ol>
<li>响应头<pre class="crayon-plain-tag">X-Terraform-Get</pre></li>
<li>HTML元素：<br />
<pre class="crayon-plain-tag">&lt;meta name="terraform-get" content="github.com/hashicorp/example" /&gt; </pre>
</li>
</ol>
</li>
</ol>
<p>如果URL的结尾是可识别的压缩格式扩展名（zip tar.bz2  tbz2 tar.gz  tgz tar.xz txz）则Terraform会跳过上面处理过程，直接下载压缩包：</p>
<pre class="crayon-plain-tag">module "vpc" {
  source = "https://example.com/vpc-module?archive=zip"
}</pre>
<div class="blog_h3"><span class="graybg">位于源子目录的模块</span></div>
<p>如果需要的模块位于源的子目录中，可以使用特殊的<pre class="crayon-plain-tag">//</pre>语法来引用：</p>
<p style="padding-left: 30px;">hashicorp/consul/aws//modules/consul-cluster<br />git::https://example.com/network.git//modules/vpc<br />https://example.com/network-module.zip//modules/vpc<br />s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/network.zip//modules/vpc<br />git::https://example.com/network.git//modules/vpc?ref=v1.2.0</p>
<div class="blog_h1"><span class="graybg">开发模块</span></div>
<p>开发模块就是构思和编写TF文件的过程，你需要考虑模块的输入（变量）、输出（值），模块读取和创建哪些资源。你只需要将这些TF文件放在一个目录中就可以了。</p>
<p>如果开发的模块可能被很多配置复用，建议在独立的版本库中管理。</p>
<p>尽管内嵌多个子目录（作为子模块）是允许的，但是建议尽可能的让目录扁平化，<span style="background-color: #c0c0c0;">可以使用模块组合，而避免深层次的目录树</span>，这可以增强可复用性。</p>
<div class="blog_h2"><span class="graybg">何时模块化</span></div>
<p>过度的模块化容易让配置难以理解。</p>
<p>一个好的模块应该通过描述架构中新概念来提升抽象层次，这个概念由一些Provider中的基本元素组成。例如，我们想基于一些CVM、一个CLB构建一个Redis集群，这样的集群就适合封装在一个模块中。</p>
<p>永远不要开发那种仅仅为了包装单个其它资源的模块。</p>
<div class="blog_h2"><span class="graybg">模块标准结构</span></div>
<pre class="crayon-plain-tag">$ tree complete-module/          # 根模块，这是标准模块结构中唯一必须的元素
.
├── README.md                    # 文档
├── main.tf                      # 建议文件名，模块主入口点，资源在此创建
├── variables.tf                 # 定义模块的输入变量
├── outputs.tf                   # 定义模块的输出值
├── ...
├── modules/                     # 嵌套的子模块
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── examples/                    # 使用模块的样例
│   ├── exampleA/
│   │   ├── main.tf
│   ├── exampleB/
│   ├── .../</pre>
<div class="blog_h2"><span class="graybg">模块组合</span></div>
<div class="blog_h3"><span class="graybg">简介</span></div>
<p>一个简单的Terraform配置，仅仅包含一个根模块，我们在这个扁平的结构中创建所有资源： </p>
<pre class="crayon-plain-tag">resource "aws_vpc" "example" {
  cidr_block = "10.1.0.0/16"
}

resource "aws_subnet" "example" {
  vpc_id = aws_vpc.example.id

  availability_zone = "us-west-2b"
  cidr_block        = cidrsubnet(aws_vpc.example.cidr_block, 4, 1)
}</pre>
<p>当引入<pre class="crayon-plain-tag">modules</pre>块后，配置变成层次化的，每个模块可以创建自己的资源，甚至有自己的下级子模块。 无节制的使用子模块会导致很深的树状配置结构，这是反模式。</p>
<div class="blog_h3"><span class="graybg">依赖反转原则</span></div>
<p>Terraform建议保持配置的扁平，仅仅引入一层子模块。假设我们需要在AWS上创建一个Consul集群，它依赖一个子网。我们可以将子网的创建封装到模块：</p>
<pre class="crayon-plain-tag">module "network" {
  source = "./modules/aws-network"

  base_cidr_block = "10.0.0.0/8"
}</pre>
<p>将Consul集群的创建封装到另外一个模块：</p>
<pre class="crayon-plain-tag">module "consul_cluster" {
  source = "./modules/aws-consul-cluster"

  vpc_id     = module.network.vpc_id
  subnet_ids = module.network.subnet_ids
}</pre>
<p>根模块通过上面两个module块使用这两个子模块，这种<span style="background-color: #c0c0c0;">简单的单层结构叫模块组合（ module composition）</span></p>
<p>上面这个例子也体现了<span style="background-color: #c0c0c0;">依赖反转</span>的原则： consul_cluster需要一个网络，但是不是它（这个模块）自己去创建网络，而是由外部创建并注入给它。</p>
<p>在未来进一步的重构中，可能由另外一个配置负责创建网络，而仅仅通过数据资源将读取网络信息：</p>
<pre class="crayon-plain-tag">data "aws_vpc" "main" {
  tags = {
    Environment = "production"
  }
}

data "aws_subnet_ids" "main" {
  vpc_id = data.aws_vpc.main.id
}

module "consul_cluster" {
  source = "./modules/aws-consul-cluster"

  vpc_id     = data.aws_vpc.main.id
  subnet_ids = data.aws_subnet_ids.main.ids
}</pre>
<div class="blog_h3"><span class="graybg">条件性创建</span></div>
<p>开发可复用模块时，一个很常见的情况是，某个依赖的资源在某些条件下不需要创建 —— 例如在某些环境下，资源预先已经存在。</p>
<p>这种情况下同样要应用依赖反转原则：将这些可能需要创建的<span style="background-color: #c0c0c0;">依赖资源作为模块的输入参数</span>：</p>
<pre class="crayon-plain-tag"># 下面这个变量代表依赖的资源
variable "ami" {
  type = object({
    # 仅仅需要定义对于本模块有意义的属性
    id           = string
    architecture = string
  })
}</pre>
<p>由模块的调用者决定创建依赖资源：</p>
<pre class="crayon-plain-tag">resource "aws_ami" "example" {
  name              = "local-copy-of-ami"
  source_ami_id     = "ami-abc123"
  source_ami_region = "eu-west-1"
}

module "example" {
  source = "./modules/example"
  ami = aws_ami.example
}</pre>
<p>还是直接使用已经存在的依赖资源：</p>
<pre class="crayon-plain-tag">data "aws_ami" "example" {
  owner = "10000"
  tags = {
    application = "example-app"
    environment = "dev"
  }
}

module "example" {
  source = "./modules/example"
  ami = data.aws_ami.example
}</pre>
<p>因为，只有调用者才清楚实际环境是什么样的。 </p>
<div class="blog_h3"><span class="graybg">多云抽象</span></div>
<p>Terraform本身没有提供适配多个云服务商的相似资源的抽象，这会屏蔽云服务商的差异性。尽管如此，作为Terraform用户来说，进行多云抽象是常见需求，特别是云迁移这样的应用场景。 </p>
<p>举例来说，任何一个云服务商的域名系统都支持域名解析。但是某些云服务商可能提供智能负载均衡、地理位置感知的解析这样高级特性。我们可以将域名系统的公共特性抽象为模块：</p>
<pre class="crayon-plain-tag">module "webserver" {
  source = "./modules/webserver"
}

locals {
  fixed_recordsets = [
    {
      name = "www"
      type = "CNAME"
      ttl  = 3600
      records = [
        "webserver01",
        "webserver02",
      ]
    },
  ]
  server_recordsets = [
    for i, addr in module.webserver.public_ip_addrs : {
      name    = format("webserver%02d", i)
      type    = "A"
      records = [addr]
    }
  ]
}</pre>
<p>上面的recordset，抽象了所有域名系统都应该支持的DNS记录集资源。</p>
<p>当针对某个云服务商实现此资源时，我们可以开发一个模块。</p>
<pre class="crayon-plain-tag"># 此模块在AWS Route53上实现了记录集资源
module "dns_records" {
  source = "./modules/route53-dns-records"
  route53_zone_id = var.route53_zone_id
  recordsets      = concat(local.fixed_recordsets, local.server_recordsets)
}</pre>
<p>需要切换云服务商时，只需要替换上述dns_records模块的source即可，指向对应的模块即可。所有这些模块都需要定义输入参数：</p>
<pre class="crayon-plain-tag">variable "recordsets" {
  type = list(object({
    name    = string
    type    = string
    ttl     = number
    records = list(string)
  }))
}</pre>
<div class="blog_h3"><span class="graybg">仅Data模块</span></div>
<p>大部分模块都会描述需要被创建和管理的基础设施对象，偶尔的情况下，模块仅仅去抓取需要的信息。</p>
<p>其中一种情况是，一套系统被划分为多个子系统，这些子系统都需要获取某种信息，这些信息可以用由一个data-only模块抓取。</p>
<div class="blog_h2"><span class="graybg">发布模块</span></div>
<p>出于复用目的的模块可以发布到Terraform Registry， 这样模块可以<a href="#module-source">很容易被所有人使用</a>。如果仅仅在组织内部共享，可以发布到私有仓库。</p>
<p>通过Git、S3、HTTP等方式发布模块也是可以的，模块支持多种源。</p>
<div class="blog_h1"><span class="graybg">提供者(providers) </span></div>
<p>所谓提供者，就是Terraform的插件/驱动，负责和特定云厂商的API或者其它任何API打交道。</p>
<p>每个<span style="background-color: #c0c0c0;">Provider都可以提供若干受管资源、数据资源</span>，供Terraform使用。 反过来说，任何资源都是由Provider提供，没有Provider，Terraform什么都做不了。</p>
<p>Provider和Terraform完全独立的分发，公共的Provider托管在Terraform仓库（Registry）。</p>
<div class="blog_h2"><span class="graybg">安装Provider</span></div>
<p>在每次Terraform运行过程中，需要的Provider会自动被安装。</p>
<p>Terraform CLI会在初始化工作目录的时候安装Provider，它能够<span style="background-color: #c0c0c0;">自动从Terraform Registry下载Provider，或者从本地镜像/缓存加载</span>。要指定缓存位置，在CLI配置文件中设置<pre class="crayon-plain-tag">plugin_cache_dir</pre>。或者设置环境变量<pre class="crayon-plain-tag">TF_PLUGIN_CACHE_DIR</pre></p>
<p>为了保证，针对一套配置，<span style="background-color: #c0c0c0;">总是安装相同版本的Provider，可以使用CLI创建一个依赖锁文件</span>，并将此文件和配置一起纳入版本管理。</p>
<div class="blog_h2"><span class="graybg">引入Provider</span></div>
<p>每个模块都必须声明它需要哪些Provider，这样Terraform才能够安装和使用它们。声明在<pre class="crayon-plain-tag">required_providers</pre>块中进行：</p>
<pre class="crayon-plain-tag">terraform {
  required_providers {
    # 该块的每个参数，启用一个Provider
    # key是Provider的本地名称，必须是模块范围内的唯一标识符
    mycloud = {
      # 源地址
      source  = "mycorp/mycloud"
      # 版本约束
      version = "~&gt; 1.0"
    }
  }
}</pre>
<p>在required_providers块之外，Terraform总是使用本地名称来引用Provider：</p>
<pre class="crayon-plain-tag">provider "mycloud" {
  # 配置mycloud这个Provider
}</pre>
<div class="blog_h3"><span class="graybg">源地址</span></div>
<p>Provider的源地址，是它的全局标识符，这个地址当然也指明了应该从何处下载Provider。</p>
<p>源地址的格式为：<pre class="crayon-plain-tag">[&lt;HOSTNAME&gt;/]&lt;NAMESPACE&gt;/&lt;TYPE&gt;</pre>，各字段说明如下： </p>
<ol>
<li>可选的主机名，默认registry.terraform.io，即Terraform Registry的主机名</li>
<li>命名空间，通常是发布Provider的组织</li>
<li>类型，通常是Provider管理的平台/系统的简短名称</li>
</ol>
<div class="blog_h3"><span class="graybg">版本约束</span></div>
<p><pre class="crayon-plain-tag">&gt;= 1.0</pre>表示要求1.0或更高版本。<pre class="crayon-plain-tag">~&gt; 1.0.4</pre>表示仅仅允许1.0.x版本。</p>
<div class="blog_h2"><span class="graybg">内置Provider</span></div>
<p>目前仅有一个内置于Terraform的Provider，名为terraform_remote_state。你不需要再配置文件中引入它，尽管如此它还是有自己的源地址terraform.io/builtin/terraform。</p>
<div class="blog_h2"><span class="graybg">私有Provider</span></div>
<p>某些组织可能会开发自己的Provider，以管理特殊的基础设施。他们可能希望在Terraform中使用这些Provider，但是却不将其发布到公共仓库中。这种情况下，，构建自己的私有仓库。更简单的，可以使用<span style="background-color: #c0c0c0;">更简单的Provider安装方法，例如自己将其存放在本地文件系统的特定目录</span>。</p>
<p>任何Provider都必须有源地址，源地址必须包含（或者隐含默认值）一个主机名。如果通过本地文件系统分发Provider，则这个主机名只是个占位符，甚至不需要可解析。你通常可以使用terraform.yourcompany.com作为主机名。你可以这样引入私有Provider：</p>
<pre class="crayon-plain-tag">terraform {
  required_providers {
    mycloud = {
      source  = "terraform.example.com/examplecorp/ourcloud"
      version = "&gt;= 1.0"
    }
  }
}</pre>
<p>选择一个隐式本地镜像目录（implied local mirror directories ），并创建目录terraform.example.com/examplecorp/<span style="background-color: #99cc00;">ourcloud</span>/1.0.0，在此目录中创建一个代表运行平台的子目录，例如linux_amd64，并将Provider的可执行文件、以及任何其它需要的文件存放到其中即可。对于Windows，可执行文件的路径可能是terraform.example.com/examplecorp/ourcloud/1.0.0/windows_amd64/<span style="background-color: #c0c0c0;">terraform-provider</span>-<span style="background-color: #99cc00;">ourcloud</span>.exe</p>
<div class="blog_h2"><span class="graybg">配置Provider</span></div>
<p>除了引入Provider，你可能还需要对其进行配置才能使用。</p>
<p>配置时，使用<span style="color: #1d1e23;">provider块。只有根模块才可以配置一个Provider。<span style="background-color: #c0c0c0;">子模块会自动从根模块继承Provider配置</span>。示例：</span></p>
<pre class="crayon-plain-tag">#        本地名称，引入Provider时指定
provider "google" {
  project = "acme-app"
  region  = "us-central1"
}</pre>
<p>具体哪些配置参数可用，取决于Provider。配置时可以使用表达式，但是<span style="background-color: #c0c0c0;">只能引用那些应用配置之前即可知的值 —— 可以安全的引用输入变量，但是不能使用那些由资源导出的属性</span>。</p>
<div class="blog_h3"><span class="graybg">alias</span></div>
<p>Provider支持两个元参数，其中一个是alias，用于定义备选的Provider配置：</p>
<pre class="crayon-plain-tag">provider "aws" {
  alias  = "west"
  region = "us-west-2"
}</pre>
<p>声明资源时，可以指定使用备选的Provider配置： </p>
<pre class="crayon-plain-tag">resource "aws_instance" "foo" {
  provider = aws.west
} </pre>
<div class="blog_h3"><span class="graybg">version</span></div>
<p>这个元参数已经弃用，是旧的管理Provider版本的方式。 </p>
<div class="blog_h2"><span class="graybg">依赖锁文件</span></div>
<p>Terraform配置文件可以引用两类外部依赖：</p>
<ol>
<li>Providers，如上个章节所述，用于和外部系统交互的插件</li>
<li>Modules，可复用的配置文件集合</li>
</ol>
<p>这两类依赖都可以独立发布，并进行版本管理。引用这些依赖时，Terraform需要知道使用什么版本。</p>
<p>配置文件中的版本约束，指定了潜在的兼容性版本范围。但是到底选择（并锁定使用）依赖的哪个版本，由<span style="background-color: #c0c0c0;">名为.terraform.lock.hcl的依赖锁文件</span>决定。注意，当前<span style="background-color: #c0c0c0;">依赖锁文件仅仅管理Provider的版本，对于Module，仍然总是拉取匹配版本约束的最新版本</span>。</p>
<p>每当运行<pre class="crayon-plain-tag">terraform init</pre>命令时，依赖锁文件会自动创建/更新。此文件应该纳入版本管理。依赖锁文件使用和TF类似的语法。</p>
<p>运行terraform init时，如果：</p>
<ol>
<li>依赖没有记录在依赖锁文件中，则尝试拉取匹配版本约束的最新版本。并将获取到的版本信息记录到依赖锁文件</li>
<li>依赖已经记录，则使用记录的版本</li>
</ol>
<p>运行<pre class="crayon-plain-tag">terraform init -upgrade</pre>会强制拉取最新的、匹配约束的版本并更新依赖锁文件。</p>
<div class="blog_h1"><span class="graybg">开发插件</span></div>
<div class="blog_h2"><span class="graybg">核心和插件</span></div>
<p>Terraform逻辑上划分为两个部分：核心和插件。Terraform核心通过RPC来调用插件。Terraform支持多种发现和加载插件的方式。Terraform插件有两类</p>
<ol>
<li>Provider：通常用于对接到某特定云服务商，在其上创建基础设施对象</li>
<li>Provisioner：对接到某种provisioner，例如Bash</li>
</ol>
<p>核心的职责包括：</p>
<ol>
<li>基础设施即代码：读取和解释配置文件和模块</li>
<li>资源状态管理</li>
<li>构造资源依赖图</li>
<li>执行计划</li>
<li>通过RPC和插件交互</li>
</ol>
<p>插件和核心一样，基于Go语言编写。Terraform使用的所有Provider和Provisioner都是插件，它们在独立进程中运行。</p>
<p>Provider插件的职责是：</p>
<ol>
<li>初始化任何必要的库，用于进行API调用</li>
<li>与基础设施提供者进行交互</li>
<li>定义映射到特定服务的资源</li>
</ol>
<p>Provisioner插件的职责是：</p>
<ol>
<li>在特定资源创建后、销毁前执行命令或脚本</li>
</ol>
<div class="blog_h2"><span class="graybg">插件的发现</span></div>
<p>当<pre class="crayon-plain-tag">terraform init</pre>运行后，Terraform会读取工作目录中的配置文件，确定需要哪些插件。并在多个位置搜索以及安装的插件，下载缺失的插件，确定使用插件的什么版本，并且更新依赖锁文件，锁定插件版本。</p>
<p>关于插件发现，有以下规则：</p>
<ol>
<li>如果已经安装了满足版本约束的插件，Terraform会使用其中最新的。即使Terraform Registry有更新的满足版本约束的插件，默认也不会主动下载。使用<pre class="crayon-plain-tag">terraform init -upgrade</pre>可以强制下载最新版本</li>
<li>如果没有安装满足版本约束的插件，且插件托管在Registry，则下载并存放到<pre class="crayon-plain-tag">.terraform/providers/</pre>目录下</li>
</ol>
<div class="blog_h2"><span class="graybg">Provider设计原则</span></div>
<div class="blog_h3"><span class="graybg">专注于单一API或SDK </span></div>
<p>Provider应该基于单一API集合，或者SDK，例如仅仅针对腾讯云API，实现对腾讯云基础实施对象CRUD的封装。</p>
<div class="blog_h3"><span class="graybg">资源应当表示单一API对象</span></div>
<p>Terraform插件定义的资源，应该对应单一的云资源，作为该云资源的声明式表示。资源通常应该提供创建/读取/删除，以及可选的更新方法。</p>
<p><span style="background-color: #c0c0c0;">对多个云资源的组合，或者其它高级特性，应该通过模块来实现</span>。</p>
<div class="blog_h3"><span class="graybg">资源及其属性的Schema应当尽可能和底层API匹配</span></div>
<p>名称、数据类型、结构，应当尽可能匹配，除非这样做会影响Provider用户的体验。</p>
<div class="blog_h3"><span class="graybg">资源应该可导入</span></div>
<p>Terraform资源应该支持<pre class="crayon-plain-tag">terraform import</pre>操作。</p>
<div class="blog_h3"><span class="graybg">注意状态和版本</span></div>
<p>一旦Provider发布，后续就面临向后兼容性问题。</p>
<div class="blog_h2"><span class="graybg">两个SDK</span></div>
<p>开发Provider时，有两个SDK可供选择：</p>
<ol>
<li>SDKv2：当前大部分现有的Provider基于此SDK开发，提供稳定的开发体验</li>
<li>Terraform Plugin Framework：新的SDK，还在积极的开发中。目标是提升开发体验，对齐Terraform新的架构</li>
</ol>
<p>如何选择：</p>
<ol>
<li>如果维护既有Provider，沿用它当前使用的SDK。如果开发全新的Provider，可以考虑使用Framework</li>
<li>如果使用的Terraform版本小于v1.0.3，则只能基于SDKv2开发。Framework基于Terraform Protocol Version 6构建，旧版本的Terraform不能下载基于Version 6的Provider</li>
<li>Framework的接口可能发生改变，考虑成本</li>
<li>是否需要Framework提供的新特性：
<ol>
<li>支持获知一个值是否在配置、状态或计划中设置</li>
<li>支持获知一个值是否null、unknown，或者是空白值</li>
<li>支持object这样的结构化类型</li>
<li>支持嵌套属性</li>
<li>支持以any类型为元素的map</li>
<li>支持获知何时一个optional或计算出的字段从配置中移除了</li>
</ol>
</li>
<li>是否需要Framework尚未实现的，SDKv2已经支持的特性：
<ol>
<li>超时支持</li>
<li>定义资源状态upgraders</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">手工构建和发布Provider</span></div>
<div class="blog_h3"><span class="graybg">构建Provider</span></div>
<p>构建Provider的时候，使用go build命令，按照构建二进制文件的常规方式即可。你也可以使用GoReleaser来自动化构建多平台的、包含checksum的、自动签名的Provider。</p>
<div class="blog_h3"><span class="graybg">准备一个发布</span></div>
<p>每个发布包含以下文件：</p>
<ol>
<li>一个或多个zip文件，其命名格式为<pre class="crayon-plain-tag">terraform-provider-{NAME}_{VERSION}_{OS}_{ARCH}.zip</pre>
<ol>
<li>zip中包含Provider的二进制文件，命名为<pre class="crayon-plain-tag">terraform-provider-{NAME}_v{VERSION}</pre></li>
</ol>
</li>
<li>一个摘要文件<pre class="crayon-plain-tag">terraform-provider-{NAME}_{VERSION}_SHA256SUMS</pre>，包含每个zip的sha256：<br />
<pre class="crayon-plain-tag">shasum -a 256 *.zip &gt; terraform-provider-{NAME}_{VERSION}_SHA256SUMS</pre>
</li>
<li>一个GPG二进制文件<pre class="crayon-plain-tag">terraform-provider-{NAME}_{VERSION}_SHA256SUMS.sig</pre>，使用密钥对上述摘要文件进行前面：<br />
<pre class="crayon-plain-tag">gpg --detach-sign terraform-provider-{NAME}_{VERSION}_SHA256SUMS</pre>
</li>
<li>发布必须是finalized的（不是一个私有的草稿）</li>
</ol>
<div class="blog_h1"><span class="graybg">基于SDKv2开发Provider</span></div>
<p>本章开发和使用一个Provider，它和虚构的咖啡店应用Hashicups进行交互。此咖啡店应用支持查看和订购咖啡，它开放公共API端点：</p>
<ol>
<li>列出咖啡品种</li>
<li>列出特定咖啡的成分</li>
</ol>
<p>以及需要身份认证的API端点：</p>
<ol>
<li>CRUD咖啡订单</li>
</ol>
<p>HashiCups Provider基于一个Golang客户端库，利用REST API访问以上API端点，管理咖啡订单。</p>
<div class="blog_h2"><span class="graybg">使用Provider</span></div>
<p>首先我们从用户角度来感受一下，如何使用HashiCups Provider管理咖啡订单。本章后续会分析和重构该Provider的源码。</p>
<div class="blog_h3"><span class="graybg">下载空白项目</span></div>
<p>执行下面的命令下载使用HashiCups Provider的Terraform配置的空白项目。此项目没有Terraform配置，但是提供了在本地运行HashiCup咖啡店应用的脚本。</p>
<pre class="crayon-plain-tag">git clone https://github.com/hashicorp/learn-terraform-hashicups-provider
cd learn-terraform-hashicups-provider</pre>
<p>执行下面的命令，在本地启动HashiCup咖啡店应用：</p>
<pre class="crayon-plain-tag">cd docker_compose &amp;&amp; docker-compose up</pre>
<p>API监听端口是19090。检查并确认服务器正常工作：</p>
<pre class="crayon-plain-tag">curl localhost:19090/health </pre>
<div class="blog_h3"><span class="graybg">安装Provider</span></div>
<p>从0.13+开始，必须在Terraform配置中声明所有依赖的Provider及其源（从哪里下载）。<span style="background-color: #c0c0c0;">源的格式为[hostname]/[namespace]/[name]，对于Terraform Registry中的hashicorp命名空间，hostname和namespace都是可选的。Terraform Registry对应的hostname为registry.terraform.io</span></p>
<p>这里用到的Provider没有托管在Registry，需要手工下载：</p>
<pre class="crayon-plain-tag">curl -LO https://github.com/hashicorp/terraform-provider-hashicups/releases/download/v0.3.1/terraform-provider-hashicups_0.3.1_linux_amd64.zip</pre>
<p>或者从源码编译：</p>
<pre class="crayon-plain-tag">git clone https://github.com/hashicorp/terraform-provider-hashicups
go mod vendor
go build -o terraform-provider-hashicups
mv terraform-provider-hashicups ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.3.1/linux_amd64</pre>
<p>并且存放到：</p>
<p style="padding-left: 30px;">~/.terraform.d/plugins/${host_name}/${namespace}/${type}/${version}/${target}<br />~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.3.1/linux_amd64/terraform-provider-hashicups_v0.3.1</p>
<div class="blog_h3"><span class="graybg">创建用户</span></div>
<pre class="crayon-plain-tag">curl -X POST localhost:19090/signup -d '{"username":"education", "password":"test123"}'</pre>
<p>登录以获得Token：</p>
<pre class="crayon-plain-tag">curl -X POST localhost:19090/signin -d '{"username":"education", "password":"test123"}'
{"UserID":1,"Username":"education","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUyNTA3ODAsInVzZXJfaWQiOjEsInVzZXJuYW1lIjoiZWR1Y2F0aW9uIn0.M4YWgRM-5Jzfy3TLj9MqeVR7nsfRmlG3vZyaeRASnhw"}</pre>
<p>将Token设置到环境变量：</p>
<pre class="crayon-plain-tag">export HASHICUPS_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MzUyNTA3ODAsInVzZXJfaWQiOjEsInVzZXJuYW1lIjoiZWR1Y2F0aW9uIn0.M4YWgRM-5Jzfy3TLj9MqeVR7nsfRmlG3vZyaeRASnhw</pre>
<div class="blog_h3"><span class="graybg">初始化工作区</span></div>
<p>添加下面的代码到main.tf：</p>
<pre class="crayon-plain-tag">terraform {
  required_providers {
    hashicups = {
      version = "~&gt; 0.3.1"
      source  = "hashicorp.com/edu/hashicups"
    }
  }
}</pre>
<p>并进行初始化：<pre class="crayon-plain-tag">terraform init</pre></p>
<div class="blog_h3"><span class="graybg">创建订单</span></div>
<p>将以下内容添加到main.tf中：</p>
<pre class="crayon-plain-tag"># 配置Provider
provider "hashicups" {
  username = "education"
  password = "test123"
}

# 定义一个名为edu的订单资源
resource "hashicups_order" "edu" {
  # 订单包含2个品种3的咖啡
  items {
    coffee {
      id = 3
    }
    quantity = 2
  }
  # 订单包含2个品种2的咖啡
  items {
    coffee {
      id = 2
    }
    quantity = 2
  }
}

# 输出edu资源，这个输出在资源创建后可用
output "edu_order" {
  value = hashicups_order.edu
}</pre>
<p>执行下面的命令获取执行计划：</p>
<pre class="crayon-plain-tag">terraform plan 

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create # +表示创建操作

Terraform will perform the following actions:
  # 这里会列出计划中包含的操作
  #                             这是一个创建操作
  # hashicups_order.edu will be created
  + resource "hashicups_order" "edu" {
      # 每个属性的值     这个表示未知值
      + id           = (known after apply)
      + last_updated = (known after apply)

      + items {
          + quantity = 2

          + coffee {
              + description = (known after apply)
              + id          = 3
              + image       = (known after apply)
              + name        = (known after apply)
              + price       = (known after apply)
              + teaser      = (known after apply)
            }
        }
      + items {
          + quantity = 2

          + coffee {
              + description = (known after apply)
              + id          = 2
              + image       = (known after apply)
              + name        = (known after apply)
              + price       = (known after apply)
              + teaser      = (known after apply)
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

# 这里显示输出值会发生的变更
Changes to Outputs:
  + edu_order = {
      + id           = (known after apply)
      + items        = [
          + {
              + coffee   = [
                  + {
                      + description = (known after apply)
                      + id          = 3
                      + image       = (known after apply)
                      + name        = (known after apply)
                      + price       = (known after apply)
                      + teaser      = (known after apply)
                    },
                ]
              + quantity = 2
            },
          + {
              + coffee   = [
                  + {
                      + description = (known after apply)
                      + id          = 2
                      + image       = (known after apply)
                      + name        = (known after apply)
                      + price       = (known after apply)
                      + teaser      = (known after apply)
                    },
                ]
              + quantity = 2
            },
        ]
      + last_updated = (known after apply)
    }</pre>
<p>执行<pre class="crayon-plain-tag">terraform apply</pre>即可应用变更。 利用<pre class="crayon-plain-tag">terraform state show</pre>命令可以显示资源状态：</p>
<pre class="crayon-plain-tag">terraform state show hashicups_order.edu 
# hashicups_order.edu:
resource "hashicups_order" "edu" {
    id = "1"

    items {
        quantity = 2

        coffee {
            id     = 3
            image  = "/nomad.png"
            name   = "Nomadicano"
            price  = 150
            teaser = "Drink one today and you will want to schedule another"
        }
    }
    items {
        quantity = 2

        coffee {
            id     = 2
            image  = "/vault.png"
            name   = "Vaulatte"
            price  = 200
            teaser = "Nothing gives you a safe and secure feeling like a Vaulatte"
        }
    }
}</pre>
<p>可以看到Schema中所有属性均被填充。</p>
<div class="blog_h3"><span class="graybg">更新订单</span></div>
<p>我们修改一下订单配置，将items[*].quantity改一下，然后看看执行计划：</p>
<pre class="crayon-plain-tag">terraform plan
hashicups_order.edu: Refreshing state... [id=1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place  # ~表示原地更新操作

Terraform will perform the following actions:
  # 这里会显示diff
  # hashicups_order.edu will be updated in-place
  ~ resource "hashicups_order" "edu" {
        id = "1"

      ~ items {
          ~ quantity = 2 -&gt; 3

            # (1 unchanged block hidden)
        }
      ~ items {
          ~ quantity = 2 -&gt; 1

            # (1 unchanged block hidden)
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Changes to Outputs:
  # 这里会显示diff
  ~ edu_order = {
      ~ items        = [
          ~ {
              ~ quantity = 2 -&gt; 3
                # (1 unchanged element hidden)
            },
          ~ {
              ~ quantity = 2 -&gt; 1
                # (1 unchanged element hidden)
            },
        ]
        # (2 unchanged elements hidden)
    }</pre>
<p>通过apply命令应用上述执行计划。</p>
<div class="blog_h3"><span class="graybg">读取配料表 </span></div>
<p>本节我们来演示如何读取已经创建的订单的咖啡的配料表：</p>
<pre class="crayon-plain-tag">data "hashicups_ingredients" "first_coffee" {
  #                               声明多次的内嵌块，自动成为数组
  coffee_id = hashicups_order.edu.items[0].coffee[0].id
}

output "first_coffee_ingredients" {
  value = data.hashicups_ingredients.first_coffee
}</pre>
<div class="blog_h3"><span class="graybg">删除订单 </span></div>
<pre class="crayon-plain-tag">terraform destroy
hashicups_order.edu: Refreshing state... [id=1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy  # -表示删除操作

Terraform will perform the following actions:

  # hashicups_order.edu will be destroyed
  - resource "hashicups_order" "edu" {
      - id           = "1" -&gt; null
      - last_updated = "Tuesday, 26-Oct-21 10:39:15 CST" -&gt; null

      - items {
          - quantity = 3 -&gt; null

          - coffee {
              - id     = 3 -&gt; null
              - image  = "/nomad.png" -&gt; null
              - name   = "Nomadicano" -&gt; null
              - price  = 150 -&gt; null
              - teaser = "Drink one today and you will want to schedule another" -&gt; null
            }
        }
      - items {
          - quantity = 1 -&gt; null

          - coffee {
              - id     = 2 -&gt; null
              - image  = "/vault.png" -&gt; null
              - name   = "Vaulatte" -&gt; null
              - price  = 200 -&gt; null
              - teaser = "Nothing gives you a safe and secure feeling like a Vaulatte" -&gt; null
            }
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Changes to Outputs:
  - edu_order                = {
      - id           = "1"
      - items        = [
          - {
              - coffee   = [
                  - {
                      - description = ""
                      - id          = 3
                      - image       = "/nomad.png"
                      - name        = "Nomadicano"
                      - price       = 150
                      - teaser      = "Drink one today and you will want to schedule another"
                    },
                ]
              - quantity = 3
            },
          - {
              - coffee   = [
                  - {
                      - description = ""
                      - id          = 2
                      - image       = "/vault.png"
                      - name        = "Vaulatte"
                      - price       = 200
                      - teaser      = "Nothing gives you a safe and secure feeling like a Vaulatte"
                    },
                ]
              - quantity = 1
            },
        ]
      - last_updated = "Tuesday, 26-Oct-21 10:39:15 CST"
    } -&gt; null
  - first_coffee_ingredients = {
      - coffee_id   = 3
      - id          = "3"
      - ingredients = [
          - {
              - id       = 1
              - name     = "ingredient - Espresso"
              - quantity = 20
              - unit     = "ml"
            },
          - {
              - id       = 3
              - name     = "ingredient - Hot Water"
              - quantity = 100
              - unit     = "ml"
            },
        ]
    } -&gt; null</pre>
<div class="blog_h2"><span class="graybg">实现读操作</span></div>
<p>签出Provider源码：</p>
<pre class="crayon-plain-tag">git clone --branch boilerplate https://github.com/hashicorp/terraform-provider-hashicups</pre>
<p>注意SDKv2的依赖：</p>
<pre class="crayon-plain-tag">github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0 </pre>
<div class="blog_h3"><span class="graybg">骨架代码</span></div>
<p>分支boilerplate包含一些样板文件。 程序的入口点如下：</p>
<pre class="crayon-plain-tag">package main

import (
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
	"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"

	"terraform-provider-hashicups/hashicups"
)

func main() {
    // 启动插件的RPC服务器端
	plugin.Serve(&amp;plugin.ServeOpts{
		ProviderFunc: func() *schema.Provider {
			return hashicups.Provider()
		},
	})
}</pre>
<p>Provider函数：</p>
<pre class="crayon-plain-tag">package hashicups

import (
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// 定义了一个Provider
func Provider() *schema.Provider {
  return &amp;schema.Provider{
    // 资源映射，从资源类型名到Schema
    ResourcesMap: map[string]*schema.Resource{},
    // 数据资源映射，从资源类型名到Schema。注意资源和数据资源本质上是同一种结构
    DataSourcesMap: map[string]*schema.Resource{},
  }
}</pre>
<div class="blog_h3"><span class="graybg">定义咖啡数据源 </span></div>
<p>上面已经定义了Provider的骨架，这里我们实现一个咖啡数据源，此数据源能够从HasiCups服务拉取所有售卖的咖啡信息。</p>
<p>建议<span style="background-color: #c0c0c0;">每个数据源都在独立的源文件中编写，并且文件名以<pre class="crayon-plain-tag">data_source_</pre>开头</span>： </p>
<pre class="crayon-plain-tag">package hashicups

import (
  "context"
  "encoding/json"
  "fmt"
  "net/http"
  "strconv"
  "time"

  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// 定义了一个Resource，其中包含Schema以及CRUD操作
func dataSourceCoffees() *schema.Resource {
  return &amp;schema.Resource{
    // 由于数据资源仅仅支持读操作，因此仅声明ReadContext
    ReadContext: dataSourceCoffeesRead,
    Schema: map[string]*schema.Schema{},
  }
}</pre>
<p>下面我们完善此数据资源的Schema，根据Terraform的最佳实践，Schema应该尽量和基础实施匹配：</p>
<pre class="crayon-plain-tag">// curl localhost:19090/coffees
[
  {
    "id": 1,
    "name": "Packer Spiced Latte",
    "teaser": "Packed with goodness to spice up your images",
    "description": "",
    "price": 350,
    "image": "/packer.png",
    "ingredients": [
      {
        "ingredient_id": 1
      },
      {
        "ingredient_id": 2
      },
      {
        "ingredient_id": 4
      }
    ]
  }
]</pre>
<p>根据服务器返回的咖啡数据结构，编写对应的Schema并且添加到上面的dataSourceCoffees方法中：</p>
<pre class="crayon-plain-tag">Schema: map[string]*schema.Schema{
			"coffees": {
				// 注意这里是列表
				Type:     schema.TypeList,
				Computed: true,
				Elem: &amp;schema.Resource{
					Schema: map[string]*schema.Schema{
						"id": {
							Type:     schema.TypeInt,
							// 此值是"计算得到的"，也就是在创建资源时（除非手工配置）会得到此值的结果
							Computed: true,
						},
						"name": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"teaser": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"description": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"price": {
							Type:     schema.TypeInt,
							Computed: true,
						},
						"image": {
							Type:     schema.TypeString,
							Computed: true,
						},
						// 复杂类型，一个列表
						"ingredients": {
							Type:     schema.TypeList,
							Computed: true,
							// 列表元素的Schema
							Elem: &amp;schema.Resource{
								Schema: map[string]*schema.Schema{
									"ingredient_id": {
										Type:     schema.TypeInt,
										Computed: true,
									},
								},
							},
							// 如果是list(string)，可以这样声明Elem
							Elem: schema.Schema{
								Type:             schema.TypeString,
							},
						},
					},
				},
			},
		},
	}
}</pre>
<p>定义好Schema后，我们需要实现读操作： </p>
<pre class="crayon-plain-tag">//                                              读结果存放在这里          这个m是meta，元参数，
//                                                                     是配置Provider的返回值，下文有说明
func dataSourceCoffeesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	client := &amp;http.Client{Timeout: 10 * time.Second}

	// 这是一个切片，用于收集警告或者错误信息
	var diags diag.Diagnostics

	// 像云API发起请求
	req, err := http.NewRequest("GET", fmt.Sprintf("%s/coffees", "http://localhost:19090"), nil)
	if err != nil {
		// 收集错误
		return diag.FromErr(err)
	}
	r, err := client.Do(req)
	if err != nil {
		return diag.FromErr(err)
	}
	defer r.Body.Close()

	// 将响应反串行化到临时对象中
	coffees := make([]map[string]interface{}, 0)
	err = json.NewDecoder(r.Body).Decode(&amp;coffees)
	if err != nil {
		return diag.FromErr(err)
	}

	// schema.ResourceData用于查询和设置资源属性
	//        此方法将响应设置到Terraform数据源，并且保证字段设置到Schema对应位置
	if err := d.Set("coffees", coffees); err != nil {
		return diag.FromErr(err)
	}

	// 设置数据资源的ID
	d.SetId(strconv.FormatInt(time.Now().Unix(), 10))

	return diags
}</pre>
<p><span style="background-color: #c0c0c0;">数据资源的ID被设置为非空值，这提示Terraform，目标资源已经被创建</span>。 作为一个列表，此数据资源没有真实的ID，因此我们这里设置为时间戳。</p>
<p>如果数据资源被从Terraform外部删除了，这里应该设置空ID：</p>
<pre class="crayon-plain-tag">if resourceDoesntExist {
  d.SetID("")
  return
}</pre>
<p>这样Terraform会自动将state中的数据资源清除掉。</p>
<p>将上面的数据资源配置到Provider中：</p>
<pre class="crayon-plain-tag">func Provider() *schema.Provider {
	return &amp;schema.Provider{
		ResourcesMap: map[string]*schema.Resource{},
		DataSourcesMap: map[string]*schema.Resource{
			"hashicups_coffees": dataSourceCoffees(),
		},
	}
}</pre>
<div class="blog_h3"><span class="graybg">使用咖啡数据源</span></div>
<p>现在我们开发一个模块，使用上面定义的咖啡数据源。首先需要编译好Provider：<pre class="crayon-plain-tag">make install</pre>。 </p>
<p>根模块配置：</p>
<pre class="crayon-plain-tag">terraform {
  required_providers {
    hashicups = {
      // 当前构建的版本是0.2
      version = "0.2"
      source  = "hashicorp.com/edu/hashicups"
    }
  }
}

// 目前Provider不支持配置
provider "hashicups" {}

// 调用coffee子模块
module "psl" {
  source = "./coffee"
  // 传递输入参数
  coffee_name = "Packer Spiced Latte"
}

// 打印coffee模块的coffee输出值
output "psl" {
  value = module.psl.coffee
}</pre>
<p>coffee子模块配置：</p>
<pre class="crayon-plain-tag">// 子模块需要声明自己的依赖
terraform {
  required_providers {
    hashicups = {
      version = "0.2"
      source  = "hashicorp.com/edu/hashicups"
    }
  }
}

// 输入变量，咖啡品类
variable "coffee_name" {
  type    = string
  default = "Vagrante espresso"
}

// 调用上面开发的数据源，拉取所有咖啡品类
data "hashicups_coffees" "all" {}

# 输出所有咖啡
output "all_coffees" {
  value = data.hashicups_coffees.all.coffees
}

output "coffee" {
  value = {
    // 遍历所有咖啡
    for coffee in data.hashicups_coffees.all.coffees :
    // 返回咖啡ID到咖啡资源的映射
    coffee.id =&gt; coffee
    // 过滤，要求名称匹配输入参数
    if coffee.name == var.coffee_name
  }
}</pre>
<p>应用根模块：</p>
<pre class="crayon-plain-tag"># terraform init &amp;&amp; terraform apply --auto-approve

psl = {
  "1" = {
    "description" = ""
    "id" = 1
    "image" = "/packer.png"
    "ingredients" = tolist([
      {
        "ingredient_id" = 1
      },
      {
        "ingredient_id" = 2
      },
      {
        "ingredient_id" = 4
      },
    ])
    "name" = "Packer Spiced Latte"
    "price" = 350
    "teaser" = "Packed with goodness to spice up your images"
  }
}</pre>
<div class="blog_h2"><span class="graybg">添加身份验证 </span></div>
<p>本节我们来演示如何为Provider增加参数，Provider的实现又是如何读取这些参数的。</p>
<div class="blog_h3"><span class="graybg">Provider的Schema </span></div>
<pre class="crayon-plain-tag">func Provider() *schema.Provider {
	return &amp;schema.Provider{
		// ...
		// 定义Provider的配置参数
		Schema: map[string]*schema.Schema{
			"username": {
				Type:        schema.TypeString,
				Optional:    true,
				DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_USERNAME", nil),
			},
			"password": {
				Type:        schema.TypeString,
				Optional:    true,
				// 敏感数据，在输出时会处理
				Sensitive:   true,
				// 默认值函数                        从环境变量读取默认值
				DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_PASSWORD", nil),
			},
		},
		ConfigureContextFunc: providerConfigure,
	}
}</pre>
<div class="blog_h3"><span class="graybg">配置Provider </span></div>
<p>用户提供的username/password，如何被Provider的ReadContext函数访问呢？这需要配置Provider。配置过程是由<pre class="crayon-plain-tag">schema.Provider</pre>的<pre class="crayon-plain-tag">ConfigureContextFunc</pre>函数负责的：</p>
<pre class="crayon-plain-tag">func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
	// 读取Provider的配置参数
	username := d.Get("username").(string)
	password := d.Get("password").(string)

	var diags diag.Diagnostics

	if (username != "") &amp;&amp; (password != "") {
		c, err := hashicups.NewClient(nil, &amp;username, &amp;password)
		if err != nil {
			return nil, diag.FromErr(err)
		}

		return c, diags
	}

	c, err := hashicups.NewClient(nil, nil, nil)
	if err != nil {
		return nil, diag.FromErr(err)
	}

	return c, diags
}</pre>
<p>可以看到配置Provider后，会返回一个interface{}，这个对象会传递给CRUD操作的最后一个参数：</p>
<pre class="crayon-plain-tag">// See Resource documentation.
type CreateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type ReadContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type UpdateContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics

// See Resource documentation.
type DeleteContextFunc func(context.Context, *ResourceData, interface{}) diag.Diagnostics</pre>
<p>一般情况下，这个interface{}是配置好的云API客户端，或者是云API配置结构。</p>
<div class="blog_h2"><span class="graybg">复杂读操作 </span></div>
<p>本节我们演示如何在读操作中使用Provider参数，更精确的说是基于这些参数配置Provider后的结果。</p>
<p>首先我们创建几个咖啡订单（用于后续查询）：</p>
<pre class="crayon-plain-tag">curl -X POST -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders -d '[{"coffee": { "id":1 }, "quantity":4}, {"coffee": { "id":3 }, "quantity":3}]'</pre>
<p>看一下订单的数据结构：</p>
<pre class="crayon-plain-tag">// curl -X GET -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders/2 

{
  "id": 2,
  "items": [
    {
      "coffee": {
        "id": 1,
        "name": "Packer Spiced Latte",
        "teaser": "Packed with goodness to spice up your images",
        "description": "",
        "price": 350,
        "image": "/packer.png",
        "ingredients": null
      },
      "quantity": 4
    },
    {
      "coffee": {
        "id": 3,
        "name": "Nomadicano",
        "teaser": "Drink one today and you will want to schedule another",
        "description": "",
        "price": 150,
        "image": "/nomad.png",
        "ingredients": null
      },
      "quantity": 3
    }
  ]
}</pre>
<p>下面我们定义对应的数据源：</p>
<pre class="crayon-plain-tag">func dataSourceOrder() *schema.Resource {
	return &amp;schema.Resource{
		// 读取操作，见下文
		ReadContext: dataSourceOrderRead,
		Schema: map[string]*schema.Schema{
			"id": {
				Type:     schema.TypeInt,
				Required: true,
			},
			"items": {
				Type:     schema.TypeList,
				Computed: true,
				Elem: &amp;schema.Resource{
					Schema: map[string]*schema.Schema{
						"coffee_id": {
							Type:     schema.TypeInt,
							Computed: true,
						},
						"coffee_name": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"coffee_teaser": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"coffee_description": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"coffee_price": {
							Type:     schema.TypeInt,
							Computed: true,
						},
						"coffee_image": {
							Type:     schema.TypeString,
							Computed: true,
						},
						"quantity": {
							Type:     schema.TypeInt,
							Computed: true,
						},
					},
				},
			},
		},
	}
}</pre>
<p>注意这个数据源的Schema和API的结构没有做对应，进行了扁平化处理。</p>
<p>读取订单的操作如下：</p>
<pre class="crayon-plain-tag">func dataSourceOrderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	// 配置Provider后，得到的是一个客户端
	c := m.(*hc.Client)
	var diags diag.Diagnostics

	orderID := strconv.Itoa(d.Get("id").(int))

	// 基于此客户端进行订单查询
	order, err := c.GetOrder(orderID)
	if err != nil {
		return diag.FromErr(err)
	}

	// 订单结构和我们数据源的结构不一致，这里做转换
	orderItems := flattenOrderItemsData(&amp;order.Items)
	if err := d.Set("items", orderItems); err != nil {
		return diag.FromErr(err)
	}

	d.SetId(orderID)

	return diags
}</pre>
<p>将此资源注册到Provider：</p>
<pre class="crayon-plain-tag">func Provider() *schema.Provider {
	return &amp;schema.Provider{
		// ...
		DataSourcesMap: map[string]*schema.Resource{
			// ...
			"hashicups_order":   dataSourceOrder(),
		},
		ConfigureContextFunc: providerConfigure,
	}
}</pre>
<p>在配置文件中使用该数据源：</p>
<pre class="crayon-plain-tag">data "hashicups_order" "order" {
  id = 1
}

output "order" {
  value = data.hashicups_order.order
}</pre>
<div class="blog_h2"><span class="graybg">诊断和调试 </span></div>
<p>本节演示如何基于日志来调试Provider，我们会添加定制的错误消息，并且显示详细的Terraform Provider日志。</p>
<p>开发Provider时需要实现很多函数，这些函数常常具有一个返回值<pre class="crayon-plain-tag">diag.Diagnostics</pre>，例如上面的CRUD操作，以及配置Provider的函数：</p>
<pre class="crayon-plain-tag">type ConfigureContextFunc func(context.Context, *ResourceData) (interface{}, diag.Diagnostics)</pre>
<p>警告/错误级别的调试信息，都要放到diag.Diagnostics中，执行Terraform CLI命令时，这些信息会自动打印。</p>
<p>当创建HashiCups客户端失败时，我们可以添加一条针对信息：</p>
<pre class="crayon-plain-tag">func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
	// ...
	if (username != "") &amp;&amp; (password != "") {
		c, err := hashicups.NewClient(nil, &amp;username, &amp;password)
		if err != nil {
			diags = append(diags, diag.Diagnostic{
				Severity: diag.Error,
				Summary:  "Unable to create HashiCups client",
				Detail:   "Unable to auth user for authenticated HashiCups client",
			})
			return nil, diags
		}

		return c, diags
	}

	c, err := hashicups.NewClient(nil, nil, nil)
	if err != nil {
		diags = append(diags, diag.Diagnostic{
			Severity: diag.Error,
			Summary:  "Unable to create HashiCups client",
			Detail:   "Unable to auth user for authenticated HashiCups client",
		})
		return nil, diags
	}

	return c, diags
}</pre>
<p>执行命令时，错误信息会打印出来：</p>
<pre class="crayon-plain-tag">terraform init &amp;&amp; terraform apply --auto-approve
## ...
module.psl.data.hashicups_coffees.all: Refreshing state...

# 摘要
Error: Unable to create HashiCups client
# 详情
Unable to auth user for authenticated HashiCups client</pre>
<div class="blog_h2"><span class="graybg">实现创建操作 </span></div>
<p>资源支持创建、修改、删除等写操作，上面编写的数据源则仅支持读。尽管资源、数据源可能指向同一类实体，但是Schema不能共用。</p>
<div class="blog_h3"><span class="graybg">定义订单资源</span></div>
<p>资源的文件名前缀通常使用<pre class="crayon-plain-tag">resource_</pre>，下面是订单资源的骨架代码：</p>
<pre class="crayon-plain-tag">package hashicups

import (
  "context"

  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceOrder() *schema.Resource {
  return &amp;schema.Resource{
    CreateContext: resourceOrderCreate,
    ReadContext:   resourceOrderRead,
    UpdateContext: resourceOrderUpdate,
    DeleteContext: resourceOrderDelete,
    Schema: map[string]*schema.Schema{},
  }
}

func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}

func resourceOrderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}

func resourceOrderUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  return resourceOrderRead(ctx, d, m)
}

func resourceOrderDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}</pre>
<p>我们需要将其注册到Provider：</p>
<pre class="crayon-plain-tag">func Provider() *schema.Provider {
	return &amp;schema.Provider{
		// ...
		ResourcesMap: map[string]*schema.Resource{
			"hashicups_order": resourceOrder(),
		},
		// ...
	}
}</pre>
<div class="blog_h3"><span class="graybg">订单Schema </span></div>
<p>不同于上问的订单数据源，这里我们设计了和API结构更加匹配的Schema：</p>
<pre class="crayon-plain-tag">Schema: map[string]*schema.Schema{
			"items": {
				Type:     schema.TypeList,
				Required: true,
				Elem: &amp;schema.Resource{
					Schema: map[string]*schema.Schema{
						"coffee": {
							Type:     schema.TypeList, // coffee明明是一个，还非得声明为列表
							MaxItems: 1,
							Required: true,
							Elem: &amp;schema.Resource{
								Schema: map[string]*schema.Schema{
									"id": {
										Type:     schema.TypeInt,
										Required: true,
									},
									"name": {
										Type:     schema.TypeString,
										Computed: true,
									},
									"teaser": {
										Type:     schema.TypeString,
										Computed: true,
									},
									"description": {
										Type:     schema.TypeString,
										Computed: true,
									},
									"price": {
										Type:     schema.TypeInt,
										Computed: true,
									},
									"image": {
										Type:     schema.TypeString,
										Computed: true,
									},
								},
							},
						},
						"quantity": {
							Type:     schema.TypeInt,
							Required: true,
						},
					},
				},
			},
		}</pre>
<p>注意和订单数据源Schema的其它几个重要区别：</p>
<ol>
<li>在资源Schema中，顶级的id属性不存在。这是因为资源中你<span style="background-color: #c0c0c0;">无法提前知道id，也不能将id作为输入参数。id是在资源创建过程中自动生成</span>的</li>
<li>在资源Schema中，items是<span style="background-color: #c0c0c0;">必须字段，而不是计算出的字段。这是因为我们在配置中声明订单资源时必须提供订单项信息</span></li>
</ol>
<div class="blog_h3"><span class="graybg">创建操作 </span></div>
<pre class="crayon-plain-tag">func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	// 元参数的真实类型是HashCups客户端
	c := m.(*hc.Client)
	var diags diag.Diagnostics

	// 从ResourceData中获得订单条目（的参数）
	items := d.Get("items").([]interface{})
	ois := []hc.OrderItem{}

	// 遍历订单条目，构造为HashCups客户端所需的OrderItem
	for _, item := range items {
		i := item.(map[string]interface{})

		co := i["coffee"].([]interface{})[0] // 只取第一个coffee，为何非要定义为列表
		coffee := co.(map[string]interface{})

		oi := hc.OrderItem{
			Coffee: hc.Coffee{
				ID: coffee["id"].(int),
			},
			Quantity: i["quantity"].(int),
		}

		ois = append(ois, oi)
	}

	// 调用客户端创建订单
	o, err := c.CreateOrder(ois)
	if err != nil {
		return diag.FromErr(err)
	}

	// 将生成的标识符设置为资源ID
	d.SetId(strconv.Itoa(o.ID))

	return diags
}</pre>
<div class="blog_h3"><span class="graybg">创建后填充状态</span></div>
<p>实现了创建操作后，必须同时实现读操作，并<span style="background-color: #c0c0c0;">在创建操作中调用读操作，这样才能在创建资源后，立即以最新资源填充state</span>：</p>
<pre class="crayon-plain-tag">func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	// ...
	d.SetId(strconv.Itoa(o.ID))

	resourceOrderRead(ctx, d, m)

	return diags
}</pre>
<div class="blog_h3"><span class="graybg">读操作 </span></div>
<pre class="crayon-plain-tag">func resourceOrderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  c := m.(*hc.Client)
  var diags diag.Diagnostics
  // 读操作时，资源的ID是已知的（从state中获取）
  orderID := d.Id()
  // 根据ID查询订单
  order, err := c.GetOrder(orderID)
  if err != nil {
    return diag.FromErr(err)
  }
  // 结构转换
  orderItems := flattenOrderItems(&amp;order.Items)
  if err := d.Set("items", orderItems); err != nil {
    return diag.FromErr(err)
  }

  return diags
}

func flattenOrderItems(orderItems *[]hc.OrderItem) []interface{} {
  if orderItems != nil {
    ois := make([]interface{}, len(*orderItems), len(*orderItems))

    for i, orderItem := range *orderItems {
      oi := make(map[string]interface{})

      oi["coffee"] = flattenCoffee(orderItem.Coffee)
      oi["quantity"] = orderItem.Quantity
      ois[i] = oi
    }

    return ois
  }

  return make([]interface{}, 0)
}

func flattenCoffee(coffee hc.Coffee) []interface{} {
  c := make(map[string]interface{})
  c["id"] = coffee.ID
  c["name"] = coffee.Name
  c["teaser"] = coffee.Teaser
  c["description"] = coffee.Description
  c["price"] = coffee.Price
  c["image"] = coffee.Image

  return []interface{}{c}
}</pre>
<div class="blog_h3"><span class="graybg">编写Terraform配置</span></div>
<p>下面的配置，创建了一个订单，并且输出其内容</p>
<pre class="crayon-plain-tag">resource "hashicups_order" "edu" {
  // 订单的Schema中，items是List，要为其声明多个元素，多次添加items块
  items {
    // 订单的Schema中，coffee也是一个List
    coffee {
      id = 3
    }
    quantity = 2
  }
  items {
    coffee {
      id = 2
    }
    quantity = 2
  }
}

output "edu_order" {
  value = hashicups_order.edu
}</pre>
<div class="blog_h2"><span class="graybg">实现更新操作</span></div>
<div class="blog_h3"><span class="graybg">更新订单Schema </span></div>
<pre class="crayon-plain-tag">func resourceOrder() *schema.Resource {
	return &amp;schema.Resource{
		// ...
		UpdateContext: resourceOrderUpdate,
		Schema: map[string]*schema.Schema{
			"last_updated": &amp;schema.Schema{
				Type:     schema.TypeString,
				Optional: true,  // 可选字段，允许配置时不提供
				Computed: true,  // 在创建时自动计算出（由Provider给出）
			},
			"items": &amp;schema.Schema{ 
			/...</pre>
<div class="blog_h3"><span class="graybg">更新操作</span></div>
<pre class="crayon-plain-tag">// 判断指定的键，对应的值是否改变了。通过对比比较配置文件前后的差异达成
	if d.HasChange("items") {
		items := d.Get("items").([]interface{})
		ois := []hc.OrderItem{}

		for _, item := range items {
			i := item.(map[string]interface{})

			co := i["coffee"].([]interface{})[0]
			coffee := co.(map[string]interface{})

			oi := hc.OrderItem{
				Coffee: hc.Coffee{
					ID: coffee["id"].(int),
				},
				Quantity: i["quantity"].(int),
			}
			ois = append(ois, oi)
		}
		// 调用HashCups API更新灯胆
		_, err := c.UpdateOrder(orderID, ois)
		if err != nil {
			return diag.FromErr(err)
		}

		// 更新 last_updated字段
		d.Set("last_updated", string(time.Now().Format(time.RFC850)))
	}
	
	// 总是重新读取订单最新状态
	return resourceOrderRead(ctx, d, m)</pre>
<div class="blog_h2"><span class="graybg">实现删除操作 </span></div>
<div class="blog_h3"><span class="graybg">删除操作 </span></div>
<pre class="crayon-plain-tag">func resourceOrderDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	c := m.(*hc.Client)
	var diags diag.Diagnostics

	orderID := d.Id()
	// 调用HashiCups API执行删除
	err := c.DeleteOrder(orderID)
	if err != nil {
		return diag.FromErr(err)
	}

	// 注意：d.SetId("") 会在本方法无错误返回的情况下，自动调用
	// 通常不需要手工调用
	d.SetId("")

	return diags
}</pre>
<p>Terraform处理该回调的返回值的逻辑：</p>
<ol>
<li>如果没有返回错误，则Terraform认为资源被删除，其所有状态信息被从state中移除</li>
<li>如果返回了错误，则Terraform认为资源仍然存在，其所有状态信息会被保留</li>
</ol>
<p>该回调：</p>
<ol>
<li><span style="background-color: #c0c0c0;">永远不应该更新资源的任何状态</span></li>
<li>总是应当处理<span style="background-color: #c0c0c0;">资源已经被删除的场景</span>，这种情况下不应该返回错误</li>
<li>如果云API不支持删除操作，则该回调应该检查资源是否存在，如果不存在则设置ID为""，确保资源从state中删除</li>
</ol>
<div class="blog_h2"><span class="graybg">实现导入操作</span></div>
<p>导入操作能够通过API拉取已经存在的订单，并且同步到Terraform state中，不进行创建订单的操作。 <span style="background-color: #c0c0c0;">导入的资源</span>和通过Terraform创建的资源已有，<span style="background-color: #c0c0c0;">被Terraform管理</span>。</p>
<div class="blog_h3"><span class="graybg">导入操作</span></div>
<pre class="crayon-plain-tag">func resourceOrder() *schema.Resource {
	return &amp;schema.Resource{
		// ...
		Importer: &amp;schema.ResourceImporter{
			StateContext: schema.ImportStatePassthroughContext,
		},
	}
}</pre>
<p>StateContext被设置为Terraform库提供的函数schema.ImportStatePassthroughContext，该函数签名为：</p>
<pre class="crayon-plain-tag">// StateContextFunc用于导入资源到Terraform state。入参是仅仅设置了ID的资源，这个ID
// 由用户提供，因此需要校验
// 返回值是ResourceData，代表需要存入state的资源状态。最简单的情况下，仅仅包含原封不动的入参
type StateContextFunc func(context.Context, *ResourceData, interface{}) ([]*ResourceData, error)</pre>
<p>函数的实现很简单，就是直接把入参返回：</p>
<pre class="crayon-plain-tag">func ImportStatePassthroughContext(ctx context.Context, d *ResourceData, m interface{}) ([]*ResourceData, error) {
	return []*ResourceData{d}, nil
}</pre>
<div class="blog_h3"><span class="graybg">导入命令 </span></div>
<pre class="crayon-plain-tag">terraform import hashicups_order.sample &lt;order_id&gt;</pre>
<p>上述命令将名为sample的hasicups_order和指定的订单ID关联起来。Terraform会调用Importer，并将ID传递给resourceOrderRead来读取完整的状态。</p>
<div class="blog_h3"><span class="graybg">关于ID</span></div>
<p>从导入操作我们可以看到，资源的唯一性ID很重要。某些情况下，我们可能需要从云API的多个字段去构造ID，例如<pre class="crayon-plain-tag">&lt;region&gt;:&lt;resource_id&gt;</pre></p>
<div class="blog_h2"><span class="graybg">发布Provider</span></div>
<p>Terraform Registry是Provider和Module的公共仓库。如果你开发了有复用价值的Provider，可以<a href="https://learn.hashicorp.com/tutorials/terraform/provider-release-publish?in=terraform/providers">考虑上传到其中</a>。</p>
<div class="blog_h2"><span class="graybg">深入Schema </span></div>
<p>几乎所有Provider为用户提供配置参数，以实现云API的访问凭证、Region信息等的可定制化。下面的资源，允许你提供uuid、name两个参数：</p>
<pre class="crayon-plain-tag">func resourceExampleResource() *schema.Resource {
    return &amp;schema.Resource{
        // 每个资源，从根上来说，是一个{键值}结构
        //                 需要为每个值指定Schema
        Schema: map[string]*schema.Schema{
            "uuid": {
                // 指定值的类型
                Type:     schema.TypeString,
                // 这个字段会在资源创建时，自动生成
                Computed: true,
            },

            "name": {
                Type:         schema.TypeString,
                // 这是必须字段，用户必须提供
                Required:     true,
                // 该字段改变后，资源会被删除、重新创建
                ForceNew:     true,
                // 该字段的校验逻辑
                ValidateFunc: validatName,
            },
        },
    }
}</pre>
<div class="blog_h3"><span class="graybg">类型</span></div>
<p>类型可以分为两大类：基本类型、聚合类型。</p>
<p>基本类型包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 90px; text-align: center;">类型</td>
<td style="width: 55%; text-align: center;">Schema示例<br />配置示例</td>
<td style="text-align: center;">状态表示</td>
</tr>
</thead>
<tbody>
<tr>
<td><span style="color: #000000;">TypeBool<br />(bool)</span></td>
<td>
<pre class="crayon-plain-tag">"encrypted": {
  Type:     schema.TypeBool,
}, </pre><br />
<pre class="crayon-plain-tag">resource "example_volume" "ex" {
  encrypted = true
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">"encrypted": "true",</pre></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>TypeInt<br />(int)</td>
<td>
<pre class="crayon-plain-tag">"cores": {
  Type:     schema.TypeInt,
},</pre><br />
<pre class="crayon-plain-tag">resource "example_compute_instance" "ex" {
  cores = 16
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">"cores": "16",</pre>
</td>
</tr>
<tr>
<td>TypeFloat<br />(float64)</td>
<td>
<pre class="crayon-plain-tag">"price": {
  Type:     schema.TypeFloat,
},</pre><br />
<pre class="crayon-plain-tag">resource "example_spot_request" "ex" {
  price = 0.37
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">"price": "0.37",</pre>
</td>
</tr>
<tr>
<td rowspan="2">
<p>TypeString</p>
<p>(string)</p>
</td>
<td>
<pre class="crayon-plain-tag">"name": {
  Type:     schema.TypeString,
},</pre><br />
<pre class="crayon-plain-tag">resource "example_spot_request" "ex" {
  description = "Managed by Terraform"
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">"description": "Managed by Terraform",</pre>
</td>
</tr>
<tr>
<td>
<p>日期时间，也使用TypeString，配合校验函数：</p>
<pre class="crayon-plain-tag">"expiration": {
  Type:         schema.TypeString,
  ValidateFunc: validation.ValidateRFC3339TimeString,
},

resource "example_resource" "ex" {
  expiration = "2006-01-02T15:04:05+07:00"
} </pre>
</td>
<td>
<pre class="crayon-plain-tag">"expiration": "2006-01-02T15:04:05+07:00",</pre>
</td>
</tr>
</tbody>
</table>
<p>聚合类型包括：
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 90px; text-align: center;">类型</td>
<td style="width: 55%; text-align: center;">Schema示例<br />配置示例</td>
<td style="text-align: center;">状态表示</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<p>TypeMap</p>
</td>
<td>
<p>(map[string]interface{})</p>
<pre class="crayon-plain-tag">"tags": {
  Type:     schema.TypeMap,
  Elem: &amp;schema.Schema{
    Type: schema.TypeString,
  },
},</pre><br />
<pre class="crayon-plain-tag">resource "example_compute_instance" "ex" {
  tags {
    env = "development"
    name = "example tag"
  }
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">#     元素数量
"tags.%": "2",
"tags.env": "development",
"tags.name": "example tag",</pre>
</td>
</tr>
<tr>
<td>TypeList</td>
<td>
<p>([]interface{})
<pre class="crayon-plain-tag">"termination_policies": {
  Type:     schema.TypeList,
  Elem: &amp;schema.Schema{
    Type: schema.TypeString,
  },
},</pre><br />
<pre class="crayon-plain-tag">resource "example_compute_instance" "ex" {
  termination_policies = ["OldestInstance",
    "ClosestToNextInstanceHour"]
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">#             元素数量
"name_servers.#": "4",
"name_servers.0": "ns-1508.awsdns-60.org",
"name_servers.1": "ns-1956.awsdns-52.co.uk",</pre>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>TypeSet</td>
<td>
<p>(*schema.Set)</p>
<pre class="crayon-plain-tag">"ingress": {
  Type:     schema.TypeSet,
  Elem: &amp;schema.Resource{
    Schema: map[string]*schema.Schema{
      "from_port": {
        Type:     schema.TypeInt,
        Required: true,
      },

      "to_port": {
        Type:     schema.TypeInt,
        Required: true,
      },
  },
}</pre><br />
<pre class="crayon-plain-tag">resource "example_security_group" "ex" {
  name        = "sg_test"              
  description = "managed by Terraform" 

  ingress {                            
    protocol    = "tcp"                
    from_port   = 80                   
    to_port     = 9000                 
    cidr_blocks = ["10.0.0.0/8"]       
  }                                    

  ingress {                            
    protocol    = "tcp"                
    from_port   = 80                   
    to_port     = 8000                 
    cidr_blocks = ["0.0.0.0/0", "10.0.0.0/8"]
  }                                    
}</pre>
</td>
<td>
<pre class="crayon-plain-tag">"ingress.#": "2",
"ingress.1061987227.cidr_blocks.#": "1",
"ingress.1061987227.cidr_blocks.0": "10.0.0.0/8",
"ingress.1061987227.description": "",
"ingress.1061987227.from_port": "80",
"ingress.1061987227.ipv6_cidr_blocks.#": "0",
"ingress.1061987227.protocol": "tcp",
"ingress.1061987227.security_groups.#": "0",
"ingress.1061987227.self": "false",</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">行为</span></div>
<p>Schema的一些字段的设置，会对Terraform的plan/apply行为产生影响。
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">影响</td>
</tr>
</thead>
<tbody>
<tr>
<td>Optional</td>
<td>是否在配置中是可选的</td>
</tr>
<tr>
<td>Required</td>
<td>是否必须在配置中提供</td>
</tr>
<tr>
<td>Computed</td>
<td>提示字段不能由用户提供，并且在terraform apply之前，其值是未知的</td>
</tr>
<tr>
<td>ForceNew</td>
<td>对资源的该字段的修改，会导致删除并重新创建资源</td>
</tr>
<tr>
<td>Default</td>
<td>如果用户没有配置，使用的默认值</td>
</tr>
<tr>
<td>DiffSuppressFunc</td>
<td>
<p>用于计算该字段的（前后值的）差异，下面的例子，不区分大小写：</p>
<pre class="crayon-plain-tag">"base_image": {
  Type:     schema.TypeString,
  DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
    if strings.ToLower(old) == strings.ToLower(new) {
      return true 
    }
    return false
  },
}, </pre>
</td>
</tr>
<tr>
<td>DefaultFunc</td>
<td>用于动态提供默认值</td>
</tr>
<tr>
<td>StateFunc</td>
<td>将字段转换为一个字符串，该字符串存储在state中</td>
</tr>
<tr>
<td>ValidateFunc</td>
<td>校验该字段</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">深入资源</span></div>
<div class="blog_h3"><span class="graybg">CustomizeDiff</span></div>
<p>Terrafrom通过比较用户提供的配置、资源的state，来确定是否需要更新。
<p>可以为schema.Resource传递CustomizeDiff，该回调和Terraform生成的Diff（表示资源的变更）一起工作，它可以：</p>
<ol>
<li>修改Diff</li>
<li>否决Diff，终止计划</li>
</ol>
<pre class="crayon-plain-tag">type CustomizeDiffFunc func(context.Context, *ResourceDiff, interface{}) error</pre>
<div class="blog_h3"><span class="graybg">超时</span></div>
<p>云上的很多操作比较耗时，例如启动操作系统、跨越网络边缘复制状态。开发Provider时，应该注意考虑云API的延迟，Terraform支持为资源的各种操作设置超时：</p>
<pre class="crayon-plain-tag">func resourceExampleInstance() *schema.Resource {
    return &amp;schema.Resource{
        // ...
        Timeouts: &amp;schema.ResourceTimeout{
            Create: schema.DefaultTimeout(45 * time.Minute),
        },
    }
}</pre>
<div class="blog_h3"><span class="graybg">重试</span></div>
<p>Terraform提供了一个重试助手函数：</p>
<pre class="crayon-plain-tag">type RetryFunc func() *RetryError

func RetryContext(ctx context.Context, timeout time.Duration, f RetryFunc) error</pre>
<p>RetryContext能够反复重试RetryFunc：</p>
<ol>
<li>timeout指定Terraform调用RetryFunc的最大用时。可以传递<pre class="crayon-plain-tag">schema.TimeoutCreate</pre>给<pre class="crayon-plain-tag">*schema.ResourceData.Timeout()</pre>获取用户配置的超时值</li>
<li>RetryFunc可以返回：
<ol>
<li>resource.NonRetryableError，这样直接导致重试终止</li>
<li>resource.RetryableError，这样会重试</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">可接受测试</span></div>
<p>基于测试用例来组织，每个用例使用1-N个Terraform配置，创建一些资源，并并验证实际创建的基础设施对象符合预期。</p>
<p>Terraform的<pre class="crayon-plain-tag">resource</pre>包提供了<pre class="crayon-plain-tag">Test()</pre>方法，该方法是Terraform可接受测试框架的入口点，它接受两个参数：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">*testing.T</pre> 来自Go语言的测试框架<br /><pre class="crayon-plain-tag">TestCase</pre>开发者提供的用于设立可接受测试的结构</p>
<p>下面是一个例子，它测试一个名为Example的Provider，被测试的资源是Widget：</p>
<pre class="crayon-plain-tag">package example

var testAccProviders map[string]*schema.Provider
var testAccProvider *schema.Provider

func init() {
	testAccProvider = Provider()
	// 被测试Provider
	testAccProviders = map[string]*schema.Provider{
		"example": testAccProvider,
	}
}

// 方法命名约定TestAccXxx
func TestAccExampleWidget_basic(t *testing.T) {
	var widgetBefore, widgetAfter example.Widget
	rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)

	// 大部分可接受测试，都是仅仅调用Test方法并退出
	// 任何时候（不管是PreCheck还是Steps...）出错，框架都会调用t.Error()方法，导致测试失败并终止
	resource.Test(t, resource.TestCase{
		// 是否单元测试。该参数允许在不考虑TF_ACC环境变量的情况下运行测试。应当仅仅用于本地资源的快速测试
		// 默认情况下，如果没有设置环境变量TF_ACC，测试会立即失败
		IsUnitTest: false,
		// 在任何Steps之前，允许的回调，通常用来检查测试需要的值（例如用于配置Provider的环境变量）存在
		PreCheck: func() { testAccPreCheck(t) },

		// map[string]*schema.Provider类型，表示将被测试的Providers。配置文件中引用的任何Provider都
		// 需要在此配置，否则用例会报错
		Providers:    testAccProviders,
		// 注意：Providers已经废弃，请使用ProviderFactories代替
		ProviderFactories: map[string]func() (*schema.Provider, error),
		// 类似ProviderFactories，但是用于基于terraform-plugin-go ProviderServer接口实现
		// Protocol V5的Provider
		ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error)
		// 类似ProviderFactories，但是用于基于terraform-plugin-go ProviderServer接口实现
		// Protocol V6的Provider
		ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error)

		// 所有Steps运行之后，并且Terraform已经针对state运行了destroy命令之后执行的回调
		// 该方法以最后一次已知的staet作为入参，通常直接使用基础设施的SDK来确认目标对象已都不存在
		// 如果应该被删除的对象仍然存在，此方法应该报错
		CheckDestroy: testAccCheckExampleResourceDestroy,
		// 允许Provider有选择性的处理错误，例如基于特定错误，跳过某些测试
		ErrorCheck ErrorCheckFunc,
		// 一个TestStep，通常对应单次apply。基础的测试包含1-2个Step，验证资源可以被创建，然后更新
		Steps: []resource.TestStep{
			{
				Config: testAccExampleResource(rName),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckExampleResourceExists("example_widget.foo", &amp;widgetBefore),
				),
			},
			{
				Config: testAccExampleResource_removedPolicy(rName),
				Check: resource.ComposeTestCheckFunc(
					testAccCheckExampleResourceExists("example_widget.foo", &amp;widgetAfter),
				),
			},
		},
	})
}</pre>
<div class="blog_h3"><span class="graybg">TestStep</span></div>
<p>每个TestStep需要Terraform Configuration作为输入，提供多种验证被测试资源行为的方法。</p>
<p>Terraform的测试框架支持两个独立的测试模式：</p>
<ol>
<li>Lifecycle模式，常用，提供1-N个配置文件并测试Provider在terraform apply时的行为</li>
<li>Import模式，测试Provider在terraform import时的行为</li>
</ol>
<p>测试模式被传递给TestStep的字段隐式的确定。</p>
<p>每个TestStep包含一个需要被Apply的配置（由Config字段给出）、0-N个校验（在Check字段中编排），多个TestStep按顺序，依次执行。</p>
<p>当需要执行多个校验时，需要使用下面的函数之一来编排：<pre class="crayon-plain-tag">ComposeTestCheckFunc</pre>、<pre class="crayon-plain-tag">ComposeAggregateTestCheckFunc</pre>。示例：</p>
<pre class="crayon-plain-tag">Steps: []resource.TestStep{
  {
    Check: resource.ComposeTestCheckFunc(
      // 检查资源是否存在于基础设施
      testAccCheckExampleResourceExists("example_widget.foo", &amp;widgetBefore), 
      // 校验资源属性
      resource.TestCheckResourceAttr("example_widget.foo", "size", "expected size"),
    ),
  },
},</pre>
<p><span style="background-color: #c0c0c0;">ComposeAggregateTestCheckFunc</span>的区别是，尽管也是顺序执行校验，但是<span style="background-color: #c0c0c0;">某个校验失败，并不会立即停止</span>，还会继续执行其它校验，收集所有错误。 </p>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg">基于日志</span></div>
<p>Terratform将所有stderr的输出都通过gRPC协议传输到CLI，并打印到控制台。编写插件时，<span style="background-color: #c0c0c0;">绝不要将日志打印到stdout，因为stdout作为Terraform内部到CLI的通信通道</span>。</p>
<p>建议使用Go内置的<pre class="crayon-plain-tag">log.Println</pre>或<pre class="crayon-plain-tag">log.Printf</pre>进行日志输出。日志的每行必须以<span style="background-color: #c0c0c0;">[日志级别]</span>开始，支持的级别包括<span style="background-color: #c0c0c0;">ERROR WARN INFO DEBUG TRACE</span>。示例：</p>
<pre class="crayon-plain-tag">log.Println("[DEBUG] Something happened!")</pre>
<p>Terraform使用环境变量<pre class="crayon-plain-tag">TF_LOG</pre>来控制Provider、CLI的日志输出级别：</p>
<pre class="crayon-plain-tag">export TF_LOG=DEBUG</pre>
<p>可以使用环境变量<pre class="crayon-plain-tag">TF_LOG_CORE</pre>、<pre class="crayon-plain-tag">TF_LOG_PROVIDER</pre>为Terraform核心、Provider设置不同的日志级别。</p>
<p>如果需要输出到文件，使用环境变量<pre class="crayon-plain-tag">TF_LOG_PATH</pre>，默认输出到CLI的stderr。</p>
<div class="blog_h3"><span class="graybg">单步跟踪</span></div>
<p>使用下面的代码，在Provider中启用单步跟踪支持：</p>
<pre class="crayon-plain-tag">func main() {
    var debugMode bool

    flag.BoolVar(&amp;debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve")
    flag.Parse()

    opts := &amp;plugin.ServeOpts{ProviderFunc: provider.New}

    if debugMode {
        //            调试模式
        err := plugin.Debug(context.Background(), "registry.terraform.io/my-org/my-provider", opts)
        if err != nil {
            log.Fatal(err.Error())
        }
        return
    }
    //     正常模式
    plugin.Serve(opts)
}</pre>
<p>关于调试模式，需要注意：</p>
<ol>
<li>在此模式下，Terraform不会启动Provider进程，你需要手工启动它：<br />
<pre class="crayon-plain-tag">dlv exec --headless ./terraform-provider-my-provider -- --debug</pre>
</li>
<li>你需要将IDE连接到到dlv进程，这时Provider会打印日志：<br />
<pre class="crayon-plain-tag">Provider started, to attach Terraform set the TF_REATTACH_PROVIDERS env var:

        TF_REATTACH_PROVIDERS='{"registry.terraform.io/my-org/my-provider":{"Protocol":"grpc","Pid":3382870,"Test":true,"Addr":{"Network":"unix","String":"/tmp/plugin713096927"}}}' </pre>
</li>
<li>你需要export上述日志输出的环境变量，然后执行Terraform CLI</li>
<li>在terraform init时不会对Provider进行约束检查</li>
<li>当遍历了Terraform资源依赖图后，Provider进程不会被重启 </li>
</ol>
<div class="blog_h3"><span class="graybg">可接受测试中的单步跟踪</span></div>
<p>在运行可接受测试的时候，直接在当前测试进程中运行Provider，因此可以进行单步跟踪而不需要特别设置。</p>
<div class="blog_h1"><span class="graybg">基于TPF开发Provider</span></div>
<p>Terraform Plugin Framework是新的（但还不稳定）Provider开发方式，关于它的优缺点，上文已经介绍过。本章主要以例子说明如何使用该框架。</p>
<p>要使用TPF，在go.mod增加依赖：</p>
<pre class="crayon-plain-tag">github.com/hashicorp/terraform-plugin-framework v0.4.2 </pre>
<div class="blog_h2"><span class="graybg">入口点</span></div>
<pre class="crayon-plain-tag">package main

import (
    "context"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "terraform-provider-hashicups/hashicups"
)

func main() {
    tfsdk.Serve(context.Background(), hashicups.New, tfsdk.ServeOpts{
        Name: "hashicups",
    })
}</pre>
<p>tfsdk.Serve函数的第二个参数，就是任何Provider需要实现的接口：</p>
<pre class="crayon-plain-tag">type Provider interface {
	// 返回配置Provider的Schema，如果Provider不需要配置返回空的Schema
	GetSchema(context.Context) (Schema, diag.Diagnostics)

	// 在Provider生命周期的最初期此方法被调用，Terraform会此方法发送用户在provider块中
	// 提供的参数，并且存放在ConfigureProviderRequest中。注意，Terraform不保证此方法调用时，
	// 所有参数都是Known的。如果Provider在某些参数在Unknown的时候仍然可被配置，建议发出警告
	// 否则发出错误
	Configure(context.Context, ConfigureProviderRequest, *ConfigureProviderResponse)

	// 返回此Provider支持的资源类型列表
	// 资源列表的键，是资源的名字，并且应当以Provider名为前缀
	GetResources(context.Context) (map[string]ResourceType, diag.Diagnostics)

	// 返回此Provider支持的数据源类型列表
	// 资源列表的键，是资源的名字，并且应当以Provider名为前缀
	GetDataSources(context.Context) (map[string]DataSourceType, diag.Diagnostics)
}</pre>
<div class="blog_h2"><span class="graybg">配置Provider</span></div>
<pre class="crayon-plain-tag">package hashicups

import (
    "context"
    "os"

    "github.com/hashicorp-demoapp/hashicups-client-go"
    "github.com/hashicorp/terraform-plugin-framework/diag"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "github.com/hashicorp/terraform-plugin-framework/types"
)

var stderr = os.Stderr

func New() tfsdk.Provider {
    return &amp;provider{}
}

type provider struct {
    configured bool
    client     *hashicups.Client
}</pre>
<div class="blog_h3"><span class="graybg">Schema</span></div>
<p>GetSchema方法，获取Provider的配置参数的Schema： </p>
<pre class="crayon-plain-tag">// GetSchema
func (p *provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
    return tfsdk.Schema{
        Attributes: map[string]tfsdk.Attribute{
            "host": {
                Type:     types.StringType,
                Optional: true,
                Computed: true,
            },
            "username": {
                Type:     types.StringType,
                Optional: true,
                Computed: true,
            },
            "password": {
                Type:      types.StringType,
                Optional:  true,
                Computed:  true,
                Sensitive: true,
            },
        },
    }, nil
}

// Provider schema struct
type providerData struct {
    Username types.String `tfsdk:"username"`
    Host     types.String `tfsdk:"host"`
    Password types.String `tfsdk:"password"`
}</pre>
<div class="blog_h3"><span class="graybg">Configure </span></div>
<pre class="crayon-plain-tag">// req 代表Terraform在配置Provider时，发送过来的请求
// resp 代表响应
func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, 
                                                  resp *tfsdk.ConfigureProviderResponse) {

	var config providerData
	// 从用户给出的Provider配置中取出数据，存放到providerData中
	diags := req.Config.Get(ctx, &amp;config)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

	// 校验username password host是否提供
	var username string
	// 值不能是未知的
	if config.Username.Unknown {
		// Cannot connect to client with an unknown value
		resp.Diagnostics.AddWarning(
			"Unable to create client",
			"Cannot use unknown value as username",
		)
		return
	}

	// 如果值没有设置（或者被明确的设置为null），则尝试从环境变量读取
	if config.Username.Null {
		username = os.Getenv("HASHICUPS_USERNAME")
	} else {
		username = config.Username.Value
	}

	// 如果配置是空字符串，并且也没有配置环境变量，则报错
	if username == "" {
		// Error vs warning - empty value must stop execution
		resp.Diagnostics.AddError(
			"Unable to find username",
			"Username cannot be an empty string",
		)
		return
	}
	// password, host类似...

	c, err := hashicups.NewClient(&amp;host, &amp;username, &amp;password)
	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to create client",
			"Unable to create hashicups client:\n\n"+err.Error(),
		)
		return
	}

	p.client = c
	p.configured = true
}</pre>
<div class="blog_h2"><span class="graybg">定义资源</span></div>
<div class="blog_h3"><span class="graybg">资源模型</span></div>
<pre class="crayon-plain-tag">package hashicups

import (
    "github.com/hashicorp/terraform-plugin-framework/types"
)

// Order -
type Order struct {
    ID          types.String `tfsdk:"id"`
    Items       []OrderItem  `tfsdk:"items"`
    LastUpdated types.String `tfsdk:"last_updated"`
}

// OrderItem -
type OrderItem struct {
    Coffee   Coffee `tfsdk:"coffee"`
    Quantity int    `tfsdk:"quantity"`
}

// Coffee -
// This Coffee struct is for Order.Items[].Coffee which does not have an
// ingredients field in the schema defined in the provider code. Since the
// resource schema must match the struct exactly (any extra field will return an
// error), this struct has Ingredients commented out.
type Coffee struct {
    ID          int          `tfsdk:"id"`
    Name        types.String `tfsdk:"name"`
    Teaser      types.String `tfsdk:"teaser"`
    Description types.String `tfsdk:"description"`
    Price       types.Number `tfsdk:"price"`
    Image       types.String `tfsdk:"image"`
    // Ingredients []Ingredient   `tfsdk:"ingredients"`
}</pre>
<p>注意：资源模型必须和资源的Schema（如下节）严格匹配，如果模型里面有某字段，而Schema没有，会导致出错。</p>
<p>TPF和SDKv2比起来，一个优势是<span style="background-color: #c0c0c0;">实现了模型字段的自动绑定</span>，不再需要一个个字段手工设置。但是这个模型不一定能和云API返回的资源结构自动的相互转换。</p>
<div class="blog_h3"><span class="graybg">资源类型</span></div>
<p>首先需要声明Provider支持哪些资源，即提供资源名称（对应resource块第2标签）到资源类型<pre class="crayon-plain-tag">tfsdk.ResourceType</pre>的映射：</p>
<pre class="crayon-plain-tag">func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
    return map[string]tfsdk.ResourceType{
        "hashicups_order": resourceOrderType{},
    }, nil
}</pre>
<p>资源类型需要实现下面的接口：</p>
<pre class="crayon-plain-tag">type ResourceType interface {
	// 返回资源的Schema
	GetSchema(context.Context) (Schema, diag.Diagnostics)

	// 创建该资源类型的Resource对象
	NewResource(context.Context, Provider) (Resource, diag.Diagnostics)
} </pre>
<div class="blog_h3"><span class="graybg">资源Schema</span></div>
<pre class="crayon-plain-tag">type resourceOrderType struct{}

func (r resourceOrderType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
    return tfsdk.Schema{
        Attributes: map[string]tfsdk.Attribute{
            "id": {
                Type: types.StringType,
                Computed: true,
            },
            "last_updated": {
                Type:     types.StringType,
                Computed: true,
            },
            "items": {
                Required: true,
                Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
                    "quantity": {
                        Type:     types.NumberType,
                        Required: true,
                    },
                    "coffee": {
                        Required: true,
                        Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{
                            "id": {
                                Type:     types.NumberType,
                                Required: true,
                            },
                            "name": {
                                Type:     types.StringType,
                                Computed: true,
                            },
                            "teaser": {
                                Type:     types.StringType,
                                Computed: true,
                            },
                            "description": {
                                Type:     types.StringType,
                                Computed: true,
                            },
                            "price": {
                                Type:     types.NumberType,
                                Computed: true,
                            },
                            "image": {
                                Type:     types.StringType,
                                Computed: true,
                            },
                        }),
                    },
                }, tfsdk.ListNestedAttributesOptions{}),
            },
        },
    }, nil
}</pre>
<div class="blog_h3"><span class="graybg">资源 </span></div>
<p>资源类型的NewResource方法返回的是Resource类型，它抽象了针对单个资源的CRUD操作：</p>
<pre class="crayon-plain-tag">type Resource interface {
	// 当Provider需要创建一个新资源的时候调用此方法。配置和planned state可以从
	// CreateResourceRequest读取。新的（实际完成创建后的）state则设置到
	// CreateResourceResponse
	Create(context.Context, CreateResourceRequest, *CreateResourceResponse)

	// 当Provider需要读取资源values来更新state时调用此方法。从
	// ReadResourceRequest读取planned state，新的state则设置到
	// ReadResourceResponse.
	Read(context.Context, ReadResourceRequest, *ReadResourceResponse)

	// 当Provider需要更新资源状态时调用此方法。配置和planned state可以从
	// UpdateResourceRequest读取。新的（更新后的）state则设置到
	// UpdateResourceResponse.
	Update(context.Context, UpdateResourceRequest, *UpdateResourceResponse)

	// 当Provider需要删除资源时调用此方法。配置从DeleteResourceRequest读取
	Delete(context.Context, DeleteResourceRequest, *DeleteResourceResponse)

	// 当前Provider需要导入一个资源时调用此方法。
	// 如果不支持导入，建议返回 ResourceImportStateNotImplemented()
	//
	// If setting an attribute with the import identifier, it is recommended
	// to use the ResourceImportStatePassthroughID() call in this method.
	ImportState(context.Context, ImportResourceStateRequest, *ImportResourceStateResponse)
}</pre>
<div class="blog_h3"><span class="graybg">实现创建接口</span></div>
<p>创建操作的关键步骤：</p>
<ol>
<li>从请求中读取plan数据，读取为模型</li>
<li>将模型转换为云API请求，并发送求</li>
<li>将响应转换并同步到模型，写入state</li>
</ol>
<p>需要注意：plan中的每个已知（known）值，必须和state中对应值的完全一样（逐字节相等），也就是说，<span style="background-color: #c0c0c0;">用户指定的配置不能被改变</span>，否则Terraform抛出错误。<span style="background-color: #c0c0c0;">Provider只能修改计划中unknown的值，而且必须解析所有unknown的值，state中不会有任何unknown的值</span>。</p>
<pre class="crayon-plain-tag">import (
    "context"
    "math/big"
    "strconv"
    "time"


    "github.com/hashicorp-demoapp/hashicups-client-go"
    "github.com/hashicorp/terraform-plugin-framework/diag"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "github.com/hashicorp/terraform-plugin-framework/types"
)


func (r resourceOrder) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    // 必须确保Provider已经被配置过
    if !r.p.configured {
        resp.Diagnostics.AddError(
            "Provider not configured",
            "The provider hasn't been configured before apply, likely because it depends on an unknown value from another resource. This leads to weird stuff happening, so we'd prefer if you didn't do that. Thanks!",
        )
        return
    }

    // 从执行计划获取订单模型
    var plan Order
    //                注意这里会自动将配置绑定到模型，不需要手工处理
    diags := req.Plan.Get(ctx, &amp;plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 遍历订单项，将其适配为HashiCups客户端需要的入参
    var items []hashicups.OrderItem
    for _, item := range plan.Items {
        items = append(items, hashicups.OrderItem{
            Coffee: hashicups.Coffee{
                ID: item.Coffee.ID,
            },
            Quantity: item.Quantity,
        })
    }

    // 调用HashiCups客户端创建订单
    order, err := r.p.client.CreateOrder(items)
    if err != nil {
        resp.Diagnostics.AddError(
            "Error creating order",
            "Could not create order, unexpected error: "+err.Error(),
        )
        return
    }

    // 将HasiCups创建的完整订单对象，适配回模型OrderItem
    var ois []OrderItem
    for _, oi := range order.Items {
        ois = append(ois, OrderItem{
            Coffee: Coffee{
                ID:          oi.Coffee.ID,
                Name:        types.String{Value: oi.Coffee.Name},
                Teaser:      types.String{Value: oi.Coffee.Teaser},
                Description: types.String{Value: oi.Coffee.Description},
                Price:       types.Number{Value: big.NewFloat(oi.Coffee.Price)},
                Image:       types.String{Value: oi.Coffee.Image},
            },
            Quantity: oi.Quantity,
        })
    }

    // 完整订单模型
    var result = Order{
        ID:          types.String{Value: strconv.Itoa(order.ID)},
        Items:       ois,
        LastUpdated: types.String{Value: string(time.Now().Format(time.RFC850))},
    }

    // 设置状态
    diags = resp.State.Set(ctx, result)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}</pre>
<div class="blog_h3"><span class="graybg">实现读取接口</span></div>
<p>读取操作更新state，使其反映（通过云API得到的）云基础设施对象的最新的、真实状态。</p>
<p>实现读操作的时候，<span style="background-color: #c0c0c0;">没有plan（用户提供的配置）可用</span>。你需要<span style="background-color: #c0c0c0;">从请求中读取当前的状态，从中取得执行调用云API所需的信息</span>。</p>
<p>在更新时，Provider理论上可以修改state中的任何值，但是主要应当：</p>
<ol>
<li>处理值漂移（drift），也就是外部系统或人员修改了Terraform所拥有（创建并在状态中管理）的资源。<span style="background-color: #c0c0c0;">漂移的值总应该反映到state中</span></li>
<li>处理<span style="background-color: #c0c0c0;">语义上没有改变的值</span>，比如某个值是个JSON，它的字段顺序可能调整了，尽管新旧值并不是逐字节相等的，但是<span style="background-color: #c0c0c0;">并不应当更新state</span></li>
</ol>
<pre class="crayon-plain-tag">func (r resourceOrder) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) {
    // 获取当前状态，状态和计划一样，都是模型
    var state Order
    diags := req.State.Get(ctx, &amp;state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }

    // 得到状态中的ID，则是访问HashiCups所需参数
    orderID := state.ID.Value

    // 通过HashiCups客户端获取资源最新信息
    order, err := r.p.client.GetOrder(orderID)
    if err != nil {
        resp.Diagnostics.AddError(
            "Error reading order",
            "Could not read orderID "+orderID+": "+err.Error(),
        )
        return
    }

    // 转换为模型
    state.Items = []OrderItem{}
    for _, item := range order.Items {
        state.Items = append(state.Items, OrderItem{
            Coffee: Coffee{
                ID:          item.Coffee.ID,
                Name:        types.String{Value: item.Coffee.Name},
                Teaser:      types.String{Value: item.Coffee.Teaser},
                Description: types.String{Value: item.Coffee.Description},
                Price:       types.Number{Value: big.NewFloat(item.Coffee.Price)},
                Image:       types.String{Value: item.Coffee.Image},
            },
            Quantity: item.Quantity,
        })
    }

    // 设置到状态
    diags = resp.State.Set(ctx, &amp;state)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}</pre>
<div class="blog_h3"><span class="graybg">实现更新操作</span></div>
<p>更新操作将用户对资源配置的修改，通过云API同步到基础设施对象，然后更新state。关键步骤：</p>
<ol>
<li>从请求中读取plan数据，并绑定到模型</li>
<li>将模型转换为云API请求</li>
<li>将响应转换并同步到模型，写入state</li>
</ol>
<p>需要注意：known的值在更新前后，必须在state中逐字节对应。state不能包含任何unknown的值。</p>
<div class="blog_h3"><span class="graybg">实现删除操作</span></div>
<p>删除操作读取state信息，发起云API请求删除对应基础设施对象，然后从state中清除资源记录。</p>
<p>清除资源记录时调用<pre class="crayon-plain-tag">State.RemoveResource</pre>方法。</p>
<div class="blog_h3"><span class="graybg">实现导入操作</span></div>
<p>ImportState方法可以创建一个初始的state，将资源纳管起来。此方法的实现必须<span style="background-color: #c0c0c0;">确保后续读操作能够正常刷新状态</span>。 </p>
<p>通常需要从请求中读取<pre class="crayon-plain-tag">req.ID</pre>，并设置到响应中：</p>
<pre class="crayon-plain-tag">resp.State.SetAttribute(ctx, path, req.ID)

// 或者，直接调用：
tfsdk.ResourceImportStatePassthroughID()</pre>
<div class="blog_h2"><span class="graybg">定义数据源</span></div>
<p>数据源的资源模型和资源没有区别，数据源仅仅支持Read方法。该方法无法使用plan或state，它只能从<pre class="crayon-plain-tag">tfsdk.ReadDataSourceRequest</pre>中读取调用云API所需的参数。 </p>
<div class="blog_h2"><span class="graybg">属性/值类型</span></div>
<p>属性就是Provider、资源、数据源的Schema的字段，属性持有最终落地到state的值。每个属性都有对应的类型。当你从config、state或者plan访问属性时，实际上是访问属性的值。</p>
<div class="blog_h3"><span class="graybg">Null和Unknown</span></div>
<p>任何类型的属性，都可以持有这两种值。</p>
<p>Null表示值不存在，通常是由于用户没有给optional属性赋值。required属性永远不会是Null。</p>
<p>Unknown表示属性的值尚不知道。Unknown值和Terraform的依赖管理有关。Terraform会构建资源之间的依赖DAG，当资源A引用了资源B的属性a，并且a此时的值是Unknown，则此时就Terraform就会转而去处理B，通过调用云API取得a的值，然后再处理A。资源创建/读取后，任何字段都不能是Unknown的。</p>
<div class="blog_h3"><span class="graybg">内置类型和值</span></div>
<p>TPF的<pre class="crayon-plain-tag">types</pre>包，提供了一系列内置属性类型。每个属性类型对应两个Go结构，其中一个用做Schema中的属性类型声明（tfsdk.Attribute.Type），另外一个在定义模型时，作为模型的字段类型。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 200px; text-align: center;">Schema属性类型<br />值（模型字段）类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">StringType<br />String</td>
<td>
<p>表示UTF-8编码的字符串</p>
</td>
</tr>
<tr>
<td>Int64Type<br />Int64</td>
<td>64bit整数</td>
</tr>
<tr>
<td>Float64Type<br />Float64</td>
<td>64bit浮点数</td>
</tr>
<tr>
<td>NumberType<br />Number</td>
<td>通用数字类型，对应Go语言的<pre class="crayon-plain-tag">*big.Float</pre></td>
</tr>
<tr>
<td>BoolType<br />Bool</td>
<td>布尔类型</td>
</tr>
<tr>
<td>ListType<br />List</td>
<td>
<p>列表类型</p>
<p>types.ListType的<pre class="crayon-plain-tag">ElemType</pre>属性，说明元素的类型</p>
<p>types.List的属性：</p>
<p style="padding-left: 30px;">ElemType，总是和ListType的ElemType一致<br />Elem，是值的列表，每个值的真实类型为ElemType<br />Null，当整个列表的值是null时，为true</p>
<p>types.List的非Null、非Unknown元素，可以直接通过其<pre class="crayon-plain-tag">ElementsAs</pre>方法访问，不需要类型断言</p>
</td>
</tr>
<tr>
<td>SetType<br />Set</td>
<td>类似于列表，但是元素唯一、无序</td>
</tr>
<tr>
<td>MapType<br />Map</td>
<td>
<p>具有string类型键的映射</p>
<p>types.Map的属性：</p>
<p style="padding-left: 30px;">ElemType，总是和MapType的ElemType一致<br />Elem，是一个映射，每个键是字符串，每个值的真实类型为ElemType<br />Null，当整个映射是null时，为true</p>
<p>types.Map的非Null、非Unknown元素，可以直接通过其<pre class="crayon-plain-tag">ElementsAs</pre>方法访问，不需要类型断言</p>
</td>
</tr>
<tr>
<td>ObjectType<br />Object</td>
<td>
<p>所谓对象，是指其它若干其它不限类型的属性的无序集合，每个属性都被赋予名字</p>
<p>你需要通过<pre class="crayon-plain-tag">AttrTypes</pre>为对象的所有属性声明名称和类型</p>
<p>types.Object的属性：</p>
<p style="padding-left: 30px;">AttrTypes，总是和ObjectType.AttrType一致<br />Attrs，从属性名到属性值的映射，每个属性都保证在其中，不管它的值是什么<br />Null，当整个Object是null时，为true</p>
<p>非Null、非Unknown的types.Object的值，可以使用<pre class="crayon-plain-tag">As</pre>方法转换为Go结构，不需要类型断言</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">访问状态/配置/计划</span></div>
<p>很多情况下Provider需要访问用户提供的配置数据、Terraform的状态、生成的执行计划中的数据。这些数据通常存放在请求对象中：</p>
<pre class="crayon-plain-tag">func (m myResource) Create(ctx context.Context,
    req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse)</pre>
<div class="blog_h3"><span class="graybg">整体读写 </span></div>
<p>最简单的访问配置/计划/状态的方法是，将其转换为一个Go类型（模型）：</p>
<pre class="crayon-plain-tag">type resourceData struct {
    Name types.String `tfsdk:"name"`
    Age types.Number `tfsdk:"age"`
    Registered types.Bool `tfsdk:"registered"`
    Pets types.List `tfsdk:"pets"`
    Tags types.Map `tfsdk:"tags"`
    Address types.Object `tfsdk:"address"`
}

func (m myResource) Create(ctx context.Context,
    req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    var plan resourceData
    diags := req.Plan.Get(ctx, &amp;plan)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    // 可以通过plan.Name.Value访问计划中的值，你需要检查
    // plan.Name.Null 判断值是否是null的
    // plan.Name.Unknown 判断值是否是unknown的
}</pre>
<p>上面这些模型，字段类型都是<pre class="crayon-plain-tag">attr.Value</pre>的实现。其好处是，能够知晓值是不是Null、Unknown的，但是带来了不必要的复杂性：</p>
<ol>
<li>这些类型都是Terraform私有的，不可能在云API的SDK中使用这些类型。因此将模型转换为SDK类型时就有了额外的负担，难以自动化转换</li>
</ol>
<p>好在<span style="background-color: #c0c0c0;">Get方法能够将值转换为Go类型</span>，因此你可以这样声明模型：</p>
<pre class="crayon-plain-tag">type resourceData struct {
  Name string `tfsdk:"name"`
  Age int64 `tfsdk:"age"`
  Registered bool `tfsdk:"registered"`
  Pets []string `tfsdk:"pets"`
  Tags map[string]string `tfsdk:"tags"`
  Address struct{
    Street string `tfsdk:"street"`
    City string `tfsdk:"city"`
    State string `tfsdk:"state"`
    Zip int64 `tfsdk:"zip"`
  } `tfsdk:"address"`
}</pre>
<p><span style="background-color: #c0c0c0;">警告：Null值/Unknown值可能无法转换，会导致报错</span>。参考下文的转换规则。</p>
<div class="blog_h3"><span class="graybg">读写单个属性 </span></div>
<p>另外一种访问配置/计划/状态的方法是，读取单个属性的值。这种情况下，不需要定义模型（除了ObjectType）：</p>
<pre class="crayon-plain-tag">func (m myResource) Create(ctx context.Context,
    req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    //                                          属性路径 
    attr, diags := req.Config.GetAttribute(ctx, tftypes.NewAttributePath().WithAttributeName("age"))
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    age := attr.(types.Number)
}</pre>
<div class="blog_h3"><span class="graybg">类型转换规则</span></div>
<p>上面提到过，Terraform能够自动将属性值转换为Go类型，这里列出转换规则：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>String</td>
<td>只要值不为null/unknown，就可以转换为string</td>
</tr>
<tr>
<td>Number</td>
<td>
<p>只要值不为null/unknown，就可以转换为多种Go数字类型</p>
<p>当转换会导致溢出、丢失精度时，返回错误</p>
</td>
</tr>
<tr>
<td>Boolean</td>
<td>只要值不为null/unknown，就可以转换为bool</td>
</tr>
<tr>
<td>
<p>List</p>
</td>
<td>只要值不为unknown，就可以转换为[]ElemType切片，如果值是unknown则返回错误</td>
</tr>
<tr>
<td>Map</td>
<td>只要值不为unknown，就可以转换为map[string]ElemType，如果值是unknown则返回错误</td>
</tr>
<tr>
<td>Object</td>
<td>
<p>只要值不为null/unknown，在满足以下约束条件时，可以转换为Go结构：</p>
<ol>
<li>结构的每个字段都定义了<pre class="crayon-plain-tag">tfsdk</pre>标签</li>
<li>tfsdk标签必须和对象的属性名字一致，或者设置为<pre class="crayon-plain-tag">-</pre>表示不映射到任何属性</li>
<li>每个属性都对应到一个Go结构字段</li>
</ol>
</td>
</tr>
<tr>
<td>转换为指针</td>
<td>值为null的时候，不会导致报错，其余和转换为非指针类型一致</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">影响转换的接口</span></div>
<p>Get方法在进行转换时，会自动Go类型实现的特殊接口。</p>
<p>如果Go类型实现了<pre class="crayon-plain-tag">tftypes.ValueConverter</pre>接口，则转换工作代理给此接口进行。</p>
<p>如果Go类型实现了<pre class="crayon-plain-tag">Unknownable</pre>接口，则Terraform认为它能够处理unknown值。类似的，如果实现了<pre class="crayon-plain-tag">Nullable</pre>接口则认为能够处理null值。</p>
<div class="blog_h3"><span class="graybg">写状态</span></div>
<p>配置、计划仅仅支持读操作，但是状态还支持写操作。</p>
<p>写状态的时候，调用响应的State.Set方法：</p>
<pre class="crayon-plain-tag">type resourceData struct {
    Name types.Strings `tfsdk:"name"`
}

func (m myResource) Create(ctx context.Context,
    req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    var newState resourceData
    newState.Name.Value = "J. Doe"

    // 持久化到状态中
    diags := resp.State.Set(ctx, &amp;newState)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}</pre>
<p>写入单个属性：</p>
<pre class="crayon-plain-tag">func (m myResource) Create(ctx context.Context,
    req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
    age := types.Number{Value: big.NewFloat(7)}
    diags := resp.State.SetAttribute(ctx, tftypes.NewAttributePath().WithAttributeName("age"), &amp;age)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}</pre>
<div class="blog_h2"><span class="graybg">可接受测试 </span></div>
<p>目前TPF框架依赖SDKv2的可接受测试框架，你需要编写和SDKv2可接受测试一样的PreCheck、TestStep...主要区别之处，是如何在TestCase中指定被测试的Provider。</p>
<p>在SDKv2中，通过设置TestCase的Provider属性，来指定map[string]*schema.Provider。在TPF中，则需要指定<pre class="crayon-plain-tag">ProtoV6ProviderFactories</pre>属性。该属性是<pre class="crayon-plain-tag">tfprotov6.ProviderServer</pre>的map。通过下面的方式创建tfprotov6.ProviderServer：</p>
<pre class="crayon-plain-tag">func TestAccTeleportFullVPCMigration(t *testing.T) {
	resource.Test(t, resource.TestCase{
		ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
			// 返回每个需要被测试的Provider
			"teleport": func() (tfprotov6.ProviderServer, error) {
				return tfsdk.NewProtocol6Server(New()), nil
			},
		},
		Steps: []resource.TestStep{
			{
				Config: `
# 不需要使用terraform块声明从何处加载Provider，因为此Provider就在运行在测试进程内
provider "teleport" {
  endpoint = "http://127.0.0.1:6080"
  token = ""
}
resource "teleport_fullvpcmigration" "fvm" {
  region               = "eu-moscow"
  vpc_id               = "vpc-10"
  src_app_id           = "10"
  src_uin              = "10"
  src_sub_account_uin  = "11"
  dest_app_id          = "20"
  dest_uin             = "20"
  dest_sub_account_uin = "21"
  operator             = "alex"
  migrate_timeout      = "15s"
}
`,
				Check:  nil,
			},
		},
	})
}</pre>
<div class="blog_h2"><span class="graybg">调试</span></div>
<div class="blog_h3"><span class="graybg">基于日志</span></div>
<p>参考SDKv2基于日志的调试。</p>
<div class="blog_h3"><span class="graybg">单步跟踪</span></div>
<p>目前TPF不支持向SDKv2那样，以调试模式启动Provider。</p>
<p>尽管如此，在可接受测试中，你可以单步跟踪Provider代码。</p>
<div class="blog_h2"><span class="graybg">dev_overrides</span></div>
<p>在开发阶段，你可以在.terraformrc中配置dev_overrides：</p>
<pre class="crayon-plain-tag">provider_installation {
  dev_overrides {
    "hashicorp.com/edu/hashicups-pf" = "/Users/Alex/.local/bin"
  }
}</pre>
<p>dev_overrides<span style="background-color: #c0c0c0;">必须放在所有安装方法的最前面</span>。 dev_overrides会让Terraform跳过适用版本检查、Checksum匹配检查。但是dev_overrides不能参与正常的Provider安装流程，它没有提供满足版本的元数据、不能产生锁文件。</p>
<p>因此，为了使用dev_overrides，你需要将编译好的插件存放在指定目录。然后<span style="background-color: #c0c0c0;">跳过terraform init，直接运行apply/plan等命令</span>。如果没有配置dev_overrides，跳过init会导致报错，提示Provider的本地缓存不存在或者录制的元数据不匹配。</p>
<div class="blog_h1"><span class="graybg">Terraform设置</span></div>
<p>特殊的块<pre class="crayon-plain-tag">terraform</pre>，可以针对当前配置设置Terraform自身的行为，例如指定应用当前配置所需Terraform的最小版本。</p>
<pre class="crayon-plain-tag">terraform {
  # 配置后端
  backend {
  }

  # 配置Terraform CLI的版本约束
  required_version = ""

  # 模块需要的所有provider的配置
  required_providers {
    aws = {
      version = "&gt;= 2.7.0"
      source = "hashicorp/aws"
    }
  }

  # 启用实验特性
  experiments = [example]
}</pre>
<div class="blog_h2"><span class="graybg">后端配置</span></div>
<p>每个Terraform配置，都可以指定一个后端：<span style="background-color: #c0c0c0;">后端决定了操作在何处执行，状态快照在何处存储</span>：</p>
<ol>
<li>所谓操作（operation）是指调用云API进行资源的CRUD</li>
<li>Terraform利用状态（state）来跟踪它管理的资源。它依赖于state来知晓配置文件中的资源和云上基础设施对象的对应关系，典型情况是将云上对象的ID和配置资源关联起来</li>
</ol>
<p>根据能力的不同，后端分为两类：</p>
<ol>
<li>增强后端，同时支持存储状态、执行操作，只有两个增强后端：<pre class="crayon-plain-tag">local</pre>、<pre class="crayon-plain-tag">remote</pre></li>
<li>标准后端，仅仅存储状态，并且<span style="background-color: #c0c0c0;">依赖于<pre class="crayon-plain-tag">local</pre>后端执行操作</span></li>
</ol>
<p>简单场景下可以使用local后端，不需要任何配置。后端配置仅仅被Terraform CLI使用，Terraform Cloud/Enterprise总是使用他们自己的状态存储，并忽略配置文件中的backend块。</p>
<div class="blog_h3"><span class="graybg">local </span></div>
<p>该后端在本地文件系统存储状态，并且使用系统API锁定状态数据。该后端在本地直接执行操作。不进行任何backend配置，默认使用的就是该后端。</p>
<p>示例配置：</p>
<pre class="crayon-plain-tag">terraform {
  backend "local" {
    # 状态存储路径
    path = "relative/path/to/terraform.tfstate"
    # 如果使用非默认工作区
    workspace_dir = ""
  }
}</pre>
<p>用在terraform_remote_state数据资源中：</p>
<pre class="crayon-plain-tag">data "terraform_remote_state" "foo" {
  backend = "local"
  config = {
    path = "${path.module}/../../terraform.tfstate"
  }
}</pre>
<p>使用此后端时，大部分Terraform CLI的、从后端读写state快照的命令，都支持以下选项：</p>
<p style="padding-left: 30px;">-state=FILENAME 读取先前状态快照时，使用的状态文件<br />-state-out=FILENAME 写入新的状态快照时，使用的状态文件<br />-backup=FILENAME 写入新状态快照时，先前状态的备份文件。取值 - 禁用备份</p>
<div class="blog_h3"><span class="graybg">remote</span></div>
<p>配合Terraform Cloud使用，在云端存储状态和执行操作。</p>
<p>在云端执行操作时，terraform plan / apply等命令在Terraform Cloud的运行环境下执行，日志则打印到本地终端。远程的plan/apply使用关联的Terraform云工作区中的变量。</p>
<div class="blog_h3"><span class="graybg">etcdv3</span></div>
<p>这是个标准后端，只能用于存储状态。示例配置：</p>
<pre class="crayon-plain-tag">terraform {
  backend "etcdv3" {
    # Etcd服务器列表
    endpoints = ["etcd-1:2379", "etcd-2:2379", "etcd-3:2379"]
    # 是否锁定状态访问
    lock      = true
    # 存储前缀
    prefix    = "terraform-state/"
    # 基于口令的身份验证
    username = "$ETCDV3_USERNAME"
    password = "$ETCDV3_PASSWORD"
    # 基于证书的身份验证
    cacert_path = ""
    cert_path = ""
    key_path = ""
    # 最大发送的请求，增大此值可以存放更大的状态，必须配合Etcd服务器配置 --max-request-bytes，默认2MB
  }
}

data "terraform_remote_state" "foo" {
  backend = "etcdv3"
  config = {
    endpoints = ["etcd-1:2379", "etcd-2:2379", "etcd-3:2379"]
    lock      = true
    prefix    = "terraform-state/"
  }
}</pre>
<div class="blog_h1"><span class="graybg">状态 </span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Terraform需要存储被管理资源的状态，从而实现三个目的：</p>
<ol>
<li><span style="background-color: #c0c0c0;">将真实基础设施对象绑定到配置中定义的资源（最核心的用途）</span></li>
<li>跟踪元数据，例如资源依赖关系。通常情况下，Terraform直接使用配置文件来跟踪资源依赖，但是当你删除配置文件片段后，此依赖关系可能被破坏，这是它会利用状态总存储的最近的依赖关系</li>
<li>提升管理大规模基础设施时的性能。状态中存储了所有资源的属性值，这样可以避免每次访问属性时都需要请求provider</li>
</ol>
<p>在执行任何操作之前，Terraform会执行refresh操作来将状态和基础设施同步。 当配置发生变动后，Terraform可能会：</p>
<ol>
<li>如果配置文件中定义了新的资源：调用Provider创建对应基础设施对象，并且<span style="background-color: #c0c0c0;">将对象的标识符和配置中的资源定义绑定</span>，存储在状态中。Terraform期望基础设施对象和资源定义的实例（使用count/foreach时一个块定义了多个实例）是一一对应关系</li>
<li>如果配置文件中删除了资源定义：调用Provider删除对应基础设施对象，并且清除状态中对应数据</li>
</ol>
<p>如上一章节所述，默认情况下后端local负责存储状态，它默认将状态存储到名为terraform.tfstate的文件中。Terraform还支持大量其它后端，将状态存储到Etcd、S3等各种存储服务中。</p>
<div class="blog_h3"><span class="graybg">查看和修改状态</span></div>
<p>尽管状态就是JSON文本，也不要直接修改它。可以使用<pre class="crayon-plain-tag">terraform state</pre>命令进行基本的修改。</p>
<p>通过<pre class="crayon-plain-tag">terraform import</pre>导入外部创建对象时，或者通过<pre class="crayon-plain-tag">terraform state rm</pre>让Terraform忘记某个既有对象时，你必须保证基础设施对象和资源定义的一一对应关系。</p>
<div class="blog_h2"><span class="graybg">terraform_remote_state</span></div>
<p>该数据源能够从<span style="background-color: #c0c0c0;">其它Terraform配置</span>的最新状态快照拉取<span style="background-color: #c0c0c0;">根模块的输出值</span>。这个模块是内置的，不需要配置。</p>
<p>目标配置使用local后端，读取其输出值的例子：</p>
<pre class="crayon-plain-tag">data "terraform_remote_state" "vpc" {
  backend = "local"
  config = {
    # 目标配置的状态文件
    path = "..."
  }
}

# Terraform &gt;= 0.12
resource "aws_instance" "foo" {
  #                                           读取输出值
  subnet_id = data.terraform_remote_state.vpc.outputs.subnet_id
}

# Terraform &lt;= 0.11
resource "aws_instance" "foo" {
  # ...
  subnet_id = "${data.terraform_remote_state.vpc.subnet_id}"
}</pre>
<p>注意：仅仅目标配置的根模块的输出值被暴露，其子模块的输出值是不可见的。你必须手工在根模块将子模块的输出值再次输出：</p>
<pre class="crayon-plain-tag">module "app" {
  source = "..."
}

output "app_value" {
  # This syntax is for Terraform 0.12 or later.
  value = module.app.example
}</pre>
<div class="blog_h2"><span class="graybg">状态锁定 </span></div>
<p>如果后端支持，Terraform在执行任何可能修改状态的操作时，会锁定状态，防止并发修改损坏数据。</p>
<p>对于大部分命令，都可以使用命令行选项<pre class="crayon-plain-tag">-lock</pre>禁用锁定，但是不推荐这样做。 </p>
<p>命令<pre class="crayon-plain-tag">force-unlock</pre>用于强制解锁，这个命令存在危险性，会导致并发修改。</p>
<div class="blog_h2"><span class="graybg">工作区</span></div>
<p>存储在backend中的状态数据，属于一个工作区。<span style="background-color: #c0c0c0;">最开始仅有一个名为default的工作区，因而对于一个Terraform配置来说，只有一个关联的state</span>。</p>
<p>某些backend支持多个工作区，包括S3、Local、Kubernetes等。</p>
<p>命令<pre class="crayon-plain-tag">terraform workspace</pre>用于管理工作区。</p>
<div class="blog_h3"><span class="graybg">何时使用工作区</span></div>
<p>工作区可以用于区分测试/生产环境，开发人员可以在测试环境中创建并行的、完整的基础设施，并且测试配置文件的变更。</p>
<p>当管理大规模系统时，应该重构被拆分出多个配置（而不是引入新工作区），这些配置甚至可能由不同团队管理。</p>
<div class="blog_h2"><span class="graybg">敏感数据</span></div>
<p>在Local后端中存储的状态，敏感数据直接明文保存在文件中。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/terraform">Terraform快速参考</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/terraform/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Ceph学习笔记</title>
		<link>https://blog.gmem.cc/ceph-study-note</link>
		<comments>https://blog.gmem.cc/ceph-study-note#comments</comments>
		<pubDate>Sat, 21 Apr 2018 09:01:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[IaaS]]></category>
		<category><![CDATA[Ceph]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=20465</guid>
		<description><![CDATA[<p>简介 Ceph是一个高性能、可扩容的分布式存储系统，它提供三大功能： 对象存储：提供RESTful接口，也提供多种编程语言绑定。兼容S3、Swift 块存储：由RBD提供，可以直接作为磁盘挂载，内置了容灾机制 文件系统：提供POSIX兼容的网络文件系统CephFS，专注于高性能、大容量存储 Ceph集群由一系列节点（机器）组成，在这些节点上运行以下组件： Ceph OSDs：OSD即对象存储守护程序，但是它并非针对对象存储。OSD负责存储数据、处理数据复制、恢复、回填（Backfilling）、再平衡。此外OSD还对其它OSD进行心跳检测，检测结果汇报给Monitor Monitors：监视器，维护集群状态的多种映射，同时提供认证和日志记录服务 MDSs：元数据服务器，存储CephFS的元数据信息 Ceph将客户端的数据作为对象存储在它的存储池中，基于CRUSH算法，Ceph计算出每个对象应该位于那个PG，计算哪个OSD负责存储PG 架构 组件层次 数据读写流程 术语 术语 说明 RADOS 可靠的、自动化的分布式对象存储（Reliable, Autonomic Distributed Object <a class="read-more" href="https://blog.gmem.cc/ceph-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/ceph-study-note">Ceph学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>Ceph是一个高性能、可扩容的分布式存储系统，它提供三大功能：</p>
<ol>
<li>对象存储：提供RESTful接口，也提供多种编程语言绑定。兼容S3、Swift</li>
<li>块存储：由RBD提供，可以直接作为磁盘挂载，内置了容灾机制</li>
<li>文件系统：提供POSIX兼容的网络文件系统CephFS，专注于高性能、大容量存储</li>
</ol>
<p>Ceph集群由一系列节点（机器）组成，在这些节点上运行以下组件：</p>
<ol>
<li>Ceph OSDs：OSD即对象存储守护程序，但是它并非针对对象存储。OSD负责<span style="background-color: #c0c0c0;">存储数据、处理数据复制、恢复、回填（B</span><span style="color: #3e4349;"><span style="background-color: #c0c0c0;">ackfilling）、再平衡</span>。此外OSD还对其它OSD进行心跳检测，检测结果汇报给Monitor</span></li>
<li>Monitors：监视器，维护集群状态的多种映射<span style="color: #3e4349;">，同时提供认证和日志记录服务</span></li>
<li>MDSs：元数据服务器，存储CephFS的元数据信息</li>
</ol>
<p>Ceph将客户端的数据作为对象存储在它的存储池中，基于CRUSH算法，Ceph<span style="background-color: #c0c0c0;">计算出每个对象应该位于那个PG，计算哪个OSD负责存储PG</span></p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<div class="blog_h3"><span class="graybg">组件层次</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/04/ceph_all_component.png"><img class="aligncenter size-full wp-image-21209" src="https://blog.gmem.cc/wp-content/uploads/2018/04/ceph_all_component.png" alt="ceph_all_component" width="477" height="629" /></a></p>
<div class="blog_h3"><span class="graybg">数据读写流程</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2018/04/rados_erasure_coding.png"><img class="aligncenter  wp-image-21141" src="https://blog.gmem.cc/wp-content/uploads/2018/04/rados_erasure_coding.png" alt="rados_erasure_coding" width="897" height="533" /></a></p>
<div class="blog_h2"><span class="graybg">术语</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">术语</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>RADOS</td>
<td>
<p>可靠的、自动化的分布式对象存储（Reliable, Autonomic Distributed Object Store）是Ceph的核心之一</p>
<p>librados是RADOS提供的库，上层的RBD、RGW和CephFS都是通过librados访问RADOS的</p>
</td>
</tr>
<tr>
<td>RGW</td>
<td>即RADOS Gateway，指Ceph的对象存储API或者RGW守护进程</td>
</tr>
<tr>
<td>RBD</td>
<td>即RADOS Block Device，指Ceph提供的基于复制性的分布式的块设备。类似于LVM中的逻辑卷，<span style="background-color: #c0c0c0;">RBD只能属于一个Pool</span></td>
</tr>
<tr>
<td>MDS</td>
<td>即Ceph元数据服务器，是CephFS服务依赖的元数据服务</td>
</tr>
<tr>
<td>CephFS</td>
<td>Ceph File System，是Ceph对外提供的文件系统服务</td>
</tr>
<tr>
<td>Pool</td>
<td>
<p>存储池是Ceph中一些<span style="background-color: #c0c0c0;">对象的逻辑分组</span>。它不是一个连续的分区，而是一个逻辑概念，类似LVM中的卷组（Volume Group）</p>
<p>存储池分为两个类型：</p>
<ol>
<li>Replicated 复制型，<span style="background-color: #c0c0c0;">对象具有多份拷贝，确保部分OSD丢失时数据不丢失</span>，需要更多的磁盘空间。复制份数可以动态调整，可以置为1</li>
<li>Erasure-coded 纠错码型，节约空间，但是速度慢，不支持所有对象操作（例如局部写）</li>
</ol>
</td>
</tr>
<tr>
<td>PG</td>
<td>
<p>归置组（Placement Group），PG是Pool组织对象的方式，便于<span style="background-color: #c0c0c0;">更好的分配数据和定位数据</span>，<span style="background-color: #c0c0c0;">Pool由若干PG组成</span></p>
<p>PG 的数量会影响Ceph集群的行为和数据的持久性。<span style="background-color: #c0c0c0;">集群扩容后可以增大PG数量：5个以下OSD设置为128即可</span></p>
<p>PG的特点：同一个PG中所有的对象，<span style="background-color: #c0c0c0;">在相同一组OSDs上被复制</span>。复制型Pool中PG可以有<span style="background-color: #c0c0c0;">一个作为主（Primary）OSD</span>，其它作为从OSD。一个对象仅仅属于一个PG，也就是说对象存储在固定的一组OSDs上</p>
<p>PG在OSD的/var/lib/ceph/osd/ceph-2/current目录下，表现为目录</p>
</td>
</tr>
<tr>
<td>CRUSH</td>
<td>
<p>CRUSH即基于可扩容哈希的受控复制（Controlled Replication Under Scalable Hashing），是一种<span style="background-color: #c0c0c0;">数据分发算法</span>，类似于哈希和一致性哈希。哈希的问题在于数据增长时<span style="background-color: #c0c0c0;">不能动态添加Bucket</span>，一致性哈希的问题在于<span style="background-color: #c0c0c0;">添加Bucket时数据迁移量比较大</span>，其他<span style="background-color: #c0c0c0;">数据分发算法依赖中心的Metadata服务器来存储元数据因而效率较低</span>，CRUSH则是通过计算、接受多维参数的来解决动态数据分发的场景</p>
<p>CRUSH算法接受的参数包括：</p>
<ol>
<li>Cluster map，也就是硬盘分布的逻辑位置，例如这有<span style="background-color: #c0c0c0;">多少个机房、多少个机柜、硬盘是如何分布的</span>等等。<span style="background-color: #c0c0c0;">Cluster map是类似树的结构</span>，<span style="background-color: #c0c0c0;">子节点是真正存储数据的device</span>，每个d<span style="background-color: #c0c0c0;">evice都有id和权重</span>，<span style="background-color: #c0c0c0;">中间节点是bucket</span>，bucket有多种类型用于不同的查询算法，例如一个机柜一个机架一个机房就是bucket</li>
<li>Placement rules，它指定了一份数据有<span style="background-color: #c0c0c0;">多少备份</span>，数据的分布有什么<span style="background-color: #c0c0c0;">限制条件</span>（例如同一份数据不能放在同一个机柜里）。每个Rule对应一系列操作：
<ol>
<li>take，选取一个bucket</li>
<li>select，选择n个类型为t的项</li>
<li>emit，提交</li>
</ol>
</li>
</ol>
<p>CRUSH与一致性哈希最大的区别在于接受的参数多了Cluster map和Placement rules，这样就可以<span style="background-color: #c0c0c0;">根据目前Cluster的状态动态调整数据位置</span>，同时通过算法<span style="background-color: #c0c0c0;">得到一致的结果</span></p>
<p>基于此算法，Ceph存储集群能够动态的<span style="background-color: #c0c0c0;">扩容、再平衡、恢复</span></p>
</td>
</tr>
<tr>
<td>Object</td>
<td>
<p>Ceph最底层的存储单元是Object，每个<span style="background-color: #c0c0c0;">Object包含元数据和原始数据</span></p>
<p>一个RBD会<span style="background-color: #c0c0c0;">包含很多个Object</span></p>
</td>
</tr>
<tr>
<td>OSD</td>
<td>
<p>对象存储守护进程（Object Storage Daemon），负责响应客户端请求返回具体数据的进程。Ceph集群中有大量OSD</p>
<p>一个节点上通常只运行一个OSD守护进程，此守护进程在<span style="background-color: #c0c0c0;">一个存储驱动器上只运行一个 filestore</span></p>
</td>
</tr>
<tr>
<td>EC</td>
<td>
<p>Erasure Code（EC），即纠删码，是一种<span style="background-color: #c0c0c0;">前向错误纠正技术</span>（Forward Error Correction，FEC），主要应用在网络传输中避免包的丢失， 存储系统利用它来<span style="background-color: #c0c0c0;">提高可靠性</span>。相比多副本复制而言， 纠删码能够<span style="background-color: #c0c0c0;">以更小的数据冗余度获得更高数据可靠性</span>， 但<span style="background-color: #c0c0c0;">编码方式较复杂，需要大量计算</span> 。纠删码只能容忍数据丢失，无法容忍数据篡改，纠删码正是得名与此</p>
<p>EC将n份原始数据，增加m份数据，并能通过n+m份中的任意n份数据，还原为原始数据。即如果有<span style="background-color: #c0c0c0;">任意小于等于m份的数据失效，仍然能通过剩下的数据还原出来</span></p>
<p>纠删码技术在分布式存储系统中的应用主要有三类：</p>
<ol>
<li>阵列纠删码（Array Code: RAID5、RAID6等）：RAID是EC的特例，RAID5只支持一个盘失效，RAID6支持两个盘失效，而EC支持多个盘失效</li>
<li>RS(Reed-Solomon)里德-所罗门类纠删码</li>
<li>LDPC(LowDensity Parity Check Code)低密度奇偶校验纠删码：目前主要用于通信、视频和音频编码等领域，与RS编码相比，LDPC<span style="background-color: #c0c0c0;">编码效率要略低，但编码和解码性能要优于RS码以及其他的纠删码</span></li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">组件</span></div>
<div class="blog_h2"><span class="graybg">MON</span></div>
<p>监视器<span style="background-color: #c0c0c0;">维护集群状态的多种映射</span>—— 包monmap<span style="color: #3e4349;">、OSD map、PG map、CRUSH map、MDS map，同时<span style="background-color: #c0c0c0;">提供认证和日志记录服</span>务。Ceph会记录Monitor、OSD、PG的每次状态变更历史（此历史称作epoch）。客户端连到单个监视器并获取当前映射就能确定所有监视器、 OSD 和元数据服务器的位置。依赖于CRUSH算法和当前集群状态映射，客户端就能计算出任何对象的位置，直连OSD读写数据。</span></p>
<p>Ceph<span style="background-color: #c0c0c0;">客户端、其它守护进程通过配置文件发现mon</span>，但是mon之间的<span style="background-color: #c0c0c0;">相互发现却依赖于monmap的本地副本</span>。所有mon会基于分布式一致性算法Paxos，确保各自本地的monmap是一致的，当新增一个mon后，所有现有mon的monmap都自动更新为最新版本。</p>
<div class="blog_h3"><span class="graybg">监视器同步</span></div>
<p>使用多个mon时，每个mon都会检查其它mon是否具有更新的集群状态映射版本 —— 存在一个或多个epoch大于当前mon的最高epoch。太过落后的mon可能会离开quorum，同步后再加入quorum。执行同步时，mon分为三类角色：</p>
<ol>
<li>Leader：具有最新版本状态映射的mon</li>
<li>Provider：同上，但是它的最新状态是从Leader同步获得</li>
<li>Requester：落后于Leader，必须获取最新集群状态映射才能重回quorum</li>
</ol>
<div class="blog_h3"><span class="graybg">时钟偏移</span></div>
<p>如果mon的时钟不同步，可能会导致：</p>
<ol>
<li>守护进程忽略收到的消息（时间戳过时）</li>
<li>消息未及时收到时，超时触发得太快或太晚</li>
</ol>
<div class="blog_h2"><span class="graybg">OSD</span></div>
<div class="blog_h3"><span class="graybg">日志</span></div>
<p>OSD使用日志的原因有两个：</p>
<ol>
<li>速度： 日志使得 OSD 可以<span style="background-color: #c0c0c0;">快速地提交小块数据的写</span>入， <span style="background-color: #c0c0c0;">Ceph 把小片、随机 IO 依次写入日志</span>，这样，后端文件系统就有可能<span style="background-color: #c0c0c0;">归并写入动作</span>，并最终提升并发承载力。因此，使用 OSD 日志能<span style="background-color: #c0c0c0;">展现出优秀的突发写性能</span>，实际上数据还没有写入 OSD ，因为文件系统把它们捕捉到了日志</li>
<li>一致性：OSD需要一个能<span style="background-color: #c0c0c0;">保证原子化复合操作的文件系统接口</span>。 OSD 把一个<span style="background-color: #c0c0c0;">操作的描述写入日志，并把操作应用到文件系统</span>。这确保了对象（例如归置组元数据）的原子更新。每隔一段时间（由filestore max sync interval 和 filestore min sync interval控制 ）， OSD 会停止写入，把日志同步到文件系统，这样允许 OSD 修整日志里的操作并重用空间。若失败， OSD 从上个同步点开始重放日志。<span style="background-color: #c0c0c0;">日志的原子性表现在，它不使用操作系统的文件缓存（基于内存）</span>，避免断电丢数据的问题</li>
</ol>
<p>注意：OSD进程在<span style="background-color: #c0c0c0;">往数据盘上刷日志数据的过程中，是停止写操作的</span>。 </p>
<p>通常使用独立SSD来存储日志，原因是：</p>
<ol>
<li>避免针对单块磁盘的双重写入 —— 先写日志，再写filestore</li>
<li>SSD性能好，可以降低延迟提升IOPS</li>
</ol>
<div class="blog_h3"><span class="graybg">OSD状态矩阵</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 70px;"> </td>
<td style="text-align: center;">IN</td>
<td style="text-align: center;">OUT</td>
</tr>
</thead>
<tbody>
<tr>
<td><strong>UP</strong></td>
<td>正常状态，OSD位于集群中，且接收数据</td>
<td>
<p>OSD虽然在运行，但是<span style="background-color: #c0c0c0;">被踢出集群 —— CRUSH不会再分配归置组给它</span></p>
</td>
</tr>
<tr>
<td><strong>DOWN</strong></td>
<td>这种状态<span style="background-color: #c0c0c0;">不正常</span>，集群处于非健康状态</td>
<td>正常状态</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Bluestore</span></div>
<p>在Luminous中，Bluestore已经代替Filestore作为默认的存储引擎。<span style="background-color: #c0c0c0;">Bluestore直接管理裸设备，不使用OS提供的文件系统接口，因此它不会收到OS缓存影响</span>。</p>
<p><span style="background-color: #c0c0c0;">使用Bluestore时，你不需要配备SSD作为独立的日志存储</span>，Bluestore不存在双重写入问题，它直接把数据落盘到块上，然后在RockDB中更新元数据（指定数据块的位置）。</p>
<p>一个基于Bluestore的OSD最多可以利用到三块磁盘，例如下面的最优化性能组合：</p>
<ol>
<li>使用HDD作为数据盘</li>
<li>使用SSD作为RockDB元数据盘</li>
<li>使用NVRAM作为RockDB WAL</li>
</ol>
<div class="blog_h2"><span class="graybg">PG</span></div>
<p>一些概念：</p>
<ol>
<li>Acting Set：牵涉到PG副本的OSD集合</li>
<li>Up Set：指Acting Set中排除掉Down掉的OSD的子集</li>
</ol>
<p>Ceph依赖于Up Set来处理客户端请求。如果 Up Set 和 Acting Set 不一致，这可能表明集群内部在重均衡或者有潜在问题。</p>
<p>写入数据前，<span style="background-color: #c0c0c0;">归置组必须处于 active 、而且应该是 clean 状态</span>。假设一存储池的归置组有 3 个副本，为让 Ceph 确定归置组的当前状态，一归置组的主 OSD （即 acting set 内的第一个 OSD ）会与第二和第三 OSD 建立连接，并就归置组的当前状态达成一致意见。</p>
<p>由于以下原因，集群状态可能显示为HEALTH WARN：</p>
<ol>
<li>刚刚创建了一个存储池，归置组还没互联好</li>
<li>归置组正在恢复</li>
<li>刚刚增加或删除了一个 OSD</li>
<li>刚刚修改了 CRUSH 图，并且归置组正在迁移</li>
<li>某一归置组的副本间的数据不一致</li>
<li>Ceph 正在洗刷一个归置组的副本</li>
<li>Ceph 没有足够空余容量来完成回填操作</li>
</ol>
<p>这些情况下，集群会自行恢复，并返回 HEALTH OK 状态，归置组全部变为active+clean。</p>
<div class="blog_h3"><span class="graybg">归置组状态表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;"><strong>Creating </strong></td>
<td>
<p>在你创建存储池时，Ceph会创建指定数量的PG，对应此状态</p>
<p><span style="background-color: #c0c0c0;">创建PG完毕后，Acting Set中的OSD将进行互联</span>，互联完毕后，PG变为Active+Clean状态，PG可以接受数据写入</p>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Peering</strong></td>
<td>
<p>Acting Set中的OSD正在进行互联，它们需要就PG中对象、元数据的状态达成一致。互联完成后，所有OSD达成一致意见，但是<span style="background-color: #c0c0c0;">不代表所有副本的内容都是最新</span>的</p>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Active</strong></td>
<td>互联完成后归置组状态会变为Active</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Clean</strong></td>
<td>主OSD和副本OSD已成功互联，并且没有偏离的归置组。 Ceph 已把归置组中的对象<span style="background-color: #c0c0c0;">复制了规定次数</span></td>
</tr>
<tr>
<td style="text-align: center;"><strong>Degraded</strong></td>
<td>
<p>当客户端向主 OSD 写入数据时，由<span style="background-color: #c0c0c0;">主 OSD 负责把数据副本写入其余副本 OSD</span> 。主 OSD 把对象写入存储器后，在<span style="background-color: #c0c0c0;">副本 OSD 创建完对象副本并报告给主 OSD 之前，主 OSD 会一直停留在 degraded 状态</span></p>
<p>如果OSD挂了， Ceph 会把分配到<span style="background-color: #c0c0c0;">此 OSD 的归置组都标记为 degraded</span>。只要它归置组仍然处于active 状态，客户端<span style="background-color: #c0c0c0;">仍可以degraded归置组写入新对象</span></p>
<p>如果OSD挂了（down）长期（ mon osd down out interval ，默认300秒）不恢复，Ceph会将其标记为out，并将其上的PG重新映射到其它OSD</p>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Recovering</strong></td>
<td>当挂掉的OSD重启（up）后，其内的PG中的对象<span style="background-color: #c0c0c0;">副本可能是落后的，副本更新期间</span>OSD处于此状态</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Backfilling</strong></td>
<td>
<p>新 OSD 加入集群时， CRUSH 会把现有集群内的部分归置组重分配给它。强制新 OSD 立即接受重分配的归置组会使之过载，<span style="background-color: #c0c0c0;">用归置组回填可使这个过程在后台开始</span></p>
<p>回填执行期间，你可能看到以下状态之一：</p>
<ol>
<li>backfill_wait，等待时机，回填尚未开始</li>
<li>backfill_too_full，需要进行回填，但是因存储空间不足而不能完成</li>
</ol>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Remapped</strong></td>
<td>负责某个PG的Acting Set发生变更时，数据需要从久集合迁移到新集合。此期间老的主OSD仍然需要提供服务，直到数据迁移完成 </td>
</tr>
<tr>
<td style="text-align: center;"><strong>Stale</strong></td>
<td>
<p>默认情况下，OSD每0.5秒会一次报告其归置组、出流量、引导和失败统计状态，此频率高于心跳</p>
<p>如果：</p>
<ol>
<li>归置组的主 OSD 所在的 Acting Set 没能向MON报告</li>
<li>或者其它MON已经报告，说主 OSD 已 down了</li>
</ol>
<p>则MONs就会把此归置组标记为 stale</p>
<p>集群运行期间，出现此状态，所有PG的<span style="background-color: #c0c0c0;">主OSD</span>挂了</p>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Inactive</strong></td>
<td>归置组不能处理读写请求，因为它们在<span style="background-color: #c0c0c0;">等着一个持有最新数据的 OSD 回到 up 状态</span></td>
</tr>
<tr>
<td style="text-align: center;"><strong>Unclean</strong></td>
<td>归置组里有些对象的<span style="background-color: #c0c0c0;">副本数未达到期望次数</span>，它们应该在恢复中</td>
</tr>
<tr>
<td style="text-align: center;"><strong>Down</strong></td>
<td>归置组的权威副本OSD宕机，必须等待其开机，或者被标记为lost才能继续</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">CRUSH</span></div>
<div class="blog_h3"><span class="graybg">简介</span></div>
<p>CRUSH 算法通过计算数据存储位置来确定如何存储和检索。 CRUSH授权Ceph 客户端直接连接 OSD ，而非通过一个中央服务器或代理。数据存储、检索算法的使用，使 Ceph 避免了单点故障、性能瓶颈、和伸缩的物理限制。</p>
<p>CRUSH 需要一张集群的 Map，利用该Map中的信息，将数据伪随机地、尽量平均地分布到整个集群的 OSD 里。此Map中包含：</p>
<ol>
<li>OSD 列表</li>
<li>把设备汇聚为物理位置的<span style="background-color: #c0c0c0;">“桶”（Bucket，也叫失败域，Failure Domain）</span>列表</li>
<li>指示 CRUSH 如何复制存储池中的数据的规则列表</li>
</ol>
<p>通过CRUSH map来建模存储设备的物理位置，Ceph能够避免潜在的关联性故障 —— 例如一个机柜中的设备可能共享电源、网络供应，它们更加可能因为断电而同时出现故障，Ceph会刻意的避免把数据副本放在同一机柜。</p>
<p>新部署的OSD自动被放置到CRUSH map中，位于一个host节点（OSD所在主机名）。在默认的CRUSH失败域（Failure Domain） 设置中，副本/EC分片会自动分配在不同的host节点上，避免单主机的单点故障。在大型集群中，管理员需要更加仔细的考虑失败域设置，将副本分散到不同的Rack、Row。</p>
<div class="blog_h3"><span class="graybg">crush location</span></div>
<p>OSD在CRUSH map中的位置，称为CRUSH location。此Location以如下形式来描述：</p>
<pre class="crayon-plain-tag"># 一系列键值对，虽然是有层次结构的，但是列出的顺序无所谓
# 键必须是有效的CRUSH type。默认支持root,regin, datacenter, room, row, pod, pdu, rack, chassis, host
# 你不需要声明所有键，默认情况下Ceph自动把新OSD放在root=default host=hostname下，因此这两个键你可以不声明
root=default row=a rack=a2 chassis=a2a host=a2a1</pre>
<p>你可以在Ceph配置文件中，用crush location选项来声明。每当OSD启动时，它会验证当前CRUSH map是否匹配crush location设置，如果不匹配会更新CRUSH map。设置下面的选项可以禁用此行为：</p>
<pre class="crayon-plain-tag">osd crush update on start = false</pre>
<div class="blog_h3"><span class="graybg">crush结构</span></div>
<p>CRUSH map是一个树状的层次结构，它是对存储设备物理位置松散的建模。 </p>
<p>在这个层次结构中，<span style="background-color: #c0c0c0;">叶子节点是Device，对应了OSD守护程序</span>（通常管理一块或几块磁盘）。设备的以name.id来识别，通常是osd.N。设备可以<span style="background-color: #c0c0c0;">关联一个设备类别（Device Class）</span>，取值例如hdd、ssd，CRUSH rule可以使用到设备类别。</p>
<p>除了<span style="background-color: #c0c0c0;">叶子节点之外的，都称为桶（Bucket）</span>，每个桶都具有类型，默认支持的类型包括root,regin, datacenter, room, row, pod, pdu, rack, chassis, host。大部分集群仅仅使用一部分类型的桶。</p>
<p>每个<span style="background-color: #c0c0c0;">节点都具有一个权重（Weight）字段</span>，指示子树负责存储的数据的比例。权重应该仅仅在叶子节点上设置，由Ceph自动向上类加。<span style="background-color: #c0c0c0;">权重的单位通常是TB</span>。</p>
<p>执行命令<pre class="crayon-plain-tag">ceph osd crush tree</pre>可以查看CRUSH的层次，包括节点权重。</p>
<div class="blog_h3"><span class="graybg">规则</span></div>
<p>CRUSH rule定义了数据如何跨越设备分布的规则。大部分情况下你可以通过命令行来创建CRUSH rule，少数情况下需要手工便捷CRUSH map。</p>
<div class="blog_h3"><span class="graybg">TUNABLES</span></div>
<p>随着Ceph的发展，CRUSH算法被不断的优化。Ceph允许你自由选择新或旧的算法变体，这依赖Tunable实现。</p>
<p>要使用新的Tunable，客户端、服务器<span style="background-color: #c0c0c0;">必须同时支持</span>。Tunable的命名就是最初支持对应算法变体的那个Ceph版本的名称（例如jewel）。</p>
<div class="blog_h2"><span class="graybg">RBD</span></div>
<div class="blog_h3"><span class="graybg">缓存</span></div>
<p>用户空间的Ceph块设备实现（librbd）不能使用Linux的页面缓存，因此它自己实现了一套基于内存的LRU缓存——RBD Cacheing。</p>
<p>此缓存的行为类似于页面缓存，当OS发送屏障/Flush请求时，内存中的脏数据被刷出到OSD。</p>
<div class="blog_h2"><span class="graybg">CephFS</span></div>
<p>这是一个POSIX兼容的文件系统，它使用Ceph的存储集群来保存数据。</p>
<p>一个Ceph集群可以有0-N个CephFS文件系统，每个CephFS具有可读名称和一个集群文件系统ID（FSCID）。每个CephFS可以指定多个处于standby状态的MDS进程。 </p>
<p>每个CephFS包含若干Rank，默认是1个。Rank可以看作是元数据分片。CephFS的每个守护进程（ceph-mds）默认情况下无Rank启动，Mon会自动为其分配Rank。每个守护进程最多持有一个Rank。</p>
<p>如果Rank没有关联到ceph-mds，则其状态为failed，否则其状态为up。</p>
<p>每个ceph-mds都有一个关联的名称，典型情况下设置为所在的节点的主机名。每当ceph-mds启动时，会获得一个GID，在进程生命周期中，它都使用此GID。</p>
<p>如果MDS进程超过 mds_beacon_grace seconds没有和MON联系，则它被标记为laggy。</p>
<div class="blog_h2"><span class="graybg">RGW</span></div>
<p>Ceph对象存储网关是基于librados构建的一套RESTful服务，提供对Ceph存储集群的访问。此服务提供两套接口：</p>
<ol>
<li>S3兼容接口：Amazon S3的子集</li>
<li>Swift兼容接口： OpenStack Swift的子集</li>
</ol>
<p>这两套接口可以<span style="background-color: #c0c0c0;">混合使用</span>。</p>
<p>对象存储网关由守护程序radosgw负责，它作为客户端和Ceph存储集群之间的媒介。radosgw<span style="background-color: #c0c0c0;">具有自己的用户管理系统</span>。</p>
<p>从firefly版本开始，对象存储网关在Civetweb上运行，Civetweb内嵌在ceph-radosw这个Daemon中。在老版本中，对象网关基于Apache+FastCGI。</p>
<div class="blog_h2"><span class="graybg">Dashboard</span></div>
<p>Ceph仪表盘是一个内置的、基于Web的管理/监控工具。通过它你能够管理集群中各种资源。仪表盘作为Ceph Manager的模块实现。</p>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">ceph orch</span></div>
<p>包含一系列集群编排有关的命令。</p>
<div class="blog_h3"><span class="graybg">orch ls</span></div>
<p>列出对编排器可见的服务：</p>
<pre class="crayon-plain-tag">ceph orch ls [&lt;service_type&gt;] [&lt;service_name&gt;] [--export] [plain|json|json-pretty|yaml] [--refresh]

ceph orch ls
# 守护进程类型              数量                     归置规则               使用的镜像
NAME                       RUNNING  REFRESHED  AGE  PLACEMENT             IMAGE NAME                            IMAGE ID      
alertmanager                   1/1  77s ago    2w   count:1               docker.io/prom/alertmanager:v0.20.0   0881eb8f169f  
crash                          2/3  79s ago    2w   *                     docker.io/ceph/ceph:v15               mix           
grafana                        1/1  77s ago    2w   count:1               docker.io/ceph/ceph-grafana:6.7.4     80728b29ad3f  
mds.cephfs                     2/3  79s ago    2w   ceph-1;ceph-2;ceph-3  docker.io/ceph/ceph:v15               mix           
mgr                            1/1  77s ago    2w   ceph-1                docker.io/ceph/ceph:v15               5b724076c58f  
mon                            2/3  79s ago    40m  count:3               docker.io/ceph/ceph:v15               mix           
nfs.ganesha                    1/1  78s ago    7d   count:1               docker.io/ceph/ceph:v15               5b724076c58f  
node-exporter                  2/3  79s ago    2w   *                     docker.io/prom/node-exporter:v0.18.1  mix           
osd.all-available-devices      2/3  79s ago    2w   *                     docker.io/ceph/ceph:v15               mix           
prometheus                     1/1  77s ago    2w   count:1               docker.io/prom/prometheus:v2.18.1     de242295e225  
rgw.china.zircon               2/3  79s ago    2w   count:3               docker.io/ceph/ceph:v15               mix</pre>
<div class="blog_h3"><span class="graybg">orch ps</span></div>
<p>列出对编排器可见的守护进程，守护进程是服务的实例：</p>
<pre class="crayon-plain-tag">orch ps [&lt;hostname&gt;] [&lt;service_name&gt;] [&lt;daemon_type&gt;] [&lt;daemon_id&gt;] [plain|json|json-pretty|yaml] [--refresh] 

ceph orch ps
NAME                            HOST    STATUS         REFRESHED  AGE  VERSION    IMAGE NAME                            IMAGE ID      CONTAINER ID  
alertmanager.ceph-1             ceph-1  running (69m)  3m ago     2w   0.20.0     docker.io/prom/alertmanager:v0.20.0   0881eb8f169f  bef9ab4dcc98  
crash.ceph-1                    ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  3bb0c129d4d4  
crash.ceph-2                    ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/ceph/ceph:v15               &lt;unknown&gt;     &lt;unknown&gt;     
crash.ceph-3                    ceph-3  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  f5c22d2c854b  
grafana.ceph-1                  ceph-1  running (69m)  3m ago     2w   6.7.4      docker.io/ceph/ceph-grafana:6.7.4     80728b29ad3f  17d84abdd9e6  
mds.cephfs.ceph-1.nivqqf        ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  4be1504a4c6f  
mds.cephfs.ceph-2.djnipz        ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/ceph/ceph:v15               &lt;unknown&gt;     &lt;unknown&gt;     
mds.cephfs.ceph-3.cgngbk        ceph-3  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  7e514989bc6c  
mgr.ceph-1.adpioc               ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  a66dd815c2b1  
mon.ceph-1                      ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  0c87ed6da097  
mon.ceph-2                      ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/ceph/ceph:v15               &lt;unknown&gt;     &lt;unknown&gt;     
mon.ceph-3                      ceph-3  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  836ec2a7c34d  
nfs.ganesha.ceph-3              ceph-3  running (68m)  3m ago     7d   3.3        docker.io/ceph/ceph:v15               5b724076c58f  440a1bcef7c5  
node-exporter.ceph-1            ceph-1  running (69m)  3m ago     2w   0.18.1     docker.io/prom/node-exporter:v0.18.1  e5a616e4b9cf  26bf34b93188  
node-exporter.ceph-2            ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/prom/node-exporter:v0.18.1  &lt;unknown&gt;     &lt;unknown&gt;     
node-exporter.ceph-3            ceph-3  running (69m)  3m ago     2w   0.18.1     docker.io/prom/node-exporter:v0.18.1  e5a616e4b9cf  fb60a5b31bfd  
osd.0                           ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/ceph/ceph:v15               &lt;unknown&gt;     &lt;unknown&gt;     
osd.1                           ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  49cad5daf8f8  
osd.2                           ceph-3  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  17ef075e16a4  
prometheus.ceph-1               ceph-1  running (69m)  3m ago     2w   2.18.1     docker.io/prom/prometheus:v2.18.1     de242295e225  7b61f27c6a0e  
rgw.china.zircon.ceph-1.dsctvb  ceph-1  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  b7d6166aae36  
rgw.china.zircon.ceph-2.ulzfto  ceph-2  error          3m ago     2w   &lt;unknown&gt;  docker.io/ceph/ceph:v15               &lt;unknown&gt;     &lt;unknown&gt;     
rgw.china.zircon.ceph-3.qjhszd  ceph-3  running (69m)  3m ago     2w   15.2.10    docker.io/ceph/ceph:v15               5b724076c58f  5d1d6d6e6899</pre>
<div class="blog_h3"><span class="graybg">orch apply</span></div>
<p>设置某种组件（服务/守护进程）的数量或者归置规则，格式：</p>
<pre class="crayon-plain-tag"># 更新守护程序副本数量、归置规则，或者apply一段YAML格式的配置
orch apply [mon|mgr|rbd-mirror|crash|alertmanager|grafana|node-exporter|prometheus] [&lt;placement&gt;]  
 [--dry-run] [plain|json|json-pretty|yaml] [--unmanaged] 

# 扩缩容iSCSI服务
orch apply iscsi &lt;pool&gt; &lt;api_user&gt; &lt;api_password&gt; [&lt;trusted_ip_list&gt;] [&lt;placement&gt;] [--dry-run]
 [plain|json|json-pretty|yaml] [--unmanaged]  

# 更新指定fs_name的MDS实例数
orch apply mds &lt;fs_name&gt; [&lt;placement&gt;] [--dry-run] [--unmanaged] [plain|json|json-pretty|yaml]

# 扩缩容NFS服务
orch apply nfs &lt;svc_id&gt; &lt;pool&gt; [&lt;namespace&gt;] [&lt;placement&gt;] [--dry-run] [plain|json|json-pretty|
 yaml] [--unmanaged] 

# 创建OSD守护进程
orch apply osd [--all-available-devices] [--dry-run] [--unmanaged] [plain|json|json-pretty|yaml]

# 为指定的Zone更新RGW实例的数量
orch apply rgw &lt;realm_name&gt; &lt;zone_name&gt; [&lt;subcluster&gt;] [&lt;port:int&gt;] [--ssl] [&lt;placement&gt;] 
[--dry-run] [plain|json|json-pretty|yaml] [--unmanaged]</pre>
<p>下面是一些简单的例子：</p>
<pre class="crayon-plain-tag"># 指定副本数
ceph orch apply mon 3
# 制定归置规则
ceph orch apply mon ceph-1 ceph-2 ceph-3

# 为所有空闲设备创建OSD
ceph orch apply osd --all-available-devices</pre>
<div class="blog_h3"><span class="graybg">orch daemon<br /></span></div>
<p>管理守护进程。</p>
<p>add子命令，添加一个守护进程：</p>
<pre class="crayon-plain-tag"># 添加守护进程
ceph orch daemon add [mon|mgr|rbd-mirror|crash|alertmanager|grafana|node-exporter|prometheus [&lt;placement&gt;]

ceph orch daemon add iscsi &lt;pool&gt; &lt;api_user&gt; &lt;api_password&gt; [&lt;trusted_ip_list&gt;] [&lt;placement&gt;]
ceph orch daemon add mds &lt;fs_name&gt; [&lt;placement&gt;] 
ceph orch daemon add nfs &lt;svc_id&gt; &lt;pool&gt; [&lt;namespace&gt;] [&lt;placement&gt;]
ceph orch daemon add osd [&lt;svc_arg&gt;]
ceph orch daemon add rgw &lt;realm_name&gt; &lt;zone_name&gt; [&lt;subcluster&gt;] [&lt;port:int&gt;] [--ssl] [&lt;placement&gt;]</pre>
<p>redeploy子命令，重新部署某个守护进程，可以指定使用的镜像：</p>
<pre class="crayon-plain-tag">ceph orch daemon redeploy &lt;name&gt; [&lt;image&gt;]</pre>
<p>如果节点上的守护进程容器被意外删除，也就是<pre class="crayon-plain-tag">podman ps</pre>看不到对应容器，可以使用redeploy命令重新部署。</p>
<p>rm子命令，删除某个守护进程：</p>
<pre class="crayon-plain-tag">ceph orch daemon rm &lt;names&gt;... [--force]</pre>
<p>你也可以启动、停止、重启、重新配置某个守护进程：</p>
<pre class="crayon-plain-tag">ceph orch daemon start|stop|restart|reconfig &lt;name&gt;</pre>
<p>如果需要启动、停止、重启、重新配置<span style="background-color: #c0c0c0;">某种服务的所有守护进程</span>：</p>
<pre class="crayon-plain-tag">ceph orch start|stop|restart|redeploy|reconfig &lt;service_name&gt; </pre>
<div class="blog_h3"><span class="graybg">orch device </span></div>
<p>管理块设备。</p>
<p>显示某些主机上的块设备：</p>
<pre class="crayon-plain-tag">ceph orch device ls [&lt;hostname&gt;...] [plain|json|json-pretty|yaml] [--refresh] [--wide]</pre>
<p>清除块设备上的内容：</p>
<pre class="crayon-plain-tag">ceph orch device zap &lt;hostname&gt; &lt;path&gt; [--force]</pre>
<div class="blog_h3"><span class="graybg">orch host</span></div>
<p>管理主机。</p>
<p>添加主机，可选的，添加标签：</p>
<pre class="crayon-plain-tag">ceph orch host add &lt;hostname&gt; [&lt;addr&gt;] [&lt;labels&gt;...]</pre>
<p>为主机添加/移除标签：</p>
<pre class="crayon-plain-tag">ceph orch host label add &lt;hostname&gt; &lt;label&gt;
ceph orch host label rm &lt;hostname&gt; &lt;label&gt;</pre>
<p>列出主机：</p>
<pre class="crayon-plain-tag">ceph orch host ls [plain|json|json-pretty|yaml]</pre>
<p>检查是否可以在不损害可用性的前提下，停止主机：</p>
<pre class="crayon-plain-tag">ceph orch host ok-to-stop &lt;hostname&gt;</pre>
<p>删除主机：</p>
<pre class="crayon-plain-tag">ceph orch host rm &lt;hostname&gt;</pre>
<p>修改主机地址：</p>
<pre class="crayon-plain-tag">ceph orch host set-addr &lt;hostname&gt; &lt;addr&gt;</pre>
<div class="blog_h3"><span class="graybg">orch osd rm</span></div>
<p>删除OSD实例：</p>
<pre class="crayon-plain-tag">ceph orch osd rm &lt;svc_id&gt;... [--replace] [--force]</pre>
<p>检查删除OSD操作的进度：</p>
<pre class="crayon-plain-tag">ceph orch osd rm status [plain|json|json-pretty|yaml]</pre>
<div class="blog_h3"><span class="graybg"> orch pause</span></div>
<p>暂停编排器的后台任务</p>
<div class="blog_h3"><span class="graybg">orch resume</span></div>
<p>恢复暂停的编排器后台任务</p>
<div class="blog_h3"><span class="graybg">orch set backend</span></div>
<p>选择编排器后端：<pre class="crayon-plain-tag">ceph orch set backend &lt;module_name&gt;</pre></p>
<div class="blog_h3"><span class="graybg">orch status</span></div>
<p>显示使用的编排器后端，以及它的状态：</p>
<pre class="crayon-plain-tag">ceph orch status [plain|json|json-pretty|yaml]

ceph orch status
Backend: cephadm
Available: True</pre>
<p>cephadm就是一个编排器后端，下文会有介绍。</p>
<div class="blog_h3"><span class="graybg">orch upgrade</span></div>
<p>升级相关操作：</p>
<pre class="crayon-plain-tag">orch upgrade check [&lt;image&gt;] [&lt;ceph_version&gt;]    # 检查镜像可用版本
orch upgrade pause                               # 暂停升级
orch upgrade resume                              # 恢复暂停的省级
orch upgrade start [&lt;image&gt;] [&lt;ceph_version&gt;]    # 触发升级
orch upgrade status                              # 升级状态
orch upgrade stop                                # 停止进行中的升级</pre>
<div class="blog_h2"><span class="graybg">ceph log </span></div>
<p>查看日志：</p>
<pre class="crayon-plain-tag">ceph log last [&lt;num:int&gt;] [debug|info|sec|warn|error] [*|cluster|audit|cephadm]

# 查看cephadm的最新日志
ceph log last cephadm</pre>
<div class="blog_h1"><span class="graybg">通过cephadm部署</span></div>
<p>Cephadm是最新的Ceph部署工具，他利用容器和Systemd，仅仅支持Octopus或者更新的版本。</p>
<p>Cephadm的外部依赖包括容器运行时（Docker或者Podman），以及Python3。</p>
<div class="blog_h2"><span class="graybg">安装Cephadm</span></div>
<pre class="crayon-plain-tag">cd /tmp
curl --silent --remote-name --location https://github.com/ceph/ceph/raw/octopus/src/cephadm/cephadm
chmod +x cephadm</pre>
<p>安装支持cephadm命令及其依赖：</p>
<pre class="crayon-plain-tag">./cephadm add-repo --release octopus
./cephadm install</pre>
<div class="blog_h2"><span class="graybg">单节点自举</span></div>
<pre class="crayon-plain-tag">cephadm bootstrap --mon-ip 10.0.2.1
cephadm shell -- ceph -s</pre>
<p>上述命令会安装一个单MON节点的Ceph集群。改命令会：</p>
<ol>
<li>创建MON和MGR守护进程到本机</li>
<li>为Ceph集群生成SSH密钥，并添加到roo用户的/root/.ssh/authorized_keys</li>
<li>生成最小化配置的 /etc/ceph/ceph.conf，用于和新集群通信</li>
<li>生成Ceph管理密钥 /etc/ceph/ceph.client.admin.keyring</li>
<li>复制公钥副本到/etc/ceph/ceph.pub</li>
</ol>
<div class="blog_h2"><span class="graybg">安装Ceph Common</span></div>
<pre class="crayon-plain-tag">cephadm install ceph-common</pre>
<p>这样你就可以直接使用ceph命令了：</p>
<pre class="crayon-plain-tag">ceph status</pre>
<div class="blog_h2"><span class="graybg">添加节点</span></div>
<p>你需要提前为节点安装好Python3</p>
<p>列出现有节点：</p>
<pre class="crayon-plain-tag">ceph orch host ls</pre>
<p>orch子命令用于Ceph集群相关的编排。</p>
<p>将集群的公钥安装到新节点的authorized_keys：</p>
<pre class="crayon-plain-tag">ssh-copy-id -f -i /etc/ceph/ceph.pub root@10.0.2.2
ssh-copy-id -f -i /etc/ceph/ceph.pub root@10.0.2.3</pre>
<p>添加节点，注意要提供主机名：</p>
<pre class="crayon-plain-tag">ceph orch host add ceph-2
ceph orch host add ceph-3</pre>
<div class="blog_h3"><span class="graybg">修改节点地址</span></div>
<pre class="crayon-plain-tag">ceph orch host set-addr ceph-1 ceph-1.gmem.cc </pre>
<div class="blog_h2"><span class="graybg">添加MON</span></div>
<pre class="crayon-plain-tag"># 设置哪些子网中的主机可以作为MON
ceph config set mon public_network 10.0.2.0/24

# 确保三个MON
ceph orch apply mon 3

# 你也可以强制指定在哪些主机上部署MON
ceph orch apply mon ceph-1 ceph-2 ceph-3</pre>
<div class="blog_h2"><span class="graybg">添加OSD</span></div>
<p>使用下面的命令，可以将集群主机所有空闲设备作为OSD：</p>
<pre class="crayon-plain-tag">ceph orch apply osd --all-available-devices</pre>
<p>将特定主机的特定磁盘作为OSD：</p>
<pre class="crayon-plain-tag">ceph orch daemon add osd ceph-1:/dev/vdb</pre>
<div class="blog_h2"><span class="graybg">部署CephFS</span></div>
<pre class="crayon-plain-tag">ceph fs volume create cephfs --placement="ceph-1 ceph-2 ceph-3"</pre>
<div class="blog_h2"><span class="graybg">部署RGW</span></div>
<pre class="crayon-plain-tag">ceph orch apply rgw china beijing '--placement=3' --port=80</pre>
<div class="blog_h2"><span class="graybg">部署NFS</span></div>
<p>NFS Ganesha是一个用户模式的NFS，支持v3  4.0 4.1 4.2，可以同时运行这些协议。</p>
<p>使用下面的命令来部署NFS Ganesha网关。</p>
<pre class="crayon-plain-tag"># 为NFS创建存储池
ceph osd pool create nfs-ganesha 64 replicated
#                   服务ID  存储池      命名空间
ceph orch apply nfs ganesha nfs-ganesha china</pre>
<div class="blog_h1"><span class="graybg">通过ceph-deploy部署</span></div>
<p>&nbsp;</p>
<p><span class="graybg">ceph-deploy</span>是一个ceph部署工具，服务器只需要提供SSH、sudo、一些Python包即可完成ceph的安装部署。</p>
<p>准备好服务器集群后，选取一台作为管理主机，在其上执行：</p>
<pre class="crayon-plain-tag">wget -q -O- 'https://download.ceph.com/keys/release.asc' | sudo apt-key add -
#                                  debian-luminous
echo deb https://download.ceph.com/debian-jewel/ $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/ceph.list
sudo apt-get update
sudo apt-get install ceph-deploy


# 也可以通过pip安装
apt install python-pip
pip install ceph-deploy


# BUG太多，直接Git最新源码安装吧
git clone https://github.com/ceph/ceph-deploy.git
cd ceph-deploy
chmod +x setup.py 
python setup.py install</pre>
<div class="blog_h2"><span class="graybg">安装NTP客户端</span></div>
<p>为了防止时钟不同步导致问题，建议<a href="/ntp-under-ubuntu">安装NTP客户端</a>，并保持和NTP服务器的同步。</p>
<div class="blog_h2"><span class="graybg">创建部署用户</span></div>
<pre class="crayon-plain-tag"># 在所有节点上执行
ansible k8s -m raw -a 'useradd -d /home/ceph-ops -m ceph-ops'
ansible k8s -m raw -a 'echo "ceph-ops ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/ceph-ops'
ansible k8s -m raw -a 'chmod 0440 /etc/sudoers.d/ceph-ops'
ansible k8s -m raw -a 'echo "ceph-ops:password" | chpasswd'

# 在管理节点上执行
# 生成密钥对，默认生成~/.ssh下的id_rsa和id_rsa.pub
ssh-keygen
# 将公钥拷贝到被管理机，便于免密码登陆
ansible k8s -m raw -a "mkdir /home/ceph-ops/.ssh"
ansible k8s -m raw -a "scp -oStrictHostKeyChecking=no root@master-node:/root/.ssh/id_rsa.pub /home/ceph-ops/.ssh/authorized_keys"
ansible k8s -m raw -a "chown -R ceph-ops:ceph-ops /home/ceph-ops/.ssh"</pre>
<div class="blog_h2"><span class="graybg">安装ceph</span></div>
<p>在集群中的管理主机上，执行：</p>
<pre class="crayon-plain-tag">ceph-deploy install {hostname [hostname] ...} --release {code-name}
# 示例：
ceph-deploy --username ceph-ops install Carbon Radon Neon Boron Xenon --release jewel
ceph-deploy --username ceph-ops install Carbon Radon Neon Boron Xenon  --release luminous


# 如果网络速度太慢，ceph-deploy会提前退出。这种情况下手工、通过代理安装为好
export http_proxy=http://10.0.0.1:8087
export https_proxy=http://10.0.0.1:8087

# 实际上就是安装这些软件
apt install ceph ceph-osd ceph-mds ceph-mon radosgw</pre>
<div class="blog_h2"><span class="graybg">卸载ceph</span></div>
<pre class="crayon-plain-tag"># 卸载软件
ceph-deploy uninstall {hostname [hostname] ...}
# 示例：
ceph-deploy --username ceph-ops uninstall  Carbon Radon Neon

# 下面的命令可以在Ubuntu上执行，清除配置文件
ceph-deploy purge {hostname [hostname] ...}
# 示例：
ceph-deploy --username ceph-ops purge  Carbon Radon Neon</pre>
<div class="blog_h2"><span class="graybg">创建集群</span></div>
<div class="blog_h3"><span class="graybg">临时配置目录</span></div>
<pre class="crayon-plain-tag"># 在管理节点上执行：
# 创建一个目录，存放ceph-deploy生成的配置文件
mkdir /tmp/ceph
cd /tmp/ceph</pre>
<div class="blog_h3"><span class="graybg">新建集群</span></div>
<pre class="crayon-plain-tag"># 创建一个新集群，host为mon节点
ceph-deploy --cluster {cluster-name} new {host [host], ...}

# 示例
ceph-deploy --username ceph-ops new Xenon</pre>
<div class="blog_h3"><span class="graybg">分发配置文件</span></div>
<pre class="crayon-plain-tag"># 修改好当前目录的ceph.conf，执行下面的命令，分发到所有节点的/etc/ceph目录
# ceph.conf至少要提供网络配置
# public network = 10.0.0.0/16
# cluster network = 10.0.0.0/16

# 示例
ceph-deploy --username ceph-ops --overwrite-conf config push Carbon Radon Neon Boron Xenon</pre>
<div class="blog_h3"><span class="graybg">配置文件示例</span></div>
<pre class="crayon-plain-tag">[client]
rbd_cache = true
rbd_cache_max_dirty = 25165824
rbd_cache_max_dirty_age = 5
rbd_cache_size = 268435456

[global]
fsid = 9b92d057-a4bc-473e-b6ab-462092fcf205
max_open_files = 131072
mon_initial_members = Carbon, Radon, Neon
mon_host = 10.0.0.100,10.0.1.1,10.0.2.1
osd pool default min size = 1
osd pool default pg num = 384
osd pool default pgp num = 384
osd pool default size = 2
mon_max_pg_per_osd = 256
auth_cluster_required = cephx
auth_service_required = cephx
auth_client_required = cephx

[mon]
mon_allow_pool_delete = true

[osd]
public network = 10.0.0.0/16
cluster network = 10.0.0.0/16
filestore max sync interval = 15
filestore min sync interval = 10
filestore op thread = 32
journal max write bytes = 1073714824
journal max write entries = 10000
journal queue max bytes = 10485760000
journal queue max ops = 50000
ms_bind_port_max = 7100
osd_client_message_size_cap = 2147483648
osd_crush_update_on_start = true
osd_deep_scrub_stride = 131072
osd_disk_threads = 4
osd_journal_size = 10240
osd_map_cache_bl_size = 128
osd_max_backfills = 4
osd_max_object_name_len = 256
osd_max_object_namespace_len = 64
osd_max_write_size = 512
osd_op_threads = 8
osd_recovery_op_priority = 4</pre>
<div class="blog_h2"><span class="graybg">初始mon</span></div>
<p>执行下面的命令，部署初始mon节点，并收集key：</p>
<pre class="crayon-plain-tag">ceph-deploy --username ceph-ops mon create-initial

# 将在当前目录生成以下文件：
# ceph.client.admin.keyring
# ceph.bootstrap-mgr.keyring
# ceph.bootstrap-osd.keyring
# ceph.bootstrap-mds.keyring
# ceph.bootstrap-rgw.keyring
# ceph.bootstrap-rbd.keyring

# 目标主机的/etc/ceph/ceph.conf被创建，如果此文件已经存在，你必须用--overwrite-conf选项重新运行上述命令</pre>
<div class="blog_h2"><span class="graybg">增减mon</span></div>
<pre class="crayon-plain-tag"># 增加
ceph-deploy mon create {host-name [host-name]...}
# 删除
ceph-deploy mon destroy {host-name [host-name]...}

# 示例：
ceph-deploy --username ceph-ops mon create Radon
ceph-deploy --username ceph-ops mon create Carbon</pre>
<div class="blog_h2"><span class="graybg">密钥收集</span></div>
<p>以ceph-deploy作为工具，将一台主机作为OSD或MDS时，需要收集MON、OSD、MDS的初始keyring：</p>
<pre class="crayon-plain-tag">ceph-deploy gatherkeys {monitor-host}

# 示例：
ceph-deploy --username ceph-ops gatherkeys Carbon Radon Neon Boron Xenon</pre>
<p>不再使用ceph-deploy或者另外建立一个新集群时，需要删除管理主机、本地目录的密钥：</p>
<pre class="crayon-plain-tag">ceph-deploy forgetkeys</pre>
<div class="blog_h2"><span class="graybg">增加OSD </span></div>
<div class="blog_h3"><span class="graybg">列出磁盘</span></div>
<pre class="crayon-plain-tag">ceph-deploy disk list {node-name [node-name]...}
# 示例：
ceph-deploy --username ceph-ops disk list Carbon Radon Neon Boron Xenon</pre>
<div class="blog_h3"><span class="graybg">擦净分区</span></div>
<p>下面的命令可以擦净（删除分区表）磁盘，以供Ceph使用 ： </p>
<pre class="crayon-plain-tag">ceph-deploy disk zap {osd-server-name} {disk-name}
# 示例（luminous）：
ceph-deploy --username ceph-ops disk zap xenial-100 /dev/vdb
ceph-deploy --username ceph-ops disk zap xenial-100 /dev/vdc
ceph-deploy --username ceph-ops disk zap Carbon     /dev/sdb
ceph-deploy --username ceph-ops disk zap Radon      /dev/sdb
ceph-deploy --username ceph-ops disk zap Neon       /dev/sdb
# 示例（jewel）：
ceph-deploy --username ceph-ops disk zap xenial-100:vdb
ceph-deploy --username ceph-ops disk zap xenial-100:vdc
ceph-deploy --username ceph-ops disk zap Carbon:sdb
ceph-deploy --username ceph-ops disk zap Radon:sdb
ceph-deploy --username ceph-ops disk zap Neon:sdb</pre>
<p>警告：所有数据会被删除。</p>
<div class="blog_h3"><span class="graybg">准备OSD</span></div>
<p>此命令在ceph-deploy 2.0.0中已经废除</p>
<p>使用prepare命令来准备磁盘，会自动创建分区。在大部分OS中，<span style="background-color: #c0c0c0;">activate会随后自动执行</span></p>
<pre class="crayon-plain-tag">ceph-deploy osd prepare {node-name}:{data-disk}[:{journal-disk}]
# 示例：
ceph-deploy --username ceph-ops osd prepare --fs-type xfs xenial-100:vdb
ceph-deploy --username ceph-ops osd prepare --fs-type xfs xenial-100:vdc
ceph-deploy --username ceph-ops osd prepare --fs-type xfs Carbon:sdb
ceph-deploy --username ceph-ops osd prepare --fs-type xfs Radon:sdb</pre>
<p>建议：将日志存储在独立磁盘中以最优化性能，如果将日志和数据存储在一起，会有损性能。</p>
<div class="blog_h3"><span class="graybg">激活OSD</span></div>
<p>此命令在ceph-deploy 2.0.0中已经废除</p>
<p>很多操作系统上，不需要手工激活OSD。</p>
<pre class="crayon-plain-tag">ceph-deploy osd activate {node-name}:{data-disk-partition}[:{journal-disk-partition}]
# 示例
ceph-deploy --username ceph-ops osd activate xenial-100:vdb
ceph-deploy --username ceph-ops osd activate xenial-100:vdc
ceph-deploy --username ceph-ops osd activate Carbon:sdb
ceph-deploy --username ceph-ops osd activate Radon:sdb</pre>
<p>激活之后，系统运行ceph-osd进程，OSD进入up+in状态。</p>
<div class="blog_h3"><span class="graybg">创建OSD </span></div>
<p>即prepare + activate：</p>
<pre class="crayon-plain-tag">ceph-deploy osd create {node-name}:{disk}[:{path/to/journal}]
# 示例
ceph-deploy osd create osdserver1:sdb:/dev/ssd1

# 示例（luminous）：
ceph-deploy --username ceph-ops osd create --data /dev/vdb --bluestore xenial-100  
ceph-deploy --username ceph-ops osd create --data /dev/vdc --bluestore xenial-100  
ceph-deploy --username ceph-ops osd create --data /dev/sdb --bluestore Carbon
ceph-deploy --username ceph-ops osd create --data /dev/sdb --bluestore Radon
# 指定分区也可以
ceph-deploy --username ceph-ops osd create --data /dev/sda3 --bluestore Carbon</pre>
<div class="blog_h2"><span class="graybg">增加MDS</span></div>
<pre class="crayon-plain-tag">ceph-deploy --username ceph-ops mds create Carbon</pre>
<div class="blog_h2"><span class="graybg">部署MGR</span></div>
<p>仅仅luminous支持，否则报错Error EACCES: access denied could not create mgr</p>
<pre class="crayon-plain-tag">ceph-deploy --username ceph-ops mgr create Carbon</pre>
<div class="blog_h2"><span class="graybg">部署RGW</span></div>
<pre class="crayon-plain-tag">ceph-deploy --username ceph-ops rgw create Radon</pre>
<p>默认情况下RGW监听7480端口，你可以验证RGW是否正常工作：</p>
<pre class="crayon-plain-tag">curl http://Carbon:7480</pre>
<div class="blog_h2"><span class="graybg">清除主机</span></div>
<p>如果只想清除 /var/lib/ceph 下的数据、并保留 Ceph 安装包，可以：</p>
<pre class="crayon-plain-tag">ceph-deploy purgedata {hostname} [{hostname} ...]
# 示例：
ceph-deploy --username ceph-ops purgedata Carbon Radon</pre>
<p>如果向同时清除数据、Ceph安装包：</p>
<pre class="crayon-plain-tag">ceph-deploy purge {hostname} [{hostname} ...]</pre>
<div class="blog_h2"><span class="graybg">管理和分发</span></div>
<div class="blog_h3"><span class="graybg">管理主机</span></div>
<p>要允许某些主机以管理员权限执行 Ceph 命令，可以：</p>
<pre class="crayon-plain-tag">ceph-deploy admin {host-name [host-name]...}

# 示例：
ceph-deploy --username ceph-ops admin Carbon Radon Neon Boron Xenon</pre>
<p>上述命令执行后，当前目录中的ceph.client.admin.keyring被分发到所有指定的主机，在这些主机上可以执行ceph -s命令了。</p>
<div class="blog_h3"><span class="graybg">分发配置文件</span></div>
<p>要将修改过的、当前目录下的配置文件分发给集群内其它主机，可以：</p>
<pre class="crayon-plain-tag">ceph-deploy config push {host-name [host-name]...}</pre>
<div class="blog_h3"><span class="graybg">拉取配置文件 </span></div>
<p>要获取某台主机的配置文件，可以：</p>
<pre class="crayon-plain-tag">ceph-deploy config pull {host-name [host-name]...} </pre>
<div class="blog_h1"><span class="graybg">配置</span></div>
<p>启动Ceph服务时，初始化进程会启动一系列守护进程，这些进程至少包含两类：</p>
<ol>
<li>ceph-mon 监控进程</li>
<li>ceph-osd  Ceph OSD的守护进程</li>
</ol>
<p>要使用Ceph文件系统功能，则需要额外运行：</p>
<ol>
<li>ceph-mds Ceph元数据服务</li>
</ol>
<p>要使用Ceph对象存储功能，则需要额外运行：</p>
<ol>
<li>ceph-rgw RADOS网关守护进程 </li>
</ol>
<div class="blog_h2"><span class="graybg">运行时配置</span></div>
<p>要列出当前使用的所有配置值，可以访问守护进程的管理套接字。</p>
<p>在节点上执行下面的命令，获得管理套接字的位置：</p>
<pre class="crayon-plain-tag">ceph-conf --name mon.$(hostname -s) --show-config-value admin_socket
# /var/run/ceph/ceph-mon.a.asok</pre>
<p>调用下面的命令列出所有配置：</p>
<pre class="crayon-plain-tag">ceph daemon /var/run/ceph/ceph-mon.a.asok config show </pre>
<p>对于osd或者其它守护进程，也可以使用上述方式获取运行时配置。</p>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<p>所有守护进程从同一个配置文件ceph.conf中检索自己感兴趣的信息。该配置文件中包含了集群身份、认证配置、集群成员、主机名、主机 IP 地址、Keyring路径、日志路径、数据路径，以及其它运行时选项。</p>
<p>按照以下顺序来搜索，后面的可以覆盖前面的：</p>
<ol>
<li>编译进二进制文件的默认值</li>
<li>ceph-mon的集群中心配置数据库</li>
<li>本机上的配置文件：
<ol>
<li>/etc/ceph/ceph.conf</li>
<li>~/.ceph/config</li>
<li>./ceph.conf</li>
</ol>
</li>
<li>环境变量：$CEPH_CONF </li>
<li>命令行参数：-c path/path</li>
<li>管理员在运行时设置的选项</li>
</ol>
<p>一个Ceph进程启动时，它会先从命令行、环境变量、本地配置文件收集配置项，然后连接ceph-mon读取集群中心配置信息，然后启动。</p>
<div class="blog_h2"><span class="graybg">配置项格式</span></div>
<p>配置项的名称唯一，标准格式是小写字母 + 下划线</p>
<p>在命令行中指定配置项时，下划线_可以替换为<span style="background-color: #c0c0c0;">短横线</span></p>
<p>在配置文件中指定配置项时，下划线可以替换为<span style="background-color: #c0c0c0;">空格或短横线</span></p>
<div class="blog_h2"><span class="graybg" style="background-color: #c0c0c0;">配置项列表</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">配置项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><em>公共选项 [global]</em></td>
</tr>
<tr>
<td>host</td>
<td>用于指定节点的主机名</td>
</tr>
<tr>
<td>mon host</td>
<td>指定mon节点的地址，逗号分隔</td>
</tr>
<tr>
<td>auth cluster required</td>
<td>集群身份验证设置，默认值cephx。如果启用，集群守护进程之间必须相互验证身份</td>
</tr>
<tr>
<td>auth service required</td>
<td>服务身份验证设置，默认值cephx。如果启用，服务端需要验证客户端身份</td>
</tr>
<tr>
<td>auth client required</td>
<td>客户端身份验证设置，默认值cephx。如果启用，客户端需要验证服务端身份</td>
</tr>
<tr>
<td>keyring</td>
<td>
<p>钥匙串的位置</p>
<p>默认值：/etc/ceph/$cluster.$name.keyring,/etc/ceph/$cluster.keyring,/etc/ceph/keyring,/etc/ceph/keyring.bin</p>
</td>
</tr>
<tr>
<td>public network</td>
<td>公共网络配置，CIDR格式，多个则用逗号分隔</td>
</tr>
<tr>
<td><strong>cluster network</strong></td>
<td>
<p>集群网络配置，CIDR格式，多个则用逗号分隔</p>
<p>如果配置了集群网， OSD 将把<span style="background-color: #c0c0c0;">心跳、对象复制和恢复流量</span>路由到集群网</p>
</td>
</tr>
<tr>
<td>fsid</td>
<td>存储集群的唯一标识，便于允许在同一套硬件上部署多个集群</td>
</tr>
<tr>
<td><strong>max open files</strong></td>
<td>
<p>设置操作系统级的 max open fds</p>
<p>建议值：131072</p>
</td>
</tr>
<tr>
<td>fatal signal handlers</td>
<td>如果设置为true，则安装 SEGV 、 ABRT 、 BUS 、 ILL 、 FPE 、 XCPU 、 XFSZ 、 SYS 信号处理器，用于产生有用的日志信息</td>
</tr>
<tr>
<td>chdir</td>
<td>进程一旦启动、运行就进入这个目录。默认 /</td>
</tr>
<tr>
<td colspan="2"><em>mon选项  [mon]</em></td>
</tr>
<tr>
<td>mon addr</td>
<td>
<p>监听地址:端口，可以针对每个mon.$id段分别配置，或者在mon段下配置：</p>
<p style="padding-left: 30px;">10.0.0.10:6789,10.0.0.11:6789,10.0.0.12:6789</p>
</td>
</tr>
<tr>
<td>mon data</td>
<td>mon存储数据的路径，默认/var/lib/ceph/mon/$cluster-$id</td>
</tr>
<tr>
<td>mon initial members</td>
<td>
<p>集群初始化监视器ID，逗号分隔。这些MON必须在线以建立quorum，正确设置此参数可能让集群更快的可用</p>
</td>
</tr>
<tr>
<td>
<p>mon osd full ratio</p>
</td>
<td>
<p>磁盘利用率总计多少认为满了，默认 .95</p>
<p>当Ceph集群利用率达到此比率时，作为<span style="background-color: #c0c0c0;">防止数据丢失的安全措施，它会阻止你读写 OSD</span></p>
<p>注意：</p>
<ol>
<li>删除OSD、OSD out都会导致利用率增加，甚至超过full ratio导致锁死</li>
<li>如果一些 OSD 快满了，但其他的仍有足够空间，你可能配错了CRUSH权重</li>
</ol>
</td>
</tr>
<tr>
<td>mon osd nearfull ratio</td>
<td>磁盘利用率总计多少认为快满了，默认.85</td>
</tr>
<tr>
<td>mon sync timeout</td>
<td>mon从其provider获取下一个更新消息的超时</td>
</tr>
<tr>
<td>mon tick interval</td>
<td>监视器的心跳间隔，单位为秒。默认5</td>
</tr>
<tr>
<td>mon clock drift allowed</td>
<td>监视器间允许的时钟漂移量。默认.050</td>
</tr>
<tr>
<td>mon timecheck interval</td>
<td>和 leader 的时间偏移检查间隔。默认300秒</td>
</tr>
<tr>
<td>mon osd min down reports</td>
<td>OSD连续多少次向mon报告某个OSD宕掉，mon才采纳，默认3</td>
</tr>
<tr>
<td>mon osd min down reporters</td>
<td>类似上面，mon要求多少个OSD都报告某个OSD宕掉，才采纳，默认1</td>
</tr>
<tr>
<td>mon osd min up ratio</td>
<td>把 OSD 标记为 down 前，保持处于 up 状态的 OSD 最小比例。默认.3</td>
</tr>
<tr>
<td>mon osd min in ratio</td>
<td>把 OSD 标记为 out 前，保持处于 in 状态的 OSD 最小比例。默认.3</td>
</tr>
<tr>
<td>mon osd auto mark in</td>
<td>是否把任何启动中的 OSD 标记为在集群中。默认false</td>
</tr>
<tr>
<td>mon osd auto mark auto out in</td>
<td>是否把正在启动、且被自动标记为 out 状态的 OSD 标记为 in 。默认true</td>
</tr>
<tr>
<td>mon osd auto mark new in</td>
<td>是否把正在启动的新 OSD 标记为 in 。默认true</td>
</tr>
<tr>
<td>mon osd down out interval</td>
<td>在 OSD 停止响应多少秒后把它标记为 down 且 out。默认300</td>
</tr>
<tr>
<td>mon osd downout subtree limit</td>
<td>最大可以把什么级别的CRUSH单元标记为out，默认rack</td>
</tr>
<tr>
<td colspan="2"><em>osd选项 [osd]</em></td>
</tr>
<tr>
<td>osd data</td>
<td>osd存储数据的路径，默认/var/lib/ceph/osd/$cluster-$id</td>
</tr>
<tr>
<td><strong>osd map cache size</strong></td>
<td>OSD map缓存大小，默认500M，建议1024</td>
</tr>
<tr>
<td><strong>osd map cache bl size</strong></td>
<td>OSD进程的In-Memory OSD map缓存大小，默认50，建议128</td>
</tr>
<tr>
<td>osd heartbeat interval</td>
<td>和其它OSD进行心跳检查的间隔，默认6秒</td>
</tr>
<tr>
<td>osd heartbeat grace</td>
<td>多久没有心跳，认为其它OSD宕掉</td>
</tr>
<tr>
<td><strong>osd max write size</strong></td>
<td>OSD一次写入的最大尺寸，默认90MB，建议512</td>
</tr>
<tr>
<td>osd mkfs options {fs-type} </td>
<td>为OSD新建文件系统时的选项</td>
</tr>
<tr>
<td>osd mount options {fs-type}</td>
<td>为OSD挂载文件系统时的选项</td>
</tr>
<tr>
<td><strong>osd journal</strong></td>
<td>
<p>OSD 日志路径，可以指向文件或块设备（例如SSD分区）</p>
<p>默认 /var/lib/ceph/osd/$cluster-$id/journal</p>
</td>
</tr>
<tr>
<td><strong>osd journal size</strong></td>
<td>
<p>日志文件的尺寸（MB），如果为0，且日志路径为块设备，则自动使用整个设备</p>
<p>推荐最少2G，有的用户则以 10GB 日志尺寸起步。合理的值是：</p>
<p style="padding-left: 30px;">osd journal size = {2 * (期望吞吐量* filestore max sync interval)}</p>
<p>期望吞吐量应考虑两个参数：<span style="background-color: #c0c0c0;">硬盘吞吐量（即持续数据传输速率）、网络吞吐量</span>，例如一个 7200 转硬盘的速度大致是 100MB/s 。硬盘和网络吞吐量中较小的一个是相对合理的吞吐量</p>
</td>
</tr>
<tr>
<td><strong>osd client message size cap</strong></td>
<td>客户端允许在内存中的最大数据量。默认524288000，建议2147483648</td>
</tr>
<tr>
<td><strong>crush location</strong></td>
<td>此OSD的CRUSH location设置</td>
</tr>
<tr>
<td>crush location hook</td>
<td>用于生成crush location的钩子 </td>
</tr>
<tr>
<td>osd max scrubs</td>
<td>
<p>OSD 的最大并发洗刷操作数</p>
<p>除了为对象复制多个副本外， Ceph 还要洗刷归置组以确保数据完整性。这种洗刷类似对象存储层的 fsck ，对于每个归置组， Ceph 生成一个所有对象的目录，并<span style="background-color: #c0c0c0;">比对每个主对象及其副本以确保没有对象丢失或错配</span>。轻微洗刷（每天）检查对象尺寸和属性，深层洗刷（每周）会读出数据并用校验和方法确认数据完整性</p>
</td>
</tr>
<tr>
<td>osd scrub begin hour</td>
<td>被调度的洗刷操作，允许的运行区间起点，默认0</td>
</tr>
<tr>
<td>osd scrub end hour</td>
<td>被调度的洗刷操作，允许的运行区间终点，默认24</td>
</tr>
<tr>
<td>osd scrub load threshold</td>
<td>系统负载高于该值，不进行洗刷操作，默认0.5</td>
</tr>
<tr>
<td>osd scrub min interval</td>
<td>系统负载不高的前提下，多久进行一次洗刷，默认每天一次，60*60*24秒</td>
</tr>
<tr>
<td>osd scrub max interval</td>
<td>不论系统负载如何，最大多久进行一次洗刷，默认每周</td>
</tr>
<tr>
<td>osd deep scrub interval</td>
<td>深度洗刷的间隔，默认每周</td>
</tr>
<tr>
<td><strong>osd deep scrub stride</strong></td>
<td><strong>深度洗刷允许读取的字节数。默认524288，建议131072</strong></td>
</tr>
<tr>
<td><strong>osd op threads</strong></td>
<td>
<p>OSD 操作线程数， 0 禁用多线程。增大数量可以增加请求处理速度，默认2，建议8</p>
<p>增加此线程数会增大CPU开销</p>
</td>
</tr>
<tr>
<td><strong>osd disk threads</strong></td>
<td>
<p>硬盘线程数，用于在后台执行磁盘密集型操作，像数据洗刷和快照修复。默认1，建议4</p>
<p>增加此线程数会增大CPU开销</p>
</td>
</tr>
<tr>
<td><strong>osd max backfills</strong></td>
<td>
<p>当集群新增或移除 OSD 时，按照 CRUSH 算法应该<span style="background-color: #c0c0c0;">重新均衡集群</span>，它会<span style="background-color: #c0c0c0;">把一些归置组移出或移入多个 OSD</span> 以回到均衡状态。<span style="background-color: #c0c0c0;">归置组和对象的迁移会导致集群运营性能显著降低</span>，为维持运营性能， Ceph 用 backfilling 来执行此迁移，它可以使得 Ceph 的<span style="background-color: #c0c0c0;">回填操作优先级低于用户读写请求</span></p>
<p>单个 OSD 允许的最大回填操作数。默认10，建议4</p>
</td>
</tr>
<tr>
<td>osd backfill full ratio</td>
<td>OSD 的占满率达到多少时拒绝接受回填请求，默认85%</td>
</tr>
<tr>
<td><strong>osd recovery op priority</strong></td>
<td>恢复操作优先级，取值1-63，值越高占用资源越高。默认10，建议4</td>
</tr>
<tr>
<td>osd recovery delay start</td>
<td>
<p>当集群启动、或某 OSD 守护进程崩溃后重启时，此 OSD 开始与其它 OSD 们建立互联（Peering），这样才能正常工作</p>
<p>如果某 OSD 崩溃并重启，通常会<span style="background-color: #c0c0c0;">落后于其他 OSD ，也就是没有同归置组内最新版本的对象</span>。这时， OSD 守护进程<span style="background-color: #c0c0c0;">进入恢复模式并检索最新数据副本</span>，并<span style="background-color: #c0c0c0;">更新运行图</span>。根据 OSD 宕掉的时间长短， OSD 的对象和归置组可能落后得厉害，另外，如果挂的是一个失效域（如一个机柜），多个 OSD 会同时重启，这样恢复时间更长、更耗资源</p>
<p>为保持性能， Ceph<span style="background-color: #c0c0c0;"> 进行恢复时会限制恢复请求数、线程数、对象块尺寸</span>，这样在降级状态下也能保持良好的性能</p>
<p>对等关系建立完毕后， Ceph 开始对象恢复前等待的时间。默认0秒</p>
</td>
</tr>
<tr>
<td><strong>osd recovery max active</strong></td>
<td>每个OSD同时处理的活跃恢复请求最大数，增大此值能加速恢复，但它们会增加OSD的负担，甚至导致其无法提供服务</td>
</tr>
<tr>
<td>osd recovery max chunk</td>
<td>恢复时一次推送的数据块的最大尺寸，可以用于防止网络拥塞</td>
</tr>
<tr>
<td>osd recovery threads</td>
<td>数据恢复时的线程数，默认1</td>
</tr>
<tr>
<td><strong>osd mount options xfs</strong></td>
<td>OSD的xfs挂载选项，默认rw,noatime,inode64，建议rw,noexec,nodev,noatime,nodiratime,nobarrier</td>
</tr>
<tr>
<td colspan="2"><em>rbd客户端调优  [client]</em></td>
</tr>
<tr>
<td><strong>rbd cache</strong></td>
<td>
<p>是否启用RBD缓存，默认true</p>
<p>用于用户空间块设备实现——librbd</p>
</td>
</tr>
<tr>
<td><strong>rbd cache size</strong></td>
<td>RBD缓存大小，默认33554432，建议268435456</td>
</tr>
<tr>
<td><strong>rbd cache max dirty</strong></td>
<td>缓存为write-back时允许的最大dirty字节数，如果为0，使用write-through。默认25165824，建议25165824</td>
</tr>
<tr>
<td><strong>rbd cache max dirty age</strong></td>
<td>在被刷新到存储盘前dirty数据存在缓存的时间，默认1秒，建议5</td>
</tr>
<tr>
<td colspan="2"><em>filestore选项 <em> [osd]</em></em></td>
</tr>
<tr>
<td>filestore max inline xattr size</td>
<td>每个对象在文件系统中存储XATTR（扩展属性）的最大尺寸，不得超过文件系统限制。默认值根据底层文件系统自动设置</td>
</tr>
<tr>
<td>filestore max inline xattrs</td>
<td>每个对象在文件系统中存储XATTR（扩展属性）的最大数量</td>
</tr>
<tr>
<td><strong>filestore max sync interval</strong></td>
<td>
<p>filestore 需要周期性地静默（暂停）写入、同步文件系统 —— <span style="background-color: #c0c0c0;">创建了一个提交点，然后就能释放相应的日志条目了</span>。</p>
<p><span style="background-color: #c0c0c0;">较高的同步频率可减小执行同步的时间及保存在日志里的数据量，但是日志利用率较低</span></p>
<p>较低的频率使得后端的文件系统能优化<span style="background-color: #c0c0c0;">归并较小的数据和元数据写入</span>，因此可能使同步更有效</p>
<p>默认5秒。建议15</p>
</td>
</tr>
<tr>
<td><strong>filestore min sync interval</strong></td>
<td>从日志到数据盘最小同步间隔。默认.01秒，建议10</td>
</tr>
<tr>
<td><strong>filestore op threads</strong></td>
<td>并发文件系统操作数。默认2，建议32</td>
</tr>
<tr>
<td>filestore flusher</td>
<td>
<p>是否启用回写器（Flusher）。回写器强制使用sync file range 来写出大块数据，这样处理可能减小最终同步的代价，禁用回写器有时可能提高性能</p>
<p>默认false</p>
</td>
</tr>
<tr>
<td>filestore flusher max fds</td>
<td>回写器的最大文件描述符数量。默认512</td>
</tr>
<tr>
<td>filestore fsync flushes journal data</td>
<td>在fsync时是否也回写日志数据</td>
</tr>
<tr>
<td><strong>filestore queue max ops</strong></td>
<td>
<p>文件存储在阻止新操作加入队列之前，可以接受的最大操作数。取值示例25000</p>
</td>
</tr>
<tr>
<td><strong>filestore queue max bytes</strong></td>
<td>文件存储单个操作的最大字节数。取值示例10485760</td>
</tr>
<tr>
<td><strong>filestore queue committing max ops</strong></td>
<td>文件存储单次可以提交的最大操作数</td>
</tr>
<tr>
<td><strong>filestore queue committing max bytes</strong></td>
<td>文件存储单次可以提交的最大字节数</td>
</tr>
<tr>
<td>filestore journal parallel</td>
<td>允许并行记日志，对 btrfs 默认开</td>
</tr>
<tr>
<td>filestore journal writeahead</td>
<td>允许预写日志，对 xfs 默认开</td>
</tr>
<tr>
<td colspan="2"><em>journal选项 <em> [osd]</em></em></td>
</tr>
<tr>
<td>journal dio</td>
<td>启用日志的Direct I/O，要求journal block align=true。默认true</td>
</tr>
<tr>
<td>journal aio</td>
<td>使用libaio库进行日志的异步写，要求journal dio =true。0.61+默认true</td>
</tr>
<tr>
<td>journal block align</td>
<td>日志按块对齐。默认true</td>
</tr>
<tr>
<td><strong>journal max write bytes</strong></td>
<td>日志写操作单次最大字节数。建议1073714824</td>
</tr>
<tr>
<td><strong>journal max write entries </strong></td>
<td>日志写操作单次最大条目数。建议10000</td>
</tr>
<tr>
<td><strong>journal queue max ops </strong></td>
<td>排队等候日志写的操作最大数。建议50000</td>
</tr>
<tr>
<td><strong>journal queue max bytes</strong></td>
<td>排队等候日志写的最大字节数。建议10485760000</td>
</tr>
<tr>
<td>journal align min size</td>
<td>对于大于此尺寸的数据，进行对齐操作 </td>
</tr>
<tr>
<td>journal zero on create</td>
<td>在创建文件系统（ mkfs ）期间用 0 填充整个日志 </td>
</tr>
<tr>
<td colspan="2"><em>pool/pg/crush相关 [global]</em></td>
</tr>
<tr>
<td><strong>osd pool default size</strong></td>
<td>对象默认副本份数</td>
</tr>
<tr>
<td><strong>osd pool default min size</strong></td>
<td>降级情况下，默认允许写操作的最小可用副本份数</td>
</tr>
<tr>
<td><strong>osd pool default pg num</strong></td>
<td>归置组的默认数量</td>
</tr>
<tr>
<td><strong>osd pool default pgp num</strong></td>
<td>为归置使用的归置组数量，默认值等同于 mkpool 的 pgp_num 参数。当前 PG 和 PGP 应该相同</td>
</tr>
<tr>
<td>mon max pool pg num</td>
<td>每个存储的最大归置组数量。默认65536</td>
</tr>
<tr>
<td>mon pg create interval</td>
<td>在同一个 OSD 里创建 PG 的间隔秒数。默认30</td>
</tr>
<tr>
<td>mon pg stuck threshold</td>
<td>多长时间无响应，则认为PG卡住了</td>
</tr>
<tr>
<td>mon pg min inactive</td>
<td>如果大于此数量的PG处于inactive状态超过mon_pg_stuck_threshold，则显示集群为HEALTH_ERR。默认1</td>
</tr>
<tr>
<td>mon pg warn min per osd</td>
<td>如果每OSD平均可用PG低于此数量，则显示集群为HEALTH_WARN。默认30</td>
</tr>
<tr>
<td>mon pg warn max per osd</td>
<td>如果每OSD平均可用PG高于此数量，则显示集群为HEALTH_WARN。默认300</td>
</tr>
<tr>
<td>osd crush chooseleaf type</td>
<td>在CRUSH 规则内用于 chooseleaf 的桶类型。用序列号而不是名字，默认1</td>
</tr>
<tr>
<td>osd crush initial weight</td>
<td>
<p>新加入到CRUSH map中的OSD的权重</p>
<p><span style="background-color: #c0c0c0;">默认情况下，权重是OSD的磁盘容量，单位TB</span></p>
</td>
</tr>
<tr>
<td>osd pool default crush rule</td>
<td>
<p>创建复制型池时，使用的默认CRUSH规则。默认-1，表示使用ID最低的规则</p>
</td>
</tr>
<tr>
<td>osd pool erasure code stripe unit</td>
<td>EC池中的对象条带尺寸</td>
</tr>
<tr>
<td>osd pool default flags</td>
<td>新存储池的默认标志</td>
</tr>
<tr>
<td colspan="2"><em>ms选项</em></td>
</tr>
<tr>
<td>ms tcp nodelay</td>
<td>禁用 nagle 算法，默认true</td>
</tr>
<tr>
<td>ms initial backoff</td>
<td>出错时重连的初始等待时间</td>
</tr>
<tr>
<td>
<p>ms max backoff</p>
</td>
<td>出错重连时等待的最大时间</td>
</tr>
<tr>
<td>ms nocrc</td>
<td>禁用网络消息的 crc 校验， CPU 不足时可提升性能</td>
</tr>
<tr>
<td colspan="2"><em>mds选项</em></td>
</tr>
<tr>
<td>mds cache memory limit</td>
<td>MDS缓存最大使用多少内存</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg" style="background-color: #c0c0c0;">配置变量</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>$cluster</td>
<td>展开为存储集群的名称，在相同硬件上运行多个集群时有用</td>
</tr>
<tr>
<td>$type</td>
<td>展开为守护进程类型，例如mds, osd, or mon</td>
</tr>
<tr>
<td>$id</td>
<td>展开为守护进程或者客户端的标识符，对于osd.0其标识符为0</td>
</tr>
<tr>
<td>$host</td>
<td>展开为主机名</td>
</tr>
<tr>
<td>$name</td>
<td>展开为$type.$id</td>
</tr>
<tr>
<td>$pid</td>
<td>展开为守护进程的PID</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg" style="background-color: #c0c0c0;">配置段落<br /></span></div>
<p>配置文件是INI格式的，可以分为以下段落：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">段落</td>
<td style="text-align: center;">用途</td>
</tr>
</thead>
<tbody>
<tr>
<td>global</td>
<td>这里的配置影响 Ceph 集群里的所有守护进程</td>
</tr>
<tr>
<td>osd</td>
<td>影响存储集群里的所有 ceph-osd 进程，覆盖global相同选项</td>
</tr>
<tr>
<td>mon</td>
<td>影响集群里的所有 ceph-mon 进程，覆盖global相同选项</td>
</tr>
<tr>
<td>mds</td>
<td>影响集群里的所有 ceph-mds 进程，覆盖global相同选项</td>
</tr>
<tr>
<td>client</td>
<td>影响所有客户端（<span style="background-color: #c0c0c0;">如挂载的 Ceph 文件系统、挂载的块设备等等</span>）</td>
</tr>
</tbody>
</table>
<p>你还可以针对特定的实例配置段落：</p>
<ol>
<li>[osd.1]，针对ID为1的OSD的配置</li>
<li>[mon.HOSTNAME]，针对名称为HOSTNAME的MON的配置</li>
</ol>
<div class="blog_h2"><span class="graybg">启动选项</span></div>
<p>进程连接ceph-mon、进行身份验证、抓取集群中心配置信息时需要一些配置，这些配置必须存放在本地：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 50%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>mon_host</td>
<td>ceph-mon的主机列表</td>
</tr>
<tr>
<td>mon_dns_serv_name</td>
<td>ceph-mon的DNS名称，默认 ceph-mon</td>
</tr>
<tr>
<td>mon_data, osd_data, mds_data, mgr_data</td>
<td>守护进程在本地存放数据的路径</td>
</tr>
<tr>
<td>keyring, keyfile,key</td>
<td>连接ceph-mon进行身份验证时，使用的凭证</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">集群中心配置</span></div>
<p>ceph-mon集群管理了配置配置选项的数据库，用于供整个集群来消费。为了简化管理，<span style="background-color: #c0c0c0;">大部分的Ceph选项应该在此数据库中管理</span>。</p>
<p>集群中心配置的分段情况，和上文的配置段落一致。</p>
<div class="blog_h3"><span class="graybg">不使用集群配置</span></div>
<p>传入命令行选项<pre class="crayon-plain-tag">--no-mon-config</pre>，可以让进程不去读取集群中心配置，使用场景：</p>
<ol>
<li>希望所有配置信息在本地文件中管理</li>
<li>ceph-mon目前宕机，但是需要进行一些维护工作</li>
</ol>
<div class="blog_h3"><span class="graybg">掩码</span></div>
<p>集群中心配置的配置项，可以关联一个掩码，用于限定选项应用到哪种守护进程、哪种客户端。例如host:foo，限制foo选项仅仅应用到运行在host上的进程或客户端。</p>
<div class="blog_h3"><span class="graybg">命令</span></div>
<p>以下命令可以用于修改集群中心配置：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ceph config dump</td>
<td>Dump出整个集群中心配置</td>
</tr>
<tr>
<td>ceph config get [who]</td>
<td>获取指定的客户端/守护进程存放在集群中心的配置，例如mds.a </td>
</tr>
<tr>
<td>ceph config set [who] [opt] [val]</td>
<td>
<p>设置指定客户端/守护进程的配置项，示例：</p>
<p><pre class="crayon-plain-tag"># 启用特定OSD的调试日志
ceph config set osd.123 debug_osd 20</pre>
</td>
</tr>
<tr>
<td>ceph tell [who] config set  [opt] [val]</td>
<td>临时设置配置项，目标重启后失效</td>
</tr>
<tr>
<td>ceph config show [who]</td>
<td>显示指定客户端/守护进程的当前使用的配置信息，可能和集群中心配置不一样</td>
</tr>
<tr>
<td>ceph config assimilate-conf -i -o</td>
<td>从-i选项读取所有配置信息，注入到集群中心配置。任何无法识别、无效的配置项存放到-o </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">存储池选项</span></div>
<p>在运行时，你可以使用<pre class="crayon-plain-tag">ceph osd pool set</pre>命令来修改这些选项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>size</td>
<td>对象副本数</td>
</tr>
<tr>
<td>min_size</td>
<td>I/O 需要的最小副本数</td>
</tr>
<tr>
<td>crush_rule</td>
<td>此存储池使用的CRUSH规则</td>
</tr>
<tr>
<td>compression_algorithm</td>
<td>BlueStore使用的压缩算法，可选值lz4, snappy, zlib, zstd</td>
</tr>
<tr>
<td>compression_mode</td>
<td>BlueStore使用的压缩模式，可选值none, passive, aggressive, force</td>
</tr>
<tr>
<td>compression_min_blob_size<br />compression_max_blob_size</td>
<td>BlueStore启用压缩的阈值</td>
</tr>
<tr>
<td>hashpspool</td>
<td>设置或者取消HASHPSPOOL标记。1设置0取消</td>
</tr>
<tr>
<td>nodelete</td>
<td>设置或取消NODELETE标记。1设置0取消</td>
</tr>
<tr>
<td>nopgchange</td>
<td>设置或取消NOPGCHANGE标记</td>
</tr>
<tr>
<td>nosizechange</td>
<td>设置或取消NOSIZECHANGE标记</td>
</tr>
<tr>
<td>write_fadvise_dontneed</td>
<td>设置或取消WRITE_FADVISE_DONTNEED标记</td>
</tr>
<tr>
<td>noscrub</td>
<td>设置或取消NOSCRUB标记</td>
</tr>
<tr>
<td>nodeep-scrub</td>
<td>设置或取消NODEEP_SCRUB标记</td>
</tr>
<tr>
<td>hit_set_type</td>
<td>启用缓存存储池的命中集跟踪，设置命中集类型，生产环境仅仅支持bloom</td>
</tr>
<tr>
<td>hit_set_count</td>
<td>为缓存存储池保留的命中集数量。此值越高， OSD消耗的内存越多</td>
</tr>
<tr>
<td>hit_set_fpp</td>
<td>bloom 命中集类型的误检率（false positive probability）</td>
</tr>
<tr>
<td>cache_target_dirty_ratio</td>
<td><span style="background-color: #c0c0c0;">缓存存储池</span>包含的修改（脏）对象达到多少比例时就把它们<span style="background-color: #c0c0c0;">回写到后端的存储池</span></td>
</tr>
<tr>
<td>cache_target_dirty_high_ratio</td>
<td>缓存存储池内包含的已修改（脏）对象达到什么比例时，缓存层代理就会更快地把脏对象刷回到后端存储池</td>
</tr>
<tr>
<td>cache_target_full_ratio</td>
<td>缓存存储池包含的干净对象达到多少比例时，缓存代理就把它们清除出缓存存储池</td>
</tr>
<tr>
<td>target_max_bytes</td>
<td>回写（Flushing）或清除（Evicting）对象的阈值，按字节数</td>
</tr>
<tr>
<td>target_max_objects</td>
<td>回写（Flushing）或清除（Evicting）对象的阈值，按对象数</td>
</tr>
<tr>
<td>scrub_min_interval</td>
<td>在负载低时，洗刷存储池的最大间隔秒数</td>
</tr>
<tr>
<td>scrub_max_interval</td>
<td>不管集群负载如何，都要洗刷存储池的最大间隔秒数</td>
</tr>
<tr>
<td>deep_scrub_interval</td>
<td>深度洗刷存储池的间隔秒数</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">运维</span></div>
<div class="blog_h2"><span class="graybg">操控集群</span></div>
<div class="blog_h3"><span class="graybg">UPSTART</span></div>
<p>Ubuntu系统下，基于ceph-deploy部署集群后，可以用这种方法来操控集群。</p>
<p>列出节点上所有Ceph进程：</p>
<pre class="crayon-plain-tag">initctl list | grep ceph</pre>
<p>启动节点上所有Ceph进程：</p>
<pre class="crayon-plain-tag">start ceph-all</pre>
<p>启动节点上特定类型的Ceph进程： </p>
<pre class="crayon-plain-tag">sudo start ceph-osd-all
sudo start ceph-mon-all
sudo start ceph-mds-all</pre>
<p>启动特定类型的Ceph进程的某个实例：</p>
<pre class="crayon-plain-tag">sudo start ceph-osd id={id}
sudo start ceph-mon id={hostname}
sudo start ceph-mds id={hostname}</pre>
<p>停止特定类型的Ceph进程的某个实例：</p>
<pre class="crayon-plain-tag">sudo stop ceph-osd id={id}
sudo stop ceph-mon id={hostname}
sudo stop ceph-mds id={hostname}</pre>
<div class="blog_h3"><span class="graybg">SYSVINIT</span></div>
<p>在 CentOS 、 Redhat 、 Fedora 和 SLES 发行版上可以通过传统的 sysvinit 运行 Ceph ， Debian/Ubuntu 的较老的版本也可以用此方法。</p>
<p>命令格式：</p>
<pre class="crayon-plain-tag"># 启动、重启或停止
sudo /etc/init.d/ceph [options] [start|restart|stop] [daemonType|daemonID]

# 示例：

# -a 表示在所有节点执行
sudo /etc/init.d/ceph -a start
sudo /etc/init.d/ceph -a stop
sudo /etc/init.d/ceph start osd
sudo /etc/init.d/ceph -a stop osd
sudo /etc/init.d/ceph start osd.0
sudo /etc/init.d/ceph stop osd.0 </pre>
<div class="blog_h2"><span class="graybg">监控集群</span></div>
<div class="blog_h3"><span class="graybg">检查集群状态</span></div>
<p>执行<pre class="crayon-plain-tag">ceph status</pre>或则<pre class="crayon-plain-tag">ceph -s</pre>可以查看集群的状态：</p>
<ol>
<li>active+clean：说明集群健康运行</li>
<li>undersized+degraded：如果有OSD节点宕机，可能进入此状态。<span style="background-color: #c0c0c0;">降级后还是可以正常读写数据</span></li>
<li>undersized+degraded+peered：如果超过min size要求的OSD宕机，则不可读写，显示为此状态。min size默认2，副本份数默认3。执行下面的命令可以修改min size：<br />
<pre class="crayon-plain-tag">ceph osd pool set rbd min_size 1</pre></p>
<p>peered相当于已经配对（PG - OSDs），但是正在等待OSD上线</p>
</li>
<li>
<p>remapped+backfilling：默认情况下，OSD宕机5分钟后会被标记为out状态，Ceph认为它已经不属于集群了。Ceph会按照一定的规则，将已经out的OSD上的PG重映射到其它OSD，并且从现存的副本来回填（Backfilling）数据到新OSD</p>
</li>
</ol>
<p>执行<pre class="crayon-plain-tag">ceph health</pre>可以查看简短的健康状态。</p>
<p>执行<pre class="crayon-plain-tag">ceph -w</pre>可以持续的监控发生在集群中的各种事件。</p>
<div class="blog_h3"><span class="graybg">检查存储用量</span></div>
<p>执行命令<pre class="crayon-plain-tag">ceph df</pre>可以查看<span style="background-color: #c0c0c0;">集群的数据用量及其在存储池内的分布情况</span>：</p>
<pre class="crayon-plain-tag">GLOBAL:
                       # 已用存储空间总量（包括所有副本）
    SIZE     AVAIL     RAW USED     %RAW USED 
    323G      318G        4966M          1.50 

# 这一段显示的数值，不包含副本、克隆、快照的用量
POOLS:
                             # 大概使用率              # 大概对象数
    NAME     ID     USED      %USED     MAX AVAIL     OBJECTS 
    rbd      1      3539M      1.14          300G        1018</pre>
<div class="blog_h3"><span class="graybg">检查MON状态</span></div>
<pre class="crayon-plain-tag"># 基本信息
ceph mon stat
# 详细信息
ceph mon dump
# 法定人数状态、monmap内容
ceph quorum_status -f json-pretty</pre>
<div class="blog_h3"><span class="graybg">检查MDS状态 </span></div>
<pre class="crayon-plain-tag">ceph mds stat
ceph mds dump</pre>
<div class="blog_h3"><span class="graybg">检查OSD状态</span></div>
<p>通过PG这个中间层，Ceph确保了数据不会被绑死在某个特定的OSD。要追踪错误根源，你需要检查归置组、以及底层的OSD。</p>
<p>执行下面的命令，获取最简短的OSD状态：</p>
<pre class="crayon-plain-tag">ceph osd stat
# 输出
12 osds: 12 up, 12 in</pre>
<p>执行<pre class="crayon-plain-tag">ceph osd dump</pre>则可以获得详细信息，包括在CRUSH map中的权重、UUID、是in还是out：</p>
<pre class="crayon-plain-tag">osd.0 up   out weight 0 up_from 70 up_thru 172 down_at 65 last_clean_interval [51,60) 10.5.39.13:6800/48 10.5.39.13:6801/48 10.5.39.13:6802/48 10.5.39.13:6803/48 exists,up 354a6547-3437-46d6-a928-f5633eb7f059
osd.1 up   in  weight 1 up_from 74 up_thru 327 down_at 63 last_clean_interval [55,60) 10.5.39.42:6800/48 10.5.39.42:6801/48 10.5.39.42:6802/48 10.5.39.42:6803/48 exists,up 0fb4bb77-7c84-45ac-919a-2cc350fc62b9</pre>
<p>执行<pre class="crayon-plain-tag">ceph osd tree</pre>可以在OSD树中打印各OSD的位置、状态、权重。如果OSD的in数量大于up数量，可以通过此命令快速定位：</p>
<pre class="crayon-plain-tag"># 仅仅包含out的OSD
ceph osd tree out
# ID CLASS WEIGHT  TYPE NAME               STATUS REWEIGHT PRI-AFF 
# -1       4.89999 root default                                    
# -2             0     host k8s-10-5-38-25                         
#  2   hdd       0         osd.2              DNE        0</pre>
<div class="blog_h3"><span class="graybg">检查PG状态</span></div>
<p>执行命令<pre class="crayon-plain-tag">ceph pg stat</pre>可以查看全局性的PG统计信息。</p>
<p>可以获取PG列表：</p>
<pre class="crayon-plain-tag"># 输出的第一列为PG ID
ceph pg dump
# 导出为JSON
ceph pg dump -o {filename} --format=json</pre>
<p>执行下面的命令可以查看PG到OSD的映射关系：</p>
<pre class="crayon-plain-tag"># PG ID 格式为 存储池号.归置组ID，归置组ID为一个十六进制数字
ceph pg map 1.13d
# 输出
osdmap e790 pg 1.13d (1.13d) -&gt; up [4,6,5] acting [4,6,5]</pre>
<p>执行<pre class="crayon-plain-tag">ceph status</pre>也可以看到PG的统计性信息。</p>
<p>执行<pre class="crayon-plain-tag">ceph pg 1.13d query</pre>可以查看某个PG的非常细节的信息。</p>
<div class="blog_h2"><span class="graybg">操控MON</span></div>
<div class="blog_h3"><span class="graybg">增加</span></div>
<pre class="crayon-plain-tag">MONID=Neon
MONADDR=10.0.3.1:6789

# 创建目录
mkdir /var/lib/ceph/mon/ceph-$MONID

# 获取密钥和monmap
ceph auth get mon. -o /tmp/keyring
ceph mon getmap -o /tmp/monmap

# 初始化Mon
sudo ceph-mon -i $MONID --mkfs --monmap /tmp/monmap --keyring /tmp/keyring

# 启动Mon
ceph-mon -i $MONID --public-addr $MONADDR</pre>
<div class="blog_h3"><span class="graybg">删除</span></div>
<pre class="crayon-plain-tag">ceph mon rm Xenon </pre>
<div class="blog_h3"><span class="graybg">monmap</span></div>
<p>导出monmap：</p>
<pre class="crayon-plain-tag">ceph mon getmap -o monmap</pre>
<p>打印monmap的内容：</p>
<pre class="crayon-plain-tag">monmaptool --print monmap</pre>
<p>从monmap中删除一个MON：</p>
<pre class="crayon-plain-tag">monmaptool  monmap --rm xenon</pre>
<p>添加一个MON到monmap中：</p>
<pre class="crayon-plain-tag">monmaptool  monmap --add Xenon 10.0.5.1:6789</pre>
<p>导入monmap到MON节点：</p>
<pre class="crayon-plain-tag">ceph-mon -i Xenon --inject-monmap monmap </pre>
<div class="blog_h2"><span class="graybg">操控OSD</span></div>
<div class="blog_h3"><span class="graybg">增加</span></div>
<pre class="crayon-plain-tag"># 空间使用率达到 near full 比率后， OSD 失败可能导致集群空间占满。因此，你需要提前扩容
# 执行下面的命令创建一个新的OSD，其OSD号会输出到控制台：

# uuid、id可选，如果不指定则自动生成。不能和现有OSD的uuid、id重复。不建议手工指定id
ceph osd create [{uuid} [{id}]]


# 如果希望OSD使用独立磁盘或者分区，可以先创建好文件系统，再挂载到适当位置
sudo mkfs -t {fstype} /dev/{drive}
# 示例
mkfs -t xfs -f /dev/sda3
# 挂载点
mkdir /var/lib/ceph/osd/ceph-{osd-num}
# 挂载
mount /dev/sda3 /var/lib/ceph/osd/ceph-14

# 初始化OSD数据目录：
ceph-osd -i {osd-num} --mkfs --mkkey

# 注册OSD认证密钥：
ceph auth add osd.{osd-num} osd 'allow *' mon 'allow rwx' -i /var/lib/ceph/osd/${cluster-name}-{osd-num}/keyring
# 示例
ceph auth add osd.14 osd 'allow *' mon 'allow rwx' -i /var/lib/ceph/osd/ceph-14/keyring

# 你需要把OSD加入到CRUSH map，这样数据才会分配到此OSD上： 
# 把OSD加入到CRUSH树的适当位置（桶）
# 如果指定了不止一个桶，则将OSD加入到最靠近叶子节点的桶中，并把此桶移动到你指定的其它桶中
# 如果你指定了root桶，则此OSD直接挂在root下，则是不建议的，CRUSH规则期望OSD位于主机这种桶类型的下级节点
ceph osd crush add {id-or-name} {weight}  [{bucket-type}={bucket-name} ...]
# 示例
ceph osd crush add 14 0.11589
# 如果设置osd_crush_update_on_start=true，则可以OSD启动后自动加入到CRUSH树并更新权重
# 警告，如果上述参数设置为false，且你没有将osd添加到适当位置，则osd可能无法承载PG</pre>
<div class="blog_h3"><span class="graybg">查询</span></div>
<pre class="crayon-plain-tag">ceph osd find 14
{
    "osd": 14,
    "ip": "10.0.1.1:6804/3146",
    "crush_location": {
        "host": "Carbon",
        "root": "default"
    }
} </pre>
<div class="blog_h3"><span class="graybg">启动</span></div>
<p>一旦启动了 OSD ，其状态就变成了 up+in ，此时可以通过ceph -w来观察数据迁移。归置组状态会变为active, some degraded objects，最终变回active+clean</p>
<pre class="crayon-plain-tag"># Debian/Ubuntu 上用 Upstart：
start ceph-osd id={osd-num}
# CentOS/RHEL 上用 sysvinit：
/etc/init.d/ceph start osd.{osd-num}
# 基于systemd的系统
systemctl start ceph-osd@14.service </pre>
<div class="blog_h3"><span class="graybg">删除</span></div>
<p>删除OSD之前，应该评估集群容量，保证操作之后，集群不会到达 near full 比率</p>
<pre class="crayon-plain-tag"># 首先从CRUSH map中移除
ceph osd crush remove {name}
# 删除其认证密钥
ceph auth del osd.{osd-num}
# 删除OSD
ceph osd rm {osd-num}</pre>
<div class="blog_h3"><span class="graybg">标记为宕机</span></div>
<pre class="crayon-plain-tag">ceph osd down {osd-num}</pre>
<div class="blog_h3"><span class="graybg">标记为踢出</span></div>
<p>踢出OSD后，Ceph会进行数据迁移，达到再平衡。归置组状态会变为active, some degraded objects，最终变回active+clean。</p>
<pre class="crayon-plain-tag">ceph osd out {osd-num}</pre>
<p>对于某些小型测试集群，踢出一个OSD即导致CRUSH进入临界状态，某些归置组一直卡在active+remapped状态。如果遇到这种情况，你可以：</p>
<pre class="crayon-plain-tag"># 把被踢出的集群重新加进来
ceph osd in {osd-num}
# 将其权重标记为0，而非踢出
ceph osd crush reweight osd.{osd-num} 0 </pre>
<p>等待数据迁移完毕后，再将OSD踢出。 </p>
<div class="blog_h3"><span class="graybg">标记为进入</span></div>
<p>你可能需要更新CRUSH map才能让新进入的OSD接受数据：</p>
<pre class="crayon-plain-tag">ceph osd in {osd-num}</pre>
<div class="blog_h3"><span class="graybg">标记为丢失</span></div>
<p>标记OSD为lost，可能导致数据丢失，谨慎：</p>
<pre class="crayon-plain-tag">ceph osd lost {id} [--yes-i-really-mean-it]</pre>
<div class="blog_h3"><span class="graybg">设置权重</span></div>
<pre class="crayon-plain-tag"># 权重默认是以TB为单位
ceph osd reweight {osd-num} {weight} </pre>
<div class="blog_h3"><span class="graybg">清理OSD</span></div>
<pre class="crayon-plain-tag">ceph osd scrub {osd-num}
# 清理所有
ceph osd scrub all</pre>
<div class="blog_h3"><span class="graybg">深度清理OSD</span></div>
<pre class="crayon-plain-tag">ceph osd deep-scrub all </pre>
<div class="blog_h3"><span class="graybg">修复OSD</span></div>
<pre class="crayon-plain-tag">ceph osd repair N</pre>
<div class="blog_h3"><span class="graybg">测试OSD性能 </span></div>
<pre class="crayon-plain-tag">ceph tell osd.N bench [TOTAL_DATA_BYTES] [BYTES_PER_WRITE]</pre>
<div class="blog_h3"><span class="graybg">空间不足处理</span></div>
<p><span style="background-color: #c0c0c0;">Ceph不允许向满的 OSD 写入数据，以免丢失数据</span>。在运营着的集群中，你应该能收到集群空间将满的警告。mon osd full ratio 默认为 0.95 ，也就是说<span style="background-color: #c0c0c0;">达到 95% 时它将阻止客户端写入数据</span>； mon osd backfillfull ratio 默认为 0.90 ，也就是说<span style="background-color: #c0c0c0;">达到容量的 90% 时它会阻塞，防止回填启动</span>； OSD 将满比率默认为 0.85 ，也就是说<span style="background-color: #c0c0c0;">达到容量的 85% 时它会产生健康警告</span>。</p>
<p>使用下面的命令临时修改设置，否则你可能没有机会清理不需要的RBD以腾出空间：</p>
<pre class="crayon-plain-tag">ceph osd set-nearfull-ratio 0.95
ceph osd set-full-ratio 0.99
ceph osd set-backfillfull-ratio 0.99</pre>
<div class="blog_h2"><span class="graybg">操控MDS</span></div>
<div class="blog_h3"><span class="graybg">增加</span></div>
<p>首先，在/var/lib/ceph/mds/mds.N创建一个数据挂载点。N是MDS的ID，通常就是主机名。</p>
<p>然后，修改Ceph配置，添加一个mds段。修改完毕后进行配置分发：</p>
<pre class="crayon-plain-tag">[mds.N]
host = {hostname}</pre>
<p> 如果启用了CephX，需要创建认证密钥：</p>
<pre class="crayon-plain-tag">sudo ceph auth get-or-create mds.N mon 'profile mds' mgr 'profile mds' mds 'allow *' osd 'allow *' &gt; \
    /var/lib/ceph/mds/ceph-N/keyring</pre>
<div class="blog_h3"><span class="graybg">移除</span></div>
<p>执行下面的命令将目标mds标记为宕机：</p>
<pre class="crayon-plain-tag">ceph mds fail &lt;mds name&gt;</pre>
<p>移除MDS的/var/lib/ceph/mds/ceph-NAME下对应目录，然后，删除/etc/systemd/system/ceph-mds.target.wants/下的对应项目：</p>
<pre class="crayon-plain-tag">systemctl stop ceph-mds@Neon.service
systemctl disable ceph-mds@Neon.service
rm -rf /var/lib/ceph/mds/ceph-Neon </pre>
<p>如果服务是通过/etc/init.d/ceph加载的，则：</p>
<pre class="crayon-plain-tag">service ceph stop
update-rc.d ceph disable</pre>
<div class="blog_h3"><span class="graybg">状态</span></div>
<p>查看守护进程的简短状态：</p>
<pre class="crayon-plain-tag">ceph mds stat</pre>
<div class="blog_h3"><span class="graybg">启动</span></div>
<pre class="crayon-plain-tag">service ceph start mds.NAME</pre>
<div class="blog_h3"><span class="graybg">引用守护进程</span></div>
<p>你可以使用多种方式来引用一个MDS守护进程：</p>
<pre class="crayon-plain-tag">ceph mds fail 5446     # 基于GID
ceph mds fail myhost   # 基于名称
ceph mds fail 3:0      # 基于FSCID:rank
ceph mds fail myfs:0   # 基于文件系统名称:rank</pre>
<div class="blog_h3"><span class="graybg">管理故障转移</span></div>
<p>和MDS进程的Standby行为相关的配置项包括：</p>
<pre class="crayon-plain-tag"># 如果设置为true则standby会持续的从Rank中读取元数据日志，从而维持一个有效的元数据缓存，这可以加速Failover
mds_standby_replay = true
# 仅仅作为具有指定名称的MDS的Standby
mds_standby_for_name = Carbon
# 仅仅作为指定Rank的Standby
mds_standby_for_rank
# 仅仅作为指定文件系统的Standby
mds_standby_for_fscid</pre>
<p>如果不进行任何配置，没有持有Rank的那些MDS进程，可以作为任何Rank的Standby。</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag"># a、b两个MDS互备，负责Rank 0
[mds.a]
mds standby replay = true
mds standby for rank = 0

[mds.b]
mds standby replay = true
mds standby for rank = 0</pre>
<div class="blog_h3"><span class="graybg">修改配置</span></div>
<pre class="crayon-plain-tag">ceph tell mds.{mds-id} config set {setting} {value}
# 示例
ceph tell mds.0 config set debug_ms 1</pre>
<div class="blog_h3"><span class="graybg">启用诊断信息</span></div>
<pre class="crayon-plain-tag">ceph mds stat</pre>
<div class="blog_h3"><span class="graybg">手工故障转移</span></div>
<p>标记当前活动MDS为失败，触发故障转移：</p>
<pre class="crayon-plain-tag">ceph mds fail 0 </pre>
<div class="blog_h2"><span class="graybg">操控CephFS</span></div>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>要创建一个文件系统，你<span style="background-color: #c0c0c0;">至少需要两个存储池，一个存放数据，另外一个存放元数据</span>。注意：</p>
<ol>
<li>元数据池的副本份数要设置的高，因为任何元数据的丢失都会导致整个文件系统不可用</li>
<li>元数据池应该使用高速存储，例如SSD，因为这对客户端操作的延迟有直接影响</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag"># ceph fs new &lt;fs_name&gt; &lt;metadata&gt; &lt;data&gt;
# 示例，可以使用现有的存储池
ceph fs new cephfs rbd-ssd rbd-hdd

# Error EINVAL: pool 'rbd-ssd' already contains some objects. Use an empty pool instead.
# 出现上述错误，可以：
ceph fs new cephfs rbd-ssd rbd-hdd --force</pre>
<p>创建了文件系统之后，在Luminous版本中，集群状态中显示：</p>
<pre class="crayon-plain-tag">mds: cephfs-1/1/1 up  {0=Carbon=up:active}</pre>
<div class="blog_h3"><span class="graybg">列出</span></div>
<pre class="crayon-plain-tag">ceph fs ls</pre>
<div class="blog_h3"><span class="graybg">状态</span></div>
<p>查看CephFS的详细状态，包括MDS列表、Rank列表等：</p>
<pre class="crayon-plain-tag">ceph fs status</pre>
<div class="blog_h3"><span class="graybg">删除</span></div>
<pre class="crayon-plain-tag">ceph fs rm &lt;filesystem name&gt; [--yes-i-really-mean-it]</pre>
<div class="blog_h3"><span class="graybg">关闭</span></div>
<pre class="crayon-plain-tag">mds set &lt;fs_name&gt; down true</pre>
<div class="blog_h3"><span class="graybg">查看选项</span></div>
<p>要获取某个文件系统的信息，可以：</p>
<pre class="crayon-plain-tag">ceph fs get cephfs</pre>
<div class="blog_h3"><span class="graybg">设置选项</span></div>
<pre class="crayon-plain-tag">fs set &lt;filesystem name&gt; &lt;var&gt; &lt;val&gt;

# 示例
# 设置单个文件的大小，默认1TB
fs set cephfs max_file_size 1099511627776</pre>
<div class="blog_h3"><span class="graybg">增减数据池</span></div>
<pre class="crayon-plain-tag">fs add_data_pool &lt;filesystem name&gt; &lt;pool name/id&gt;
fs rm_data_pool &lt;filesystem name&gt; &lt;pool name/id&gt;</pre>
<div class="blog_h3"><span class="graybg">设为默认 </span></div>
<p>如果集群中有多个文件系统，而客户端在挂载时没有明确指定使用哪个，则使用默认文件系统：</p>
<pre class="crayon-plain-tag">ceph fs set-default cephfs</pre>
<div class="blog_h3"><span class="graybg">使用EC池 </span></div>
<p>EC池可以作为Ceph的数据池，但是需要启用overwirte：</p>
<pre class="crayon-plain-tag">ceph osd pool set my_ec_pool allow_ec_overwrites true</pre>
<p>注意：EC池不能用来存储元数据。</p>
<div class="blog_h3"><span class="graybg">配额</span></div>
<p>CephFS支持对<span style="background-color: #c0c0c0;">任何一个子目录进行配额</span>。但是，需要注意以下限制：</p>
<ol>
<li>需要客户端协作，因此被篡改过的客户端可以突破配额</li>
<li>配额不是非常精确的</li>
<li>内核客户端，仅仅在4.17+才支持配额。用户空间客户端fuse、libcephfs都支持配额</li>
</ol>
<p>设置配额（设置为0则移除配额）：</p>
<pre class="crayon-plain-tag">setfattr -n ceph.quota.max_bytes -v 100000000 /some/dir     # 按字节数
setfattr -n ceph.quota.max_files -v 10000 /some/dir         # 按文件数</pre>
<p>查看配额：</p>
<pre class="crayon-plain-tag">getfattr -n ceph.quota.max_bytes /some/dir
getfattr -n ceph.quota.max_files /some/dir </pre>
<div class="blog_h2"><span class="graybg">挂载CephFS</span></div>
<div class="blog_h3"><span class="graybg">内核驱动</span></div>
<p>你可以直接使用Linux内核提供的驱动来挂载CephFS：</p>
<pre class="crayon-plain-tag">mkdir /mnt/cephfs
mount -t ceph 10.0.1.1:6789:/ /mnt/cephfs</pre>
<p>如何启用了CephX，需要指定访问密钥，否则会报22错误：</p>
<pre class="crayon-plain-tag">mount -t ceph 10.0.1.1:6789:/ /mnt/cephfs -o name=admin,secret=AQDRNBZbCp3WMBAAynSCCFPtILwHeI3RLDADKA==
# 或者指定包含密钥的文件
mount -t ceph 10.0.1.1:6789:/ /mnt/cephfs -o name=admin,secretfile=/etc/ceph/admin.secret</pre>
<p>如果报can't read superblock，说明客户端内核不支持。</p>
<p>要实现自动挂载，你需要修改fstab：</p>
<pre class="crayon-plain-tag">{ipaddress}:{port}:/ {mountpoint} {fs-type} [name=username,secret=key|secretfile=file],[{mount.options}]

# 示例
10.0.1.1:6789:/ /mnt/cephfs  ceph name=admin,secretfile=/etc/ceph/cephfs.key,noatime,_netdev    0       2</pre>
<div class="blog_h3"><span class="graybg">FUSE</span></div>
<p>要在用户空间挂载CephFS，你需要：</p>
<ol>
<li>将Ceph配置文件拷贝到客户端，命名为/etc/ceph/ceph.conf</li>
<li>将Keyring拷贝到客户端，命名为/etc/ceph/ceph.keyring：<br />
<pre class="crayon-plain-tag">sudo scp -i ~/Documents/puTTY/gmem.key root@xenon.gmem.cc:/etc/ceph/ceph.client.admin.keyring /etc/ceph/ceph.keyring </pre>
</li>
<li>执行挂载：<br />
<pre class="crayon-plain-tag">sudo ceph-fuse -m 10.0.1.1:6789 /mnt/cephfs
# ceph-fuse[847]: starting ceph client                                                                                                                                                     
# 2018-06-07 19:18:25.503086 7fa5c44e1000 -1 init, newargv = 0x7fa5cd643b40 newargc=9                                                                                                      
# ceph-fuse[847]: starting fuse</pre>
</li>
</ol>
<p>如果有多个CephFS，你可以为ceph-fuse指定命令行选项--client_mds_namespace，或者在客户端的ceph.conf中添加client_mds_namespace配置。</p>
<p>要实现自动挂载，你需要修改fstab：</p>
<pre class="crayon-plain-tag">none    /mnt/ceph  fuse.ceph ceph.id={user-ID}[,ceph.conf={path/to/conf.conf}],_netdev,defaults  0 0

# 示例
none    /mnt/ceph  fuse.ceph ceph.id=admin,_netdev,defaults  0 0
none    /mnt/ceph  fuse.ceph ceph.id=admin,ceph.conf=/etc/ceph/ceph.conf,_netdev,defaults  0 0 </pre>
<div class="blog_h2"><span class="graybg">操控存储池</span></div>
<div class="blog_h3"><span class="graybg">设置默认参数</span></div>
<pre class="crayon-plain-tag"># 设置新建存储池时使用的默认参数
osd pool default pg num = 128
osd pool default pgp num = 128</pre>
<div class="blog_h3"><span class="graybg">运行时修改参数</span></div>
<pre class="crayon-plain-tag">ceph osd pool set {pool-name} option-name num
# 示例
ceph osd pool set  .rgw.root pg_num 128
ceph osd pool set  .rgw.root pgp_num 128 </pre>
<div class="blog_h3"><span class="graybg">创建存储池</span></div>
<pre class="crayon-plain-tag"># 创建存储池
# crush-ruleset-name：使用的默认CRUSH规则集名称
# 复制型的默认规则集由选项osd pool default crush replicated ruleset控制
# 
ceph osd pool create {pool-name} {pg-num} [{pgp-num}] [replicated] \
        [crush-ruleset-name] [expected-num-objects]
ceph osd pool create {pool-name} {pg-num}  {pgp-num}   erasure \
        [erasure-code-profile] [crush-ruleset-name] [expected_num_objects]


# 示例
ceph osd pool create rbd-ssd 384 replicated replicated_rule_ssd</pre>
<div class="blog_h3"><span class="graybg">初始化存储池</span></div>
<p>创建存储池之后，在管理节点上，使用rbd工具来初始化池：</p>
<pre class="crayon-plain-tag">rbd pool init &lt;pool-name&gt;</pre>
<div class="blog_h3"><span class="graybg">读写存储池配置</span></div>
<pre class="crayon-plain-tag"># 修改存储池配置
ceph osd pool set {pool-name} {key} {value}
# 读取存储池配置
ceph osd pool get {pool-name} {key}</pre>
<div class="blog_h3"><span class="graybg">列出存储池</span></div>
<pre class="crayon-plain-tag">ceph osd lspools
# 输出
# 1 rbd,3 rbd-ssd,4 rbd-hdd,</pre>
<div class="blog_h3"><span class="graybg">列出池中对象</span></div>
<pre class="crayon-plain-tag"># 列出存储池中的对象
rados -p rbd ls</pre>
<div class="blog_h3"><span class="graybg">显示池用量</span></div>
<pre class="crayon-plain-tag"># 显示所有存储池的使用情况
rados df

# 或者
ceph df

# 更多细节
ceph df detail
# USED       %USED       MAX AVAIL     OBJECTS     DIRTY     READ      WRITE      RAW USED 
# 用量       用量百分比                对象数量              读速度    写数量     用量x副本份数</pre>
<div class="blog_h3"><span class="graybg">存储池配额</span></div>
<pre class="crayon-plain-tag"># 设置最大对象数量、最大字节数
ceph osd pool set-quota {pool-name} [max_objects {obj-count}] [max_bytes {bytes}]
# 示例：
ceph osd pool set-quota data max_objects 10000</pre>
<p>要取消配额，设置为0即可。 </p>
<div class="blog_h3"><span class="graybg">存储池快照</span></div>
<pre class="crayon-plain-tag"># 制作存储池快照
ceph osd pool mksnap {pool-name} {snap-name}
# 删除存储池快照
ceph osd pool rmsnap {pool-name} {snap-name}</pre>
<div class="blog_h3"><span class="graybg">删除存储池</span></div>
<pre class="crayon-plain-tag"># 删除存储池
ceph osd pool delete {pool-name} [{pool-name} --yes-i-really-really-mean-it]

# 示例
ceph osd pool rm rbd-ssd rbd-ssd --yes-i-really-really-mean-it
ceph osd pool rm rbd-hdd rbd-hdd --yes-i-really-really-mean-it</pre>
<div class="blog_h3"><span class="graybg">清空存储池</span></div>
<pre class="crayon-plain-tag"># 列出池中对象，逐个删除
for i in `rados -p rbd-ssd ls`; do echo $i; rados -p rbd-ssd rm $i; done</pre>
<div class="blog_h2"><span class="graybg">操控镜像</span></div>
<p>镜像就是块设备，所谓块是一系列连续的字节序列（例如512KB）。基于块的存储接口，是磁盘、CD、软盘、甚至磁带都使用的，是存储对象最广泛使用的方式。</p>
<p>Ceph的块设备具有以下特点：thin-provisioned（精简配备）、可改变大小、跨越多OSD存储。</p>
<div class="blog_h3"><span class="graybg">列出镜像</span></div>
<pre class="crayon-plain-tag">rbd ls {poolname}</pre>
<p>如果不指定池名称，则列出默认池中的镜像。</p>
<p>下面的命令可以列出池中延迟删除的镜像：</p>
<pre class="crayon-plain-tag">rbd trash ls {poolname} </pre>
<div class="blog_h3"><span class="graybg">查看镜像磁盘占用</span></div>
<pre class="crayon-plain-tag">rbd du --pool rbd-ssd</pre>
<p>注意：rbd info输出的是thin provisioning的大小，不是实际磁盘空间占用。</p>
<p>除了上面的命令，还可以：</p>
<pre class="crayon-plain-tag">rbd diff k8s/kubernetes-dynamic-pvc | awk '{ SUM += $2 } END { print SUM/1024/1024 " MB" }' </pre>
<div class="blog_h3"><span class="graybg">查看镜像信息</span></div>
<pre class="crayon-plain-tag">rbd info {pool-name}/{image-name}
rbd info {image-name}

# 输出示例：
rbd image 'kubernetes-dynamic-pvc-0783b011-6a04-11e8-a266-3e299ab03dc6':
        # 总大小（thin-provisioning的大小，不是实际占用磁盘大小），分布在多少个对象中
        size 2048 MB in 512 objects
        order 22 (4096 kB objects)
        block_name_prefix: rbd_data.7655b643c9869
        format: 2
        features: layering
        flags:
        create_timestamp: Thu Jun  7 11:36:58 2018</pre>
<div class="blog_h3"><span class="graybg">查看镜像状态</span></div>
<p>可以看到什么客户端在使用（watch）镜像：</p>
<pre class="crayon-plain-tag">rbd status k8s/kubernetes-dynamic-pvc-ca081cd3-01a0-11eb-99eb-ce0c4cdcd662
# Watchers:
#         watcher=192.168.106.18:0/756489925 client.254697953 cookie=18446462598732840981 </pre>
<div class="blog_h3"><span class="graybg">创建镜像</span></div>
<pre class="crayon-plain-tag">rbd create --size {megabytes} {pool-name}/{image-name}
# 示例
# 创建大小为1G的镜像
rbd create test --size 1G</pre>
<p>如果不指定存储池，则在默认池中创建镜像。</p>
<div class="blog_h3"><span class="graybg">修改镜像大小</span></div>
<pre class="crayon-plain-tag"># 修改镜像大小
rbd --image test resize --size 2G
# 不但可以扩大，还可以缩小
rbd --image test resize --size 1G --allow-shrink</pre>
<div class="blog_h3"><span class="graybg">映射为块设备</span></div>
<pre class="crayon-plain-tag"># 将镜像映射为本地块设备，可以进行格式化、挂载
rbd map test
# 格式化
mkfs.xfs -f /dev/rbd0
# 挂载
mount /dev/rbd0 /test

# 显示映射到本地块设备的镜像
rbd showmapped

# 卸载
umount /dev/rbd0
# 解除映射
rbd unmap /dev/rbd0</pre>
<div class="blog_h3"><span class="graybg">删除镜像</span></div>
<pre class="crayon-plain-tag">rbd rm {pool-name}/{image-name}

rbd --image test rm</pre>
<div class="blog_h3"><span class="graybg">延迟删除</span></div>
<pre class="crayon-plain-tag"># 放入回收站
rbd trash mv {pool-name}/{image-name}
# 彻底删除
rbd trash rm {pool-name}/{image-id}
# 还原
rbd trash restore {image-id}</pre>
<div class="blog_h3"><span class="graybg">快照管理</span></div>
<pre class="crayon-plain-tag"># 创建快照
rbd snap create --image test --snap test_snap
# 列出镜像的所有快照
rbd snap ls --image test

# 回滚到指定快照
rbd snap rollback --image test --snap test_snap
# 另一种写法
rbd snap rollback rbd/test@test_snap

# 删除快照，注意删除是异步进行的，空间不会立刻释放
rbd snap rm --image test --snap test_snap
rbd snap purge --image test

# 保护快照
rbd snap protect --image test --snap test_snap
# 取消保护
rbd snap unprotect --image test --snap test_snap

# 清除指定镜像的所有快照
rbd snap purge {pool-name}/{image-name}</pre>
<div class="blog_h3"><span class="graybg">镜像克隆</span></div>
<pre class="crayon-plain-tag"># 克隆镜像，注意只有镜像格式2才支持克隆
# 从快照创建克隆
rbd clone --image test --snap test_snap test_clone
# 列出快照的所有克隆
rbd children --image test --snap test_snap
# 将父镜像（被克隆的镜像的快照）的数据扁平化到子镜像，从而解除父子关联
rbd flatten --image test_clone</pre>
<div class="blog_h2"><span class="graybg">镜像镜像</span></div>
<p>从Jewel开始，RBD镜像可以异步的跨越两个集群进行镜像（Mirroring）。通过配置，你可以镜像池中的所有、或者一部分镜像。</p>
<div class="blog_h3"><span class="graybg">启用镜像复制</span></div>
<pre class="crayon-plain-tag">rbd mirror pool enable {pool-name} {mode}

# 启用名为local的集群的镜像复制，默认为pool
rbd --cluster local mirror pool enable image-pool pool
rbd --cluster remote mirror pool enable image-pool pool</pre>
<p>mode取值：</p>
<ol>
<li>pool，池中所有启用了journaling特性的镜像都被复制</li>
<li>image，只有明确配置的镜像才进行复制 </li>
</ol>
<div class="blog_h3"><span class="graybg">禁用镜像复制</span></div>
<pre class="crayon-plain-tag">rbd mirror pool disable {pool-name}

rbd --cluster local mirror pool disable image-pool
rbd --cluster remote mirror pool disable image-pool</pre>
<div class="blog_h2"><span class="graybg">操控对象</span></div>
<div class="blog_h3"><span class="graybg">创建对象</span></div>
<pre class="crayon-plain-tag"># 在池中创建一个对象，其内容来自文件
echo "Hello World" &gt; /tmp/file
rados -p rbd put helloworld /tmp/file</pre>
<div class="blog_h3"><span class="graybg">查看对象</span></div>
<pre class="crayon-plain-tag"># 查看对象
rados -p rbd ls | grep helloworld</pre>
<div class="blog_h2"><span class="graybg">操控CRUSH</span></div>
<pre class="crayon-plain-tag"># 根据CRUSH Map，列出OSD树
ceph osd tree
#                 缩进显示树层次
# ID  CLASS WEIGHT  TYPE NAME               STATUS REWEIGHT PRI-AFF 
#  -1       5.73999 root default                                    
#  -2       0.84000     host k8s-10-5-38-25                         
#   0   hdd 0.84000         osd.0               up  1.00000 1.00000 
#  -5       0.45000     host k8s-10-5-38-70                         
#   1   hdd 0.45000         osd.1               up  1.00000 1.00000 

# 移动桶的位置
# 将rack01移动到{root=default} 
ceph osd crush move rack01 root=default</pre>
<div class="blog_h2"><span class="graybg">操控PG</span></div>
<div class="blog_h3"><span class="graybg">镜像和PG对应关系</span></div>
<pre class="crayon-plain-tag"># 显示镜像和PG的关系
ceph osd map rbd  test
#                                                 此镜像存放在1.b5这个PG中
#                                                        此PG位于 osd.3 osd.1 osd.6中
#                                                                      主副本 位于osd.3中
# osdmap e26 pool 'rbd' (1) object 'test' -&gt; pg 1.40e8aab5 (1.b5) -&gt; up ([3,1,6], p3) acting ([3,1,6], p3)

# 显示PG和镜像的关系
ceph pg map 1.c0
# osdmap e1885 pg 1.c0 (1.c0) -&gt; up [9,8] acting [9,8]</pre>
<div class="blog_h3"><span class="graybg">Dump出PG统计信息</span></div>
<p>Dump出所有PG：</p>
<pre class="crayon-plain-tag">pg dump {all|summary|sum|delta|pools|osds|pgs|pgs_brief [all|summary|sum|delta|pools|osds|pgs|pgs_brief...]}
# 示例
ceph pg dump [--format {format}]  # format取值plain或json</pre>
<p>Dump出卡在指定状态中的PG的统计信息：</p>
<pre class="crayon-plain-tag"># threshold默认30秒
ceph pg dump_stuck inactive|unclean|stale|undersized|degraded [--format {format}] [-t|--threshold {seconds}]</pre>
<div class="blog_h3"><span class="graybg">修复PG</span></div>
<pre class="crayon-plain-tag">ceph pg repair 1.c0
# instructing pg 1.c0 on osd.9 to repair</pre>
<div class="blog_h3"><span class="graybg">优先回填或修复</span></div>
<pre class="crayon-plain-tag">ceph pg force-backfill &lt;pgid&gt; [&lt;pgid&gt;...]  
ceph pg force-recovery &lt;pgid&gt; [&lt;pgid&gt;...] 
# 取消
ceph pg cancel-force-backfill &lt;pgid&gt; [&lt;pgid&gt;...]
ceph pg cancel-force-recovery &lt;pgid&gt; [&lt;pgid&gt;...]</pre>
<div class="blog_h2"><span class="graybg">调整PG数量</span></div>
<div class="blog_h3"><span class="graybg">计算PG合理值</span></div>
<p>参考<a href="http://ceph.com/pgcalc/">官网的算法</a>进行计算。</p>
<div class="blog_h3"><span class="graybg">确保集群健康</span></div>
<p>执行调整之前，必须保证集群处于健康状态。</p>
<div class="blog_h3"><span class="graybg">调整数据同步参数</span></div>
<p>为避免调整PG数量导致业务性能受到严重影响，应该调整一些参数：</p>
<pre class="crayon-plain-tag">ceph tell osd.* injectargs '--osd-max-backfills 1'
ceph tell osd.* injectargs '--osd-recovery-max-active 1'</pre>
<p>其它相关的参数还包括：</p>
<pre class="crayon-plain-tag">osd_backfill_scan_min = 4 
osd_backfill_scan_max = 32 
osd recovery threads = 1 
osd recovery op priority = 1</pre>
<div class="blog_h3"><span class="graybg">调整PG数量 </span></div>
<p>按照2的幂进行翻倍增长，例如原来32个，可以先调整为64个。</p>
<p>注意：不要一下子把PG设置为太大的值，这会导致大规模的rebalance，影响系统性能。</p>
<div class="blog_h3"><span class="graybg">调整PGP数量</span></div>
<p>等到上一步操作后，集群变为Active+Clean状态后，再将pgp_num设置的和pg_num一致。 </p>
<div class="blog_h2"><span class="graybg">操控RGW</span></div>
<div class="blog_h3"><span class="graybg">手工安装</span></div>
<pre class="crayon-plain-tag"># 在RGW节点安装软件
# yum install ceph-radosgw

RGW_HOST=$(hostname -s)

# 在RGW节点，配置ceph.conf
cat &lt;&lt; EOF &gt;&gt; /etc/ceph/ceph.conf
[client.rgw.$RGW_HOST]
rgw_frontends = "civetweb port=7480"
EOF

# 拷贝配置到所有Ceph节点

# 在RGW节点，创建数据目录
mkdir -p /var/lib/ceph/radosgw/ceph-rgw.$RGW_HOST

# 在RGW节点，创建用户，输出Keyring
ceph auth get-or-create client.rgw.$RGW_HOST osd 'allow rwx' mon 'allow rw' \
    -o /var/lib/ceph/radosgw/ceph-rgw.$RGW_HOST/keyring
chown -R ceph:ceph /var/lib/ceph/radosgw

# 在RGW节点，启用Systemd服务
systemctl enable ceph-radosgw.target
systemctl enable ceph-radosgw@rgw.$RGW_HOST
systemctl start ceph-radosgw@rgw.$RGW_HOST</pre>
<div class="blog_h3"><span class="graybg">修改端口</span></div>
<pre class="crayon-plain-tag">[client.rgw.Carbon]
rgw_frontends = "civetweb port=80"</pre>
<p>推送修改后的配置文件后，重启RGW服务：</p>
<pre class="crayon-plain-tag">systemctl restart ceph-radosgw.service</pre>
<div class="blog_h3"><span class="graybg">启用SSL </span></div>
<pre class="crayon-plain-tag">[client.rgw.Carbon]
# 指定包含了私钥和证书的PEM
rgw_frontends = civetweb port=443s ssl_certificate=/etc/ceph/private/keyandcert.pem
# Luminous开始，可以同时绑定SSL和非SSL端口
rgw_frontends = civetweb port=80+443s ssl_certificate=/etc/ceph/private/keyandcert.pem</pre>
<div class="blog_h3"><span class="graybg">配置桶分片</span></div>
<p>RGW在index_pool池中存放桶（Bucket）索引数据，此池默认名为.rgw.buckets.index。</p>
<p>从0.94版本开始，支持对桶索引进行分片，避免<span style="background-color: #c0c0c0;">单个桶中对象数量过多时出现性能瓶颈</span>：</p>
<pre class="crayon-plain-tag"># 每个桶的最大索引分片数，默认0，表示不支持分片
rgw_override_bucket_index_max_shards = 0</pre>
<p>你可以在global段配置上面的选项。 </p>
<div class="blog_h3"><span class="graybg">启用用户</span></div>
<p>要使用RGW的RESTful接口，你需要：</p>
<ol>
<li>创建初始的S3接口的用户</li>
<li>创建Swift接口的子用户</li>
<li>验证用户可以访问网关</li>
</ol>
<p>要创建S3接口用户，需要在网关机上执行：</p>
<pre class="crayon-plain-tag">radosgw-admin user create --uid="rgw" --display-name="rgw"</pre>
<p>access_key、secret_key会打印在屏幕上，要访问网关，客户端必须提供这两个key：</p>
<pre class="crayon-plain-tag">{
    "user_id": "rgw",
    "display_name": "rgw",
    "email": "",
    "suspended": 0,
    "max_buckets": 1000,
    "auid": 0,
    "subusers": [],
    "keys": [
        {
            "user": "rgw",
            "access_key": "IN01UCU1M1996LK6OM88",
            "secret_key": "AuuAbroSUlWLykbQHCbFLVO6RU2ozUEjIFkYeoqc"
        }
    ],
    "swift_keys": [],
    "caps": [],
    "op_mask": "read, write, delete",
    "default_placement": "",
    "placement_tags": [],
    "bucket_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "user_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "temp_url_keys": [],
    "type": "rgw"
}</pre>
<p>要创建Swift子用户，需要在网关机上执行：</p>
<pre class="crayon-plain-tag">radosgw-admin subuser create --uid=alex --subuser=alex:swift --access=full</pre>
<p>你需要为Swift子用户创建secret key：</p>
<pre class="crayon-plain-tag">radosgw-admin key create --subuser=alex:swift --key-type=swift --gen-secret</pre>
<p>现在，你可以用自己熟悉的语言的S3、Swift客户端来验证用户是否可用。 </p>
<div class="blog_h3"><span class="graybg">操控桶</span></div>
<pre class="crayon-plain-tag">radosgw-admin bucket list                # 列出桶
radosgw-admin bucket limit check         # 显示桶的分片情况
radosgw-admin bucket link                # 将桶链接到用户
radosgw-admin bucket unlink              # 取消桶到用户的链接
radosgw-admin bucket stats               # 显示桶的统计信息
radosgw-admin bucket rm                  # 删除桶
radosgw-admin bucket check               # 检查桶索引
radosgw-admin bucket reshard             # 对桶进行重分片
radosgw-admin bucket sync disable        # 禁止桶同步
radosgw-admin bucket sync enable         # 启用桶同步</pre>
<p>要创建桶，你需要使用合法的User ID + AWS Access Key发起请求，Ceph没有提供对应的命令行。需要注意以下约束：</p>
<ol>
<li>桶名称必须唯一</li>
<li>桶名称不能格式化为IP地址</li>
<li>桶名称在3-63字符之间</li>
<li>桶名称不得包含大写字母、下划线，但是可以包含短横线</li>
<li>桶名称必须以小写字母或数字开头</li>
<li>桶名称必须由一系列的标签组成，每个标签用点号.分隔</li>
</ol>
<p>我们可以使用MinIO客户端创建桶：</p>
<pre class="crayon-plain-tag"># 添加配置
#                                               access_key           secret_key
mc config host add rgw https://rgw.gmem.cc:7480 IN01UCU1M1996LK6OM88 AuuAbroSUlWLykbQHCbFLVO6RU2ozUEjIFkYeoqc

# 创建桶
mc mb rgw/test</pre>
<p>现在通过Rgw命令行可以看到这个桶：</p>
<pre class="crayon-plain-tag">radosgw-admin buckets list
# [
#     "test"
# ]</pre>
<div class="blog_h2"><span class="graybg">管理身份验证</span></div>
<p>Ceph默认开启了cephx协议，加密认证需要消耗少量的资源。</p>
<p>启用cephx后，Cephe会自动在包括/etc/ceph/ceph.$name.keyring在内的位置寻找钥匙串，你可以指定keyring选项来修改默认路径，但是不推荐。</p>
<div class="blog_h3"><span class="graybg">手工启用</span></div>
<p>在禁用了cephx的集群上，启用它的步骤为：</p>
<ol>
<li>创建 client.admin 密钥：<br />
<pre class="crayon-plain-tag"># 如果你使用的自动部署工具已经生成此文件，切勿执行此命令，会覆盖
ceph auth get-or-create client.admin mon 'allow *' mds 'allow *' osd 'allow *' -o /etc/ceph/ceph.client.admin.keyring</pre>
</li>
<li>创建mon集群所需的钥匙串、密钥：<br />
<pre class="crayon-plain-tag">ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'</pre>
</li>
<li>将上述钥匙串复制到所有mon的mon data目录，例如：<br />
<pre class="crayon-plain-tag">cp /tmp/ceph.mon.keyring /var/lib/ceph/mon/ceph-a/keyring</pre>
</li>
<li>为每个OSD生成密钥：<br />
<pre class="crayon-plain-tag">ceph auth get-or-create osd.{$id} mon 'allow rwx' osd 'allow *' -o /var/lib/ceph/osd/ceph-{$id}/keyring</pre>
</li>
<li>
<p>为每个 MDS 生成密钥：</p>
<pre class="crayon-plain-tag">ceph auth get-or-create mds.{$id} mon 'allow rwx' osd 'allow *' mds 'allow *' -o /var/lib/ceph/mds/ceph-{$id}/keyring</pre>
</li>
<li>
<p>添加以下内容到配置文件的global段：
<pre class="crayon-plain-tag">auth cluster required = cephx
auth service required = cephx
auth client required = cephx</pre>
</li>
<li>
<p>启动或重启Ceph集群：  
<pre class="crayon-plain-tag"># 停止当前节点上的所有Ceph守护进程
sudo stop ceph-all
sudo start ceph-all</pre>
</li>
</ol>
<div class="blog_h3"><span class="graybg">禁用认证</span></div>
<p>修改配置文件global段：
<pre class="crayon-plain-tag">auth cluster required = none
auth service required = none
auth client required = none</pre>
<p>然后重启Ceph集群。 </p>
<div class="blog_h3"><span class="graybg">身份验证命令</span></div>
<pre class="crayon-plain-tag"># 列出keyring
ceph auth ls
# 添加OSD的keyring
ceph auth add {osd} {--in-file|-i} {path-to-osd-keyring}</pre>
<div class="blog_h2"><span class="graybg">管理CRUSH map</span> </div>
<div class="blog_h3"><span class="graybg">手工管理</span></div>
<p>任何时后你都可以Dump、反编译、修改、编译、注入CURSH map。如果要完全基于手工方式管理，不使用自动生成的CRUSH map，可以设置：</p>
<pre class="crayon-plain-tag">osd crush update on start = false</pre>
<div class="blog_h3"><span class="graybg">查看Dump</span></div>
<p>执行命令<pre class="crayon-plain-tag">ceph osd crush dump</pre>，可以将整个CRUSH导出为可读形式：</p>
<pre class="crayon-plain-tag"># 下面的输出时安装后最初的状态，没有任何OSD

{   
    # 设备列表，最初为空                                                                                                                                                                       
    "devices": [],   
    # 桶类型定义列表                                                                                                                                                               
    "types": [                                                                                                                                                                           
        {                                                                                                                                                                                
            "type_id": 0,                                                                                                                                                                
            "name": "osd"                                                                                                                                                                
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 1,                                                                                                                                                                
            "name": "host"                                                                                                                                                               
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 2,                                                                                                                                                                
            "name": "chassis"                                                                                                                                                            
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 3,                                                                                                                                                                
            "name": "rack"                                                                                                                                                               
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 4,                                                                                                                                                                
            "name": "row"                                                                                                                                                                
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 5,                                                                                                                                                                
            "name": "pdu"                                                                                                                                                                
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 6,                                                                                                                                                                
            "name": "pod"                                                                                                                                                                
        },                                                                                                                                                                               
        {                                                                                                                                                                                
            "type_id": 7,                                                                                                                                                                
            "name": "room"                                                                                                                                                               
        },
        {
            "type_id": 8,
            "name": "datacenter"
        },
        {
            "type_id": 9,
            "name": "region"
        },
        {
            "type_id": 10,
            "name": "root"
        }
    ],
    # 桶列表，可以形成树状结构
    "buckets": [
        {
            "id": -1,
            "name": "default",
            "type_id": 10,
            "type_name": "root",
            "weight": 0,
            "alg": "straw2",
            "hash": "rjenkins1",
            "items": []
        }
        # 加入一个OSD节点（基于目录），自动生成如下两个Bucket：
        {                                                                                                                                                                          
            "id": -2,                                                                                                                                                              
            "name": "k8s-10-5-38-25",                                                                                                                                              
            "type_id": 1,                                                                                                                                                          
            "type_name": "host",                                                                                                                                                   
            "weight": 55050,                                                                                                                                                       
            "alg": "straw2",                                                                                                                                                       
            "hash": "rjenkins1",                                                                                                                                                   
            "items": [                                                                                                                                                             
                {                                                                                                                                                                  
                    "id": 0,                                                                                                                                                       
                    "weight": 55050,                                                                                                                                               
                    "pos": 0                                                                                                                                                       
                }                                                                                                                                                                  
            ]                                                                                                                                                                      
        },                                                                                                                                                                         
        {                                                                                                                                                                          
            "id": -3,                                                                                                                                                              
            "name": "k8s-10-5-38-25~hdd",                                                                                                                                          
            "type_id": 1,                                                                                                                                                          
            "type_name": "host",                                                                                                                                                   
            "weight": 55050,                                                                                                                                                       
            "alg": "straw2",                                                                                                                                                       
            "hash": "rjenkins1",                                                                                                                                                   
            "items": [                                                                                                                                                             
                {
                    "id": 0,
                    "weight": 55050,
                    "pos": 0
                }
            ]
        },

    ],
    # 规则列表
    "rules": [
        {
            "rule_id": 0,
            "rule_name": "replicated_rule",
            # 所属规则集
            "ruleset": 0,
            # 此规则是否用于RAID，取值replicated 或 raid4
            "type": 1,
            # 如果Pool的副本份数不在此范围内，则CRUSH不会使用当前规则
            "min_size": 1,
            "max_size": 10,
            "steps": [
                {
                    # 选择一个桶，并迭代其子树
                    "op": "take",
                    "item": -1,
                    "item_name": "default"
                },
                {
                    # 在上一步的基础上，确定每个副本如何放置
                    "op": "chooseleaf_firstn",
                    # 取值0，此Step适用pool-num-replicas个副本（所有）
                    # 取值&gt;0 &amp; &lt; pool-num-replicas，适用num个副本
                    # 取值&lt;0，适用pool-num-replicas -num个副本
                    "num": 0,
                    "type": "host"
                },
                {
                    "op": "emit"
                }
            ]
        }
    ],
    # 可微调参数，以及一些状态信息
    "tunables": {                                                                                                                                                                        
        "choose_local_tries": 0,                                                                                                                                                         
        "choose_local_fallback_tries": 0,                                                                                                                                                
        "choose_total_tries": 50,                                                                                                                                                        
        "chooseleaf_descend_once": 1,                                                                                                                                                    
        "chooseleaf_vary_r": 1,                                                                                                                                                          
        "chooseleaf_stable": 1,                                                                                                                                                          
        "straw_calc_version": 1,                                                                                                                                                         
        "allowed_bucket_algs": 54,    
        # 使用的Profile，执行ceph osd crush tunables hammer后此字段改变，连带其它tunables字段自动改变                                                                                                                                                   
        "profile": "jewel",                                                                                                                                                              
        "optimal_tunables": 1,                                                                                                                                                           
        "legacy_tunables": 0,                                                                                                                                                            
        "minimum_required_version": "jewel",                                                                                                                                             
        "require_feature_tunables": 1,                                                                                                                                                   
        "require_feature_tunables2": 1,                                                                                                                                                  
        "has_v2_rules": 0,                                                                                                                                                               
        "require_feature_tunables3": 1,                                                                                                                                                  
        "has_v3_rules": 0,                                                                                                                                                               
        "has_v4_buckets": 1,                                                                                                                                                             
        "require_feature_tunables5": 1,
        "has_v5_rules": 0
    },
    "choose_args": {}
}</pre>
<div class="blog_h3"><span class="graybg">编辑Dump</span></div>
<p>执行下面的命令，导出当前Map：</p>
<pre class="crayon-plain-tag">ceph osd getcrushmap -o curshmap</pre>
<p>然后，需要反编译为文本：</p>
<pre class="crayon-plain-tag">crushtool -d curshmap -o curshmap.src</pre>
<p>源文件内容示例：</p>
<pre class="crayon-plain-tag"># begin crush map
tunable choose_local_tries 0
tunable choose_local_fallback_tries 0
tunable choose_total_tries 50
tunable chooseleaf_descend_once 1
tunable chooseleaf_vary_r 1
tunable straw_calc_version 1
tunable allowed_bucket_algs 54

# devices

# types
type 0 osd
type 1 host
type 2 chassis
type 3 rack
type 4 row
type 5 pdu
type 6 pod
type 7 room
type 8 datacenter
type 9 region.Values.storageclass.fsType
type 10 root

# buckets
root default {
        id -1           # do not change unnecessarily
        # weight 0.000
        alg straw2           
        hash 0  # rjenkins1
}
# rules
rule replicated_rule {
        id 0
        type replicated
        min_size 1
        max_size 10
        step take default
        step chooseleaf firstn 0 type host
        step emit
}

# end crush map</pre>
<p>我们可以根据实际需要，对源文件进行修改，例如将算法改为straw，解决CentOS 7上CEPH_FEATURE_CRUSH_V4 1000000000000特性不满足的问题：</p>
<pre class="crayon-plain-tag">sed -i 's/straw2/straw/g' curshmap.src</pre>
<p>修改源文件完毕后，执行下面的命令编译：</p>
<pre class="crayon-plain-tag">crushtool -c curshmap.src -o curshmap</pre>
<p>最后，注入最新编译的Map：</p>
<pre class="crayon-plain-tag">ceph osd setcrushmap -i curshmap
# 会输出修订版号 </pre>
<div class="blog_h3"><span class="graybg">修改设备类型</span></div>
<p>默认情况下，Ceph自动根据硬件类型，设置OSD的设备类型为hdd, ssd或nvme。你可以手工进行设置：</p>
<pre class="crayon-plain-tag"># 你需要移除当前设置的设备类型，才能重新设置
ceph osd crush rm-device-class &lt;osd-name&gt; [...]
# 示例
ceph osd crush rm-device-class osd.3 osd.4 osd.5 osd.6 osd.7 osd.8 osd.0 osd.10 osd.1 osd.12 osd.2 osd.13 


ceph osd crush set-device-class &lt;class&gt; &lt;osd-name&gt; [...]
# 示例
ceph osd crush set-device-class ssd osd.3 osd.4 osd.5 osd.6 osd.7 osd.8 osd.0 osd.10 osd.1 osd.12 osd.2 osd.13</pre>
<div class="blog_h3"><span class="graybg">查看规则</span></div>
<p>列出集群中的CRUSH rule：</p>
<pre class="crayon-plain-tag">ceph osd crush rule ls</pre>
<p>Dump出规则的内容：</p>
<pre class="crayon-plain-tag">ceph osd crush rule dump</pre>
<div class="blog_h3"><span class="graybg">删除规则</span></div>
<pre class="crayon-plain-tag">ceph osd crush rule rm replicated_rule_ssd</pre>
<div class="blog_h3"><span class="graybg">创建规则 </span></div>
<p>创建一个规则，仅仅使用指定类型的设备：</p>
<pre class="crayon-plain-tag">ceph osd crush rule create-replicated &lt;rule-name&gt; &lt;root&gt; &lt;failure-domain&gt; &lt;class&gt;
# 示例：仅仅使用ssd类型的设备，失败域为host，也就是数据副本必须位于不同的主机上
ceph osd crush rule create-replicated replicated_rule_ssd default host ssd
ceph osd crush rule create-replicated replicated_rule_hdd default host hdd</pre>
<div class="blog_h3"><span class="graybg">应用规则</span></div>
<p>为存储池指定所使用的规则：</p>
<pre class="crayon-plain-tag">ceph osd pool set &lt;pool-name&gt; crush_rule &lt;rule-name&gt;
# 修改规则
ceph osd pool set rbd-ssd crush_rule replicated_rule_ssd
# 创建存储池时指定规则
ceph osd pool create rbd-ssd 384 replicated replicated_rule_ssd</pre>
<div class="blog_h3"><span class="graybg">编辑规则</span></div>
<p>CRUSH rule的语法如下：</p>
<pre class="crayon-plain-tag">rule &lt;rulename&gt; {
        ruleset &lt;ruleset&gt;
        type [ replicated | erasure ]
        min_size &lt;min-size&gt;
        max_size &lt;max-size&gt;
        # 根据桶名称来选取CRUSH子树，并迭代，可限定设备类型
        step take &lt;bucket-name&gt; [class &lt;device-class&gt;]
        # choose：选择指定数量、类型的桶
        # chooseleaf：选择指定数量、类型的桶，并选择每个这些桶的一个叶子节点
        step [choose|chooseleaf] [firstn|indep] &lt;N&gt; &lt;bucket-type&gt;
        step emit
}</pre>
<p>示例一，将主副本存放在SSD中，第二副本存放在HDD中：</p>
<pre class="crayon-plain-tag">rule ssd-primary-affinity {
    ruleset 0
    type replicated
    min_size 2
    max_size 3
    # 选择名为SSD的桶
    step take ssd
    # 在上述桶中的host类型的子树中选择叶子节点，存储1个副本（第一个）
    step chooseleaf firstn 1 type host
    # 执行
    step emit
    # 选择名为HDD的桶
    step take hdd
    # 在上述桶中的host类型的子树中选择叶子节点，存储N-1个副本（所有其它副本）
    step chooseleaf firstn -1 type host
    step emit
}</pre>
<p> 示意二，在第一个机架上存储两个副本，第二个机架上存储一个副本：</p>
<pre class="crayon-plain-tag">rule 3_rep_2_racks {
   ruleset 1
   type replicated
   min_size 2
   max_size 3
   step take default
   # 选择一个Rack，存储2个副本
   step choose firstn 2 type rack
   # 在上述选定的Rack中选择Host
   step chooseleaf firstn 2 type host
   step emit
}</pre>
<div class="blog_h3"><span class="graybg">增加OSD</span></div>
<p>如果要添加OSD到CRUSH map中，执行：</p>
<pre class="crayon-plain-tag">ceph osd crush set {name} {weight} root={root} [{bucket-type}={bucket-name} ...]
# 示例
ceph osd crush set osd.14 0 host=xenial-100
ceph osd crush set osd.0 1.0 root=default datacenter=dc1 room=room1 row=foo rack=bar host=foo-bar-1</pre>
<div class="blog_h3"><span class="graybg">调整OSD权重</span></div>
<pre class="crayon-plain-tag">ceph osd crush reweight {name} {weight}</pre>
<div class="blog_h3"><span class="graybg">移除OSD</span></div>
<pre class="crayon-plain-tag">ceph osd crush remove {name}</pre>
<div class="blog_h3"><span class="graybg">增加Bucket</span></div>
<pre class="crayon-plain-tag">ceph osd crush add-bucket {bucket-name} {bucket-type}</pre>
<div class="blog_h3"><span class="graybg">移动Bucket </span></div>
<pre class="crayon-plain-tag">ceph osd crush move {bucket-name} {bucket-type}={bucket-name}, [...]</pre>
<div class="blog_h3"><span class="graybg">移除Bucket</span></div>
<pre class="crayon-plain-tag">ceph osd crush remove {bucket-name} </pre>
<div class="blog_h3"><span class="graybg">调整Tunable</span></div>
<pre class="crayon-plain-tag"># 自动优化
ceph osd crush tunables optimal
# 最大兼容性，存在老旧内核的cephfs/rbd客户端时
ceph osd crush tunables legacy


# 选择一个PROFILE，例如jewel
ceph osd crush tunables {PROFILE} </pre>
<div class="blog_h3"><span class="graybg">避免作为主OSD</span></div>
<pre class="crayon-plain-tag"># weight在0-1之间，默认1，值越小，CRUSH 越避免将目标OSD作为主
ceph osd primary-affinity &lt;osd-id&gt; &lt;weight&gt;</pre>
<div class="blog_h3"><span class="graybg">分配Pool到特定OSD</span></div>
<p>使用CRUSH rule，可以限定某个Pool仅仅使用一部分OSD：</p>
<pre class="crayon-plain-tag"># SSD主机
host ceph-osd-ssd-server-1 {
      id -1
      alg straw
      hash 0
      item osd.0 weight 1.00
      item osd.1 weight 1.00
}

# HDD主机
host ceph-osd-hdd-server-1 {
      id -3
      alg straw
      hash 0
      item osd.4 weight 1.00
      item osd.5 weight 1.00
}

# HDD的根桶
root hdd {
      id -5
      alg straw
      hash 0
      item ceph-osd-hdd-server-1 weight 2.00
}

# SSD的根桶
root ssd {
      id -6
      alg straw
      hash 0
      item ceph-osd-ssd-server-1 weight 2.00
}


# 仅仅使用HDD的规则
rule hdd {
      ruleset 3
      type replicated
      min_size 0
      max_size 10
      step take hdd
      # 选择
      step chooseleaf firstn 0 type host
      step emit
}

# 仅仅使用SSD的规则
rule ssd {
      ruleset 4
      type replicated
      min_size 0
      max_size 4
      step take ssd
      step chooseleaf firstn 0 type host
      step emit
}

# 在SSD上存储主副本，其它副本存放在HDD
rule ssd-primary {
      ruleset 5
      type replicated
      min_size 5
      max_size 10
      step take ssd
      step chooseleaf firstn 1 type host
      step emit
      step take hdd
      step chooseleaf firstn -1 type host
      step emit
}</pre>
<div class="blog_h2"><span class="graybg">修改日志尺寸</span></div>
<p>前提条件：</p>
<ol>
<li>集群处于OK状态</li>
<li>所有PG处于active+clean状态</li>
</ol>
<p>步骤，针对每个需要改变尺寸的OSD，一个个的处理：</p>
<ol>
<li>修改Cephe配置，设置<pre class="crayon-plain-tag">osd_journal_size = NEWSIZE</pre></li>
<li>禁止数据迁移（防止OSD进入out状态）：<pre class="crayon-plain-tag">ceph osd set noout</pre></li>
<li>停止目标OSD实例</li>
<li>刷出缓存：<pre class="crayon-plain-tag">ceph-osd -i  OSDID --flush-journal</pre></li>
<li>删除日志：<br />
<pre class="crayon-plain-tag"># 基于Helm部署时，需要到宿主机上的osd_directory下寻找对应目录
cd /var/lib/ceph/osd/ceph-osd.OSDID
rm journal </pre>
</li>
<li>创建一个新的日志文件：<pre class="crayon-plain-tag">ceph-osd --mkjournal -i OSDID</pre></li>
<li>启动OSD</li>
<li>验证新的日志尺寸被使用：<br />
<pre class="crayon-plain-tag"># Helm安装的情况下，需要在OSD容器中执行
ceph --admin-daemon /var/run/ceph/ceph-osd.OSDID.asok config get osd_journal_size</pre>
</li>
<li>
<p>确保集群处于OK状态，所有PG处于active+clean状态 </p>
</li>
</ol>
<p>处理完所有OSD后，执行： <pre class="crayon-plain-tag">ceph osd unset noout</pre>，清除noout标记</p>
<div class="blog_h2"><span class="graybg">性能测试</span></div>
<div class="blog_h3"><span class="graybg">rbd bench</span></div>
<pre class="crayon-plain-tag"># 默认4K，可以--io-size定制
# 默认16线程，可以--io-threads定制

# 随机读
rbd bench -p rbd-hdd --image benchmark --io-total 128M --io-type read --io-pattern rand
# elapsed: 25 ops:  32768 ops/sec:  1284.01  bytes/sec: 5259316.53
# elapsed: 15 ops: 327680 ops/sec: 20891.46  bytes/sec: 85571410.91
# HDD差20倍

# 顺序读
rbd bench -p rbd-hdd --image benchmark --io-total 64M --io-type read --io-pattern seq
# elapsed: 46 ops:  163840 ops/sec:   3528.06  bytes/sec: 14450938.87
# elapsed: 45 ops: 1638400 ops/sec:   35672.87 bytes/sec: 146116057.32
# HDD差10倍

# 随机写
rbd bench -p rbd-hdd --image benchmark --io-total 128M --io-type write --io-pattern rand
# elapsed: 85  ops:  32768 ops/sec: 383.24  bytes/sec: 1569743.22
# elapsed: 111 ops: 327680 ops/sec: 2936.78 bytes/sec: 12029055.24
# HDD差7倍

# 顺序写
rbd bench -p rbd-hdd --image benchmark --io-total 128M --io-type write --io-pattern seq
# elapsed: 3  ops: 32768  ops/sec:  9382.16 bytes/sec: 38429334.91
# elapsed: 17 ops: 327680 ops/sec: 18374.69 bytes/sec: 75262749.05
# HDD差1倍</pre>
<div class="blog_h2"><span class="graybg">运行时修改参数</span></div>
<p>要动态、临时（重启后消失）的修改组件的参数，可以使用tell命令。</p>
<div class="blog_h3"><span class="graybg">控制恢复进程</span></div>
<pre class="crayon-plain-tag"># 临时修改所有OSD和恢复相关的选项
ceph tell osd.* injectargs '--osd-max-backfills 1'             # 并发回填操作数
ceph tell osd.* injectargs '--osd-recovery-threads 1'          # 恢复线程数量
ceph tell osd.* injectargs '--osd-recovery-op-priority 1'      # 恢复线程优先级  
ceph tell osd.* injectargs '--osd-client-op-priority 63'       # 客户端线程优先级
ceph tell osd.* injectargs '--osd-recovery-max-active 1'       # 最大活跃的恢复请求数</pre>
<div class="blog_h2"><span class="graybg">管理Watcher</span></div>
<div class="blog_h3"><span class="graybg">加入黑名单</span></div>
<p>可以将RBD上的Watcher加入黑名单，这样可以解除RBD的Watcher，再其它机器上挂载RBD：</p>
<pre class="crayon-plain-tag">rbd status  kubernetes-dynamic-pvc-22d9e659-6e31-11e8-92e5-c6b9f35768f0                                                                                                     
# Watchers:                                                                                                                                                                                
#         watcher=10.0.3.1:0/158685765 client.3524447 cookie=18446462598732840965

# 添加到黑名单
ceph osd blacklist add 10.0.3.1:0/158685765
# blacklisting 10.0.3.1:0/158685765 until 2018-08-21 18:04:31.855791 (3600 sec)

rbd status  kubernetes-dynamic-pvc-22d9e659-6e31-11e8-92e5-c6b9f35768f0
# Watchers: none</pre>
<div class="blog_h3"><span class="graybg">显示黑名单</span></div>
<pre class="crayon-plain-tag">ceph osd blacklist ls
# listed 1 entries
# 10.0.3.1:0/158685765 2018-08-21 18:04:31.855791</pre>
<div class="blog_h3"><span class="graybg">移除黑名单</span></div>
<pre class="crayon-plain-tag">ceph osd blacklist rm 10.0.3.1:0/158685765</pre>
<div class="blog_h3"><span class="graybg">清空黑名单 </span></div>
<pre class="crayon-plain-tag">ceph osd blacklist clear</pre>
<div class="blog_h2"><span class="graybg">RADOS管理</span></div>
<div class="blog_h3"><span class="graybg">管理池</span></div>
<pre class="crayon-plain-tag"># 列出池
rados lspools
.rgw.root
default.rgw.control
default.rgw.meta
default.rgw.log
rbd
rbd-ssd
rbd-hdd

# 创建池pool-name，使用auid 123，使用crush规则4
mkpool pool-name [123[ 4]] 

# 复制池的内容
cppool pool-name dest-pool

# 移除池
rmpool pool-name pool-name --yes-i-really-really-mean-it

# 清空池中对象
purge pool-name --yes-i-really-really-mean-it

# 显示每个池的对象数量、空间占用情况
rados df

# 列出池中对象
rados ls -p rbd

# 将池的所有者设置为auid 123
rados chown 123  -p rbd</pre>
<div class="blog_h3"><span class="graybg">管理快照</span> </div>
<pre class="crayon-plain-tag"># 列出池快照
rados lssnap -p rbd

# 创建池快照
rados mksnap snap-name -p rbd

# 删除池快照
rados rmsnap mksnap snap-name -p rbd

# 从快照中恢复对象
rados rollback &lt;obj-name&gt; &lt;snap-name&gt;

# 列出对象的快照
rados listsnaps &lt;obj-name&gt;</pre>
<div class="blog_h3"><span class="graybg">读写对象</span></div>
<pre class="crayon-plain-tag"># 读对象
rados get object-name /tmp/obj -p rbd

# 使用指定的偏移量写对象
rados put object-name /tmp/obj --offset offset

# 附加内容到对象
rados append &lt;obj-name&gt; [infile

# 截断对象为指定的长度
rados truncate &lt;obj-name&gt; length

# 创建对象
rados create &lt;obj-name&gt;

# 移除对象
rados rm &lt;obj-name&gt; ...[--force-full]

# 复制对象
rados cp &lt;obj-name&gt; [target-obj]</pre>
<div class="blog_h3"><span class="graybg">读写对象属性</span></div>
<pre class="crayon-plain-tag"># 列出扩展属性
rados listxattr &lt;obj-name&gt;
# 获取扩展属性
rados getxattr &lt;obj-name&gt; attr
# 设置扩展属性
rados setxattr &lt;obj-name&gt; attr val
# 移除扩展属性
rados rmxattr &lt;obj-name&gt; attr

# 显示属性
rados stat &lt;obj-name&gt;</pre>
<div class="blog_h3"><span class="graybg">列出不一致PG</span></div>
<pre class="crayon-plain-tag">rados list-inconsistent-pg pool-name</pre>
<div class="blog_h3"><span class="graybg">列出不一致对象</span></div>
<pre class="crayon-plain-tag">rados list-inconsistent-obj  40.14  --format=json-pretty</pre>
<div class="blog_h3"><span class="graybg">列出不一致快照</span></div>
<pre class="crayon-plain-tag">rados list-inconsistent-snapset 40.14 </pre>
<div class="blog_h2"><span class="graybg">配置仪表盘</span></div>
<div class="blog_h3"><span class="graybg">启用仪表盘</span></div>
<pre class="crayon-plain-tag">ceph mgr module enable dashboard</pre>
<div class="blog_h3"><span class="graybg">SSL支持配置</span></div>
<pre class="crayon-plain-tag"># 使用自签名证书
ceph dashboard create-self-signed-cert

# 使用外部提供的证书
ceph dashboard set-ssl-certificate -i dashboard.crt
ceph dashboard set-ssl-certificate-key -i dashboard.key

# 禁用SSL
ceph config set mgr mgr/dashboard/ssl false</pre>
<div class="blog_h3"><span class="graybg">设置用户</span></div>
<pre class="crayon-plain-tag">ceph dashboard ac-user-create admin  administrator -i - &lt;&lt;&lt;"pswd"</pre>
<div class="blog_h3"><span class="graybg">管理RGW</span></div>
<pre class="crayon-plain-tag"># 创建用户
radosgw-admin user create --uid=rgw --display-name=rgw --system

# 设置access_key和secret_key
ceph dashboard set-rgw-api-access-key -i - &lt;&lt;&lt; "$(radosgw-admin user info --uid=rgw | jq -r .keys[0].access_key)"
ceph dashboard set-rgw-api-secret-key -i - &lt;&lt;&lt; "$(radosgw-admin user info --uid=rgw | jq -r .keys[0].secret_key)"

# 禁用SSL校验
ceph dashboard set-rgw-api-ssl-verify False</pre>
<div class="blog_h1"><span class="graybg">诊断</span></div>
<div class="blog_h2"><span class="graybg">调试日志</span></div>
<p>注意：详尽的日志每小时可能超过 1GB ，如果你的系统盘满了，这个节点就会停止工作。</p>
<div class="blog_h3"><span class="graybg">临时启用调试日志</span></div>
<pre class="crayon-plain-tag"># 通过中心化配置下发
ceph tell osd.0 config set debug_osd 0/5

# 到目标主机上，针对OSD进程设置
ceph daemon osd.0 config set debug_osd 0/5</pre>
<div class="blog_h3"><span class="graybg">配置日志级别</span></div>
<p>可以为各子系统定制日志级别：</p>
<pre class="crayon-plain-tag"># debug {subsystem} = {log-level}/{memory-level}

[global]
        debug ms = 1/5
[mon]
        debug mon = 20
        debug paxos = 1/5
        debug auth = 2
[osd]
        debug osd = 1/5
        debug filestore = 1/5
        debug journal = 1
        debug monc = 5/20
[mds]
        debug mds = 1
        debug mds balancer = 1
        debug mds log = 1
        debug mds migrator = 1</pre>
<p>子系统列表：</p>
<table class=" full-width">
<thead>
<tr>
<td class="head" style="text-align: center;">
<p><strong>子系统</strong></p>
</td>
<td class="head" style="text-align: center;">
<p><strong>日志级别</strong></p>
</td>
<td class="head" style="text-align: center;">
<p><strong>内存日志级别</strong></p>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>default</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>lockdep</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>context</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>crush</td>
<td>
<p>1</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>mds</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mds <span class="pre">balancer</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mds <span class="pre">locker</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mds <span class="pre">log</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mds <span class="pre">log</span> <span class="pre">expire</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mds <span class="pre">migrator</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>buffer</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>timer</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>filer</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>striper</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>objecter</td>
<td>
<p>0</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>rados</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rbd</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rbd <span class="pre">mirror</span></td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rbd <span class="pre">replay</span></td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>journaler</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>objectcacher</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>client</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>osd</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>optracker</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>objclass</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>filestore</td>
<td>
<p>1</p>
</td>
<td>
<p>3</p>
</td>
</tr>
<tr>
<td>journal</td>
<td>
<p>1</p>
</td>
<td>
<p>3</p>
</td>
</tr>
<tr>
<td>ms</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mon</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>monc</td>
<td>
<p>0</p>
</td>
<td>
<p>10</p>
</td>
</tr>
<tr>
<td>paxos</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>tp</td>
<td>
<p>0</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>auth</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>crypto</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>finisher</td>
<td>
<p>1</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>reserver</td>
<td>
<p>1</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>heartbeatmap</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>perfcounter</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rgw</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rgw <span class="pre">sync</span></td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>civetweb</td>
<td>
<p>1</p>
</td>
<td>
<p>10</p>
</td>
</tr>
<tr>
<td>javaclient</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>asok</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>throttle</td>
<td>
<p>1</p>
</td>
<td>
<p>1</p>
</td>
</tr>
<tr>
<td>refs</td>
<td>
<p>0</p>
</td>
<td>
<p>0</p>
</td>
</tr>
<tr>
<td>compressor</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>bluestore</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>bluefs</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>bdev</td>
<td>
<p>1</p>
</td>
<td>
<p>3</p>
</td>
</tr>
<tr>
<td>kstore</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>rocksdb</td>
<td>
<p>4</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>leveldb</td>
<td>
<p>4</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>memdb</td>
<td>
<p>4</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>fuse</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mgr</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>mgrc</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>dpdk</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
<tr>
<td>eventtrace</td>
<td>
<p>1</p>
</td>
<td>
<p>5</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">加快日志滚动</span></div>
<p>如果磁盘空间有限，可以配置/etc/logrotate.d/ceph，加快日志滚动：</p>
<pre class="crayon-plain-tag">rotate 7
weekly
size 500M
compress
sharedscripts</pre>
<p>然后设置定时任务，定期检查并清理： </p>
<pre class="crayon-plain-tag">30 * * * * /usr/sbin/logrotate /etc/logrotate.d/ceph &gt;/dev/null 2&gt;&amp;1</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">集成K8S</span></div>
<div class="blog_h2"><span class="graybg">基于Helm</span></div>
<div class="blog_h3"><span class="graybg">简介</span></div>
<p>为了将Ceph部署到K8S集群中，可以利用<a href="https://github.com/ceph/ceph-helm/">ceph-helm项目</a>。 目前此项目存在一些限制：</p>
<ol>
<li>public和cluster网络必须一样</li>
<li>如果Storage的用户不是admin，你需要在Ceph集群中手工创建用户，并在K8S中创建对应的Secrets</li>
<li>ceph-mgr只能运行单副本</li>
</ol>
<div class="blog_h3"><span class="graybg">仓库</span></div>
<p>执行下面的命令把ceph-helm添加到本地Helm仓库：</p>
<pre class="crayon-plain-tag"># 此项目使用Helm本地仓库保存Chart，如果没有启动本地存储，请启动
nohup /usr/local/bin/helm serve  --address 0.0.0.0:8879 &gt; /dev/null 2&gt;&amp;1 &amp;

git clone https://github.com/ceph/ceph-helm
pushd ceph-helm/ceph
make
popd
# 构建成功后Chart归档文件位于 ./ceph-0.1.0.tgz</pre>
<div class="blog_h3"><span class="graybg">覆盖值</span></div>
<p>可用值的说明如下：</p>
<pre class="crayon-plain-tag"># 部署哪些组件
deployment:
  ceph: true
  storage_secrets: true
  client_secrets: true
  rbd_provisioner: true
  rgw_keystone_user_and_endpoints: false

# 修改这些值可以指定其它镜像
images:
  ks_user: docker.io/kolla/ubuntu-source-heat-engine:3.0.3
  ks_service: docker.io/kolla/ubuntu-source-heat-engine:3.0.3
  ks_endpoints: docker.io/kolla/ubuntu-source-heat-engine:3.0.3
  bootstrap: docker.io/ceph/daemon:tag-build-master-luminous-ubuntu-16.04
  dep_check: docker.io/kolla/ubuntu-source-kubernetes-entrypoint:4.0.0
  daemon: docker.io/ceph/daemon:tag-build-master-luminous-ubuntu-16.04
  ceph_config_helper: docker.io/port/ceph-config-helper:v1.7.5
  # 如果使用官方提供的StorageClass，你需要扩展kube-controller镜像，否则报executable file not found in $PATH
  rbd_provisioner: quay.io/external_storage/rbd-provisioner:v0.1.1
  minimal: docker.io/alpine:latest
  pull_policy: "IfNotPresent"

# 不同Ceph组件使用什么节点选择器
labels:
  jobs:
    node_selector_key: ceph-mon
    node_selector_value: enabled
  mon:
    node_selector_key: ceph-mon
    node_selector_value: enabled
  mds:
    node_selector_key: ceph-mds
    node_selector_value: enabled
  osd:
    node_selector_key: ceph-osd
    node_selector_value: enabled
  rgw:
    node_selector_key: ceph-rgw
    node_selector_value: enabled
  mgr:
    node_selector_key: ceph-mgr
    node_selector_value: enabled

pod:
  dns_policy: "ClusterFirstWithHostNet"
  replicas:
    rgw: 1
    mon_check: 1
    rbd_provisioner: 2
    mgr: 1
  affinity:
      anti:
        type:
          default: preferredDuringSchedulingIgnoredDuringExecution
        topologyKey:
          default: kubernetes.io/hostname
  # 如果集群资源匮乏，可以调整下面的资源请求
  resources:
    enabled: false
    osd:
      requests:
        memory: "256Mi"
        cpu: "100m"
      limits:
        memory: "1024Mi"
        cpu: "1000m"
    mds:
      requests:
        memory: "10Mi"
        cpu: "100m"
      limits:
        memory: "50Mi"
        cpu: "500m"
    mon:
      requests:
        memory: "50Mi"
        cpu: "100m"
      limits:
        memory: "100Mi"
        cpu: "500m"
    mon_check:
      requests:
        memory: "5Mi"
        cpu: "100m"
      limits:
        memory: "50Mi"
        cpu: "500m"
    rgw:
      requests:
        memory: "5Mi"
        cpu: "100m"
      limits:
        memory: "50Mi"
        cpu: "500m"
    rbd_provisioner:
      requests:
        memory: "5Mi"
        cpu: "100m"
      limits:
        memory: "50Mi"
        cpu: "500m"
    mgr:
      requests:
        memory: "5Mi"
        cpu: "100m"
      limits:
        memory: "50Mi"
        cpu: "500m"
    jobs:
      bootstrap:
        limits:
          memory: "1024Mi"
          cpu: "2000m"
        requests:
          memory: "128Mi"
          cpu: "100m"
      secret_provisioning:
        limits:
          memory: "1024Mi"
          cpu: "2000m"
        requests:
          memory: "128Mi"
          cpu: "100m"
      ks_endpoints:
        requests:
          memory: "128Mi"
          cpu: "100m"
        limits:
          memory: "1024Mi"
          cpu: "2000m"
      ks_service:
        requests:
          memory: "128Mi"
          cpu: "100m"
        limits:
          memory: "1024Mi"
          cpu: "2000m"
      ks_user:
        requests:
          memory: "128Mi"
          cpu: "100m"
        limits:
          memory: "1024Mi"
          cpu: "2000m"

secrets:
  keyrings:
    mon: ceph-mon-keyring
    mds: ceph-bootstrap-mds-keyring
    osd: ceph-bootstrap-osd-keyring
    rgw: ceph-bootstrap-rgw-keyring
    mgr: ceph-bootstrap-mgr-keyring
    admin: ceph-client-admin-keyring
  identity:
    admin: ceph-keystone-admin
    user: ceph-keystone-user
    user_rgw: ceph-keystone-user-rgw

# !! 根据实际情况网络配置
network:
  public:   10.0.0.0/16
  cluster:  10.0.0.0/16
  port:
    mon: 6789
    rgw: 8088

# !! 在此添加需要的Ceph配置项
conf:
  # 对象存储网关服务相关
  rgw_ks:
    config:
      rgw_keystone_api_version: 3
      rgw_keystone_accepted_roles: "admin, _member_"
      rgw_keystone_implicit_tenants: true
      rgw_s3_auth_use_keystone: true
  ceph:
    override:
    append:
    config:
      global:
        mon_host: null
      osd:
        ms_bind_port_max: 7100

ceph:
  rgw_keystone_auth: false
  enabled:
    mds: true
    rgw: true
    mgr: true
  storage:
    # 基于目录的OSD，在宿主机上存储的路径
    # /var/lib/ceph-helm/osd会挂载到容器的/var/lib/ceph/osd目录
    osd_directory: /var/lib/ceph-helm
    mon_directory: /var/lib/ceph-helm
    # 将日志收集到/var/log，便于fluentd来采集
    mon_log: /var/log/ceph/mon
    osd_log: /var/log/ceph/osd

# !! 是否启用基于目录的OSD，需要配合节点标签ceph-osd=enabled
# 存储的位置由上面的storage.osd_directory确定，沿用现有的文件系统
osd_directory:
  enabled: false

# 如果设置为1，则允许Ceph格式化磁盘，这会导致数据丢失
enable_zap_and_potentially_lose_data: true
# !! 基于块设备的OSD，需要配合节点标签ceph-osd-device-dev-***=enabled
osd_devices:
  - name: dev-vdb
    # 使用的块设备
    device: /dev/vdb
    # 日志可以存储到独立块设备上，提升性能，如果不指定，存放在device
    journal: /dev/vdc
    # 是否删除其分区表
    zap: "1"

bootstrap:
  enabled: false
  script: |
    ceph -s
    function ensure_pool () {
      ceph osd pool stats $1 || ceph osd pool create $1 $2
    }
    ensure_pool volumes 8

# 启用的mgr模块
ceph_mgr_enabled_modules:
  - restful
  - status

# 配置mgr模块
ceph_mgr_modules_config:
  dashboard:
    port: 7000
  localpool:
    failure_domain: host
    subtree: rack
    pg_num: "128"
    num_rep: "3"
    min_size: "2"

# 在部署/升级后，执行下面的命令
# 这些命令通过kubectl来执行
ceph_commands:
- ceph osd pool create  pg_num
- ceph osd crush tunables 

# Kubernetes 存储类配置
storageclass:
  provision_storage_class: true
  provisioner: ceph.com/rbd
  # 存储类名称
  name: ceph-rbd
  monitors: nullcurshmap.src
  # 使用的RBD存储池的名称
  pool: rbd
  admin_id: admin
  admin_secret_name: pvc-ceph-conf-combined-storageclass
  admin_secret_namespace: ceph
  user_id: admin
  user_secret_name: pvc-ceph-client-key
  # RBD设备的镜像格式和特性
  image_format: "2"
  image_features: layering

endpoints:
  # 集群域名后缀
  cluster_domain_suffix: k8s.gmem.cc
  identity:
    name: keystone
    namespace: null
    auth:
      admin:
        region_name: RegionOne
        username: admin
        password: password
        project_name: admin
        user_domain_name: default
        project_domain_name: default
      user:
        role: admin
        region_name: RegionOne
        username: swift
        password: password
        project_name: service
        user_domain_name: default
        project_domain_name: default
    hosts:
      default: keystone-api
      public: keystone
    host_fqdn_override:
      default: null
    path:
      default: /v3
    scheme:
      default: http
    port:
      admin:
        default: 35357
      api:
        default: 80
  object_store:
    name: swift
    namespace: null
    hosts:
      default: ceph-rgw
    host_fqdn_override:
      default: null
    path:
      default: /swift/v1
    scheme:
      default: http
    port:
      api:
        default: 8088
  ceph_mon:
    namespace: null
    hosts:
      default: ceph-mon
    host_fqdn_override:
      default: null
    port:
      mon:
        default: 6789</pre>
<p>Ext4文件系统上基于目录的OSD配置，覆盖值示例：</p>
<pre class="crayon-plain-tag">network:
  public: 10.0.0.0/8
  cluster: 10.0.0.0/8

conf:
  ceph:
    config:
      global:
        # Ext4文件系统
        filestore_xattr_use_omap: true
      osd:
        ms_bind_port_max: 7100
        # Ext4文件系统
        osd_max_object_name_len: 256
        osd_max_object_namespace_len: 64
        osd_crush_update_on_start : false

ceph:
  storage:
    osd_directory: /var/lib/ceph-helm
    mon_directory: /var/lib/ceph-helm
    mon_log: /var/log/ceph/mon
    osd_log: /var/log/ceph/osd

# 和操作系统共享一个分区，基于目录的OSD
osd_directory:
  enabled: true

storageclass:
  name: ceph-rbd
  pool: rbd</pre>
<div class="blog_h3"><span class="graybg">创建K8S资源</span></div>
<p>为Ceph创建名字空间：</p>
<pre class="crayon-plain-tag">kubectl create namespace ceph</pre>
<p>创建RBAC资源：</p>
<pre class="crayon-plain-tag">kubectl create -f ceph-helm/ceph/rbac.yaml</pre>
<p>为了部署Ceph集群，需要为K8S集群中，不同角色（参与到Ceph集群中的角色）的节点添加标签： </p>
<ol>
<li>ceph-mon=enabled，部署mon的节点上添加</li>
<li>ceph-mgr=enabled，部署mgr的节点上添加</li>
<li>ceph-osd=enabled，部署基于设备、基于目录的OSD的节点上添加</li>
<li>ceph-osd-device-NAME=enabled。部署基于设备的OSD的节点上添加，其中NAME需要替换为上面 ceph-overrides.yaml中的OSD设备名，即：
<ol>
<li>ceph-osd-device-dev-vdb=enabled</li>
<li>ceph-osd-device-dev-vdc=enabled</li>
</ol>
</li>
</ol>
<p>对应的K8S命令：</p>
<pre class="crayon-plain-tag"># 部署Ceph Monitor的节点
kubectl label node xenial-100 ceph-mon=enabled ceph-mgr=enabled
# 对于每个OSD节点
kubectl label node xenial-100 ceph-osd=enabled ceph-osd-device-dev-vdb=enabled ceph-osd-device-dev-vdc=enabled
kubectl label node xenial-101 ceph-osd=enabled ceph-osd-device-dev-vdb=enabled ceph-osd-device-dev-vdc=enabled</pre>
<div class="blog_h3"><span class="graybg">Release</span></div>
<pre class="crayon-plain-tag">helm install --name=ceph local/ceph --namespace=ceph -f ceph-overrides.yaml</pre>
<div class="blog_h3"><span class="graybg">检查状态</span></div>
<p>确保所有Pod正常运行：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph get pods
NAME                                    READY     STATUS    RESTARTS   AGE
ceph-mds-7cb7c647c7-7w6pc               0/1       Pending   0          18h
ceph-mgr-66cb85cbc6-hsm65               1/1       Running   3          1h
ceph-mon-check-758b88d88b-2r975         1/1       Running   1          1h
ceph-mon-gvtq6                          3/3       Running   3          1h
ceph-osd-dev-vdb-clj5f                  1/1       Running   15         1h
ceph-osd-dev-vdb-hldw5                  1/1       Running   15         1h
ceph-osd-dev-vdb-l4v6t                  1/1       Running   15         1h
ceph-osd-dev-vdb-v5jmd                  1/1       Running   15         1h
ceph-osd-dev-vdb-wm4v4                  1/1       Running   15         1h
ceph-osd-dev-vdb-zwr65                  1/1       Running   15         1h
ceph-osd-dev-vdc-27wfk                  1/1       Running   15         1h
ceph-osd-dev-vdc-4w4fn                  1/1       Running   15         1h
ceph-osd-dev-vdc-cpkxh                  1/1       Running   15         1h
ceph-osd-dev-vdc-twmwq                  1/1       Running   15         1h
ceph-osd-dev-vdc-x8tpb                  1/1       Running   15         1h
ceph-osd-dev-vdc-zfrll                  1/1       Running   15         1h
ceph-rbd-provisioner-5544dcbcf5-n846s   1/1       Running   4          18h
ceph-rbd-provisioner-5544dcbcf5-t84bz   1/1       Running   3          18h
ceph-rgw-7f97b5b85d-nc5fq               0/1       Pending   0          18h</pre>
<p>其中MDS、RGW的Pod处于Pending状态，这是由于没有给任何节点添加标签：</p>
<pre class="crayon-plain-tag"># rgw即RADOS Gateway，是Ceph的对象存储网关服务，它是基于librados接口封装的FastCGI服务
# 提供存储和管理对象数据的REST API。对象存储适用于图片、视频等各类文件
# rgw兼容常见的对象存储API，例如绝大部分Amazon S3 API、OpenStack Swift API
ceph-rgw=enabled
# mds即Metadata Server，用于支持文件系统
ceph-mds=enabled</pre>
<p>现在从监控节点，检查一下Ceph集群的状态：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph exec -ti ceph-mon-gvtq6 -c ceph-mon -- ceph -s
  cluster:
    # 集群标识符
    id:     08adecc5-72b1-4c57-b5b7-a543cd8295e7
    health: HEALTH_OK
 
  services:
    # 监控节点
    mon: 1 daemons, quorum xenial-100
    # 管理节点
    mgr: xenial-100(active)
    # OSD（Ceph Data Storage Daemon）
    osd: 12 osds: 12 up, 12 in
  
  data:
    # 存储池、PG数量
    pools:   0 pools, 0 pgs
    # 对象数量
    objects: 0 objects, 0 bytes
    # 磁盘的用量，如果是基于文件系统的OSD，则操作系统用量也计算在其中
    usage:   1292 MB used, 322 GB / 323 GB avail

    # 所有PG都未激活，不可用
    pgs:     100.000% pgs not active
             # undersize是由于OSD数量不足（复制份数3，此时仅仅一个OSD），peerd表示128个PG配对到OSD
             128 undersized+peered
    # 将复制份数设置为1后，输出变为
    pgs:     100.000% pgs not active
             128 creating+peering
    # 过了一小段时间后，输出变为
    pgs: 128 active+clean
    # 到这里，PVC才能被提供，否则PVC状态显示 Provisioning，Provisioner日志中出现类似下面的：
    # attempting to acquire leader lease...
    # successfully acquired lease to provision for pvc ceph/ceph-pvc
    # stopped trying to renew lease to provision for pvc ceph/ceph-pvc, timeout reached</pre>
<p>如果K8S集群没有默认StorageClass，可以设置：</p>
<pre class="crayon-plain-tag">kubectl patch storageclass ceph-rbd -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'</pre>
<p>这样没有显式声明StorageClass的PVC将自动通过ceph-rbd进行卷提供。 </p>
<div class="blog_h3"><span class="graybg">创建存储池</span></div>
<pre class="crayon-plain-tag"># 创建具有384个PG的名为rbd的复制存储池
ceph osd pool create rbd 384 replicated
ceph osd pool set rbd min_size 1

# 开发环境下，可以把Replica份数设置为1
ceph osd pool set rbd size 1
# min_size 会自动被设置的比size小
# 减小size后，可以立即看到ceph osd status的used变小

# 初始化池，最好在所有节点加入后，调整好CURSH Map后执行
rbd pool init rbd

# 可以创建额外的用户，例如下面的，配合Value storageclass.user_id=k8s使用
ceph auth get-or-create-key client.k8s mon 'allow r' osd 'allow rwx pool=rbd' | base64
# 如果使用默认用户admin，则不需要生成上面这步。admin权限也是足够的


# 其它命令
# 查看块设备使用情况（需要MGR）
ceph osd status
+----+------------+-------+-------+--------+---------+--------+---------+-----------+
| id |    host    |  used | avail | wr ops | wr data | rd ops | rd data |   state   |
+----+------------+-------+-------+--------+---------+--------+---------+-----------+
| 0  | xenial-100 |  231M | 26.7G |    0   |  3276   |    0   |     0   | exists,up |
| 1  | xenial-103 |  216M | 26.7G |    0   |   819   |    0   |     0   | exists,up |
| 2  | xenial-101 |  253M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
| 3  | xenial-103 |  286M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
| 4  | xenial-101 |  224M | 26.7G |    0   |  1638   |    0   |     0   | exists,up |
| 5  | xenial-105 |  211M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
| 6  | xenial-100 |  243M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
| 7  | xenial-102 |  224M | 26.7G |    0   |  2457   |    0   |     0   | exists,up |
| 8  | xenial-102 |  269M | 26.7G |    0   |  1638   |    0   |     0   | exists,up |
| 9  | xenial-104 |  252M | 26.7G |    0   |  2457   |    0   |     0   | exists,up |
| 10 | xenial-104 |  231M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
| 11 | xenial-105 |  206M | 26.7G |    0   |     0   |    0   |     0   | exists,up |
+----+------------+-------+-------+--------+---------+--------+---------+-----------+</pre>
<div class="blog_h3"><span class="graybg">使用存储池</span></div>
<p>可以先使用ceph命令尝试创建RBD并挂载：</p>
<pre class="crayon-plain-tag"># 镜像格式默认2
# format 1 - 此格式兼容所有版本的 librbd 和内核模块，但是不支持较新的功能，像克隆。此格式目前已经废弃
# 2 - librbd 和 3.11 版以上内核模块才支持。此格式增加了克隆支持，未来扩展更容易
rbd create  test --size 1G --image-format 2 --image-feature layering

# 映射为本地块设备，如果卡住，可能有问题，一段时间后会有提示
rbd map test
# CentOS 7 下可能出现如下问题：
#   rbd: sysfs write failed
#   In some cases useful info is found in syslog - try "dmesg | tail".
#   rbd: map failed: (5) Input/output error
# dmesg | tail
#   [1180891.928386] libceph: mon0 10.5.39.41:6789 feature set mismatch, 
#     my 2b84a042a42 &lt; server's 40102b84a042a42, missing 401000000000000                                            
#   [1180891.934804] libceph: mon0 10.5.39.41:6789 socket error on read
# 解决办法是把Bucket算法从straw2改为straw

# 挂载为目录
fdisk /dev/rbd0
mkfs.ext4 /dev/rbd0
mkdir /test
mount /dev/rbd0 /test

# 测试性能
# 1MB块写入
sync; dd if=/dev/zero of=/test/data bs=1M count=512; sync
# 512+0 records in
# 512+0 records out
# 536870912 bytes (537 MB) copied, 4.44723 s, 121 MB/s
# 16K随机写
fio -filename=/dev/rbd0 -direct=1 -iodepth 1 -thread -rw=randwrite -ioengine=psync -bs=16k -size=512M -numjobs=30 -runtime=60 -name=test
# WRITE: bw=35.7MiB/s (37.5MB/s), 35.7MiB/s-35.7MiB/s (37.5MB/s-37.5MB/s), io=2148MiB (2252MB), run=60111-60111msec 
# 16K随机读
fio -filename=/dev/rbd0 -direct=1 -iodepth 1 -thread -rw=randread -ioengine=psync -bs=16k -size=512M -numjobs=30 -runtime=60 -name=test
# READ: bw=110MiB/s (116MB/s), 110MiB/s-110MiB/s (116MB/s-116MB/s), io=6622MiB (6943MB), run=60037-60037msec

# 删除测试镜像
umount /test
rbd unmap test
rbd remove test</pre>
<p>确认Ceph RBD可以挂载、读写后，创建一个PVC：</p>
<pre class="crayon-plain-tag">kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: ceph-pvc
  namespace: ceph
spec:
  accessModes:
   - ReadWriteOnce
  resources:
    requests:
       storage: 1Gi
  storageClassName: ceph-rbd</pre>
<p>查看PVC是否绑定到PV：</p>
<pre class="crayon-plain-tag">kubectl -n ceph create -f ceph-pvc.yaml

kubectl -n ceph get pvc

# NAME       STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# ceph-pvc   Bound     pvc-43caef06-46b4-11e8-bed8-deadbeef00a0   1Gi        RWO            ceph-rbd       3s

# 在Monitor节点上确认RBD设备已经创建
rbd ls
# kubernetes-dynamic-pvc-fbddb77d-46b5-11e8-9204-8a12961e4b47
rbd info kubernetes-dynamic-pvc-fbddb77d-46b5-11e8-9204-8a12961e4b47
# rbd image 'kubernetes-dynamic-pvc-fbddb77d-46b5-11e8-9204-8a12961e4b47':
#         size 128 MB in 32 objects
#         order 22 (4096 kB objects)
#         block_name_prefix: rbd_data.11412ae8944a
#         format: 2
#         features: layering
#         flags: 
#         create_timestamp: Mon Apr 23 05:20:07 2018</pre>
<p>需要在其它命名空间中使用此存储池时，拷贝一下Secret：</p>
<pre class="crayon-plain-tag">kubectl -n ceph get secrets/pvc-ceph-client-key -o json --export | jq '.metadata.namespace = "default"' | kubectl create -f - </pre>
<div class="blog_h3"><span class="graybg">卸载</span></div>
<pre class="crayon-plain-tag">helm delete ceph --purge
kubectl delete namespace ceph</pre>
<p>此外，如果要<span style="background-color: #c0c0c0;">重新安装，一定要把所有节点的一下目录清除掉</span>：</p>
<pre class="crayon-plain-tag">rm -rf /var/lib/ceph-helm
rm -rf /var/lib/ceph</pre>
<div class="blog_h2"><span class="graybg">既有Ceph集群</span></div>
<p>只需要安装相应的Provisioner，配置适当的StorageClass即可。示例：</p>
<ol>
<li>Provisioner：<a href="https://git.gmem.cc/alex/helm-charts/src/branch/master/ceph-provisioners">https://git.gmem.cc/alex/helm-charts/src/branch/master/ceph-provisioners</a></li>
<li>安装脚本：<a href="https://git.gmem.cc/alex/k8s-init/src/branch/master/4.infrastructure/0.ceph-external.sh">https://git.gmem.cc/alex/k8s-init/src/branch/master/4.infrastructure/0.ceph-external.sh</a></li>
</ol>
<div class="blog_h3"><span class="graybg">基于CephFS的卷</span></div>
<p>Kubernetes卷的动态Provisioning，目前需要依赖于external-storage项目，K8S没有提供内置的Provisioner。此项目存在不少问题，生产环境下可以考虑<a href="/kubernetes-study-note#static-provisioning-ceph">静态提供</a>。</p>
<p>Provisioner会自动在Ceph集群的默认CephFS中创建“卷”，Ceph支持基于libcephfs+librados来<span style="background-color: #c0c0c0;">实现一个基于CephFS目录的虚拟卷</span>。</p>
<p>你可以在默认CephFS中看到volumes/kubernetes目录。kubernetes目录对应一个虚拟卷组。每个PV对应了它的一个子目录。</p>
<div class="blog_h1"><span class="graybg">性能调优</span></div>
<div class="blog_h2"><span class="graybg">最佳实践</span></div>
<div class="blog_h3"><span class="graybg">基本</span></div>
<ol>
<li>监控节点对于集群的正确运行非常重要，应当为其分配独立的硬件资源。如果跨数据中心部署，监控节点应该分散在不同数据中心或者可用性区域</li>
<li><span style="background-color: #c0c0c0;">日志可能会让集群的吞吐量减半</span>。理想情况下，应该<span style="background-color: #c0c0c0;">在不同磁盘上运行操作系统、OSD数据、OSD日志</span>。对于高吞吐量工作负载，<span style="background-color: #c0c0c0;">考虑使用SSD进行日志存储</span></li>
<li>纠删编码（Erasure coding）可以用于存储大容量的<span style="background-color: #c0c0c0;">一次性写、非频繁读、性能要求不高</span>的数据。纠删编码存储消耗小，但是IOPS也降低</li>
<li>目录项（Dentry）和inode缓存可以提升性能，特别是存在很多小对象的情况下</li>
<li>使用<span style="background-color: #c0c0c0;">缓存分层（Cache Tiering）可以大大提升集群性能</span>。此技术可以在热、冷Tier之间自动的进行数据迁移。为了最大化性能，请<span style="background-color: #c0c0c0;">使用SSD作为缓存池、并且在低延迟节点上部署缓存池</span></li>
<li>部署奇数个数的监控节点，以便仲裁投票（Quorum Voting），建议使用3-5个节点。更多的节点可以增强集群的健壮性，但是mon之间需要保持数据同步，这会影响性能</li>
<li>诊断性能问题的时候，总是从最底层（磁盘、网络）开始，然后再检查块设备、对象网关等高层接口</li>
<li>在大型集群里用<span style="background-color: #c0c0c0;">单独的集群网络（Cluster Newwork）可显著地提升性能和安全性</span></li>
</ol>
<div class="blog_h3"><span class="graybg">文件系统</span></div>
<ol>
<li>限制最大文件大小，创建极大的文件会导致删除过程很缓慢</li>
<li>避免在生产环境下使用试验特性。你应该使用<span style="background-color: #c0c0c0;">单个活动MDS、不使用快照（默认如此）</span></li>
<li>避免增大max_mds，可能导致大于1个的MDS处于Active</li>
<li>客户端选择：
<ol>
<li>FUSE，容易使用、容易升级和服务器集群保持一致</li>
<li>内核，性能好</li>
<li>不同客户端的功能并不完全一致，例如FUSE支持客户端配额，内核不支持</li>
</ol>
</li>
<li>对于Ceph 10.x，最好使用4.x内核。如果必须使用老内核，你应该使用FUSE作为客户端</li>
</ol>
<div class="blog_h2"><span class="graybg">基础设施要求</span></div>
<div class="blog_h3"><span class="graybg">处理器</span></div>
<p>OSD需要消耗CPU资源，可以将其绑定到一个核心上。如果使用纠删码，则需要更多的CPU资源。此外，集群处于Recovery状态时，OSD的CPU消耗显著增加</p>
<p>MON不怎么消耗CPU资源，几个G内存的单核心物理机即可。</p>
<p>MDS相当消耗CPU资源，考虑4核心或更多CPU。如果依赖于CephFS处理大量工作，应当分配专用物理机</p>
<div class="blog_h3"><span class="graybg">内存</span></div>
<p>MON/MDS需要不少于2G内存。OSD通常需要1G内存（和存储容量有关）。此外，集群处于Recovery状态时，OSD的内存消耗显著增加，因此配备2G内存更好</p>
<div class="blog_h3"><span class="graybg">网络</span></div>
<p>最好具有万兆网络，公共网络、集群网络需要物理隔离（双网卡连接到独立交换机）。对于数百TB规模的集群，千兆网络也能够正常工作</p>
<p>集群网络往往消耗更多的带宽，此外，高性能的集群网络对于Recovery的效率很重要。</p>
<p>如果交换机支持，应当启用Jumbo帧，可以提升网络吞吐量。</p>
<div class="blog_h3"><span class="graybg">磁盘</span></div>
<p>在生产环境下，最好让OSD使用独立的驱动器，如果和OS共享驱动，最好使用独立的分区。</p>
<p>通常使用SATA SSD作为日志存储，预算足够可以考虑PCIE SSD。<a href="http://www.sebastien-han.fr/blog/2014/10/10/ceph-how-to-test-if-your-ssd-is-suitable-as-a-journal-device/">Intel S3500</a>的4K随机写 IOPS可达10K+</p>
<p>关于RAID：</p>
<ol>
<li>最好不要使用RAID</li>
<li>如果有RAID卡，并且磁盘数量太多，而对应的内存数量不足（每个OSD大概需要2G内存），可以RAID0</li>
<li>不要使用RAID5，因为随机IO的性能降低</li>
</ol>
<p>关于filestore：</p>
<ol>
<li>建议使用SSD存储日志，以减少访问时间、读取延迟，实现吞吐量的显著提升</li>
<li>可以为创建SSD分区，每个分区作为一个OSD的日志存储，但是最好不要超过4个</li>
</ol>
<div class="blog_h3"><span class="graybg">BIOS设置</span></div>
<ol>
<li>启用超线程Hyper-Threading技术</li>
<li>关闭节能</li>
<li>关闭NUMA</li>
</ol>
<div class="blog_h3"><span class="graybg">内核参数</span></div>
<pre class="crayon-plain-tag"># 修改pid max
# 执行命令
echo 4194303 &gt; /proc/sys/kernel/pid_max
# 或者
sysctl -w kernel.pid_max=4194303

# read_ahead, 数据预读到内存，提升磁盘读操作能力
echo "8192" &gt; /sys/block/sda/queue/read_ahead_kb

# 禁用交换文件
echo "vm.swappiness = 0" | tee -a /etc/sysctl.conf

# I/O Scheduler：SSD使用用noop，SATA/SAS使用deadline
echo "deadline" &gt; /sys/block/sda/queue/scheduler
echo "noop" &gt; /sys/block/sda/queue/scheduler</pre>
<div class="blog_h3"><span class="graybg">文件系统</span></div>
<p>底层文件系统的稳定性和性能对于Ceph很重要。在开发、非关键部署时可以使用btrfs，这也是未来的方向。关键的生产环境下应该使用XFS。</p>
<p>在高可扩容性的存储环境下，XFS和btrfs相比起ext3/4有很大<span style="background-color: #c0c0c0;">优势</span>。<span style="background-color: #c0c0c0;">XFS和btrfs都是日志式文件系统，更健壮，容易从崩溃、断电中恢复</span>。日志文件系统会在执行写操作之前，把需要进行的变更记录到日志。</p>
<p>OSD依赖于底层文件系统的扩展属性（Extended Attributes，XATTRs），来存储各种内部对象状态和属性。XFS支持64KB的XATTRs，但是ext4就太小了，你应该为运行在ext4上的OSD配置：</p>
<pre class="crayon-plain-tag"># 新版本Ceph此配置项已经没了
filestore xattr use omap = true</pre>
<p>关于文件系统的一些知识：</p>
<ol>
<li>XFS 、 btrfs 和 ext4 都是日志文件系统</li>
<li>XFS很成熟</li>
<li>btrfs相对年轻，他是一个写时复制（COW）文件系统，因而支持可写文件系统快照。此外它还支持透明压缩、完整性校验</li>
</ol>
<div class="blog_h2"><span class="graybg">归置组</span></div>
<div class="blog_h3"><span class="graybg">PG数量</span></div>
<p>PG的数量应当总是和PGP相同。PGP是为了实现定位而创建的PG。再平衡仅仅再pgp_num被修改后才会触发，仅仅修改pg_num不会触发。</p>
<p>随着OSD数量的变化，选取适当的PG数量很重要。因为PG数量对<span style="background-color: #c0c0c0;">集群行为、数据持久性</span>（Durability，灾难性事件发生时保证数据堡丢失）有很大影响。此外，归置组很耗计算资源，所以<span style="background-color: #c0c0c0;">很多存储池x很多归置组会导致性能下降</span>。建议的取值：</p>
<ol>
<li>对于小于5个OSD的集群：设置为128</li>
<li>5-10个OSD的集群：设置为512</li>
<li>10-50个OSD的集群：设置为1024</li>
<li>超过50个OSD的集群，需要自己权衡，利用pgcalc来计算适合的PG数量</li>
</ol>
<p>《Ceph分布式存储学习指南》一书中建议的PG数量算法：</p>
<p style="padding-left: 30px;">每个池的PG数量 = OSD总数 * 100 / 最大副本数 / 池数</p>
<p>计算结果需要向上舍入到2的N次幂。此外该书倾向于让所有池具有相同的PG数量。</p>
<div class="blog_h2"><span class="graybg">再平衡</span></div>
<ol>
<li>加入新的OSD后，考虑设置权重为0，然后逐渐增加权重，这样可以避免性能下降</li>
</ol>
<div class="blog_h1"><span class="graybg">监控</span></div>
<div class="blog_h2"><span class="graybg">ceph-exporter</span></div>
<p>DigitalOcean开源了Ceph的Exporter，本文使用<a href="https://github.com/gmemcc/ceph_exporter">gmemcc的fork</a>版本。Ceph Exporter和MON节点通信，所有信息都通过rados_mon_command()调用获得。</p>
<p>此Exporter可以在任意Ceph客户端节点上运行，和任何形式的Ceph客户端一样，你需要提供ceph.conf、ceph.USER.keyring两个配置文件。</p>
<div class="blog_h3"><span class="graybg">常用选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>telemetry.addr</td>
<td>监听的地址和端口，示例：*:9100</td>
</tr>
<tr>
<td>telemetry.path</td>
<td>查询指标的URL路径，示例：/metrics</td>
</tr>
<tr>
<td>ceph.config</td>
<td>Ceph配置文件路径</td>
</tr>
<tr>
<td>ceph.user</td>
<td>使用的Ceph用户，示例：admin</td>
</tr>
<tr>
<td>exporter.config</td>
<td>Ceph Exporter配置文件的位置，示例：/etc/ceph/exporter.yml</td>
</tr>
<tr>
<td>rgw.mode</td>
<td>是否收集RGW的指标：0禁用，1启用，2后台</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">构建镜像</span></div>
<p>你可以直接使用预构建好的镜像：<pre class="crayon-plain-tag">digitalocean/ceph_exporter:2.0.1-luminous</pre></p>
<p>或者，从源码构建：</p>
<pre class="crayon-plain-tag">sudo apt install librados-dev

git clone https://github.com/gmemcc/ceph_exporter.git
cd ceph_exporter
go install
make
docker build -t docker.gmem.cc/digitalocean/ceph_exporter .</pre>
<div class="blog_h2"><span class="graybg">内置Prometheus支持</span></div>
<p>对于Luminous12.2或者Mimic13.2版本，MGR已经内置了Prometheus模块，不再需要ceph_exporter了。</p>
<p>执行下面的命令启用Prometheus模块：</p>
<pre class="crayon-plain-tag">ceph mgr module enable prometheus</pre>
<p>然后，你就可以访问任意MGR节点的http://MGR_HOST:9283/metrics，抓取指标了。Prometheus配置示例：</p>
<pre class="crayon-plain-tag">scrape_configs:               
- job_name: ceph                           
  static_configs:                          
  - targets:
    # 列出所有MGR节点，防止故障转移时数据丢失                      
    - 10.0.1.1:9283                        
    labels:                                
      cluster: ceph</pre>
<p>Grafana仪表盘可以参考这个示例：<a href="https://grafana.com/dashboards/7056">https://grafana.com/dashboards/7056</a></p>
<div class="blog_h1"><span class="graybg">附录</span></div>
<div class="blog_h2"><span class="graybg">CEPH_FEATURE</span></div>
<p>R表示必须支持的特性，S表示该版本内核可以支持，-*-表示从这个版本开始支持。</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 20%; text-align: center;" align="left"><strong>Feature</strong></td>
<td style="width: 20%; text-align: center;" align="right"><strong>OCT</strong></td>
<td align="center"><strong>3.8</strong></td>
<td align="center"><strong>3.9</strong></td>
<td align="center"><strong>3.10</strong></td>
<td align="center"><strong>3.14</strong></td>
<td align="center"><strong>3.15</strong></td>
<td align="center"><strong>3.18</strong></td>
<td align="center"><strong>4.1</strong></td>
<td align="center"><strong>4.5</strong></td>
<td align="center"><strong>4.6</strong></td>
</tr>
</tbody>
<tbody>
<tr>
<td align="left">CEPH_FEATURE_NOSRCADDR</td>
<td align="right">2</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_SUBSCRIBE2</td>
<td align="right">10</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-R-</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_RECONNECT_SEQ</td>
<td align="right">40</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-R-</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_PGID64</td>
<td align="right">200</td>
<td align="center"> </td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_PGPOOL3</td>
<td align="right">800</td>
<td align="center"> </td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSDENC</td>
<td align="right">2000</td>
<td align="center"> </td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
<td align="center">R</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_TUNABLES</td>
<td align="right">40000</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_MSG_AUTH</td>
<td align="right">800000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_TUNABLES2</td>
<td align="right">2000000</td>
<td align="center"> </td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_REPLY_CREATE_INODE</td>
<td align="right">8000000</td>
<td align="center"> </td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSDHASHPSPOOL</td>
<td align="right">40000000</td>
<td align="center"> </td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSD_CACHEPOOL</td>
<td align="right">800000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_V2</td>
<td align="right">1000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_EXPORT_PEER</td>
<td align="right">2000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSD_ERASURE_CODES***</td>
<td align="right">4000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSDMAP_ENC</td>
<td align="right">8000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_TUNABLES3</td>
<td align="right">20000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_OSD_PRIMARY_AFFINITY</td>
<td align="right">20000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_V4 ****</td>
<td align="right">1000000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_CRUSH_TUNABLES5</td>
<td align="right">200000000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
</tr>
<tr>
<td align="left">CEPH_FEATURE_NEW_OSDOPREPLY_ENCODING</td>
<td align="right">400000000000000</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">-S-</td>
<td align="center">S</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">版本代码</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">代码</td>
<td style="text-align: center;">版本</td>
</tr>
</thead>
<tbody>
<tr>
<td>Luminous</td>
<td>12</td>
</tr>
<tr>
<td>Jewel</td>
<td>10</td>
</tr>
<tr>
<td colspan="2"><em>已归档版本</em></td>
</tr>
<tr>
<td>argonaut</td>
<td>0.48</td>
</tr>
<tr>
<td>bobtail</td>
<td>0.56</td>
</tr>
<tr>
<td>Cuttlefish</td>
<td>0.61</td>
</tr>
<tr>
<td>Dumpling</td>
<td>0.67 </td>
</tr>
<tr>
<td>Emperor</td>
<td>0.72 </td>
</tr>
<tr>
<td>Firefly</td>
<td>0.80 </td>
</tr>
<tr>
<td>Giant</td>
<td>0.87 </td>
</tr>
<tr>
<td>Hammer</td>
<td>0.94 </td>
</tr>
<tr>
<td>Infernalis</td>
<td>9.2.0 </td>
</tr>
<tr>
<td>Kraken</td>
<td>11.0.2 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">参考资源</span></div>
<ol>
<li><a href="https://tobegit3hub1.gitbooks.io/ceph_from_scratch/content/">Ceph From Scratch</a></li>
<li><a href="http://docs.ceph.com/docs/giant/rados/operations/control/">CEPH CONTROL COMMANDS</a></li>
</ol>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">Firefly</span></div>
<ol>
<li><span style="background-color: #c0c0c0;">纠删码</span>支持</li>
<li><span style="background-color: #c0c0c0;">缓存分层</span></li>
<li>键/值 OSD后端</li>
<li>独立的RadosGW（使用civetweb）</li>
</ol>
<div class="blog_h2"><span class="graybg">Giant</span></div>
<ol>
<li>LRC纠删码</li>
<li>CephFS日志恢复，诊断工具</li>
</ol>
<div class="blog_h2"><span class="graybg">Hammer</span></div>
<ol>
<li>RGW对象版本化</li>
<li>对象桶分片</li>
<li><span style="background-color: #c0c0c0;">Crush straw2算法</span></li>
</ol>
<div class="blog_h2"><span class="graybg">Infernalis</span></div>
<ol>
<li>纠删码到达稳定，支持很多新特性</li>
<li>支持Swift API新特性，例如对象过期设置</li>
</ol>
<div class="blog_h2"><span class="graybg">Jewel</span></div>
<ol>
<li><span style="background-color: #c0c0c0;">CephFS到达稳定</span></li>
<li>RGW多站点可达（支持主/主配置）</li>
<li>AWS4兼容</li>
<li><span style="background-color: #c0c0c0;">RBD镜像（mirroring）</span></li>
<li>引入BlueStore</li>
</ol>
<div class="blog_h2"><span class="graybg">Kraken</span></div>
<ol>
<li>BlueStore到达稳定</li>
<li>AsyncMessenger</li>
<li>RGW：通过ES来索引元数据</li>
<li>S3桶生命周期API支持</li>
<li>RGW：支持导出为NFS v3接口</li>
<li>Rados支持在基于纠删码的池上进行overwrite操作</li>
<li>基于纠删码池的RBD卷</li>
</ol>
<div class="blog_h2"><span class="graybg">Luminous</span></div>
<ol>
<li>集成Web仪表盘Ceph Dashboard</li>
<li>直接管理裸设备的<span style="background-color: #c0c0c0;">BlueStore到达稳定并且作为默认选项</span></li>
<li>纠删码池完全支持overwirte，可以和CephFS/RDB一起使用</li>
<li>引入<span style="background-color: #c0c0c0;">组件ceph-mgr</span>，如果该组件停止，则指标不会更新，某些依赖于指标的请求，例如ceph df，无法工作</li>
<li>可扩容能力提升，可部署10000OSD的集群</li>
<li>每个<span style="background-color: #c0c0c0;">OSD支持关联一个设备类</span>（例如hdd/ssd），允许CRUSH规则将数据简单地映射到系统中的设备的子集。通常不需要手动编写CRUSH规则或手动编辑CRUSH</li>
<li>支持优化CRUSH权重，以保持OSD之间数据的近乎完美的分布</li>
<li>OSD可以根据后端设备是HDD还是SSD来调整其默认配置</li>
<li><span style="background-color: #c0c0c0;">RGW引入了上传对象的服务器端加密</span>，用于管理加密密钥的三个选项有：自动加密（仅推荐用于测试设置），客户提供的类似于Amazon SSE-C规范的密钥，以及通过使用外部密钥管理服务</li>
<li>RGW具有初步的类似AWS的存储桶策略API支持。现在，策略是一种表达一系列新授权概念的方式。未来，这将成为附加身份验证功能的基础，例如STS和组策略等</li>
<li>RGW通过使用rados命名空间合并了几个元数据索引池</li>
<li>引入组件<span style="background-color: #c0c0c0;">rbd-mirror，负责RBD卷的镜像复制</span></li>
<li>rbd trash命令支持延迟的镜像删除</li>
<li>镜像可以通过rbd mirroringreplay delay配置选项支持可配置的复制延迟</li>
<li><span style="background-color: #c0c0c0;">多Active MDS到达稳定状态</span></li>
</ol>
<div class="blog_h2"><span class="graybg"> Mimic</span></div>
<ol>
<li>引入一个新的、全功能和美观的<span style="background-color: #c0c0c0;">仪表盘 Dashboard V2</span></li>
<li>RADOS：配置选项可被mon中心化存储和管理</li>
<li>RADOS：在恢复和再平衡时，mon使用的磁盘大大减小</li>
<li>RADOS：引入一个异步恢复特性，减少在OSD从错误恢复期间，请求的tail latency</li>
<li>RGW：支持将一个Zone（或者Buckets的子集）复制到外部云服务，例如S3</li>
<li>RGW：支持S3多因子身份验证</li>
<li>RGW：前端Beast到达stable</li>
<li>CephFS：使用多MDS时快照功能到达stable</li>
<li>RBD：镜像克隆不需要显式的protect/unprotect步骤</li>
<li>RBD：<span style="background-color: #c0c0c0;">镜像支持深克隆</span>（包含parent镜像、关联快照的数据的克隆）到新池，支持修改数据布局</li>
</ol>
<div class="blog_h2"><span class="graybg">Nautilus</span></div>
<ol>
<li><span style="background-color: #c0c0c0;">仪表盘：增加很多新功能</span>，全方位的查看各种指标，对Ceph进行管理</li>
<li>RADOS：每个<span style="background-color: #c0c0c0;">池的PG数量 ，现在可以随时减小</span>了。集群可以根据用量和管理员的提示，自动优化PG数量</li>
<li>RADOS：v2 wire protocol支持传输加密</li>
<li>RADOS：mon/osd使用的物理设备的健康指标（例如SMART）可以得到跟踪，并且在预期出现磁盘失败前进行警告</li>
<li>RADOS：在recovery/backfill时，OSD更有效的优先恢复重要的PG、对象</li>
<li>RADOS：例如磁盘失败后的恢复，这样长期运行的后台操作的进度，在ceph status中报告</li>
<li>RGW：<span style="background-color: #c0c0c0;">Beast替换civetweb作为默认Web前端</span></li>
<li>CephFS：MDS稳定性得到很大优化，特别是针对大缓存、长时间运行的具有大内存的客户端</li>
<li>CephFS：在Rook管理的环境下，<span style="background-color: #c0c0c0;">CephFS可以通过NFS-Ganesha集群暴露出去</span></li>
<li>CephFS：支持查询进行中的<span style="background-color: #c0c0c0;">针对MDS的scrub的进度</span></li>
<li>RBD：镜像可以在最小宕机时间内进行迁移，这可以支持在池之间迁移镜像、修改镜像布局</li>
<li>RBD：rbd perf image iotop 以及 rbd perf image iostat命令可以<span style="background-color: #c0c0c0;">提供iostat风格的、针对所有RBD镜像的监控</span></li>
<li>RBD：ceph-mgr的Prometheus Exporter支持暴露所有RBD镜像的IO监控指标</li>
<li>RBD：在一个池中，可以<span style="background-color: #c0c0c0;">使用命名空间来隔离RBD</span>，实现多租户</li>
</ol>
<div class="blog_h2"><span class="graybg">Octopus</span></div>
<ol>
<li>新的工具cephadm，用于支持容器化部署</li>
<li>Health警告可以被临时/永久的静默</li>
<li>Dashboard：UI增强、安全性增强</li>
<li>管理功能的增强</li>
<li>RADOS：从N版引入的PG括缩，默认启用</li>
<li>RADOS：Bluestore包含了多项改进和性能增强</li>
<li>RBD：镜像支持一种新的，基于快照的模式，不再依赖于journaling特性</li>
<li>RBD：克隆操作保持源镜像的稀疏性（sparseness）</li>
<li>RBD：改进了rbd-nbd 工具，可以使用新的内核特性</li>
<li>RBD：缓存性能增强</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">CephFS问题诊断</span></div>
<div class="blog_h3"><span class="graybg">无法创建</span></div>
<p>创建新CephFS报错Error EINVAL: pool 'rbd-ssd' already contains some objects. Use an empty pool instead，解决办法：</p>
<pre class="crayon-plain-tag">ceph fs new cephfs rbd-ssd rbd-hdd --force</pre>
<div class="blog_h3"><span class="graybg">mds.0 is damaged</span></div>
<p>断电后出现此问题。MDS进程报错： Error recovering journal 0x200: (5) Input/output error。诊断过程：</p>
<pre class="crayon-plain-tag"># 健康状况
ceph health detail
# HEALTH_ERR mds rank 0 is damaged; mds cluster is degraded
# mds.0 is damaged

# 文件系统详细信息，可以看到唯一的MDS Boron启动不了
ceph fs status
# cephfs - 0 clients
# ======
# +------+--------+-----+----------+-----+------+
# | Rank | State  | MDS | Activity | dns | inos |
# +------+--------+-----+----------+-----+------+
# |  0   | failed |     |          |     |      |
# +------+--------+-----+----------+-----+------+
# +---------+----------+-------+-------+
# |   Pool  |   type   |  used | avail |
# +---------+----------+-------+-------+
# | rbd-ssd | metadata |  138k |  106G |
# | rbd-hdd |   data   | 4903M | 2192G |
# +---------+----------+-------+-------+

# +-------------+
# | Standby MDS |
# +-------------+
# |    Boron    |
# +-------------+

# 显示错误原因
ceph tell mds.0 damage
# terminate called after throwing an instance of 'std::out_of_range'
#   what():  map::at
# Aborted

# 尝试修复，无效
ceph mds repaired 0

# 尝试导出CephFS日志，无效
cephfs-journal-tool journal export backup.bin
# 2019-10-17 16:21:34.179043 7f0670f41fc0 -1 Header 200.00000000 is unreadable
# 2019-10-17 16:21:34.179062 7f0670f41fc0 -1 journal_export: Journal not readable, attempt object-by-object dump with `rados`Error ((5) Input/output error)

# 尝试重日志修复，无效
# 尝试将journal中所有可回收的 inodes/dentries 写到后端存储（如果版本比后端更高）
cephfs-journal-tool event recover_dentries summary
# Events by type:
# Errors: 0
# 2019-10-17 16:22:00.836521 7f2312a86fc0 -1 Header 200.00000000 is unreadable

# 尝试截断日志，无效
cephfs-journal-tool journal reset 
# got error -5from Journaler, failing
# 2019-10-17 16:22:14.263610 7fe6717b1700  0 client.6494353.journaler.resetter(ro) error getting journal off disk
# Error ((5) Input/output error)


# 删除重建，数据丢失
ceph fs rm cephfs  --yes-i-really-mean-it



## 又一次遇到此问题

# 深度清理，发现200.00000000存在数据不一致
ceph osd deep-scrub all
40.14 shard 14: soid 40:292cf221:::200.00000000:head data_digest
  0x6ebfd975 != data_digest 0x9e943993 from auth oi 40:292cf221:::200.00000000:head
  (22366'34 mds.0.902:1 dirty|data_digest|omap_digest s 90 uv 34 dd 9e943993 od ffffffff alloc_hint [0 0 0])                                                                                  
40.14 deep-scrub 0 missing, 1 inconsistent objects
40.14 deep-scrub 1 errors

# 查看RADOS不一致对象详细信息
rados list-inconsistent-obj  40.14  --format=json-pretty
{
    "epoch": 23060,
    "inconsistents": [
        {
            "object": {
                "name": "200.00000000",
            },
            "errors": [],
            "union_shard_errors": [
                # 错误原因，校验信息不一致
                "data_digest_mismatch_info"
            ],
            "selected_object_info": {
                "oid": {
                    "oid": "200.00000000",
                },
            },
            "shards": [
                {
                    "osd": 7,
                    "primary": true,
                    "errors": [],
                    "size": 90,
                    "omap_digest": "0xffffffff"
                },
                {
                    "osd": 14,
                    "primary": false,

# errors：分片之间存在不一致，而且无法确定哪个分片坏掉了，原因：
#    data_digest_mismatch 此副本的摘要信息和主副本不一样
#    size_mismatch 此副本的数据长度和主副本不一致
#    read_error 可能存在磁盘错误
                    "errors": [
                        # 这里的原因是两个副本的摘要不一致
                        "data_digest_mismatch_info"
                    ],
                    "size": 90,
                    "omap_digest": "0xffffffff",
                    "data_digest": "0x6ebfd975"
                }
            ]
        }
    ]
}
# 转为处理inconsistent问题，停止OSD.14，Flush 日志，启动OSD.14，执行PG修复
# 无效…… 执行PG修复后Ceph会自动以权威副本覆盖不一致的副本，但是并非总能生效，
# 例如，这里的情况，主副本的数据摘要信息丢失

# 删除故障对象
rados -p rbd-ssd  rm 200.00000000</pre>
<div class="blog_h2"><span class="graybg">OSD问题诊断</span></div>
<div class="blog_h3"><span class="graybg">启动后立即崩溃</span></div>
<p>通常可以认为属于Ceph的Bug。这些Bug可能因为数据状态引发，有些时候将崩溃OSD的权重清零，可以恢复：</p>
<pre class="crayon-plain-tag"># 尝试解决osd.17启动后立即宕机
ceph osd reweight 17 0</pre>
<div class="blog_h2"><span class="graybg">PG问题诊断</span></div>
<div class="blog_h3"><span class="graybg">所有PG卡在unkown</span></div>
<p>如果创建一个存储池后，其所有PG都卡在此状态，可能原因是CRUSH map不正常。你可以配置osd_crush_update_on_start为true让集群自动调整CRUSH map。</p>
<div class="blog_h3"><span class="graybg">卡在peering</span></div>
<p><span class="graybg">ceph -s显示如下状态，长期不恢复：</span></p>
<pre class="crayon-plain-tag">cluster:                 
    health: HEALTH_WARN                                                    
            Reduced data availability: 2 pgs inactive, 2 pgs peering
            19 slow requests are blocked &gt; 32 sec
  data:
    pgs:     0.391% pgs not active
             510 active+clean
             2   peering</pre>
<p>此案例中，使用此PG的Pod呈Known状态。</p>
<p>检查卡在inactive状态的PG：</p>
<pre class="crayon-plain-tag">ceph pg dump_stuck inactive

PG_STAT STATE   UP     UP_PRIMARY ACTING ACTING_PRIMARY  
17.68   peering [3,12]          3 [3,12]              3
16.32   peering [4,12]          4 [4,12]              4</pre>
<p>输出其中一个PG的诊断信息，片断如下：</p>
<pre class="crayon-plain-tag">// ceph pg 17.68 query
{                                                   
    "info": {                                                
        "stats": {
            "state": "peering",
            "stat_sum": {
                "num_objects_dirty": 5
            },
            "up": [
                3,
                12
            ],
            "acting": [
                3,
                12
            ],
            // 因为哪个OSD而阻塞
            "blocked_by": [
                12
            ],
            "up_primary": 3,
            "acting_primary": 3
        }
    },
    "recovery_state": [
        // 如果顺利，第一个元素应该是 "name": "Started/Primary/Active"
        {
            "name": "Started/Primary/Peering/GetInfo",
            "enter_time": "2018-06-11 18:32:39.594296",
            // 但是，卡在向OSD 12 请求信息这一步上
            "requested_info_from": [
                {
                    "osd": "12"
                }
            ]
        },
        {
            "name": "Started/Primary/Peering",
        },
        {
            "name": "Started",
        }
    ]
}</pre>
<p>没有获得osd-12阻塞Peering的明确原因。 </p>
<p>查看日志，osd-12位于10.0.0.104，osd-3位于10.0.0.100，后者为Primary OSD。</p>
<p>osd-3日志，在18:26开始出现，和所有其它OSD之间心跳检测失败。此时10.0.0.100负载很高，卡死。</p>
<p>osd-12日志，在18:26左右大量出现：</p>
<pre class="crayon-plain-tag">osd.12 466 heartbeat_check: no reply from 10.0.0.100:6803 osd.4 since back 2018-06-11 18:26:44.973982 ...</pre>
<p>直到18:44分仍然无法进行心跳检测，重启osd-12后一切恢复正常。 </p>
<div class="blog_h3"><span class="graybg">incomplete</span></div>
<p>检查无法完成的PG：</p>
<pre class="crayon-plain-tag">ceph pg dump_stuck

# PG_STAT STATE      UP     UP_PRIMARY ACTING ACTING_PRIMARY
# 17.79   incomplete [9,17]          9 [9,17]              9
# 32.1c   incomplete [16,9]         16 [16,9]             16
# 17.30   incomplete [16,9]         16 [16,9]             16
# 31.35   incomplete [9,17]          9 [9,17]              9</pre>
<p>查询PG 17.30的诊断信息：</p>
<pre class="crayon-plain-tag">// ceph pg  17.30 query
{
  "state": "incomplete",
  "info": {
    "pgid": "17.30",
    "stats": {
      // 被osd.11阻塞而无法完成，此osd已经不存在
      "blocked_by": [
        11
      ],
      "up_primary": 16,
      "acting_primary": 16
    }
  },
  // 恢复的历史记录
  "recovery_state": [
    {
      "name": "Started/Primary/Peering/Incomplete",
      "enter_time": "2018-06-17 04:48:45.185352",
      // 最终状态，此PG没有完整的副本
      "comment": "not enough complete instances of this PG"
    },
    {
      "name": "Started/Primary/Peering",
      "enter_time": "2018-06-17 04:48:45.131904",
      "probing_osds": [
        "9",
        "16",
        "17"
      ],
      // 期望检查已经不存在的OSD
      "down_osds_we_would_probe": [
        11
      ],
      "peering_blocked_by_detail": [
        {
          "detail": "peering_blocked_by_history_les_bound"
        }
      ]
    }
  ]
}</pre>
<p>可以看到17.30期望到osd.11寻找权威数据，而osd.11已经永久丢失了。这种情况下，可以尝试强制标记PG为complete。</p>
<p>首先，停止PG的主OSD：<pre class="crayon-plain-tag">service ceph-osd@16 stop</pre>  </p>
<p>然后，运行下面的工具：</p>
<pre class="crayon-plain-tag">ceph-objectstore-tool --data-path /var/lib/ceph/osd/ceph-16  --pgid 17.30 --op mark-complete
# Marking complete 
# Marking complete succeeded</pre>
<p>最后，重启PG的主OSD：<pre class="crayon-plain-tag">service ceph-osd@16 start</pre>  </p>
<div class="blog_h3"><span class="graybg">单副本导致的stale</span></div>
<p>不做副本的情况下，单个OSD宕机即导致数据不可用：</p>
<pre class="crayon-plain-tag">ceph health detail 
# 注意Acting Set仅仅有一个成员
# pg 2.21 is stuck stale for 688.372740, current state stale+active+clean, last acting [7]
# 但是其它PG的Acting Set则不是
# pg 3.4f is active+recovering+degraded, acting [9,1]</pre>
<p>如果OSD的确出现硬件故障，则数据丢失。此外，你也无法对这种PG进行查询操作。</p>
<div class="blog_h3"><span class="graybg">inconsistent</span></div>
<p>定位出问题PG的主OSD，停止它，刷出日志，然后修复PG：</p>
<pre class="crayon-plain-tag">ceph health detail
# HEALTH_ERR 2 scrub errors; Possible data damage: 2 pgs inconsistent
# OSD_SCRUB_ERRORS 2 scrub errors
# PG_DAMAGED Possible data damage: 2 pgs inconsistent
#     pg 15.33 is active+clean+inconsistent, acting [8,9]
#     pg 15.61 is active+clean+inconsistent, acting [8,16]

# 查找OSD所在机器
ceph osd find 8
 
# 登陆到osd.8所在机器
systemctl stop ceph-osd@8.service
ceph-osd -i 8 --flush-journal
systemctl start ceph-osd@8.service
ceph pg repair 15.61</pre>
<div class="blog_h2"><span class="graybg">对象问题诊断</span></div>
<div class="blog_h3"><span class="graybg">unfound</span></div>
<p>持有对象权威副本的OSD宕机或被剔除，会导致该问题出现。例如两个配对的OSD（共同处理某个PG）：</p>
<ol>
<li>osd.1宕机</li>
<li>osd.2独自处理了一些写操作</li>
<li>osd1开机</li>
<li>osd.1+osd2配对，由于osd.2独自的写操作，缺失的对象排队等候在osd.1上恢复</li>
<li>恢复完成之前，osd.2宕机，或者被移除</li>
</ol>
<p>在上面这个事件序列中，osd.1知道权威副本存在，但是却找不到，这种情况下<span style="background-color: #c0c0c0;">针对目标对象的请求会被阻塞</span>，直到权威副本的持有者osd上线。</p>
<p>执行下面的命令，定位存在问题的PG：</p>
<pre class="crayon-plain-tag">ceph health detail | grep unfound
# OBJECT_UNFOUND 1/90055 objects unfound (0.001%)
#     pg 33.3e has 1 unfound objects
#    pg 33.3e is active+recovery_wait+degraded, acting [17,6], 1 unfound</pre>
<p>进一步，定位存在问题的对象：</p>
<pre class="crayon-plain-tag">// ceph pg 33.3e list_missing
{
    "offset": {
        "oid": "",
        "key": "",
        "snapid": 0,
        "hash": 0,
        "max": 0,
        "pool": -9223372036854775808,
        "namespace": ""
    },
    "num_missing": 1,
    "num_unfound": 1,
    "objects": [
        {
            "oid": {
                // 丢失的对象
                "oid": "obj_delete_at_hint.0000000066",
                "key": "",
                "snapid": -2,
                "hash": 2846662078,
                "max": 0,
                "pool": 33,
                "namespace": ""
            },
            "need": "1723'1412",
            "have": "0'0",
            "flags": "none",
            "locations": []
        }
    ],
    "more": false
}</pre>
<p>如果丢失的对象太多，more会显示为true。</p>
<p>执行下面的命令，可以查看PG的诊断信息：</p>
<pre class="crayon-plain-tag">// ceph pg 33.3e query
{
  "state": "active+recovery_wait+degraded",
  "recovery_state": [
    {
      "name": "Started/Primary/Active",
      "enter_time": "2018-06-16 15:03:32.873855",
      // 丢失的对象所在的OSD
      "might_have_unfound": [
        {
          "osd": "6",
          "status": "already probed"
        },
        {
          "osd": "11",
          "status": "osd is down"
        }
      ],
    } 
  ]
}</pre>
<p>上面输出中的osd.11，先前已经出现硬件故障，被移除了。这意味着unfound的对象已经不可恢复。你可以标记：</p>
<pre class="crayon-plain-tag"># 回滚到前一个版本，如果是新创建对象则忘记其存在。不支持EC池
ceph pg 33.3e mark_unfound_lost revert
# 让Ceph忘记unfound对象的存在
ceph pg 33.3e mark_unfound_lost delete </pre>
<div class="blog_h2"><span class="graybg">ceph-deploy</span></div>
<div class="blog_h3"><span class="graybg">TypeError: 'Logger' object is not callable</span></div>
<p>/usr/lib/python2.7/dist-packages/ceph_deploy/osd.py第376行，替换为：</p>
<pre class="crayon-plain-tag">LOG.info(line.decode('utf-8')) </pre>
<div class="blog_h3"><span class="graybg">Could not locate executable 'ceph-volume' make sure it is installed and available</span></div>
<p>应该安装ceph-deploy的1.5.39版本，2.0.0版本仅仅支持luminous：</p>
<pre class="crayon-plain-tag">apt remove ceph-deploy
apt install ceph-deploy=1.5.39 -y</pre>
<div class="blog_h3"><span class="graybg">部署MON后ceph-s卡死</span></div>
<p>在我的环境下，是因为MON节点识别的public addr为LVS的虚拟网卡的IP地址导致。修改配置，显式指定MON的IP地址即可：</p>
<pre class="crayon-plain-tag">[mon.master01-10-5-38-24]
public addr = 10.5.38.24 
cluster addr = 10.5.38.24

[mon.master02-10-5-38-39]
public addr = 10.5.38.39
cluster addr = 10.5.38.39

[mon.master03-10-5-39-41]
public addr = 10.5.39.41
cluster addr = 10.5.39.41</pre>
<div class="blog_h2"><span class="graybg">ceph-helm</span></div>
<p>在我的环境下部署，出现一系列和权限有关的问题，如果你遇到相同问题且不关心安全性，可以修改配置：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph edit configmap ceph-etc
apiVersion: v1
data:
  ceph.conf: |
    [global]
    fsid = 08adecc5-72b1-4c57-b5b7-a543cd8295e7
    mon_host = ceph-mon.ceph.svc.k8s.gmem.cc
    # 添加以下三行
    auth client required = none
    auth cluster required = none
    auth service required = none
    [osd]
    # 在大型集群里用单独的“集群”网可显著地提升性能
    cluster_network = 10.0.0.0/16
    ms_bind_port_max = 7100
    public_network = 10.0.0.0/16
kind: ConfigMap</pre>
<p>如果需要保证集群安全，请参考下面几个案例。</p>
<div class="blog_h3"><span class="graybg">ceph-mgr报Operation not permitted</span></div>
<p>问题现象：</p>
<p>此Pod一直无法启动，查看容器日志，发现：</p>
<p>timeout 10 ceph --cluster ceph auth get-or-create mgr.xenial-100 mon 'allow profile mgr' osd 'allow *' mds 'allow *' -o /var/lib/ceph/mgr/ceph-xenial-100/keyring</p>
<p>0 librados: client.admin authentication error (1) Operation not permitted</p>
<p>问题分析：</p>
<p>连接到可以访问的ceph-mon，执行命令：</p>
<pre class="crayon-plain-tag">kubectl -n ceph exec -it ceph-mon-nhx52 -c ceph-mon -- ceph</pre>
<p>发现报同样的错误。这说明client.admin的Keyring有问题。登陆到ceph-mon，获取Keyring列表：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph exec -it ceph-mon-nhx52 -c ceph-mon bash
# ceph --cluster=ceph  --name mon. --keyring=/var/lib/ceph/mon/ceph-xenial-100/keyring auth list   
 
installed auth entries:

client.admin
        key: AQAXPdtaAAAAABAA6wd1kCog/XtV9bSaiDHNhw==
        auid: 0
        caps: [mds] allow
        caps: [mgr] allow *
        caps: [mon] allow *
        caps: [osd] allow *

client.bootstrap-mds
        key: AQAgPdtaAAAAABAAFPgqn4/zM5mh8NhccPWKcw==
        caps: [mon] allow profile bootstrap-mds
client.bootstrap-osd
        key: AQAUPdtaAAAAABAASbfGQ/B/PY4Imoa4Gxsa2Q==
        caps: [mon] allow profile bootstrap-osd
client.bootstrap-rgw
        key: AQAJPdtaAAAAABAAswtFjgQWahHsuy08Egygrw==
        caps: [mon] allow profile bootstrap-rgw</pre>
<p>而当前使用的client.admin的Keyring内容为：</p>
<pre class="crayon-plain-tag">[client.admin]
  key = AQAda9taAAAAABAAgWIsgbEiEsFRJQq28hFgTQ==
  auid = 0
  caps mds = "allow"
  caps mon = "allow *"
  caps osd = "allow *"
  caps mgr = "allow *"</pre>
<p>内容不一致。使用auth list获得的client.admin的Keyring，可以发现是有效的：</p>
<pre class="crayon-plain-tag">ceph --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-xenial-100/keyring auth get client.admin &gt; client.admin.keyyring
ceph --name client.admin --keyring client.admin.keyyring # OKskydns_skydns_dns_cachemiss_count_total{instance="172.27.100.134:10055"}</pre>
<p>检查一下各Pod的/etc/ceph/ceph.client.admin.keyring，可以发现都是从Secret ceph-client-admin-keyring挂载的。那么这个Secret是如何生成的呢？执行命令：</p>
<pre class="crayon-plain-tag">kubectl -n ceph get job --output=yaml --export | grep ceph-client-admin-keyring -B 50</pre>
<p>可以发现Job ceph-storage-keys-generator负责生成该Secret。 查看其Pod日志可以生成Keyring、创建Secret的记录。进一步查看Pod的资源定义，可以看到负责创建的脚本/opt/ceph/ceph-storage-key.sh挂载自ConfigMap ceph-bin中的ceph-storage-key.sh。</p>
<p>解决此问题<span style="background-color: #c0c0c0;">最简单的办法</span>就是修改Secret，将其修改为集群中实际有效的Keyring：</p>
<pre class="crayon-plain-tag"># 导出Secret定义
kubectl -n ceph get  secret ceph-client-admin-keyring --output=yaml --export &gt; ceph-client-admin-keyring
# 获得有效Keyring的Base64编码
cat client.admin.keyyring | base64
# 将Secret中的编码替换为上述Base64，然后重新创建Secret
kubectl -n ceph apply -f ceph-client-admin-keyring</pre>
<p>此外Secret<span style="background-color: #c0c0c0;"> pvc-ceph-client-key中存放的也是admin用户的Key</span>，其内容也需要替换到有效的：</p>
<pre class="crayon-plain-tag">kubectl -n ceph edit secret  pvc-ceph-client-key</pre>
<div class="blog_h3"><span class="graybg">pvc无法提供</span></div>
<p>原因和上一个问题类似，还是权限问题。</p>
<p>查看无法绑定的PVC日志：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph describe pvc
 Normal   Provisioning        53s   ceph.com/rbd ceph-rbd-provisioner-5544dcbcf5-n846s 708edb2c-4619-11e8-abf2-e672650d97a2  External provisioner is provisioning volume for claim
"ceph/ceph-pvc"
  Warning  ProvisioningFailed  53s   ceph.com/rbd ceph-rbd-provisioner-5544dcbcf5-n846s 708edb2c-4619-11e8-abf2-e672650d97a2  Failed to provision volume with StorageClass "general"
: failed to create rbd image: exit status 1, command output: 2018-04-22 13:44:35.269967 7fb3e3e3ad80 -1 did not load config file, using default settings.
2018-04-22 13:44:35.297828 7fb3e3e3ad80 -1 auth: unable to find a keyring on /etc/ceph/ceph.client.admin.keyring,/etc/ceph/ceph.keyring,/etc/ceph/keyring,/etc/ceph/keyring.bin: (2)
 No such file or directoryConnection to localhost closed by remote host.
Connection to localhost closed.e3e3ad80  0 librados: client.admin authentication error (1) Operation not permitted</pre>
<p>rbd-provisioner需要读取StorageClass定义，获取需要的凭证信息：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph get storageclass --output=yaml
apiVersion: v1                                                                                                                                                                      
items:                                                                                                                                                                              
- apiVersion: storage.k8s.io/v1                                                                                                                                                     
  kind: StorageClass                                                                                                                                                                
  metadata:                                                                                                                                 
    name: general
  parameters:
    adminId: admin
    adminSecretName: pvc-ceph-conf-combined-storageclass
    adminSecretNamespace: ceph
    imageFeatures: layering
    imageFormat: "2"
    monitors: ceph-mon.ceph.svc.k8s.gmem.cc:6789
    pool: rbd
    userId: admin
    userSecretName: pvc-ceph-client-key
  provisioner: ceph.com/rbd
  reclaimPolicy: Delete</pre>
<p>可以看到牵涉到两个Secret：pvc-ceph-conf-combined-storageclass、pvc-ceph-client-key，你需要把正确的Keyring内容写入其中。</p>
<div class="blog_h3"><span class="graybg">pvc无法Attach</span></div>
<p>症状：PVC可以Provision，RBD可以通过Ceph命令挂载，但是Pod无法启动，Describe之显示：</p>
<p style="padding-left: 30px;">auth: unable to find a keyring on /etc/ceph/keyring: (2) No such file or directory<br />monclient(hunting): authenticate NOTE: no keyring found; disabled cephx authentication<br />librados: client.admin authentication error (95) Operation not supported</p>
<p>解决办法：把ceph.client.admin.keyring拷贝一份为 /etc/ceph/keyring即可。</p>
<div class="blog_h3"><span class="graybg">ceph-osd报Operation not permitted</span></div>
<p>原因和上一个问题一样。查看无法启动的容器日志：</p>
<pre class="crayon-plain-tag">kubectl -n ceph logs ceph-osd-dev-vdb-bjnbm -c osd-prepare-pod
# ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring health                                                             
# 0 librados: client.bootstrap-osd authentication error (1) Operation not permitted                                                         
# [errno 1] error connecting to the cluster</pre>
<p>进一步查看，可以发现/var/lib/ceph/bootstrap-osd/ceph.keyring挂载自ceph-bootstrap-osd-keyring下的ceph.keyring：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph get secret ceph-bootstrap-osd-keyring --output=yaml --export
apiVersion: v1
data:
  ceph.keyring: W2NsaWVudC5ib290c3RyYXAtb3NkXQogIGtleSA9IEFRQVlhOXRhQUFBQUFCQUFSQ2l1bVY1NFpOU2JGVWwwSDZnYlJ3PT0KICBjYXBzIG1vbiA9ICJhbGxvdyBwcm9maWxlIGJvb3RzdHJhcC1vc2QiCgo=
kind: Secret
metadata:
  creationTimestamp: null
  name: ceph-bootstrap-osd-keyring
  selfLink: /api/v1/namespaces/ceph/secrets/ceph-bootstrap-osd-keyring
type: Opaque

# BASE64解码后：
[client.bootstrap-osd]
  key = AQAYa9taAAAAABAARCiumV54ZNSbFUl0H6gbRw==
  caps mon = "allow profile bootstrap-osd"</pre>
<p>获得实际有效的Keyring：</p>
<pre class="crayon-plain-tag">kubectl -n ceph exec -it ceph-mon-nhx52 -c ceph-mon -- ceph --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-xenial-100/keyring auth get client.bootstrap-osd
# 注意上述命令的输出的第一行exported keyring for client.bootstrap-osd不属于Keyring
[client.bootstrap-osd]
        key = AQAUPdtaAAAAABAASbfGQ/B/PY4Imoa4Gxsa2Q==
        caps mon = "allow profile bootstrap-osd"</pre>
<p>修改Secret：<pre class="crayon-plain-tag">kubectl -n ceph edit secret ceph-bootstrap-osd-keyring</pre> 替换为上述Keyring。</p>
<div class="blog_h3"><span class="graybg">ceph-osd报No cluster conf with fsid</span></div>
<p>报错信息：</p>
<pre class="crayon-plain-tag"># kubectl -n ceph logs  ceph-osd-dev-vdc-cpkxh -c osd-activate-pod
ceph_disk.main.Error: Error: No cluster conf found in /etc/ceph with fsid 08adecc5-72b1-4c57-b5b7-a543cd8295e7
# 每个OSD都包同样的错误</pre>
<p>对应的配置文件内容： </p>
<pre class="crayon-plain-tag">kubectl -n ceph get configmap ceph-etc --output=yaml
apiVersion: v1
data:
  ceph.conf: |
    [global]
    fsid = a4426e8a-c46d-4407-95f1-911a23a0dd6e
    mon_host = ceph-mon.ceph.svc.k8s.gmem.cc
    [osd]
    cluster_network = 10.0.0.0/16
    ms_bind_port_max = 7100
    public_network = 10.0.0.0/16
kind: ConfigMap
metadata:
  name: ceph-etc
  namespace: ceph</pre>
<p>可以看到，fsid不一致。修改一下ConfigMap中的fsid即可解决此问题。  </p>
<div class="blog_h3"><span class="graybg">容器无法Attach PV</span></div>
<p>报错信息：</p>
<p style="padding-left: 30px;">describe pod报错：timeout expired waiting for volumes to attach/mount for pod<br />kubelet报错：executable file not found in $PATH, rbd output</p>
<p>原因分析：动态提供的持久卷，包含两个阶段：</p>
<ol>
<li>卷提供，原本由控制平面负责，controller-manager中需要包含rbd命令，才能在Ceph集群中创建供K8S使用的镜像。目前这个职责由<a href="https://github.com/kubernetes-incubator/external-storage">external_storage</a>项目的rbd-provisioner完成</li>
<li>卷依附/分离，由使用卷的Pod所在的Node的kubelet负责完成。这些Node需要安装rbd命令，并提供有效的配置文件</li>
</ol>
<p>解决方案：</p>
<pre class="crayon-plain-tag"># 安装软件
apt install -y ceph-common
# 从ceph-mon拷贝以下文件：
# /etc/ceph/ceph.client.admin.keyring
# /etc/ceph/ceph.conf</pre>
<p>应用上述方案后，如果继续报错：rbd: map failed exit status 110, rbd output: rbd: sysfs write failed In some cases useful info is found in syslog。则查看一下系统日志：</p>
<pre class="crayon-plain-tag">dmesg | tail

# [ 3004.833252] libceph: mon0 10.0.0.100:6789 feature set mismatch, my 106b84a842a42 
#     &lt; server's 40106b84a842a42, missing 400000000000000
# [ 3004.840980] libceph: mon0 10.0.0.100:6789 missing required protocol features</pre>
<p>对照本文前面的特性表，可以发现内核版本必须4.5+才可以（CEPH_FEATURE_NEW_OSDOPREPLY_ENCODING）。 </p>
<p>最简单的办法就是升级一下内核：</p>
<pre class="crayon-plain-tag"># Desktop
apt install --install-recommends linux-generic-hwe-16.04 xserver-xorg-hwe-16.04 -y
# Server
apt install --install-recommends linux-generic-hwe-16.04 -y

sudo apt-get remove linux-headers-4.4.* -y &amp;&amp; \
sudo apt-get remove linux-image-4.4.* -y &amp;&amp; \
sudo apt-get autoremove -y &amp;&amp; \
sudo update-grub</pre>
<p>或者，将tunables profile调整到hammer版本的Ceph：</p>
<pre class="crayon-plain-tag">ceph osd crush tunables hammer
# adjusted tunables profile to hammer</pre>
<div class="blog_h3"><span class="graybg">OSD启动失败报文件名太长</span></div>
<p>报错信息：ERROR: osd init failed: (36) File name too long</p>
<p>报错原因：使用的文件系统为EXT4，存储的xattrs大小有限制，有条件的话最好使用XFS</p>
<p>解决办法：修改配置文件，如下：</p>
<pre class="crayon-plain-tag">osd_max_object_name_len = 256
osd_max_object_namespace_len = 64</pre>
<div class="blog_h3"><span class="graybg">无法打开/proc/0/cmdline</span></div>
<p>报错信息：Fail to open '/proc/0/cmdline' error No such file or directory</p>
<p>报错原因：在CentOS 7上，将ceph-mon和ceph-osd（基于目录）部署在同一节点（基于Helm）报此错误，分离后问题消失。此外部署mon的那些节点还设置了虚IP，其子网和Ceph的Cluster/Public网络相同，这导致了某些OSD监听的地址不正确。</p>
<p>再次遇到此问题，原因是一个虚拟网卡lo:ngress使用和eth0相同的网段，导致OSD使用了错误的网络。</p>
<p>解决办法是写死OSD监听地址：</p>
<pre class="crayon-plain-tag">[osd.2]                                                                                                                                                                                    
public addr = 10.0.4.1                                                                                                                                                                     
cluster addr = 10.0.4.1</pre>
<div class="blog_h3"><span class="graybg">无法挂载RBD</span></div>
<p>报错信息：Input/output error，结合dmesg | tail可以看到更细节的报错</p>
<p>报错原因，可能情况：</p>
<ol>
<li>CentOS7下报错，提示客户端不满足特性CEPH_FEATURE_CRUSH_V4（1000000000000）。解决办法，将Bucket算法改为straw。注意，之后加入的OSD仍然默认使用straw2，使用的镜像的标签为tag-build-master-luminous-ubuntu-16.04。</li>
</ol>
<div class="blog_h3"><span class="graybg">write error: File name too long</span></div>
<p><a href="https://github.com/kubernetes-incubator/external-storage/tree/master/ceph/cephfs">external storage中的CephFS</a>可以正常Provisioning，但是尝试读写数据时报此错误。原因是文件路径过长，和底层文件系统有关，为了兼容部分Ext文件系统的机器，我们限制了osd_max_object_name_len。</p>
<p>解决办法，不使用UUID，而使用namespace + pvcname来命名目录。修改cephfs-provisioner.go，118行：</p>
<pre class="crayon-plain-tag">// create random share name
share := fmt.Sprintf("%s-%s", options.PVC.Namespace,options.PVC.Name)
// create random user id
user := fmt.Sprintf("%s-%s", options.PVC.Namespace,options.PVC.Name)</pre>
<p>重新编译即可。 </p>
<div class="blog_h2"><span class="graybg">K8S相关</span></div>
<div class="blog_h3"><span class="graybg">rbd image *** is still being used</span></div>
<p>describe pod发现：</p>
<pre class="crayon-plain-tag">rbd image rbd-unsafe/kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 is still being used</pre>
<p>说明有其它客户端正在占用此镜像。如果尝试删除镜像，你会发现无法成功：</p>
<pre class="crayon-plain-tag">rbd rm rbd-unsafe/kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 

librbd::image::RemoveRequest: 0x560e39df9af0 check_image_watchers: image has watchers - not removing
Removing image: 0% complete...failed.
rbd: error: image still has watchers
This means the image is still open or the client using it crashed. Try again after closing/unmapping it or waiting 30s for the crashed client to timeout. </pre>
<p>要知道watcher是谁，可以执行：</p>
<pre class="crayon-plain-tag">rbd status rbd-unsafe/kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 
Watchers:
        watcher=10.5.39.12:0/1652752791 client.94563 cookie=18446462598732840961</pre>
<p>可以发现10.5.39.12正在占用镜像。</p>
<p>另一种获取watcher的方法是，使用rbd的header对象。执行下面的命令获取rbd的诊断信息：</p>
<pre class="crayon-plain-tag">rbd info rbd-unsafe/kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 

rbd image 'kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6':
        size 8192 MB in 2048 objects
        order 22 (4096 kB objects)
        block_name_prefix: rbd_data.134474b0dc51
        format: 2
        features: layering
        flags: 
        create_timestamp: Wed Jul 11 17:49:51 2018</pre>
<p>字段block_name_prefix的值rbd_data.134474b0dc51，将data换为header即为header对象。然后使用命令：</p>
<pre class="crayon-plain-tag">rados listwatchers -p rbd-unsafe rbd_header.134474b0dc51

watcher=10.5.39.12:0/1652752791 client.94563 cookie=18446462598732840961</pre>
<p>既然知道10.5.39.12占用镜像，断开连接即可。 在此机器上执行下面的命令，显示当前映射的rbd镜像列表：</p>
<pre class="crayon-plain-tag">rbd showmapped

id pool       image                                                       snap device    
0  rbd-unsafe kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 -    /dev/rbd0 
1  rbd-unsafe kubernetes-dynamic-pvc-0729f9a6-84f0-11e8-9b75-5a3f858854b1 -    /dev/rbd1</pre>
<p>此机器上的rbd0虽然映射，但是没有挂载。解除映射：</p>
<pre class="crayon-plain-tag">rbd unmap /dev/rbd0</pre>
<p>再次检查rbd镜像状态，发现没有watcher了：</p>
<pre class="crayon-plain-tag">rbd status rbd-unsafe/kubernetes-dynamic-pvc-c0ac2cff-84ef-11e8-9a2a-566b651a72d6 

Watchers: none</pre>
<div class="blog_h3"><span class="graybg">rbd: map failed signal: aborted (core dumped)</span></div>
<p>kubectl describe报错Unable to mount volumes for pod... timeout expired waiting for volumes to attach or mount for pod...</p>
<p>检查发现目标rbd没有Watcher，Pod所在机器的Kubectl报错rbd: map failed signal: aborted (core dumped)。此前曾经在该机器上执行过rbd unmap操作。</p>
<p>手工 rbd map后问题消失。</p>
<div class="blog_h2"><span class="graybg">断电后无法启动OSD</span></div>
<div class="blog_h3"><span class="graybg"> journal do_read_entry: bad header magic</span></div>
<p>报错信息：journal do_read_entry(156389376): bad header magic......FAILED assert(interval.last &gt; last)</p>
<p>这是12.2版本<a href="http://tracker.ceph.com/issues/21142">已知的BUG</a>，断电后可能出现OSD无法启动，可能导致数据丢失。</p>
<div class="blog_h2"><span class="graybg">Couldn't init storage provider (RADOS)</span></div>
<p>RGW实例无法启动，通过journalctl看到上述信息。</p>
<p>要查看更多信息，需要查看RGW日志：</p>
<pre class="crayon-plain-tag">2020-10-22 16:51:55.771035 7fb1b0f20e80  0 ceph version 12.2.5 (cad919881333ac92274171586c827e01f554a70a) luminous (stable), process (unknown), pid 2546439
2020-10-22 16:51:55.792872 7fb1b0f20e80  0 librados: client.rgw.ceph02 authentication error (22) Invalid argument
2020-10-22 16:51:55.793450 7fb1b0f20e80 -1 Couldn't init storage provider (RADOS)</pre>
<p>可以发现是和身份验证有关的问题。</p>
<p>通过<pre class="crayon-plain-tag">systemctl status ceph-radosgw@rgw.$RGW_HOST</pre>得到命令行，手工运行：</p>
<pre class="crayon-plain-tag">radosgw -f --cluster ceph  --name client.rgw.ceph02 --setuser ceph --setgroup ceph -d --debug_ms 1</pre>
<p>发现报错和上面一样。尝试增加--keyring参数，问题解决：</p>
<pre class="crayon-plain-tag">radosgw -f --cluster ceph  --name client.rgw.ceph02        \
  --setuser ceph --setgroup ceph -d --debug_ms 1           \
  --keyring=/var/lib/ceph/radosgw/ceph-rgw.ceph02/keyring</pre>
<p>看来是Systemd服务没有找到keyring导致。 </p>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">禁用IPv6的机器上无法开启Prometheus模块</span></div>
<p>报错信息：Unhandled exception from module 'prometheus' while running on mgr.master01-10-5-38-24: error('No socket could<br /> be created',)  </p>
<p>解决办法：<pre class="crayon-plain-tag">ceph config-key set mgr/prometheus/server_addr 0.0.0.0</pre></p>
<div class="blog_h3"><span class="graybg">反复警告mon... clock skew</span></div>
<p>原因是时钟不同步警告阈值太低，在global段增加配置并重启MON：</p>
<pre class="crayon-plain-tag">mon clock drift allowed = 2
mon clock drift warn backoff = 30</pre>
<p>或者执行下面的命令即时生效：</p>
<pre class="crayon-plain-tag">ceph tell mon.* injectargs '--mon_clock_drift_allowed=2'
ceph tell mon.* injectargs '--mon_clock_drift_warn_backoff=30'</pre>
<p>或者检查ntp相关配置，保证时钟同步精度。</p>
<div class="blog_h3"><span class="graybg">深度清理导致高IO</span></div>
<p>深度清理很消耗IO，如果长时间无法完成，可以禁用：</p>
<pre class="crayon-plain-tag">ceph osd set noscrub
ceph osd set nodeep-scrub</pre>
<p>问题解决后，可以再启用： </p>
<pre class="crayon-plain-tag">ceph osd unset noscrub
ceph osd unset nodeep-scrub </pre>
<p>使用CFQ作为IO调度器时，可以调整OSD IO线程的优先级：</p>
<pre class="crayon-plain-tag"># 设置调度器
echo cfq &gt; /sys/block/sda/queue/scheduler

# 检查当前某个OSD的磁盘线程优先级类型
ceph daemon osd.4 config get osd_disk_thread_ioprio_class

# 修改IO优先级
ceph tell osd.* injectargs '--osd_disk_thread_ioprio_priority 7'
# IOPRIO_CLASS_RT最高 IOPRIO_CLASS_IDLE最低
ceph tell osd.* injectargs '--osd_disk_thread_ioprio_class idle'</pre>
<p>如果上述措施没有问题时，可以考虑配置以下参数：</p>
<pre class="crayon-plain-tag">osd_deep_scrub_stride = 131072                                                                                                                                                           
# 每次Scrub的块数量范围
osd_scrub_chunk_min = 1                                                                                                                                                                  
osd_scrub_chunk_max = 5                                                                                                                                                                  
osd scrub during recovery = false                                                                                                                                                        
osd deep scrub interval = 2592000                                                                                                                                                        
osd scrub max interval = 2592000                                                                                                                                                         
# 单个OSD并发进行的Scrub个数
osd max scrubs = 1       
# Scrub起止时间                                                                                                                                                                
osd max begin hour = 2                                                                                                                                                                   
osd max end hour = 6                                                                                                                                                                     
# 系统负载超过多少则禁止Scrub
osd scrub load threshold = 4                                                                                                                                                             
# 每次Scrub后强制休眠0.1秒
osd scrub sleep = 0.1                                                                                                                                                                      
# 线程优先级
osd disk thread ioprio priority = 7
osd disk thread ioprio class = idle</pre>
<div class="blog_h3"><span class="graybg">强制unmap</span></div>
<p>如果Watcher被黑名单，则尝试Unmap镜像时会报错：rbd: sysfs write failed rbd: unmap failed: (16) Device or resource busy</p>
<p>可以使用下面的命令强制unmap：<pre class="crayon-plain-tag">rbd unmap -o force ...</pre> </p>
<div class="blog_h3"><span class="graybg">增加pg_num和pgp_num后无法A+C</span></div>
<p>部分PG状态卡死，可能原因是OSD允许的PG数量受限，修改全局配置项mon_max_pg_per_osd并重启MON即可。</p>
<p>此外注意：调整PG数量后，<span style="background-color: #c0c0c0;">一定要进入A+C状态后，再进行下一次调整</span>。</p>
<div class="blog_h3"><span class="graybg">无法删除RBD镜像</span></div>
<p>下面第二个镜像对应的K8S PV已经删除：</p>
<pre class="crayon-plain-tag">rbd ls
# kubernetes-dynamic-pvc-35350b13-46b8-11e8-bde0-a2c14c93573f
# kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4</pre>
<p>但是对应的RBD没有删除，手工删除：</p>
<pre class="crayon-plain-tag">rbd remove kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4</pre>
<p>报错：</p>
<p>2018-04-23 13:37:25.559444 7f919affd700 -1 librbd::image::RemoveRequest: 0x5598e77831d0 check_image_watchers: image has watchers - not removing<br />Removing image: 0% complete...failed.<br />rbd: error: image still has watchers<br />This means the image is still open or the client using it crashed. Try again after closing/unmapping it or waiting 30s for the crashed client to timeout.</p>
<p>查看RBD状态：</p>
<pre class="crayon-plain-tag"># rbd info kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4
rbd image 'kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4':
        size 8192 MB in 2048 objects
        order 22 (4096 kB objects)
        block_name_prefix: rbd_data.1003e238e1f29
        format: 2
        features: layering
        flags: 
        create_timestamp: Mon Apr 23 11:42:59 2018

#rbd status kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4
Watchers:
        watcher=10.0.0.101:0/4275384344 client.65597 cookie=18446462598732840963</pre>
<p>到10.0.0.101这台机器上查看：</p>
<pre class="crayon-plain-tag"># df | grep e6e3339859d4
/dev/rbd2        8125880  251560   7438508   4% /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-kubernetes-dynamic-pvc-78740b26-46eb-11e8-8349-e6e3339859d4</pre>
<p>重启Kubelet后可以删除RBD。</p>
<div class="blog_h3"><span class="graybg">Error EEXIST: entity osd.9 exists but key does not match</span></div>
<pre class="crayon-plain-tag"># 删除密钥
ceph auth del osd.9
# 重新收集目标主机的密钥
ceph-deploy --username ceph-ops gatherkeys Carbon</pre>
<div class="blog_h3"><span class="graybg">创建新Pool后无法Active+Clean</span></div>
<pre class="crayon-plain-tag">pgs:     12.413% pgs unknown                                                                                                                                                         
             20.920% pgs not active                                                                                                                                                      
             768 active+clean                                                                                                                                                            
             241 creating+activating                                                                                                                                                     
             143 unknown</pre>
<p>可能是由于PG总数太大导致，降低PG数量后很快Active+Clean </p>
<div class="blog_h3"><span class="graybg">Orphaned pod无法清理</span></div>
<p>报错信息：Orphaned pod "a9621c0e-41ee-11e8-9407-deadbeef00a0" found, but volume paths are still present on disk : There were a total of 1 errors similar to this. Turn up verbosity to see them</p>
<p>临时解决办法：</p>
<p>rm -rf /var/lib/kubelet/pods/a9621c0e-41ee-11e8-9407-deadbeef00a0/volumes/rook.io~rook/</p>
<div class="blog_h3"><span class="graybg">osd启动报错：ERROR: osd init failed: (1) Operation not permitted</span></div>
<p>可能原因是OSD使用的keyring和MON不一致。对于ID为14的OSD，将宿主机/var/lib/ceph/osd/ceph-14/keyring的内容替换为<pre class="crayon-plain-tag">ceph auth get osd.14</pre>的输出前两行即可。</p>
<div class="blog_h3"><span class="graybg">Mount failed with '(11) Resource temporarily unavailable'</span></div>
<p>在没有停止OSD的情况下执行ceph-objectstore-tool命令，会出现此错误。</p>
<div class="blog_h3"><span class="graybg">neither `public_addr` nor `public_network` keys are defined for monitors</span></div>
<p>通过ceph-deploy添加MON节点时出现此错误，将public_network配置添加到配置文件的global段即可。</p>
<div class="blog_h3"><span class="graybg">journalctl删除pv后卡在Terminating</span></div>
<p>可能原因：</p>
<ol>
<li>对应的PVC没有删除，还在引用此PV。先删除PV即可</li>
</ol>
<div class="blog_h3"><span class="graybg">chown: cannot access '/var/log/ceph': No such file or directory</span></div>
<p>OSD无法启动，报上面的错误，可以配置：</p>
<pre class="crayon-plain-tag">ceph:
  storage:
    osd_log: /var/log </pre>
<div class="blog_h3"><span class="graybg">HEALTH_WARN application not enabled on</span></div>
<pre class="crayon-plain-tag">#池 # 功能
ceph osd pool application enable rbd block-devices</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/ceph-study-note">Ceph学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/ceph-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>OpenStack学习笔记</title>
		<link>https://blog.gmem.cc/openstack-study-note</link>
		<comments>https://blog.gmem.cc/openstack-study-note#comments</comments>
		<pubDate>Fri, 19 Jan 2018 09:00:57 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[IaaS]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=13588</guid>
		<description><![CDATA[<p>简介 OpenStack是一个开源的IaaS解决方案，使用它，你可以通过仪表盘或者利用OpenStack API控制/Provision大规模的计算、存储、网络资源池。 通过“驱动”，OpenStack支持大量商业、开源的计算、存储、网络相关技术框架，从而能够管理各种各样的基础设施。不管是裸金属机器、虚拟机、还是容器，都可以基于OpenStack进行管理，并共享网络、存储等底层资源： Kubernetes、CloudFoundry等PaaS平台可以构建在OpenStack之上。 项目组成 OpenStack由若干子项目组成，它们围绕着计算、存储、网络这三个核心概念组织： 计算：提供并管理网络中大量的虚拟机，主要由Nova子项目负责 存储：供服务器、应用程序使用的对象存储、块存储，分别由Swift、Cinder子项目负责 网络：可拔插、可扩容、API驱动的网络和IP管理 这三类子项目还具有一些共享的服务：identity、镜像管理（image management）、一个基于Web的UI接口。 总览图 OpenStack项目的总览图如下，其中粗体标出了它的核心子项目，包括Horizon、Heat、Nova、Neutron、Swift、Cinder等： 服务交互 以Bigdata as Service场景为例，各子项目的交互关系如下图： 简单的说明一下： Keystone提供身份验证服务 Ceilometer提供监控服务 Horizon提供一个管理UI <a class="read-more" href="https://blog.gmem.cc/openstack-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/openstack-study-note">OpenStack学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>OpenStack是一个开源的IaaS解决方案，使用它，你可以通过仪表盘或者利用OpenStack API控制/Provision大规模的<span style="background-color: #c0c0c0;">计算、存储、网络资源池</span>。</p>
<p>通过“驱动”，OpenStack支持大量商业、开源的计算、存储、网络相关技术框架，从而能够管理各种各样的基础设施。不管是裸金属机器、虚拟机、还是容器，都可以基于OpenStack进行管理，并共享网络、存储等底层资源：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/overview-diagram-new.svg"><img class="aligncenter" style="width: 90%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/overview-diagram-new.svg" alt="" /></a>Kubernetes、CloudFoundry等PaaS平台可以构建在OpenStack之上。</p>
<div class="blog_h2"><span class="graybg">项目组成</span></div>
<p>OpenStack由若干子项目组成，它们围绕着计算、存储、网络这三个核心概念组织：</p>
<ol>
<li>计算：提供并管理网络中大量的虚拟机，主要由Nova子项目负责</li>
<li>存储：供服务器、应用程序使用的对象存储、块存储，分别由Swift、Cinder子项目负责</li>
<li>网络：可拔插、可扩容、API驱动的网络和IP管理</li>
</ol>
<p>这三类子项目还具有一些共享的服务：identity、镜像管理（image management）、一个基于Web的UI接口。</p>
<div class="blog_h3"><span class="graybg">总览图</span></div>
<p>OpenStack项目的总览图如下，其中粗体标出了它的核心子项目，包括<span style="background-color: #c0c0c0;">Horizon、Heat、Nova、Neutron、Swift、Cinder</span>等：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack-map-v20190402.svg"><img style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack-map-v20190402.svg" alt="" /></a></p>
<div class="blog_h2"><span class="graybg">服务交互</span></div>
<p>以Bigdata as Service场景为例，各子项目的交互关系如下图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack_kilo_conceptual_arch.png"><img style="width: 100%;" src="https://blog.gmem.cc/wp-content/uploads/2019/04/openstack_kilo_conceptual_arch.png" alt="" /></a></p>
<p>简单的说明一下：</p>
<ol>
<li>Keystone提供身份验证服务</li>
<li>Ceilometer提供监控服务</li>
<li>Horizon提供一个管理UI</li>
<li>Nova负责分配虚拟机</li>
<li>Glance提供镜像服务，镜像文件存放在Swift中</li>
<li>Cinder为虚拟机提供块存储卷</li>
<li>Cinder将卷备份到Swift中</li>
<li>Neuron为虚拟机提供网络连接</li>
</ol>
<p>每个子项目，或者叫OpenStack服务，都通过公共的Identity Service进行身份验证，<span style="background-color: #c0c0c0;">服务之间通过公共API进行交互</span>。每个<span style="background-color: #c0c0c0;">服务至少包含一个API进程</span>，此进程监听API请求，进行预处理然后转交给服务的其它部分进行处理。</p>
<p>每个<span style="background-color: #c0c0c0;">服务可以有多个进程</span>，这些<span style="background-color: #c0c0c0;">进程之间的通信方式通常是AMQP</span>。<span style="background-color: #c0c0c0;">服务的状态持久化在数据库中</span>。多种消息代理、RDBMS被支持，例如RabbitMQ、MySQL、MariaDB。</p>
<div class="blog_h2"><span class="graybg">客户端访问</span></div>
<p>用户访问OpenStack的方式有几种：</p>
<ol>
<li>通过Horizon提供的Web仪表盘</li>
<li>提供CLI客户端</li>
<li>提供SDK进行编程</li>
</ol>
<p>不管是何种方式，在底层都会向不同的OpenStack服务发送REST请求。</p>
<div class="blog_h2"><span class="graybg">组件简介</span></div>
<div class="blog_h3"><span class="graybg">Nova</span></div>
<p>Nova是OpenStack云中的计算组织控制器。支持OpenStack云中实例（instances）生命周期的所有活动都由Nova处理。这样使得Nova成为一个负责管理计算资源、网络、认证、所需可扩展性的平台。</p>
<div class="blog_h3"><span class="graybg">Neutron</span></div>
<p>Neutron是openstack核心项目之一，提供云计算环境下的虚拟网络功能。OpenStack网络（neutron）管理OpenStack环境中所有虚拟网络基础设施（VNI），物理网络基础设施（PNI）的接入层。</p>
<div class="blog_h3"><span class="graybg">Cinder</span></div>
<p>Cinder接口提供了一些标准功能，允许创建和附加块设备到虚拟机，如“创建卷”，“删除卷”和“附加卷”。还有更多高级的功能，支持扩展容量的能力，快照和创建虚拟机镜像克隆。</p>
<div class="blog_h3"><span class="graybg">Octavia</span></div>
<p>Octavia 是 openstack lbaas的支持的一种后台程序，提供为虚拟机流量的负载均衡。实质是类似于trove，调用 nova 以及neutron的api生成一台安装好haproxy和keepalived软件的虚拟机，并连接到目标网路。</p>
<div class="blog_h3"><span class="graybg">Swift</span></div>
<p>Swift 不是文件系统或者实时的数据存储系统，而是对象存储，用于长期存储永久类型的静态数据。这些数据可以检索、调整和必要时进行更新。Swift最适合虚拟机镜像、图片、邮件和存档备份这类数据的存储。</p>
<div class="blog_h3"><span class="graybg">Glance</span></div>
<p>Glance（OpenStack Image Service）是一个提供发现，注册，和下载镜像的服务。Glance提供了虚拟机镜像的集中存储。通过 Glance 的 RESTful API，可以查询镜像元数据、下载镜像。虚拟机的镜像可以很方便的存储在各种地方，从简单的文件系统到对象存储系统（比如 OpenStack Swift）。</p>
<div class="blog_h3"><span class="graybg">Horizon</span></div>
<p>Horizon 为 Openstack 提供一个 WEB 前端的管理界面 (UI 服务 )通过 Horizon 所提供的 DashBoard 服务 , 管理员可以使用通过 WEB UI 对 Openstack 整体云环境进行管理 , 并可直观看到各种操作结果与运行状态。</p>
<div class="blog_h3"><span class="graybg">Ironic</span></div>
<p>Ironic包含一个API和多个插件，用于安全性和容错性地提供物理服务器。它可以和nova结合被使用为hypervisor驱动，或者用bifrost使用为独立服务。默认情况下，它会使用PXE和IPMI去与裸金属机器去交互。Ironic也支持使用供应商的插件而实现额外的功能。</p>
<div class="blog_h3"><span class="graybg">Cyborg</span></div>
<p>Cyborg（以前称为Nomad）旨在为加速资源（即FPGA,GPU,SoC, NVMe SSD,DPDK/SPDK,eBPF/XDP …）提供通用管理框架。</p>
<div class="blog_h3"><span class="graybg">Kolla</span></div>
<p>kolla 的使命是为 openstack 云平台提供生产级别的、开箱即用的交付能力。kolla 的基本思想是一切皆容器，将所有服务基于 Docker 运行，并且保证一个容器只跑一个服务（进程），做到最小粒度的运行 docker。</p>
<div class="blog_h3"><span class="graybg">Kuryr</span></div>
<p>Kubernetes Kuryr是 OpenStack Neutron 的子项目，其主要目标是通过该项目来整合 OpenStack 与Kubernetes 的网络。该项目在 Kubernetes 中实现了原生 Neutron-based 的网络，因此<span style="background-color: #c0c0c0;">使用 Kuryr-Kubernetes 可以让你的 OpenStack VM 与 Kubernetes Pods 能够选择在同一个子网上运作</span>，并且能够使用 Neutron 的 L3 与 Security Group 来对网络进行路由，以及阻挡特定来源 Port。</p>
<div class="blog_h3"><span class="graybg">Manila</span></div>
<p>Manila项目全称是File Share Service，文件共享即服务，用来提供云上的文件共享，支持CIFS协议和NFS协议。</p>
<div class="blog_h3"><span class="graybg">Tacker</span></div>
<p>Tacker是一个在OpenStack内部孵化的项目, 他的作用是NVF管理器，用于管理NVF的生命周期。Tacker的重点是配置VNF, 并监视他们。如果需要，还可重启和/或扩展（自动修复）NVF。整个进程贯穿ETSIMANO所描述的整个生命周期。</p>
<div class="blog_h1"><span class="graybg">命令</span></div>
<div class="blog_h2"><span class="graybg">openstack</span></div>
<div class="blog_h3"><span class="graybg">configuration</span></div>
<p>显示详细配置信息：</p>
<pre class="crayon-plain-tag"># --unmask表示明文显示密码
openstack configuration show [--mask | --unmask]</pre>
<div class="blog_h3"><span class="graybg">domain</span></div>
<p>一个Domain，是用户、组、项目的集合。任何组、项目仅仅属于单个Domain。</p>
<pre class="crayon-plain-tag">openstack domain create
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--or-show]
    # 禁止删除或修改，除非去掉此标记
    [--immutable | --no-immutable]
    &lt;domain-name&gt;

openstack domain delete &lt;domain&gt; [&lt;domain&gt; ...]

openstack domain list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--enabled]

openstack domain set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--immutable | --no-immutable]
    &lt;domain&gt;

openstack domain show &lt;domain&gt;</pre>
<div class="blog_h3"><span class="graybg">project</span></div>
<p>管理项目</p>
<pre class="crayon-plain-tag">openstack project create
    [--domain &lt;domain&gt;]
    [--parent &lt;project&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--property &lt;key=value&gt;]
    [--or-show]
    [--immutable | --no-immutable]
    [--tag &lt;tag&gt;]
    &lt;project-name&gt;

openstack project delete [--domain &lt;domain&gt;] &lt;project&gt; [&lt;project&gt; ...]

openstack project list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--parent &lt;parent&gt;]
    [--user &lt;user&gt;]
    [--my-projects]
    [--long]
    [--sort &lt;key&gt;[:&lt;direction&gt;]]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--tags-any &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags-any &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack project set
    [--name &lt;name&gt;]
    [--domain &lt;domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--property &lt;key=value&gt;]
    [--immutable | --no-immutable]
    [--tag &lt;tag&gt;]
    [--clear-tags]
    [--remove-tag &lt;tag&gt;]
    &lt;project&gt;

openstack project show
    [--domain &lt;domain&gt;]
    [--parents]
    [--children]
    &lt;project&gt; </pre>
<div class="blog_h3"><span class="graybg">project purge</span></div>
<p>清除和指定项目关联的资源</p>
<pre class="crayon-plain-tag">openstack project purge
    [--dry-run]
    [--keep-project]
    (--auth-project | --project &lt;project&gt;)
    [--project-domain &lt;project-domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">group</span></div>
<p>用户的组。</p>
<pre class="crayon-plain-tag"># 添加用户到组
openstack group add user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;
    [&lt;user&gt; ...]
openstack group remove user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;
    [&lt;user&gt; ...]

# 检查组是否包含用户
openstack group contains user
    [--group-domain &lt;group-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;group&gt;
    &lt;user&gt;

# 创建组
openstack group create
    [--domain &lt;domain&gt;]
    [--description &lt;description&gt;]
    [--or-show]
    &lt;group-name&gt;


openstack group delete [--domain &lt;domain&gt;] &lt;group&gt; [&lt;group&gt; ...]

openstack group list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--long]
openstack group show [--domain &lt;domain&gt;] &lt;group&gt;

openstack group set
    [--domain &lt;domain&gt;]
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    &lt;group&gt; </pre>
<div class="blog_h3"><span class="graybg">user</span></div>
<p>用户管理</p>
<pre class="crayon-plain-tag">openstack user create
    # 用户的默认domain
    [--domain &lt;domain&gt;]
    # 用户的默认project
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 指定密码
    [--password &lt;password&gt;]
    # 交互的输入密码
    [--password-prompt]
    [--email &lt;email-address&gt;]
    [--description &lt;description&gt;]
    # 禁止连续身份验证失败后，锁定用户
    [--ignore-lockout-failure-attempts]
    [--no-ignore-lockout-failure-attempts]
    # 禁止密码过期
    [--ignore-password-expiry]
    [--no-ignore-password-expiry]
    # 禁止首次使用必须修改密码
    [--ignore-change-password-upon-first-use]
    [--no-ignore-change-password-upon-first-use]
    # 锁定密码，不给修改
    [--enable-lock-password]
    [--disable-lock-password]
    # 启用多因子身份验证
    [--enable-multi-factor-auth]
    [--disable-multi-factor-auth]
    [--multi-factor-auth-rule &lt;rule&gt;]
    # 启用/禁用
    [--enable | --disable]
    # 显示已有的用户
    [--or-show]
    &lt;name&gt;
openstack user delete [--domain &lt;domain&gt;] &lt;user&gt; [&lt;user&gt; ...]

openstack user list
    [--sort-column SORT_COLUMN]
    [--domain &lt;domain&gt;]
    [--group &lt;group&gt; | --project &lt;project&gt;]
    [--long]
openstack user show [--domain &lt;domain&gt;] &lt;user&gt;

# 修改密码
openstack user password set
    [--password &lt;new-password&gt;]
    [--original-password &lt;original-password&gt;]


openstack user set
    [--name &lt;name&gt;]
    [--domain &lt;domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--password &lt;password&gt;]
    [--password-prompt]
    [--email &lt;email-address&gt;]
    [--description &lt;description&gt;]
    [--ignore-lockout-failure-attempts]
    [--no-ignore-lockout-failure-attempts]
    [--ignore-password-expiry]
    [--no-ignore-password-expiry]
    [--ignore-change-password-upon-first-use]
    [--no-ignore-change-password-upon-first-use]
    [--enable-lock-password]
    [--disable-lock-password]
    [--enable-multi-factor-auth]
    [--disable-multi-factor-auth]
    [--multi-factor-auth-rule &lt;rule&gt;]
    [--enable | --disable]
    &lt;user&gt; </pre>
<div class="blog_h3"><span class="graybg">role</span></div>
<p>可以创建角色，将角色映射给用户或组。</p>
<pre class="crayon-plain-tag"># 将角色赋予组或用户
openstack role add
    [--system &lt;system&gt; | --domain &lt;domain&gt; | --project &lt;project&gt;]
    [--user &lt;user&gt; | --group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--inherited]
    [--role-domain &lt;role-domain&gt;]
    &lt;role&gt;
openstack role remove
    [--system &lt;system&gt; | --domain &lt;domain&gt; | --project &lt;project&gt;]
    [--user &lt;user&gt; | --group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--inherited]
    [--role-domain &lt;role-domain&gt;]
    &lt;role&gt;

# 创建角色
openstack role create
    [--description &lt;description&gt;]
    [--domain &lt;domain&gt;]
    [--or-show]
    [--immutable | --no-immutable]
    &lt;role-name&gt;

openstack role delete [--domain &lt;domain&gt;] &lt;role&gt; [&lt;role&gt; ...]

openstack role list [--sort-column SORT_COLUMN] [--domain &lt;domain&gt;]

openstack role set
    [--description &lt;description&gt;]
    [--domain &lt;domain&gt;]
    [--name &lt;name&gt;]
    [--immutable | --no-immutable]
    &lt;role&gt;

openstack role show [--domain &lt;domain&gt;] &lt;role&gt; </pre>
<div class="blog_h3"><span class="graybg">role assignment</span></div>
<p>角色和用户的映射关系。</p>
<pre class="crayon-plain-tag">openstack role assignment list
    [--role &lt;role&gt;]
    [--role-domain &lt;role-domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--group &lt;group&gt;]
    [--group-domain &lt;group-domain&gt;]
    [--domain &lt;domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--effective]
    [--inherited]
    [--names] </pre>
<div class="blog_h3"><span class="graybg">implied role</span></div>
<p>指定角色之间的包含关系。</p>
<pre class="crayon-plain-tag">#                             被隐含的角色           目标角色
openstack implied role create --implied-role &lt;role&gt; &lt;role&gt;

openstack implied role delete --implied-role &lt;role&gt; &lt;role&gt;

openstack implied role list [--sort-column SORT_COLUMN] </pre>
<div class="blog_h3"><span class="graybg">trust</span></div>
<p>提供特定项目中，用户之间的角色代理，支持可选的替身机制（impersonation）。需要 OS-TRUST扩展。</p>
<div class="blog_h3"><span class="graybg">consumer</span></div>
<p>在Identity服务的OS-OAUTH1扩展中使用，用于创建request token 、access token，仅仅支持Identity v3。</p>
<pre class="crayon-plain-tag">openstack consumer create [--description &lt;description&gt;]
openstack consumer delete &lt;consumer&gt; [&lt;consumer&gt; ...]
openstack consumer list [--sort-column SORT_COLUMN]
openstack consumer set [--description &lt;description&gt;] &lt;consumer&gt;
openstack consumer show &lt;consumer&gt;</pre>
<div class="blog_h3"><span class="graybg">credential</span></div>
<p>管理凭证：</p>
<pre class="crayon-plain-tag">openstack credential create
    [--type &lt;type&gt;]
    [--project &lt;project&gt;]
    &lt;user&gt;
    &lt;data&gt;

openstack credential delete &lt;credential-id&gt; [&lt;credential-id&gt; ...]

openstack credential list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--type &lt;type&gt;]

openstack credential set
    --user &lt;user&gt;
    --type &lt;type&gt;
    --data &lt;data&gt;
    [--project &lt;project&gt;]
    &lt;credential-id&gt;

openstack credential show &lt;credential-id&gt; </pre>
<div class="blog_h3"><span class="graybg">application credential</span></div>
<p>使用应用凭证，用户可以给自己的应用程序授予对云资源的有限访问权限。</p>
<pre class="crayon-plain-tag"># 创建新的凭证
openstack application credential create
    [--secret &lt;secret&gt;]
    [--role &lt;role&gt;]
    [--expiration &lt;expiration&gt;]
    [--description &lt;description&gt;]
    [--unrestricted]
    [--restricted]
    [--access-rules &lt;access-rules&gt;]
    &lt;name&gt;

openstack application credential delete
    &lt;application-credential&gt;
    [&lt;application-credential&gt; ...]

openstack application credential list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]

openstack application credential show &lt;application-credential&gt;</pre>
<div class="blog_h3"><span class="graybg">access rule</span></div>
<p>对应用程序凭证的权限进行细粒度控制。每个访问规则包含一下要素：</p>
<p>服务类型 + 请求路径 + 请求方法</p>
<pre class="crayon-plain-tag">openstack access rule delete &lt;access-rule&gt; [&lt;access-rule&gt; ...]

openstack access rule list
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]

openstack access rule show &lt;access-rule&gt;</pre>
<div class="blog_h3"><span class="graybg">token</span></div>
<p>创建或吊销一个令牌</p>
<pre class="crayon-plain-tag">openstack token issue

openstack token revoke &lt;token&gt; </pre>
<div class="blog_h3"><span class="graybg">access token</span></div>
<p>Identity服务的OS-OAUTH1扩展使用访问令牌。consumer可以代表被授权用户来获得新的Identity API token。</p>
<pre class="crayon-plain-tag">openstack access token create
    # consumer的键/密钥
    --consumer-key &lt;consumer-key&gt;
    --consumer-secret &lt;consumer-secret&gt;
    --request-key &lt;request-key&gt;
    --request-secret &lt;request-secret&gt;
    --verifier &lt;verifier&gt;</pre>
<div class="blog_h3"><span class="graybg">request token</span></div>
<p>Identity服务的OS-OAUTH1扩展使用请求令牌。consumer使用此令牌来请求access token</p>
<pre class="crayon-plain-tag"># 验证请求令牌
openstack request token authorize
    --request-key &lt;request-key&gt;
    --role &lt;role&gt;

# 创建请求令牌
openstack request token create
    --consumer-key &lt;consumer-key&gt;
    --consumer-secret &lt;consumer-secret&gt;
    --project &lt;project&gt;
    [--domain &lt;domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">policy</span></div>
<p>策略是一组规则，可以被远程服务消费。</p>
<pre class="crayon-plain-tag">openstack policy create [--type &lt;type&gt;] &lt;filename&gt;

openstack policy delete &lt;policy&gt; [&lt;policy&gt; ...]

openstack policy list [--sort-column SORT_COLUMN] [--long]

openstack policy set [--type &lt;type&gt;] [--rules &lt;filename&gt;] &lt;policy&gt;

openstack policy show &lt;policy&gt; </pre>
<div class="blog_h3"><span class="graybg">network rbac</span></div>
<p>基于RBAC的、针对网络资源的授权控制策略。让用户获得某个项目的网络资源的访问权限</p>
<pre class="crayon-plain-tag">openstack network rbac create
    --type &lt;type&gt;
    --action &lt;action&gt;
    (--target-project &lt;target-project&gt; | --target-all-projects)
    [--target-project-domain &lt;target-project-domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;rbac-object&gt;

openstack network rbac delete &lt;rbac-policy&gt; [&lt;rbac-policy&gt; ...]

openstack network rbac list
    [--sort-column SORT_COLUMN]
    [--type &lt;type&gt;]
    [--action &lt;action&gt;]
    [--long]

openstack network rbac set
    [--target-project &lt;target-project&gt;]
    [--target-project-domain &lt;target-project-domain&gt;]
    &lt;rbac-policy&gt;

openstack network rbac show &lt;rbac-policy&gt; </pre>
<div class="blog_h3"><span class="graybg">quota</span></div>
<p>很多API都支持资源配额</p>
<pre class="crayon-plain-tag">openstack quota set
    # 为指定的class设置配额
    [--class]
    # 核心数配额
    [--cores &lt;cores&gt;]
    # 固定IP数量配额
    [--fixed-ips &lt;fixed-ips&gt;]
    [--injected-file-size &lt;injected-file-size&gt;]
    [--injected-path-size &lt;injected-path-size&gt;]
    [--injected-files &lt;injected-files&gt;]
    [--instances &lt;instances&gt;]
    [--key-pairs &lt;key-pairs&gt;]
    [--properties &lt;properties&gt;]
    [--ram &lt;ram&gt;]
    [--server-groups &lt;server-groups&gt;]
    [--server-group-members &lt;server-group-members&gt;]
    [--backups &lt;backups&gt;]
    [--backup-gigabytes &lt;backup-gigabytes&gt;]
    [--gigabytes &lt;gigabytes&gt;]
    [--per-volume-gigabytes &lt;per-volume-gigabytes&gt;]
    [--snapshots &lt;snapshots&gt;]
    [--volumes &lt;volumes&gt;]
    [--floating-ips &lt;floating-ips&gt;]
    [--secgroup-rules &lt;secgroup-rules&gt;]
    [--secgroups &lt;secgroups&gt;]
    [--networks &lt;networks&gt;]
    [--subnets &lt;subnets&gt;]
    [--ports &lt;ports&gt;]
    [--routers &lt;routers&gt;]
    [--rbac-policies &lt;rbac-policies&gt;]
    [--subnetpools &lt;subnetpools&gt;]
    [--volume-type &lt;volume-type&gt;]
    [--force]
    # 此配额针对的项目或者class
    &lt;project/class&gt;

openstack quota list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--detail]
    (--compute | --volume | --network)

openstack quota show [--class | --default] [&lt;project/class&gt;]

# 显示针对所有项目的默认配额
openstack quota show --default

# 增大默认安装下admin项目的资源配额
openstack quota set --cores 32 admin
openstack quota set --ram 131072 admin
openstack quota set --gigabytes 8192 admin
openstack quota set --volumes 32 admin
openstack quota set --snapshots 32 admin
openstack quota set --instances 16 admin</pre>
<div class="blog_h3"><span class="graybg">limit</span></div>
<p>用于在项目级别进行资源配额。</p>
<pre class="crayon-plain-tag"># 创建一个配额
openstack limit create
    [--description &lt;description&gt;]
    # 此配额影响的区域
    [--region &lt;region&gt;]
    # 此配额针对的项目
    --project &lt;project&gt;
    # 负责资源的服务
    --service &lt;service&gt;
    # 资源额度
    --resource-limit &lt;resource-limit&gt;
    &lt;resource-name&gt;

openstack limit delete &lt;limit-id&gt; [&lt;limit-id&gt; ...]

openstack limit list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--region &lt;region&gt;]
    [--project &lt;project&gt;]

openstack limit set
    [--description &lt;description&gt;]
    [--resource-limit &lt;resource-limit&gt;]
    &lt;limit-id&gt;

openstack limit show &lt;limit-id&gt;</pre>
<div class="blog_h3"><span class="graybg">registered limit</span></div>
<p>用于定义OpenStack部署中的默认资源限制</p>
<pre class="crayon-plain-tag">openstack registered limit create
    [--description &lt;description&gt;]
    [--region &lt;region&gt;]
    --service &lt;service&gt;
    --default-limit &lt;default-limit&gt;
    &lt;resource-name&gt;

openstack registered limit delete
    &lt;registered-limit-id&gt;
    [&lt;registered-limit-id&gt; ...]

openstack registered limit list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--region &lt;region&gt;]

openstack registered limit set
    [--service &lt;service&gt;]
    [--resource-name &lt;resource-name&gt;]
    [--default-limit &lt;default-limit&gt;]
    [--description &lt;description&gt;]
    [--region &lt;region&gt;]
    &lt;registered-limit-id&gt;

openstack registered limit show &lt;registered-limit-id&gt; </pre>
<div class="blog_h3"><span class="graybg">limits</span></div>
<p>显示计算、存储资源用量的限制</p>
<pre class="crayon-plain-tag">openstack limits show
    [--sort-column SORT_COLUMN]
    (--absolute | --rate)
    [--reserved]
    [--project &lt;project&gt;]
    [--domain &lt;domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">usage</span></div>
<p>显示项目的资源用量</p>
<pre class="crayon-plain-tag">openstack usage list
    [--sort-column SORT_COLUMN]
    # 用量统计的起始时间，默认4周前
    [--start &lt;start&gt;]
    # 用量统计的结束日期，默认明天
    [--end &lt;end&gt;]

openstack usage show
    [--project &lt;project&gt;]
    [--start &lt;start&gt;]
    [--end &lt;end&gt;] </pre>
<div class="blog_h3"><span class="graybg">region</span></div>
<p>区域是OpenStack部署中的最大的分区。你可以配置多个sub-region，甚至形成树形结构。</p>
<pre class="crayon-plain-tag">openstack region create
    [--parent-region &lt;region-id&gt;]
    [--description &lt;description&gt;]
    &lt;region-id&gt;

openstack region delete &lt;region-id&gt; [&lt;region-id&gt; ...]

openstack region list
    [--sort-column SORT_COLUMN]
    [--parent-region &lt;region-id&gt;]

openstack region set
    [--parent-region &lt;region-id&gt;]
    [--description &lt;description&gt;]
    &lt;region-id&gt;

openstack region show &lt;region-id&gt; </pre>
<div class="blog_h3"><span class="graybg">availability zone</span></div>
<p>可用区是云存储、计算、网络服务的逻辑分区。对等的AZ具有构成HA的效果，这和Region不同。</p>
<pre class="crayon-plain-tag"># 列出可用区
openstack availability zone list
    [--sort-column SORT_COLUMN]
    [--compute]
    [--network]
    [--volume]
    [--long] </pre>
<div class="blog_h3"><span class="graybg">aggregate</span></div>
<p>聚合是一组分组host的机制：</p>
<pre class="crayon-plain-tag"># 添加/删除主机到聚合中
openstack aggregate add host &lt;aggregate&gt; &lt;host&gt;
openstack aggregate remove host &lt;aggregate&gt; &lt;host&gt;

# 为聚合请求缓存镜像
openstack aggregate cache image &lt;aggregate&gt; &lt;image&gt; [&lt;image&gt; ...]

# 创建一个聚合，可以看到聚合是在某个AZ内部的
openstack aggregate create
    [--zone &lt;availability-zone&gt;]
    [--property &lt;key=value&gt;]
    &lt;name&gt;

openstack aggregate delete &lt;aggregate&gt; [&lt;aggregate&gt; ...]

# 为聚合设置元数据（键值对），然后为Flavor设置scope为aggregate_instance_extra_specs的
# 额外规格，规格键值和元数据一致，可以将Flavor映射到聚合。从Flavor创建的实例将位于聚合的主机中
openstack aggregate set
    [--name &lt;name&gt;]
    [--zone &lt;availability-zone&gt;]
    [--property &lt;key=value&gt;]
    [--no-property]
    &lt;aggregate&gt;
openstack aggregate unset [--property &lt;key&gt;] &lt;aggregate&gt;

openstack aggregate show &lt;aggregate&gt;
openstack aggregate list [--sort-column SORT_COLUMN] [--long]</pre>
<div class="blog_h3"><span class="graybg">host</span></div>
<p>运行Hypervisor的物理机器。</p>
<pre class="crayon-plain-tag">openstack host list [--sort-column SORT_COLUMN] [--zone &lt;zone&gt;]

openstack host set
    [--enable | --disable]
    [--enable-maintenance | --disable-maintenance]
    &lt;host&gt;

openstack host show [--sort-column SORT_COLUMN] &lt;host&gt;</pre>
<div class="blog_h3"><span class="graybg">hypervisor</span></div>
<pre class="crayon-plain-tag">openstack hypervisor list
    [--sort-column SORT_COLUMN]
    [--matching &lt;hostname&gt;]
    [--long]

openstack hypervisor show &lt;hypervisor&gt;</pre>
<div class="blog_h3"><span class="graybg">hypervisor stats </span> </div>
<pre class="crayon-plain-tag">openstack hypervisor stats show </pre>
<div class="blog_h3"><span class="graybg">keypair</span></div>
<p>OpenSSH公钥管理，用于访问创建的server（虚拟机）。</p>
<pre class="crayon-plain-tag"># 创建公钥
# 如果什么参数都不指定，则生成新的公钥
openstack keypair create
    [--public-key &lt;file&gt; | --private-key &lt;file&gt;]
    [--type &lt;type&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;name&gt;

openstack keypair delete
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;key&gt;
    [&lt;key&gt; ...]

openstack keypair list
    [--sort-column SORT_COLUMN]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]

openstack keypair show
    [--public-key]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    &lt;key&gt;</pre>
<div class="blog_h3"><span class="graybg">versions show</span></div>
<p>显示所有服务的版本、端点、是否弃用之类的信息</p>
<pre class="crayon-plain-tag">openstack versions show </pre>
<div class="blog_h3"><span class="graybg">catalog</span></div>
<p>显示服务的类型、名称、端点列表：</p>
<pre class="crayon-plain-tag">openstack catalog list [--sort-column SORT_COLUMN]
openstack catalog show &lt;service&gt;

openstack catalog list
# +-----------+-----------+-----------------------------------------+
# | Name      | Type      | Endpoints                               |
# +-----------+-----------+-----------------------------------------+
# | glance    | image     | zircon                                  |
# |           |           |   public: http://os.gmem.cc:9292        |
# |           |           |                                         |
# | keystone  | identity  | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:5000/v3/  |
# |           |           | zircon                                  |
# |           |           |   public: http://os.gmem.cc:5000/v3/    |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:5000/v3/     |
# |           |           |                                         |
# | nova      | compute   | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:8774/v2.1 |
# |           |           | zircon                                  |
# |           |           |   public: http://os.gmem.cc:8774/v2.1   |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:8774/v2.1    |
# |           |           |                                         |
# | placement | placement | zircon                                  |
# |           |           |   public: http://os.gmem.cc:8778        |
# |           |           | zircon                                  |
# |           |           |   admin: http://os.gmem.cc:8778         |
# |           |           | zircon                                  |
# |           |           |   internal: http://os.gmem.cc:8778      |
# |           |           |                                         |
# +-----------+-----------+-----------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">extension</span></div>
<p>很多OpenStack API包含API扩展，这些扩展提供额外的功能。</p>
<pre class="crayon-plain-tag"># 列出API扩展
openstack extension list
    [--sort-column SORT_COLUMN]
    [--compute]
    [--identity]
    [--network]
    [--volume]
    [--long]

# 显示API扩展
openstack extension show &lt;extension&gt;</pre>
<div class="blog_h3"><span class="graybg">endpoint</span></div>
<p>管理服务的API端点</p>
<pre class="crayon-plain-tag"># 关联项目到端点
openstack endpoint add project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint&gt;
    &lt;project&gt;
openstack endpoint remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint&gt;
    &lt;project&gt;

# 创建新的端点
openstack endpoint create
    # 所属的区域
    [--region &lt;region-id&gt;]
    [--enable | --disable]
    # 端点所属的服务
    &lt;service&gt;
    # admin, public 还是 internal
    &lt;interface&gt;
    &lt;url&gt;

openstack endpoint delete &lt;endpoint-id&gt; [&lt;endpoint-id&gt; ...]

openstack endpoint list
    [--sort-column SORT_COLUMN]
    [--service &lt;service&gt;]
    [--interface &lt;interface&gt;]
    [--region &lt;region-id&gt;]
    [--endpoint &lt;endpoint-group&gt; | --project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]

openstack endpoint set
    [--region &lt;region-id&gt;]
    [--interface &lt;interface&gt;]
    [--url &lt;url&gt;]
    [--service &lt;service&gt;]
    [--enable | --disable]
    &lt;endpoint-id&gt;

openstack endpoint show &lt;endpoint&gt;</pre>
<div class="blog_h3"><span class="graybg">endpoint group</span></div>
<p>一组端点，可以一起关联到项目。</p>
<pre class="crayon-plain-tag"># 关联端点组到项目
openstack endpoint group add project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint-group&gt;
    &lt;project&gt;
openstack endpoint group remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;endpoint-group&gt;
    &lt;project&gt;


# 创建端点组
openstack endpoint group create
    [--description DESCRIPTION]
    &lt;name&gt;
    &lt;filename&gt;
openstack endpoint group delete &lt;endpoint-group&gt; [&lt;endpoint-group&gt; ...]
openstack endpoint group set
    [--name &lt;name&gt;]
    [--filters &lt;filename&gt;]
    [--description &lt;description&gt;]
    &lt;endpoint-group&gt;
openstack endpoint group list
    [--sort-column SORT_COLUMN]
    [--endpointgroup &lt;endpoint-group&gt; | --project &lt;project&gt;]
    [--domain &lt;domain&gt;]
openstack endpoint group show &lt;endpointgroup&gt; </pre>
<div class="blog_h3"><span class="graybg">flavor</span></div>
<p>表示一种虚拟机的规格。</p>
<pre class="crayon-plain-tag">openstack flavor create
    [--id &lt;id&gt;]
    # 内存大小，MB
    [--ram &lt;size-mb&gt;]
    # 磁盘大小，GB
    [--disk &lt;size-gb&gt;]
    # 临时磁盘大小
    [--ephemeral &lt;size-gb&gt;]
    # 交换分区大小
    [--swap &lt;size-mb&gt;]
    # VCPU数量，默认1
    [--vcpus &lt;vcpus&gt;]
    # RX/TX 因子，默认1.0
    [--rxtx-factor &lt;factor&gt;]
    # 是否可以被其它项目使用
    [--public | --private]
    [--property &lt;key=value&gt;]
    # 所属项目
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 传统的名字是  XX.SIZE_NAME 格式，现在已经没有要求。不排除某些工具依赖于这种名称格式
    &lt;flavor-name&gt;

openstack flavor list
    [--sort-column SORT_COLUMN]
    [--public | --private | --all]
    [--long]
    [--marker &lt;flavor-id&gt;]
    [--limit &lt;num-flavors&gt;]

openstack flavor set
    [--no-property]
    [--property &lt;key=value&gt;]
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;flavor&gt;
openstack flavor unset
    [--property &lt;key&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    &lt;flavor&gt;

openstack flavor show &lt;flavor&gt;


# 创建一个Flavor
openstack flavor create FLAVOR_NAME --id FLAVOR_ID \
    --ram RAM_IN_MB --disk ROOT_DISK_IN_GB --vcpus NUMBER_OF_VCPUS

# 创建Flavor并分配给一个租户
openstack flavor create --public m1.extra_tiny --id auto \
    --ram 256 --disk 0 --vcpus 1
openstack flavor set --project PROJECT_ID m1.extra_tiny



# 列出所有flavor
openstack flavor create --id 0 --vcpus 1 --ram 64 --disk 1 m1.nano
openstack flavor create --id 1 --vcpus 1 --ram 512 --disk 8 m1.tiny
openstack flavor create --id 2 --vcpus 1 --ram 2048 --disk 32 m1.small
openstack flavor create --id 3 --vcpus 2 --ram 4096 --disk 64 m1.medium
openstack flavor create --id 4 --vcpus 4 --ram 8192 --disk 128 m1.large
openstack flavor create --id 5 --vcpus 8 --ram 16384 --disk 256 m1.xlarge

openstack flavor list
# +----+-----------+-------+------+-----------+-------+-----------+
# | ID | Name      |   RAM | Disk | Ephemeral | VCPUs | Is Public |
# +----+-----------+-------+------+-----------+-------+-----------+
# | 0  | m1.nano   |    64 |    1 |         0 |     1 | True      |
# | 1  | m1.tiny   |   512 |    8 |         0 |     1 | True      |
# | 2  | m1.small  |  2048 |   32 |         0 |     1 | True      |
# | 3  | m1.medium |  4096 |   64 |         0 |     2 | True      |
# | 4  | m1.large  |  8192 |  128 |         0 |     4 | True      |
# | 5  | m1.xlarge | 16384 |  256 |         0 |     8 | True      |
# +----+-----------+-------+------+-----------+-------+-----------+</pre>
<div class="blog_h3"><span class="graybg">image</span></div>
<p>管理镜像。</p>
<pre class="crayon-plain-tag"># 允许项目访问镜像
openstack image add project
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;
    &lt;project&gt;
openstack image remove project
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;
    &lt;project&gt;

# 创建镜像
openstack image create
    [--id &lt;id&gt;]
    # 镜像容器格式：ami, ari, aki, bare, docker, ova, ovf。默认bare
    [--container-format &lt;container-format&gt;]
    # 镜像磁盘格式：ami, ari, aki, vhd, vmdk, raw, qcow2, vhdx, vdi, iso, ploop。默认raw
    [--disk-format &lt;disk-format&gt;]
    # 启动镜像需要的最小磁盘尺寸
    [--min-disk &lt;disk-gb&gt;]
    # 启动镜像需要的最小内存
    [--min-ram &lt;ram-mb&gt;]
    # 镜像文件       从卷生成镜像
    [--file &lt;file&gt; | --volume &lt;volume&gt;]
    # 从卷生成镜像时，即便卷正在使用，也强制生成镜像
    [--force]
    # 使用本地私钥签名镜像
    [--sign-key-path &lt;sign-key-path&gt;]
    # 用于镜像签名校验的，位于key manager中的certificate的UUID
    [--sign-cert-id &lt;sign-cert-id&gt;]
    # 保护镜像防止被删除
    [--protected | --unprotected]
    # 公共      项目私有    可被社区使用    共享
    [--public | --private | --community | --shared]
    [--property &lt;key=value&gt;]
    [--tag &lt;tag&gt;]
    [--project &lt;project&gt;]
    [--import]
    [--project-domain &lt;project-domain&gt;]
    &lt;image-name&gt;

# 设置镜像属性
openstack image set
    [--name &lt;name&gt;]
    [--min-disk &lt;disk-gb&gt;]
    [--min-ram &lt;ram-mb&gt;]
    [--container-format &lt;container-format&gt;]
    [--disk-format &lt;disk-format&gt;]
    [--protected | --unprotected]
    [--public | --private | --community | --shared]
    [--property &lt;key=value&gt;]
    [--tag &lt;tag&gt;]
    [--architecture &lt;architecture&gt;]
    [--instance-id &lt;instance-id&gt;]
    [--kernel-id &lt;kernel-id&gt;]
    [--os-distro &lt;os-distro&gt;]
    [--os-version &lt;os-version&gt;]
    [--ramdisk-id &lt;ramdisk-id&gt;]
    [--deactivate | --activate]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--accept | --reject | --pending]
    &lt;image&gt;
openstack image unset [--tag &lt;tag&gt;] [--property &lt;property-key&gt;] &lt;image&gt;

# 删除镜像
openstack image delete &lt;image&gt; [&lt;image&gt; ...]

# 将镜像保存到本地
openstack image save [--file &lt;filename&gt;] &lt;image&gt;

# 列出镜像
openstack image member list
    [--sort-column SORT_COLUMN]
    [--project-domain &lt;project-domain&gt;]
    &lt;image&gt;

# 显示镜像信息
openstack image show [--human-readable] &lt;image&gt;


# 示例
# 上传镜像
openstack image create --public --disk-format qcow2 --container-format bare \
  --file cirros-0.5.1-x86_64-disk.img cirros-0.5.1-amd64
# 将卷上传为镜像，上传的QCOW2镜像，会自动shrink
# 注意，默认情况下只有Available状态的卷能够上传为镜像，In-use的用--force也不行
# Force upload to image is disabled, Force option will be ignored.
openstack image create --volume 840e9e25-192c-401b-83f5-898fd82839c4 --force centos8-amd64-prepared</pre>
<div class="blog_h3"><span class="graybg">compute agent</span></div>
<p>计算代理是和Hypervisor相关的，且仅仅被 XenAPI hypervisor driver支持。</p>
<div class="blog_h3"><span class="graybg">compute service</span></div>
<p>Nova相关的服务。</p>
<pre class="crayon-plain-tag">openstack compute service delete &lt;service&gt; [&lt;service&gt; ...]

openstack compute service list
    [--sort-column SORT_COLUMN]
    [--host &lt;host&gt;]
    [--service &lt;service&gt;]
    [--long]

openstack compute service set
    [--enable | --disable]
    [--disable-reason &lt;reason&gt;]
    [--up | --down]
    &lt;host&gt;
    &lt;service&gt;</pre>
<div class="blog_h3"><span class="graybg">console log</span></div>
<p>显示虚拟机的控制台日志：</p>
<pre class="crayon-plain-tag">openstack console log show [--lines &lt;num-lines&gt;] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">console url</span></div>
<p>打印各种类型的控制台URL：</p>
<pre class="crayon-plain-tag">openstack console url show
    [--novnc | --xvpvnc | --spice | --rdp | --serial | --mks]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server group</span></div>
<p>基于某种策略对服务器进行分组。</p>
<pre class="crayon-plain-tag">openstack server group create [--policy &lt;policy&gt;] &lt;name&gt;

openstack server group delete &lt;server-group&gt; [&lt;server-group&gt; ...]

openstack server group list
    [--sort-column SORT_COLUMN]
    [--all-projects]
    [--long]

openstack server group show &lt;server-group&gt;</pre>
<div class="blog_h3"><span class="graybg"><a id="server-create"></a>server create</span></div>
<p>创建一个实例（虚拟机，也叫服务器server） </p>
<pre class="crayon-plain-tag">openstack server create
    # --image 从镜像创建服务器的启动磁盘
    # --volume 将卷作为服务器的启动磁盘，会自动创建一个块设备，映射boot index为0
    #          在很多Hypervisor（例如libvirt/kvm）这个磁盘将是vda
    #          不要使用 –block-device-mapping  选项来为此磁盘创建重复的映射
    (--image &lt;image&gt; | --image-property &lt;key=value&gt; | --volume &lt;volume&gt;)
    # 服务器的密码
    [--password &lt;password&gt;]
    # 服务器的风格
    --flavor &lt;flavor&gt;
    # 加入安全组，可以指定多个
    [--security-group &lt;security-group&gt;]
    # 注入到此服务器的OpenSSH公钥
    [--key-name &lt;key-name&gt;]
    # 设置属性
    [--property &lt;key=value&gt;]
    # 在启动之前，注入到镜像中的文件。可以指定多个
    [--file &lt;dest-filename=source-filename&gt;]
    # 从metadata服务器来serve的用户数据文件。用于配置新实例
    [--user-data &lt;user-data&gt;]
    [--description &lt;description&gt;]
    # 在哪个AZ中创建此服务器，可以指定：
    #   &lt;zone-name&gt;:&lt;host-name&gt;:&lt;node-name&gt;
    #   &lt;zone-name&gt;::&lt;node-name&gt;
    #   &lt;zone-name&gt;:&lt;host-name&gt;
    #   &lt;zone-name&gt;
    [--availability-zone &lt;zone-name&gt;]
    # 指定在某个宿主机上创建实例
    [--host &lt;host&gt;]
    [--hypervisor-hostname &lt;hypervisor-hostname&gt;]
    # 和 --image 或 --image-property 一起使用时，该选项自动创建boot index 为0的块设备
    # 并且告知计算服务，从镜像创建卷+卷的大小（GB）。此卷在实例销毁后不会删除
    # 不能和 --volume 联用
    [--boot-from-volume &lt;volume-size&gt;]
    # 在服务器上创建额外的块设备，格式：
    #   &lt;dev-name&gt;=&lt;id&gt;:&lt;type&gt;:&lt;size(GB)&gt;:&lt;delete-on-terminate&gt;
    #     dev-name 为块设备名称，例如vdb xvdc
    #     id 为卷、卷快照、镜像的名字或ID
    #     type：volume, snapshot 或 image。默认volume
    #     size：卷的大小
    #     delete-on-terminate：true或false
    [--block-device-mapping &lt;dev-name=mapping&gt;]
    # 在服务器上创建NIC。要创建多个NIC，则指定多次
    #     net-id和port-id互斥，不能同时指定
    #     v4-fixed-ip 此NIC的IPv4固定IP地址
    #     v6-fixed-ip 此NIC的IPv6固定IP地址
    #     auto 由计算服务自动分配一个网络。不能和其它参数一起使用
    #     none 不连接到网络。不能和其它参数一起使用
    [--nic &lt;net-id=net-uuid,v4-fixed-ip=ip-addr,v6-fixed-ip=ip-addr,port-id=port-uuid,auto,none&gt;]
    # 创建一个NIC，并且连接到该网络。可以指定多次，以创建多个NIC并连接到多个网络
    [--network &lt;network&gt;]
    # 创建一个NIC，并且连接到该端口。可以指定多次，以创建多个NIC并连接到多个端口
    [--port &lt;port&gt;]
    # 提供给nova-scheduler的提示信息
    [--hint &lt;key=value&gt;]
    # 启用config drive
    [--use-config-drive | --no-config-drive | --config-drive &lt;config-drive-volume&gt;|True]
    # 启动的实例数量
    [--min &lt;count&gt;]
    [--max &lt;count&gt;]
    # 等待实例构建完成
    [--wait]
    [--tag &lt;tag&gt;]
    # 实例的名字
    &lt;server-name&gt;


# 从镜像启动虚拟机，并挂载一个非启动磁盘
## 创建非启动磁盘
openstack volume create --size 8 my-volume
## 创建虚拟机         从镜像创建
nova boot --flavor 2 --image 98901246-af91-43d8-b5e6-a4506aa8f369 \
# 添加块设备      源是卷        卷的ID                                   挂载为卷    虚拟机删除后保留卷  
  --block-device source=volume,id=d620d971-b160-4c4e-8652-2513d74e2080,dest=volume,shutdown=preserve \
  myInstanceWithVolume


# 从SOURCE创建启动卷，并从该卷启动虚拟机
# SOURCE： volume, snapshot, image, 或者 blank
# DEST：volume或local
nova boot --flavor FLAVOR --block-device \
  source=SOURCE,id=ID,dest=DEST,size=SIZE,shutdown=PRESERVE,bootindex=INDEX  NAME

# 从镜像创建一个可启动卷。如果指定--image参数，则Cinder自动将卷标记为可启动的
openstack volume create --image IMAGE_ID --size SIZE_IN_GB bootable_volume


# 挂载Swap或者临时磁盘到虚拟机
nova boot --flavor FLAVOR --image IMAGE_ID --swap 512  --ephemeral size=2 NAME</pre>
<div class="blog_h3"><span class="graybg">server add fixed ip</span></div>
<p>为服务器添加固定IP地址：</p>
<pre class="crayon-plain-tag">openstack server add fixed ip
    # 请求的固定IP地址
    [--fixed-ip-address &lt;ip-address&gt;]
    [--tag &lt;tag&gt;]
    &lt;server&gt;
    &lt;network&gt;

openstack server remove fixed ip &lt;server&gt; &lt;ip-address&gt;</pre>
<div class="blog_h3"><span class="graybg">server add floating ip</span></div>
<p>为服务器添加浮动IP地址：</p>
<pre class="crayon-plain-tag">openstack server add floating ip
    # 和此浮动IP地址关联的固定IP地址，使用第一个具有此固定IP地址的、此服务器上的port
    [--fixed-ip-address &lt;ip-address&gt;]
    &lt;server&gt;
    # 分配给上述第一个服务器port的浮动IP地址
    &lt;ip-address&gt;

openstack server remove floating ip &lt;server&gt; &lt;ip-address&gt;</pre>
<div class="blog_h3"><span class="graybg">server add network</span></div>
<p>将服务器连接到某个网络 </p>
<pre class="crayon-plain-tag">openstack server add network [--tag &lt;tag&gt;] &lt;server&gt; &lt;network&gt;

openstack server remove network &lt;server&gt; &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">server add port </span></div>
<p>将某个端口连接到服务器</p>
<pre class="crayon-plain-tag">openstack server add port [--tag &lt;tag&gt;] &lt;server&gt; &lt;port&gt;

openstack server remove port &lt;server&gt; &lt;port&gt;</pre>
<div class="blog_h3"><span class="graybg">server add security group</span></div>
<p>将服务器添加到安全组</p>
<pre class="crayon-plain-tag">openstack server add security group &lt;server&gt; &lt;group&gt;

openstack server remove security group &lt;server&gt; &lt;group&gt;</pre>
<div class="blog_h3"><span class="graybg">server add volume </span></div>
<p>挂载（Attach）一个卷给服务器</p>
<pre class="crayon-plain-tag">openstack server add volume
    # 服务器上的内部设备名
    [--device &lt;device&gt;]
    [--tag &lt;tag&gt;]
    # 如果服务器被销毁，此卷是否被删除
    [--enable-delete-on-termination | --disable-delete-on-termination]
    &lt;server&gt;
    &lt;volume&gt;

openstack server remove volume &lt;server&gt; &lt;volume&gt;

# 示例
openstack server add volume cirros-amd64 cirros-amd64-diskb</pre>
<div class="blog_h3"><span class="graybg">server migration list</span></div>
<p>服务器迁移，就是将一台宿主机上的实例，转移到另外一台上运行。</p>
<p>OpenStack支持四种迁移模式：热迁移、冷迁移、升降配（resize）、重建（evacuation）</p>
<pre class="crayon-plain-tag"># 显示迁移历史的列表
openstack server migration list
    [--sort-column SORT_COLUMN]
    [--server &lt;server&gt;]
    [--host &lt;host&gt;]
    [--status &lt;status&gt;]
    [--type &lt;type&gt;]
    [--marker &lt;marker&gt;]
    [--limit &lt;limit&gt;]
    [--changes-since &lt;changes-since&gt;]
    [--changes-before &lt;changes-before&gt;]
    [--project &lt;project&gt;]
    [--user &lt;user&gt;]</pre>
<div class="blog_h3"><span class="graybg">server resize</span></div>
<p>扩/缩容服务器为新的flavor。实现方式是：</p>
<ol>
<li>创就一个新的服务器</li>
<li>复制文件到新服务器</li>
</ol>
<p>扩/缩容操作分为两步完成：第一步迁移，第二步确认</p>
<pre class="crayon-plain-tag">openstack server resize
    [--flavor &lt;flavor&gt; | --confirm | --revert]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server resize confirm</span></div>
<pre class="crayon-plain-tag">openstack server resize confirm &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server resize revert</span></div>
<pre class="crayon-plain-tag">openstack server resize revert &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate</span></div>
<p>将服务器迁移到另外一个宿主机上。</p>
<p>迁移操作是基于resize操作实现的：</p>
<ol>
<li>创建一个新的实例，使用相同的flavor</li>
<li>从原始磁盘上拷贝内容到新磁盘</li>
</ol>
<p>和resize一样，迁移操作是分两步完成的：</p>
<ol>
<li>执行上述两步的迁移操作</li>
<li>让用户确认，迁移是否成功并移除酒实例，还是执行revert操作 —— 删除新实例并重启老的</li>
</ol>
<pre class="crayon-plain-tag">openstack server migrate
    # 不宕机迁移
    [--live-migration]
    # 目标主机 
    [ --host &lt;hostname&gt;]
    [--shared-migration | --block-migration]
    [--disk-overcommit | --no-disk-overcommit]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate confirm</span></div>
<p>确认迁移</p>
<pre class="crayon-plain-tag">openstack server migrate confirm &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server migrate revert </span></div>
<p>撤销迁移 </p>
<pre class="crayon-plain-tag">openstack server migrate revert &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server evacuate</span></div>
<p>将服务器在另外一个宿主机上重建。这个命令的使用场景是：<span style="background-color: #c0c0c0;">实例已经运行，但是后来它所在的宿主机宕掉了</span>。 也就是说，仅当管理此实例的compute service宕机了，才可以使用此命令。</p>
<p>如果服务器实例使用临时的（ephemeral）root磁盘，此<span style="background-color: #c0c0c0;">磁盘位于非共享存储上，则使用原始的glance镜像重建服务器。连接到原实例的port、挂载的卷被保留</span>。</p>
<p><span style="background-color: #c0c0c0;">如果服务器实例从volume启动，或者跟磁盘位于共享存储上，则新建实例会重用</span>此启动盘。</p>
<div class="blog_h3"><span class="graybg">server pause</span></div>
<p>暂停服务器，状态保存在内存中</p>
<pre class="crayon-plain-tag">openstack server pause &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server unpause</span></div>
<p>取消暂停服务器</p>
<pre class="crayon-plain-tag">openstack server unpause &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server suspend</span></div>
<p>暂停服务器，状态保存在磁盘中</p>
<pre class="crayon-plain-tag">openstack server suspend &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server resume</span></div>
<p>从暂停中恢复</p>
<pre class="crayon-plain-tag">openstack server resume &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server restore</span></div>
<p>回退状态为软删除的服务器</p>
<pre class="crayon-plain-tag">openstack server restore &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server reboot</span></div>
<p>重启服务器</p>
<pre class="crayon-plain-tag">#                       强行立即重启
openstack server reboot [--hard | --soft] [--wait] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server start</span></div>
<p>启动服务器</p>
<pre class="crayon-plain-tag">openstack server start &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server stop</span></div>
<p>停止服务器</p>
<pre class="crayon-plain-tag">openstack server stop &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server rebuild</span></div>
<p>重建服务器</p>
<pre class="crayon-plain-tag">openstack server rebuild
    [--image &lt;image&gt;]
    [--password &lt;password&gt;]
    [--property &lt;key=value&gt;]
    [--description &lt;description&gt;]
    [--key-name &lt;key-name&gt; | --key-unset]
    [--wait]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server rescue</span></div>
<p>让服务器进入rescue模式</p>
<pre class="crayon-plain-tag">openstack server rescue
    [--image &lt;image&gt;]
    [--password &lt;password&gt;]
    &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server unrescue</span></div>
<p>从rescue模式恢复：</p>
<pre class="crayon-plain-tag">openstack server unrescue &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server shelve</span></div>
<p>将服务器实例作为镜像，保存在glance中，然后在宿主机上删除此服务器 </p>
<pre class="crayon-plain-tag">openstack server shelve &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server unshelve</span></div>
<p>将shelve的实例恢复</p>
<pre class="crayon-plain-tag">openstack server unshelve
    [--availability-zone AVAILABILITY_ZONE]
    &lt;server&gt;
    [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server ssh</span></div>
<p>通过SSH连接到服务器</p>
<pre class="crayon-plain-tag">openstack server ssh
    [--login &lt;login-name&gt;]
    [--port &lt;port&gt;]
    [--identity &lt;keyfile&gt;]
    [--option &lt;config-options&gt;]
    [-4 | -6]
    [--public | --private | --address-type &lt;address-type&gt;]
    &lt;server&gt;


# 自动使用当前用户的SSH key
openstack server ssh --private -4 --login cirros cirros-amd64-0</pre>
<div class="blog_h3"><span class="graybg">server dump create</span></div>
<p>创建服务器的Dump文件。这会触发一个crash dump（例如Linux的kdump） </p>
<pre class="crayon-plain-tag">openstack server dump create &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server delete</span></div>
<p>（软）删除服务器</p>
<pre class="crayon-plain-tag">openstack server delete [--wait] &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server set</span></div>
<p>设置服务器属性</p>
<pre class="crayon-plain-tag">openstack server set
    [--name &lt;new-name&gt;]
    [--root-password]
    [--property &lt;key=value&gt;]
    [--state &lt;state&gt;]
    [--description &lt;description&gt;]
    [--tag &lt;tag&gt;]
    &lt;server&gt;

openstack server unset
    [--property &lt;key&gt;]
    [--description]
    [--tag &lt;tag&gt;]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server lock</span></div>
<p>锁定实例，这样非admin用户就不能对它进行任何操作。</p>
<pre class="crayon-plain-tag">openstack server lock [--reason &lt;reason&gt;] &lt;server&gt; [&lt;server&gt; ...]</pre>
<div class="blog_h3"><span class="graybg">server unlock</span></div>
<p>解锁服务器</p>
<pre class="crayon-plain-tag">openstack server unpause &lt;server&gt; [&lt;server&gt; ...] </pre>
<div class="blog_h3"><span class="graybg">server list</span></div>
<pre class="crayon-plain-tag">openstack server list
    [--sort-column SORT_COLUMN]
    [--availability-zone &lt;availability-zone&gt;]
    [--reservation-id &lt;reservation-id&gt;]
    [--ip &lt;ip-address-regex&gt;]
    [--ip6 &lt;ip-address-regex&gt;]
    [--name &lt;name-regex&gt;]
    [--instance-name &lt;server-name&gt;]
    [--status &lt;status&gt;]
    [--flavor &lt;flavor&gt;]
    [--image &lt;image&gt;]
    [--host &lt;hostname&gt;]
    [--all-projects]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--user &lt;user&gt;]
    [--user-domain &lt;user-domain&gt;]
    [--long]
    [-n | --name-lookup-one-by-one]
    [--marker &lt;server&gt;]
    [--limit &lt;num-servers&gt;]
    [--deleted]
    [--changes-before &lt;changes-before&gt;]
    [--changes-since &lt;changes-since&gt;]
    [--locked | --unlocked]
    [--tags &lt;tag&gt;]
    [--not-tags &lt;tag&gt;]</pre>
<div class="blog_h3"><span class="graybg">server show</span></div>
<pre class="crayon-plain-tag">#                     显示诊断信息
openstack server show [--diagnostics] &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server backup</span></div>
<p>备份一个运行中的服务器实例，将其磁盘保存为镜像，存放在Glance中。</p>
<pre class="crayon-plain-tag"># 创建备份
openstack server backup create
    # 备份镜像的名字，默认为服务器名
    [--name &lt;image-name&gt;]
    # 填充镜像的backup_type字段
    [--type &lt;backup-type&gt;]
    # 保存的备份数量
    [--rotate &lt;count&gt;]
    [--wait]
    &lt;server&gt;</pre>
<div class="blog_h3"><span class="graybg">server image</span></div>
<p>从运行中的实例创建磁盘镜像，并存放到Glance中。</p>
<pre class="crayon-plain-tag">openstack server image create [--name &lt;image-name&gt;] [--wait] &lt;server&gt; </pre>
<div class="blog_h3"><span class="graybg">server event</span></div>
<p>服务器事件，记录了针对服务器的各种操作。事件由操作类型（create, delete, reboot ...）+ 操作结果（success, error） + 开始/结束时间组成。</p>
<pre class="crayon-plain-tag"># 事件列表
openstack server event list
    [--sort-column SORT_COLUMN]
    [--long]
    &lt;server&gt;

# 显示事件
openstack server event show &lt;server&gt; &lt;request-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network service provider</span></div>
<p>一个网络服务提供者，表示一个特定的、实现了网络服务的驱动：</p>
<pre class="crayon-plain-tag">openstack network service provider list [--sort-column SORT_COLUMN]</pre>
<div class="blog_h3"><span class="graybg">network</span></div>
<p>所谓网络，是指一个独立的（isolated）的L2网段。OpenStack具有两种类型的网络：</p>
<ol>
<li>project：完全隔离的、不和其它项目共享的网络。自服务网络</li>
<li>provider：映射到现有的、数据中心中的物理网络，为server或其它资源提供外部网络访问</li>
</ol>
<p>仅仅管理员可以创建provider网络</p>
<pre class="crayon-plain-tag">openstack network create
    # 是否允许跨项目共享
    [--share | --no-share]
    # 是否启用网络
    [--enable | --disable]
    # 所属项目
    [--project &lt;project&gt;]
    [--description &lt;description&gt;]
    # MTU设置
    [--mtu &lt;mtu&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 在什么AZ中创建此网络。需要Network Availability Zone扩展
    # 此选项可以指定多次，表示网络跨越多个AZ
    [--availability-zone-hint &lt;availability-zone&gt;]
    # 是否启用端口安全，如果指定，则此网络上创建的端口自动应用安全设置
    [--enable-port-security | --disable-port-security]
    # 是否外部网络，如果是，则需要external-net扩展
    [--external | --internal]
    # 是否作为默认外部网络
    [--default | --no-default]
    # 应用到此网络的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    # 指定此网络是VLAN透明的
    [--transparent-vlan | --no-transparent-vlan]
    # 此虚拟网络所基于其实现的物理机制（physical mechanism）
    #   例如  flat, geneve, gre, local, vlan, vxlan
    [--provider-network-type &lt;provider-network-type&gt;]
    # 此虚拟网络所基于的物理网络的名字
    [--provider-physical-network &lt;provider-physical-network&gt;]
    # 对于VLAN网络，指定VLAN ID
    # 对于GENEVE/GRE/VXLAN，指定Tunnel ID
    [--provider-segment &lt;provider-segment&gt;]
    # 此网络的DNS domain，需要DNS integration扩展
    [--dns-domain &lt;dns-domain&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    # IPv4的CIDR
    --subnet &lt;subnet&gt;
    &lt;name&gt;

openstack network delete &lt;network&gt; [&lt;network&gt; ...]

openstack network list
    [--sort-column SORT_COLUMN]
    [--external | --internal]
    [--long]
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]
    [--status &lt;status&gt;]
    [--provider-network-type &lt;provider-network-type&gt;]
    [--provider-physical-network &lt;provider-physical-network&gt;]
    [--provider-segment &lt;provider-segment&gt;]
    [--agent &lt;agent-id&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack network set
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--share | --no-share]
    [--description &lt;description]
    [--mtu &lt;mtu]
    [--enable-port-security | --disable-port-security]
    [--external | --internal]
    [--default | --no-default]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    [--provider-network-type &lt;provider-network-type&gt;]
    [--provider-physical-network &lt;provider-physical-network&gt;]
    [--provider-segment &lt;provider-segment&gt;]
    [--dns-domain &lt;dns-domain&gt;]
    &lt;network&gt;
openstack network unset [--tag &lt;tag&gt; | --all-tag] &lt;network&gt;

openstack network show &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">subnet</span></div>
<p>子网是一段IP地址以及关联的配置状态。当新的Port接入到网络中时，子网用于向Port分配IP地址。</p>
<pre class="crayon-plain-tag">openstack subnet create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # --subnet-pool 该子网从此池中获得自己的CIDR
    # --use-default-subnet-pool 使用默认的子网池
    [--subnet-pool &lt;subnet-pool&gt; | --use-prefix-delegation USE_PREFIX_DELEGATION | --use-default-subnet-pool]
    # 从子网池中分配子网时，前缀的长度
    [--prefix-length &lt;prefix-length&gt;]
    # CIDR格式的子网IP地址范围
    [--subnet-range &lt;subnet-range&gt;]
    # 是否启用DHCP
    [--dhcp | --no-dhcp]
    # 是否在DNS中发布固定IP
    [--dns-publish-fixed-ip | --no-dns-publish-fixed-ip]
    # 指定子网的网关。三种形式：
    #   &lt;ip-address&gt; 将指定的IP地址作为网关
    #   auto 自动在子网内部选择网关地址，默认
    #   none 不使用网关
    [--gateway &lt;gateway&gt;]
    # 子网的IP版本，如果使用了subnet pool，则IP版本取决于子网池，该选项被忽略
    [--ip-version {4,6}]
    # IPv6 RA（Router Advertisement）模式
    [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
    # IPv6地址模式
    [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}]
    # 关联到此子网的网段，网段属于某个网络
    [--network-segment &lt;network-segment&gt;]
    # 此子网所属的网络
    --network &lt;network&gt;
    [--description &lt;description&gt;]
    # 此子网的DHCP自动分配IP地址的范围。可以指定多个
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    # 此子网使用的DNS服务器
    [--dns-nameserver &lt;dns-nameserver&gt;]
    # 为子网添加额外的路由，示例 destination=10.10.0.0/16,gateway=192.168.71.254 网关为下一跳地址
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    # 子网的服务类型，必须指定为有效的、某个网络端口的device owner，例如network:floatingip_agent_gateway
    # 可以指定多个，以支持多个服务类型
    [--service-type &lt;service-type&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack subnet delete &lt;subnet&gt; [&lt;subnet&gt; ...]

openstack subnet list
    [--sort-column SORT_COLUMN]
    [--long]
    [--ip-version &lt;ip-version&gt;]
    [--dhcp | --no-dhcp]
    [--service-type &lt;service-type&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--network &lt;network&gt;]
    [--gateway &lt;gateway&gt;]
    [--name &lt;name&gt;]
    [--subnet-range &lt;subnet-range&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
openstack subnet show &lt;subnet&gt;

openstack subnet set
    [--name &lt;name&gt;]
    [--dhcp | --no-dhcp]
    [--dns-publish-fixed-ip | --no-dns-publish-fixed-ip]
    [--gateway &lt;gateway&gt;]
    [--network-segment &lt;network-segment&gt;]
    [--description &lt;description&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    [--no-allocation-pool]
    [--dns-nameserver &lt;dns-nameserver&gt;]
    [--no-dns-nameservers]
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--no-host-route]
    [--service-type &lt;service-type&gt;]
    &lt;subnet&gt;
openstack subnet unset
    [--allocation-pool start=&lt;ip-address&gt;,end=&lt;ip-address&gt;]
    [--dns-nameserver &lt;dns-nameserver&gt;]
    [--host-route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--service-type &lt;service-type&gt;]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;subnet&gt;</pre>
<div class="blog_h3"><span class="graybg">subnet pool</span></div>
<p>子网池中包含若干CIDR格式的子网前缀，这些前缀用于分配给子网。</p>
<pre class="crayon-plain-tag"># 创建一个子网池
openstack subnet pool create
    # 子网池的前缀
    --pool-prefix &lt;pool-prefix&gt;
    # 子网池默认前缀长度
    [--default-prefix-length &lt;default-prefix-length&gt;]
    # 最小/最大前缀长度
    [--min-prefix-length &lt;min-prefix-length&gt;]
    [--max-prefix-length &lt;max-prefix-length&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 关联到此池的address scope对象的名字或ID
    [--address-scope &lt;address-scope&gt;]
    # 是否作为默认子网池
    [--default | --no-default]
    [--share | --no-share]
    [--description &lt;description&gt;]
    # 默认的，每个项目的配额 —— 可以从池中分配的IP数量
    [--default-quota &lt;num-ip-addresses&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack subnet pool delete &lt;subnet-pool&gt; [&lt;subnet-pool&gt; ...]

openstack subnet pool list
    [--sort-column SORT_COLUMN]
    [--long]
    [--share | --no-share]
    [--default | --no-default]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--name &lt;name&gt;]
    [--address-scope &lt;address-scope&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
openstack subnet pool show &lt;subnet-pool&gt;

openstack subnet pool set
    [--name &lt;name&gt;]
    [--pool-prefix &lt;pool-prefix&gt;]
    [--default-prefix-length &lt;default-prefix-length&gt;]
    [--min-prefix-length &lt;min-prefix-length&gt;]
    [--max-prefix-length &lt;max-prefix-length&gt;]
    [--address-scope &lt;address-scope&gt; | --no-address-scope]
    [--default | --no-default]
    [--description &lt;description&gt;]
    [--default-quota &lt;num-ip-addresses&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;subnet-pool&gt;
openstack subnet pool unset [--tag &lt;tag&gt; | --all-tag] &lt;subnet-pool&gt;</pre>
<div class="blog_h3"><span class="graybg">address scope</span></div>
<p>表示IPv4或IPv6的地址范围，属于某个特定项目，可以被多个项目共享。</p>
<pre class="crayon-plain-tag"># 创建地址范围
openstack address scope create
    [--ip-version {4,6}]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 是否可以在项目之间共享
    [--share | --no-share]
    &lt;name&gt;

openstack address scope delete &lt;address-scope&gt; [&lt;address-scope&gt; ...]

openstack address scope list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--ip-version &lt;ip-version&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]

openstack address scope set
    [--name &lt;name&gt;]
    [--share | --no-share]
    &lt;address-scope&gt;

openstack address scope show &lt;address-scope&gt; </pre>
<div class="blog_h3"><span class="graybg">router</span></div>
<p>虚拟路由器是一个逻辑组件，能够在不同网络之间分发数据包。虚拟路由器也提供L3和NAT转发功能，让虚拟网络中的服务器能够访问外部流量。</p>
<pre class="crayon-plain-tag"># 将端口添加到路由器
openstack router add port &lt;router&gt; &lt;port&gt;
openstack router remove port &lt;router&gt; &lt;port&gt;

# 将子网连接到路由器
openstack router add subnet &lt;router&gt; &lt;subnet&gt;
openstack router remove subnet &lt;router&gt; &lt;subnet&gt;

# 在路由器的路由表中添加一个静态路由
openstack router add route
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    &lt;router&gt;
openstack router remove route
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    &lt;router&gt;

# 创建路由器
openstack router create
    [--enable | --disable]
    # 集中还是分布式的
    [--distributed | --centralized]
    # 是否高可用
    [--ha | --no-ha]
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--availability-zone-hint &lt;availability-zone&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

# 删除路由器
openstack router delete &lt;router&gt; [&lt;router&gt; ...]

# 列出路由器
openstack router list
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    [--enable | --disable]
    [--long]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--agent &lt;agent-id&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

# 设置属性
openstack router set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--distributed | --centralized]
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--no-route]
    [--ha | --no-ha]
    [--external-gateway &lt;network&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--enable-snat | --disable-snat]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;router&gt;
openstack router unset
    [--route destination=&lt;subnet&gt;,gateway=&lt;ip-address&gt;]
    [--external-gateway]
    [--qos-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;router&gt;

# 显示路由器信息
openstack router show &lt;router&gt; </pre>
<div class="blog_h3"><span class="graybg">port</span></div>
<p>端口是网络上的接入点，它可以将单个设备（例如server上的NIC）连接到网络。端口也描述了其关联的网络配置，例如MAC地址、IP地址</p>
<pre class="crayon-plain-tag">openstack port create
    # 端口所属的网络
    --network &lt;network&gt;
    [--description &lt;description&gt;]
    # 端口的设备ID
    [--device &lt;device-id&gt;]
    # 端口的MAC地址
    [--mac-address &lt;mac-address&gt;]
    # 使用端口的实体，例如network:dhcp
    [--device-owner &lt;device-owner&gt;]
    # 端口的VNIC类型。默认normal
    # direct | direct-physical | macvtap | normal | baremetal | virtio-forwarder
    [--vnic-type &lt;vnic-type&gt;]
    # 在宿主机上分配端口
    [--host &lt;host-id&gt;]
    # 此端口所属的DNS域
    [--dns-domain dns-domain]
    # 此端口的DNS名称
    [--dns-name &lt;dns-name&gt;]
    # 调度此端口所需的NUMA亲和性策略
    [--numa-policy-required | --numa-policy-preferred | --numa-policy-legacy]
    # 此端口期望的IP和/或子网。可以指定多次
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt; | --no-fixed-ip]
    [--binding-profile &lt;binding-profile&gt;]
    # 启用/禁用端口
    [--enable | --disable]
    # 启用uplink状态传播
    [--enable-uplink-status-propagation | --disable-uplink-status-propagation]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 额外分配给此端口的DHCP选项
    [--extra-dhcp-option name=&lt;name&gt;[,value=&lt;value&gt;,ip-version={4,6}]]
    # 关联到此端口的安全组
    [--security-group &lt;security-group&gt; | --no-security-group]
    # 关联到此端口的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    # 是否启用端口安全
    [--enable-port-security | --disable-port-security]
    # 添加允许的IP/MAC地址。可以指定多次
    # 不被允许的IP地址，不能作为发往此端口的IP目的地址
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack port delete &lt;port&gt; [&lt;port&gt; ...]

openstack port list
    [--sort-column SORT_COLUMN]
    [--device-owner &lt;device-owner&gt;]
    [--host &lt;host-id&gt;]
    [--network &lt;network&gt;]
    [--router &lt;router&gt; | --server &lt;server&gt; | --device-id &lt;device-id&gt;]
    [--mac-address &lt;mac-address&gt;]
    [--long]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;,ip-substring=&lt;ip-substring&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]

openstack port set
    [--description &lt;description&gt;]
    [--device &lt;device-id&gt;]
    [--mac-address &lt;mac-address&gt;]
    [--device-owner &lt;device-owner&gt;]
    [--vnic-type &lt;vnic-type&gt;]
    [--host &lt;host-id&gt;]
    [--dns-domain dns-domain]
    [--dns-name &lt;dns-name&gt;]
    [--numa-policy-required | --numa-policy-preferred | --numa-policy-legacy]
    [--enable | --disable]
    [--name &lt;name&gt;]
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--no-fixed-ip]
    [--binding-profile &lt;binding-profile&gt;]
    [--no-binding-profile]
    [--qos-policy &lt;qos-policy&gt;]
    [--security-group &lt;security-group&gt;]
    [--no-security-group]
    [--enable-port-security | --disable-port-security]
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--no-allowed-address]
    [--data-plane-status &lt;status&gt;]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;port&gt;
openstack port unset
    [--fixed-ip subnet=&lt;subnet&gt;,ip-address=&lt;ip-address&gt;]
    [--binding-profile &lt;binding-profile-key&gt;]
    [--security-group &lt;security-group&gt;]
    [--allowed-address ip-address=&lt;ip-address&gt;[,mac-address=&lt;mac-address&gt;]]
    [--qos-policy]
    [--data-plane-status]
    [--numa-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;port&gt;

openstack port show &lt;port&gt; </pre>
<div class="blog_h3"><span class="graybg">security group</span></div>
<p>安全组是虚拟的网络防火墙，网络中的服务器、端口等资源可以受其影响。</p>
<p>安全组是安全组规则的容器。</p>
<pre class="crayon-plain-tag"># 创建安全组
openstack security group create
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    # 是否五状态
    [--stateful | --stateless]
    [--project-domain &lt;project-domain&gt;]
    [--tag &lt;tag&gt; | --no-tag]
    &lt;name&gt;

openstack security group delete &lt;group&gt; [&lt;group&gt; ...]

openstack security group list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--all-projects]

openstack security group set
    [--name &lt;new-name&gt;]
    [--description &lt;description&gt;]
    [--stateful | --stateless]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;group&gt;
openstack security group unset [--tag &lt;tag&gt; | --all-tag] &lt;group&gt;

openstack security group show &lt;group&gt;</pre>
<div class="blog_h3"><span class="graybg">security group rule</span></div>
<p>安全组中的一条规则。</p>
<pre class="crayon-plain-tag"># 创建一条规则
openstack security group rule create
    # 此规则针对的远程IP，可以使用CIDR
    #   0.0.0.0/0 表示默认IPv4规则
    #   ::/0 表示默认IPv6规则
    [--remote-ip &lt;ip-address&gt; | --remote-group &lt;group&gt;]
    # 目标（远程）端口。可以使用端口范围，例如  137:139
    # 对于TCP/UDP必须，对于ICMP忽略此字段
    [--dst-port &lt;port-range&gt;]
    # 协议： 默认any表示任何协议
    #   ah, dccp, egp, esp, gre, icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, 
    #   ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp 
    [--protocol &lt;protocol&gt;]
    [--description &lt;description&gt;]
    # 针对特定的ICMP类型
    [--icmp-type &lt;icmp-type&gt;]
    [--icmp-code &lt;icmp-code&gt;]
    # 此规则针对入站还是出站流量，默认ingress
    [--ingress | --egress]
    # 以太网上的流量类型  IPv4, IPv6
    [--ethertype &lt;ethertype&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 所属的组
    &lt;group&gt;

openstack security group rule delete &lt;rule&gt; [&lt;rule&gt; ...]

openstack security group rule list
    [--sort-column SORT_COLUMN]
    [--protocol &lt;protocol&gt;]
    [--ethertype &lt;ethertype&gt;]
    [--ingress | --egress]
    [--long]
    [--all-projects]
    [&lt;group&gt;]

openstack security group rule show &lt;rule&gt;


# 完全开放默认安全组
openstack security group rule create --remote-ip 0.0.0.0/0  --protocol any --ingress --ethertype IPv4 default
openstack security group rule create --remote-ip ::/0  --protocol any --ingress --ethertype IPv6 default
openstack security group rule create --remote-ip 0.0.0.0/0  --protocol any --egress --ethertype IPv4 default
openstack security group rule create --remote-ip ::/0  --protocol any --egress --ethertype IPv6 default</pre>
<div class="blog_h3"><span class="graybg">network auto allocated topology</span></div>
<p>可以让管理员快速的设置某个项目的外部连接性。每个项目只能有一个此对象。</p>
<pre class="crayon-plain-tag">openstack network auto allocated topology create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--check-resources]
    [--or-show]

openstack network auto allocated topology delete
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]</pre>
<div class="blog_h3"><span class="graybg">network flavor</span></div>
<p>扩展network flavor允许用户在创建资源时，选择管理员配置的“网络风格”</p>
<pre class="crayon-plain-tag"># 添加一个service profile到network flavor
openstack network flavor add profile &lt;flavor&gt; &lt;service-profile&gt;
openstack network flavor remove profile &lt;flavor&gt; &lt;service-profile&gt;

# 创建network flavor
openstack network flavor create
    # 此flavor应用到的网络服务类型，例如VPN
    # 执行 openstack network service provider list  获得网络服务类型列表
    --service-type &lt;service-type&gt;
    [--description DESCRIPTION]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--enable | --disable]
    &lt;name&gt;

openstack network flavor delete &lt;flavor&gt; [&lt;flavor&gt; ...]

openstack network flavor list [--sort-column SORT_COLUMN]

openstack network flavor set
    [--description DESCRIPTION]
    [--disable | --enable]
    [--name &lt;name&gt;]
    &lt;flavor&gt;

openstack network flavor show &lt;flavor&gt;</pre>
<div class="blog_h3"><span class="graybg">network flavor profile</span></div>
<p>用于管理员创建/删除/列出/显示网络服务的profile。</p>
<pre class="crayon-plain-tag">openstack network flavor profile create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--driver DRIVER]
    [--metainfo METAINFO]

openstack network flavor profile delete
    &lt;flavor-profile&gt;
    [&lt;flavor-profile&gt; ...]

openstack network flavor profile list [--sort-column SORT_COLUMN]

openstack network flavor profile set
    [--project-domain &lt;project-domain&gt;]
    [--description &lt;description&gt;]
    [--enable | --disable]
    [--driver DRIVER]
    [--metainfo METAINFO]
    &lt;flavor-profile&gt;

openstack network flavor profile show &lt;flavor-profile&gt;</pre>
<div class="blog_h3"><span class="graybg">network meter </span></div>
<p>允许管理员来度量某个IP范围的流量。需要L3 metering extension</p>
<pre class="crayon-plain-tag">openstack network meter create
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]
    &lt;name&gt;

openstack network meter delete &lt;meter&gt; [&lt;meter&gt; ...]

openstack network meter list [--sort-column SORT_COLUMN]

openstack network meter show &lt;meter&gt;</pre>
<div class="blog_h3"><span class="graybg">network meter rule</span></div>
<p>为某个meter设置度量网络流量的规则。需要L3 metering extension</p>
<pre class="crayon-plain-tag">openstack network meter rule create
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--exclude | --include]
    [--ingress | --egress]
    [--remote-ip-prefix &lt;remote-ip-prefix&gt;]
    [--source-ip-prefix &lt;remote-ip-prefix&gt;]
    [--destination-ip-prefix &lt;remote-ip-prefix&gt;]
    &lt;meter&gt;

openstack network meter rule delete
    &lt;meter-rule-id&gt;
    [&lt;meter-rule-id&gt; ...]

openstack network meter rule list [--sort-column SORT_COLUMN]

openstack network meter rule show &lt;meter-rule-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network qos policy</span></div>
<p>将一组网络QoS规则组合到一起，可以应用到一个网络或端口。</p>
<pre class="crayon-plain-tag">openstack network qos policy create
    [--description &lt;description&gt;]
    [--share | --no-share]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--default | --no-default]
    &lt;name&gt;

openstack network qos policy delete &lt;qos-policy&gt; [&lt;qos-policy&gt; ...]

openstack network qos policy list
    [--sort-column SORT_COLUMN]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--share | --no-share]

openstack network qos policy set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--share | --no-share]
    [--default | --no-default]
    &lt;qos-policy&gt;

openstack network qos policy show &lt;qos-policy&gt;</pre>
<div class="blog_h3"><span class="graybg">network qos rule</span></div>
<p>上述policy中的一个规则</p>
<pre class="crayon-plain-tag">openstack network qos rule create
    # QoS规则类型
    # minimum-bandwidth, dscp-marking, bandwidth-limit
    [--type &lt;type&gt;]
    # 最大带宽
    [--max-kbps &lt;max-kbps&gt;]
    # 最大突发带宽。如果不指定或者设置为0表示自动，为80%的最大带宽，适合于典型的TCP流量
    [--max-burst-kbits &lt;max-burst-kbits&gt;]
    # DSCP标记，可以是0，或者8-56之间的偶数（42 44 50 52 54不可以）
    [--dscp-mark &lt;dscp-mark&gt;]
    # 最小保障的带宽
    [--min-kbps &lt;min-kbps&gt;]
    # 此规则是用于入站还是出站的流量（从当前项目的角度）
    [--ingress | --egress]
    # 此规则加到哪个策略中
    &lt;qos-policy&gt;

openstack network qos rule delete &lt;qos-policy&gt; &lt;rule-id&gt;

openstack network qos rule list
    [--sort-column SORT_COLUMN]
    &lt;qos-policy&gt;

openstack network qos rule set
    [--max-kbps &lt;max-kbps&gt;]
    [--max-burst-kbits &lt;max-burst-kbits&gt;]
    [--dscp-mark &lt;dscp-mark&gt;]
    [--min-kbps &lt;min-kbps&gt;]
    [--ingress | --egress]
    &lt;qos-policy&gt;
    &lt;rule-id&gt;

openstack network qos rule show &lt;qos-policy&gt; &lt;rule-id&gt;</pre>
<div class="blog_h3"><span class="graybg">network segment</span></div>
<p>表示一个网络中的隔离的L2的段。一个（虚拟）网络可以包含多个段，同一个网络中的段的L2通信不被保证。</p>
<pre class="crayon-plain-tag"># 创建一个网络段
openstack network segment create
    [--description &lt;description&gt;]
    # 物理网络的名字
    [--physical-network &lt;physical-network&gt;]
    # 段的名字
    [--segment &lt;segment&gt;]
    # 此段属于的虚拟网络的名字
    --network &lt;network&gt;
    # 此段的网络类型：flat, geneve, gre, local, vlan, vxlan
    --network-type &lt;network-type&gt;
    &lt;name&gt;

openstack network segment delete
    &lt;network-segment&gt;
    [&lt;network-segment&gt; ...]

openstack network segment list
    [--sort-column SORT_COLUMN]
    [--long]
    [--network &lt;network&gt;]

openstack network segment set
    [--description &lt;description&gt;]
    [--name &lt;name&gt;]
    &lt;network-segment&gt;

openstack network segment show &lt;network-segment&gt;</pre>
<div class="blog_h3"><span class="graybg">network segment range</span></div>
<p>用于多租户下的网络段分配。可以让管理员全局的，或者基于用户的，来控制网络段范围。</p>
<pre class="crayon-plain-tag">openstack network segment range create
    [--private | --shared]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    --network-type &lt;network-type&gt;
    [--physical-network &lt;physical-network-name&gt;]
    --minimum &lt;minimum-segmentation-id&gt;
    --maximum &lt;maximum-segmentation-id&gt;
    &lt;name&gt;

openstack network segment range delete
    &lt;network-segment-range&gt;
    [&lt;network-segment-range&gt; ...]

openstack network segment range list
    [--sort-column SORT_COLUMN]
    [--long]
    [--used | --unused]
    [--available | --unavailable]

openstack network segment range set
    [--name &lt;name&gt;]
    [--minimum &lt;minimum-segmentation-id&gt;]
    [--maximum &lt;maximum-segmentation-id&gt;]
    &lt;network-segment-range&gt;

openstack network segment range show &lt;network-segment-range&gt; </pre>
<div class="blog_h3"><span class="graybg">network agent</span></div>
<p>所谓网络代理，负责（在节点上）处理各种任务，以实现虚拟网络。网络代理包括：</p>
<ol>
<li>neutron-dhcp-agent，负责提供DHCP服务给虚拟机</li>
<li>neutron-l3-agent，负责在自服务网络中提供路由</li>
<li>neutron-metering-agent</li>
<li>neutron-lbaas-agent</li>
</ol>
<pre class="crayon-plain-tag"># 添加网络到代理
openstack network agent add network [--dhcp] &lt;agent-id&gt; &lt;network&gt;
openstack network agent remove network [--dhcp] &lt;agent-id&gt; &lt;network&gt;

# 添加路由器到代理
openstack network agent add router [--l3] &lt;agent-id&gt; &lt;router&gt;
openstack network agent remove router [--l3] &lt;agent-id&gt; &lt;router&gt;

# 删除代理
openstack network agent delete &lt;network-agent&gt; [&lt;network-agent&gt; ...]

# 设置代理属性
openstack network agent set
    [--description &lt;description&gt;]
    [--enable | --disable]
    &lt;network-agent&gt;

# 列出代理
openstack network agent list
    [--sort-column SORT_COLUMN]
    [--agent-type &lt;agent-type&gt;]
    [--host &lt;host&gt;]
    [--network &lt;network&gt; | --router &lt;router&gt;]
    [--long]

# 显示代理详细信息
openstack network agent show &lt;network-agent&gt; </pre>
<div class="blog_h3"><span class="graybg">ip availability</span></div>
<p>显示网络可用的IP地址</p>
<pre class="crayon-plain-tag"># IP可用数量
openstack ip availability list

# 显示详细信息
openstack ip availability show &lt;network&gt;</pre>
<div class="blog_h3"><span class="graybg">floating ip</span></div>
<p>管理浮动IP</p>
<pre class="crayon-plain-tag"># 创建一个浮动IP
openstack floating ip create
    # 在哪个子网上创建浮动IP
    [--subnet &lt;subnet&gt;]
    # 浮动IP授予哪个端口
    [--port &lt;port&gt;]
    # 浮动IP的值
    [--floating-ip-address &lt;ip-address&gt;]
    # 映射到浮动IP的固定IP
    [--fixed-ip-address &lt;ip-address&gt;]
    # 浮动IP的QoS策略
    [--qos-policy &lt;qos-policy&gt;]
    [--description &lt;description&gt;]
    [--project &lt;project&gt;]
    # 浮动IP的DNS名
    [--dns-domain &lt;dns-domain&gt;]
    [--dns-name &lt;dns-name&gt;]
    [--project-domain &lt;project-domain&gt;]
    # 添加标记
    [--tag &lt;tag&gt; | --no-tag]
    # 从什么网络来分配浮动IP
    &lt;network&gt;
openstack floating ip unset
    [--port]
    [--qos-policy]
    [--tag &lt;tag&gt; | --all-tag]
    &lt;floating-ip&gt;

openstack floating ip delete &lt;floating-ip&gt; [&lt;floating-ip&gt; ...]
openstack floating ip set
    [--port &lt;port&gt;]
    [--fixed-ip-address &lt;ip-address&gt;]
    [--description &lt;description&gt;]
    [--qos-policy &lt;qos-policy&gt; | --no-qos-policy]
    [--tag &lt;tag&gt;]
    [--no-tag]
    &lt;floating-ip&gt;

openstack floating ip show &lt;floating-ip&gt;
openstack floating ip list
    [--sort-column SORT_COLUMN]
    [--network &lt;network&gt;]
    [--port &lt;port&gt;]
    [--fixed-ip-address &lt;ip-address&gt;]
    [--floating-ip-address &lt;ip-address&gt;]
    [--long]
    [--status &lt;status&gt;]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--router &lt;router&gt;]
    [--tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-tags &lt;tag&gt;[,&lt;tag&gt;,...]]
    [--not-any-tags &lt;tag&gt;[,&lt;tag&gt;,...]]</pre>
<div class="blog_h3"><span class="graybg">floating ip pool</span></div>
<p>浮动IP池管理</p>
<pre class="crayon-plain-tag">openstack floating ip pool list [--sort-column SORT_COLUMN]</pre>
<div class="blog_h3"><span class="graybg">floating ip port forwarding </span></div>
<p>创建浮动IP端口转发规则</p>
<pre class="crayon-plain-tag">openstack floating ip port forwarding create
    # 端口上的固定IPv4地址，浮动IP将其作为转发目标
    --internal-ip-address &lt;internal-ip-address&gt;
    # 转发到的端口
    --port &lt;port&gt;
    # 固定地址上的端口
    --internal-protocol-port &lt;port-number&gt;
    # 浮动IP上的端口
    --external-protocol-port &lt;port-number&gt;
    # 协议
    --protocol &lt;protocol&gt;
    [--description &lt;description&gt;]
    # 此转发规则针对的浮动IP（的IP或ID）
    &lt;floating-ip&gt;</pre>
<div class="blog_h3"><span class="graybg">volume service</span></div>
<p>管理卷服务</p>
<pre class="crayon-plain-tag">openstack volume service list
    [--host &lt;host&gt;]
    [--service &lt;service&gt;]
    [--long]

openstack volume service set
    [--enable | --disable]
    [--disable-reason &lt;reason&gt;]
    &lt;host&gt;
    &lt;service&gt;</pre>
<div class="blog_h3"><span class="graybg">volume</span></div>
<p>管理卷</p>
<pre class="crayon-plain-tag"># 创建卷
openstack volume create
    # 卷的大小，单位GB
    [--size &lt;size&gt;]
    # 卷的类型
    [--type &lt;volume-type&gt;]
    # 将镜像、快照或者另外一个卷，作为新卷的数据来源
    [--image &lt;image&gt; | --snapshot &lt;snapshot&gt; | --source &lt;volume&gt; ]
    [--description &lt;description&gt;]
    # 指定一个alternate用户、项目
    [--user &lt;user&gt;]
    [--project &lt;project&gt;]
    # 在指定可用区中创建卷
    [--availability-zone &lt;availability-zone&gt;]
    # 将卷加入到一致性组
    [--consistency-group &lt;consistency-group&gt;]
    [--property &lt;key=value&gt; [...] ]
    # 提供给卷调度器的提示信息
    [--hint &lt;key=value&gt; [...] ]
    # 卷是否需要支持多重挂载
    [--multi-attach]
    # 是否将卷标记为可启动磁盘
    [--bootable | --non-bootable]
    # 是否只读卷
    [--read-only | --read-write]
    &lt;name&gt;
# 删除卷
openstack volume delete
    [--force | --purge]
    &lt;volume&gt; [&lt;volume&gt; ...]

# 列出卷
openstack volume list
    [--project &lt;project&gt; [--project-domain &lt;project-domain&gt;]]
    [--user &lt;user&gt; [--user-domain &lt;user-domain&gt;]]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--all-projects]
    [--long]
    [--limit &lt;num-volumes&gt;]
    [--marker &lt;volume&gt;]
# 显示卷的详细信息
openstack volume show
    &lt;volume&gt;

# 设置卷属性
openstack volume set
    [--name &lt;name&gt;]
    [--size &lt;size&gt;]
    [--description &lt;description&gt;]
    [--no-property]
    [--property &lt;key=value&gt; [...] ]
    [--image-property &lt;key=value&gt; [...] ]
    [--state &lt;state&gt;]
    [--attached | --detached ]
    [--type &lt;volume-type&gt;]
    [--retype-policy &lt;retype-policy&gt;]
    [--bootable | --non-bootable]
    [--read-only | --read-write]
    &lt;volume&gt;
openstack volume unset
    [--property &lt;key&gt;]
    [--image-property &lt;key&gt;]
    &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume type</span></div>
<p>管理卷类型</p>
<pre class="crayon-plain-tag">openstack volume type create
    [--description &lt;description&gt;]
    [--public | --private]
    [--property &lt;key=value&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-provider &lt;provider&gt;]
    [--encryption-cipher &lt;cipher&gt;]
    [--encryption-key-size &lt;key-size&gt;]
    [--encryption-control-location &lt;control-location&gt;]
    &lt;name&gt;
openstack volume type delete
    &lt;volume-type&gt; [&lt;volume-type&gt; ...]

openstack volume type list
    [--long]
    [--default | --public | --private]
    [--encryption-type]
openstack volume type show
    [--encryption-type]
    &lt;volume-type&gt;


openstack volume type set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--property &lt;key=value&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-provider &lt;provider&gt;]
    [--encryption-cipher &lt;cipher&gt;]
    [--encryption-key-size &lt;key-size&gt;]
    [--encryption-control-location &lt;control-location&gt;]
    &lt;volume-type&gt;
openstack volume type unset
    [--property &lt;key&gt; [...] ]
    [--project &lt;project&gt;]
    [--project-domain &lt;project-domain&gt;]
    [--encryption-type]
    &lt;volume-type&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backend</span></div>
<pre class="crayon-plain-tag"># 显示卷后端特性
openstack volume backend capability show
    [--sort-column SORT_COLUMN]
    &lt;host&gt;

# 列出后端池
openstack volume backend pool list [--sort-column SORT_COLUMN] [--long] </pre>
<div class="blog_h3"><span class="graybg">volume migrate</span></div>
<p>将卷迁移到一个新的宿主机 </p>
<pre class="crayon-plain-tag">openstack volume migrate
    # 目标主机
    --host &lt;host&gt;
    # 使用一般性的，基于复制的迁移。跳过存储驱动可能的优化
    [--force-host-copy]
    # 锁定卷，防止迁移被其它操作中止
    [--lock-volume]
    &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume snapshot</span></div>
<p>管理卷的快照</p>
<pre class="crayon-plain-tag"># 创建一个卷快照
openstack volume snapshot create
    # 目标卷
    [--volume &lt;volume&gt;]
    [--description &lt;description&gt;]
    # 即使在使用中，也创建快照
    [--force]
    [--property &lt;key=value&gt; [...] ]
    [--remote-source &lt;key=value&gt; [...]]
    &lt;snapshot-name&gt;
openstack volume snapshot delete
    [--force]
    &lt;snapshot&gt; [&lt;snapshot&gt; ...]


# 列出和显示
openstack volume snapshot list
    [--all-projects]
    [--project &lt;project&gt; [--project-domain &lt;project-domain&gt;]]
    [--long]
    [--limit &lt;num-snapshots&gt;]
    [--marker &lt;snapshot&gt;]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--volume &lt;volume&gt;]
openstack volume snapshot show
    &lt;snapshot&gt;

# 设置属性
openstack volume snapshot set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--no-property]
    [--property &lt;key=value&gt; [...] ]
    [--state &lt;state&gt;]
    &lt;snapshot&gt;
openstack volume snapshot unset
    [--property &lt;key&gt;]
    &lt;snapshot&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backup</span></div>
<p>管理卷备份</p>
<pre class="crayon-plain-tag"># 创建卷备份
openstack volume backup create
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    # 保存到哪个对象容器
    [--container &lt;container&gt;]
    # 备份快照
    [--snapshot &lt;snapshot&gt;]
    # 允许备份使用中的卷
    [--force]
    # 进行增量备份
    [--incremental]
    &lt;volume&gt;

# 删除卷备份
openstack volume backup delete [--force] &lt;backup&gt; [&lt;backup&gt; ...]

# 列出卷备份
openstack volume backup list
    [--sort-column SORT_COLUMN]
    [--long]
    [--name &lt;name&gt;]
    [--status &lt;status&gt;]
    [--volume &lt;volume&gt;]
    [--marker &lt;volume-backup&gt;]
    [--limit &lt;num-backups&gt;]
    [--all-projects]

# 显示备份详细信息
openstack volume backup show &lt;backup&gt;

# 导出卷备份详细信息
openstack volume backup record export &lt;backup&gt;

# 导入卷备份详细信息
openstack volume backup record import
    &lt;backup_service&gt;
    &lt;backup_metadata&gt;

# 设置属性
openstack volume backup set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    [--state &lt;state&gt;]
    &lt;backup&gt;</pre>
<div class="blog_h3"><span class="graybg">volume backup restore</span></div>
<p>从备份中恢复卷</p>
<pre class="crayon-plain-tag">openstack volume backup restore &lt;backup&gt; &lt;volume&gt;</pre>
<div class="blog_h3"><span class="graybg">volume qos</span></div>
<p>管理卷关联的QoS规格，将QoS规格关联到卷类型</p>
<pre class="crayon-plain-tag"># 关联QoS规格到一个卷类型
openstack volume qos associate
    &lt;qos-spec&gt;
    &lt;volume-type&gt;
volume qos disassociate

# 创建一个QoS规格
openstack volume qos create
    [--consumer &lt;consumer&gt;]
    [--property &lt;key=value&gt; [...] ]
    &lt;name&gt;

# 删除一个QoS规格
volume qos delete

# 列出和显示
volume qos list
openstack volume qos show
    &lt;qos-spec&gt;

# 设置属性
openstack volume qos set
    [--property &lt;key=value&gt; [...] ]
    &lt;qos-spec&gt;
openstack volume qos unset
    [--property &lt;key&gt; [...] ]
    &lt;qos-spec&gt; </pre>
<div class="blog_h3"><span class="graybg">consistency group</span></div>
<p>可以让一组卷同时进行快照，以保证数据一致性</p>
<pre class="crayon-plain-tag"># 将卷加入/移除一致性组
openstack consistency group add volume
    &lt;consistency-group&gt;
    &lt;volume&gt; [&lt;volume&gt; ...]
openstack consistency group remove volume
    &lt;consistency-group&gt;
    &lt;volume&gt; [&lt;volume&gt; ...]


# 创建一致性组
openstack consistency group create
    --volume-type &lt;volume-type&gt; | --consistency-group-source &lt;consistency-group&gt; | --consistency-group-snapshot &lt;consistency-group-snapshot&gt;
    [--description &lt;description&gt;]
    [--availability-zone &lt;availability-zone&gt;]
    [&lt;name&gt;]

# 删除一致性组
openstack consistency group delete
    # 即使出错也强制删除
    [--force]
    &lt;consistency-group&gt; [&lt;consistency-group&gt; ...]



openstack consistency group list
    [--all-projects]
    [--long]
openstack consistency group set
    [--name &lt;name&gt;]
    [--description &lt;description&gt;]
    &lt;consistency-group&gt;
openstack consistency group show
    &lt;consistency-group&gt;</pre>
<div class="blog_h3"><span class="graybg">container</span></div>
<p>定义Object Storage V1中的一个命名空间。</p>
<div class="blog_h3"><span class="graybg">object</span></div>
<p>管理对象存储中的对象。</p>
<pre class="crayon-plain-tag"># 上传对象到container
openstack object create
    [--sort-column SORT_COLUMN]
    [--name &lt;name&gt;]
    &lt;container&gt;
    &lt;filename&gt;
    [&lt;filename&gt; ...]

# 删除对象
openstack object delete &lt;container&gt; &lt;object&gt; [&lt;object&gt; ...]

# 列出对象
openstack object list
    [--sort-column SORT_COLUMN]
    [--delimiter &lt;delimiter&gt;]
    [--marker &lt;marker&gt;]
    [--end-marker &lt;end-marker&gt;]
    [--limit &lt;num-objects&gt;]
    [--long]
    [--all]
    &lt;container&gt;

# 下载对象到本地
openstack object save [--file &lt;filename&gt;] &lt;container&gt; &lt;object&gt;

# 设置对象属性
openstack object set --property &lt;key =value&gt; &lt;container&gt; &lt;object&gt;
openstack object unset --property &lt;key&gt; &lt;container&gt; &lt;object&gt;

# 显示对象信息
openstack object show &lt;container&gt; &lt;object&gt;</pre>
<div class="blog_h3"><span class="graybg">object store account</span></div>
<p>账户是container - objects树结构的最根部。</p>
<pre class="crayon-plain-tag">openstack object store account set --property &lt;key =value&gt;
openstack object store account unset --property &lt;key&gt;

openstack object store account show </pre>
<div class="blog_h2"><span class="graybg">nova</span></div>
<p>很多功能和openstack命令重复，建议使用openstack命令，仅仅在使用某些高级特性时，才需要底层的nova命令。</p>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<pre class="crayon-plain-tag">OS_USERNAME
OS_PASSWORD
OS_PROJECT_NAME
OS_PROJECT_ID
OS_PROJECT_DOMAIN_NAME
OS_PROJECT_DOMAIN_ID
OS_USER_DOMAIN_NAME
OS_USER_DOMAIN_ID
# Keystone端点URL
OS_AUTH_URL
OS_COMPUTE_API_VERSION
OS_REGION_NAME
# 逗号分隔的，受信任的镜像证书ID
OS_TRUSTED_IMAGE_CERTIFICATE_IDS</pre>
<div class="blog_h3"><span class="graybg">add-secgroup</span></div>
<p>为虚拟机添加安全组</p>
<div class="blog_h3"><span class="graybg">list-secgroup</span></div>
<p>列出指定虚拟机的安全组</p>
<div class="blog_h3"><span class="graybg">remove-secgroup</span></div>
<p>将虚拟机从安全组移除</p>
<div class="blog_h3"><span class="graybg">agent-create</span></div>
<p>创建agent build， 类似的agent-delete、agent-list、agent-modify命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">aggregate-create</span></div>
<p>管理服务器聚合， 类似的aggregate-delete、aggregate-list、aggregate-update、aggregate-show命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">aggregate-add-host</span></div>
<p>添加虚拟机到聚合中。类似的aggregate-remove-host用于移除虚拟机</p>
<div class="blog_h3"><span class="graybg">aggregate-cache-images</span></div>
<p>缓存镜像到聚合的所有虚拟机中</p>
<div class="blog_h3"><span class="graybg">aggregate-set-metadata</span></div>
<p>更新和聚合关联的元数据</p>
<div class="blog_h3"><span class="graybg">server-group-create</span></div>
<p>创建（基于策略的）虚拟机分组。 类似的server-group-delete、server-group-list、server-group-get命令完成相关CRUD操作</p>
<div class="blog_h3"><span class="graybg">availability-zone-list</span></div>
<p>列出可用区</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>列出虚拟机</p>
<div class="blog_h3"><span class="graybg">update</span></div>
<p>修改虚拟机的名字或描述</p>
<div class="blog_h3"><span class="graybg">show</span></div>
<p>显示单个虚拟机的详细信息</p>
<div class="blog_h3"><span class="graybg">ssh</span></div>
<p>SSH到虚拟机</p>
<div class="blog_h3"><span class="graybg">start</span></div>
<p>启动虚拟机</p>
<div class="blog_h3"><span class="graybg">stop</span></div>
<p>停止虚拟机</p>
<div class="blog_h3"><span class="graybg">backup</span></div>
<p>通过创建backup类型的快照，来备份一个虚拟机</p>
<div class="blog_h3"><span class="graybg">boot</span></div>
<p>启动一个新的虚拟机</p>
<div class="blog_h3"><span class="graybg">clear-password</span></div>
<p>从元数据服务器上清除某个虚拟机的管理密码，不会改变实例的密码</p>
<div class="blog_h3"><span class="graybg">get-password</span></div>
<p>获取虚拟机的管理密码，调用元数据服务器，而不是虚拟机自身</p>
<div class="blog_h3"><span class="graybg">set-password</span></div>
<p>设置虚拟机密码</p>
<div class="blog_h3"><span class="graybg">console-log</span></div>
<p>获取虚拟机控制台日志</p>
<div class="blog_h3"><span class="graybg">reset-state</span></div>
<p>重置虚拟机状态</p>
<div class="blog_h3"><span class="graybg">lock</span></div>
<p>锁定虚拟机，非管理员将无法对虚拟机进行操作</p>
<div class="blog_h3"><span class="graybg">unlock</span></div>
<p>解锁虚拟机</p>
<div class="blog_h3"><span class="graybg">pause</span></div>
<p>在内存中暂停虚拟机</p>
<div class="blog_h3"><span class="graybg">unpause</span></div>
<p>解除内存中暂停的虚拟机</p>
<div class="blog_h3"><span class="graybg">suspend</span></div>
<p>暂停虚拟机到磁盘</p>
<div class="blog_h3"><span class="graybg">resume</span></div>
<p>恢复暂停到磁盘的虚拟机</p>
<div class="blog_h3"><span class="graybg">reboot</span></div>
<p>重启虚拟机</p>
<div class="blog_h3"><span class="graybg">rescue</span></div>
<p>重启虚拟机进入救援模式 —— 从虚拟机的初始镜像或另外一个特定镜像启动虚拟机，将当前book disk挂载为非boot disk</p>
<div class="blog_h3"><span class="graybg">unrescue</span></div>
<p>重启虚拟机，进入正常模式</p>
<div class="blog_h3"><span class="graybg">trigger-crash-dump</span></div>
<p>触发虚拟机crash dump</p>
<div class="blog_h3"><span class="graybg">delete</span></div>
<p>立即关机，同时删除实例</p>
<div class="blog_h3"><span class="graybg">restore</span></div>
<p>恢复一个软删除的实例</p>
<div class="blog_h3"><span class="graybg">force-delete</span></div>
<p>强制删除一个虚拟机</p>
<div class="blog_h3"><span class="graybg">rebuild</span></div>
<p>重建（关机、re-image、启动）虚拟机</p>
<div class="blog_h3"><span class="graybg">shelve</span></div>
<p>保存虚拟机为镜像</p>
<div class="blog_h3"><span class="graybg">unshelve</span></div>
<p>将镜像化的虚拟机恢复</p>
<div class="blog_h3"><span class="graybg">shelve-offload</span></div>
<p>将shelved的虚拟机从宿主机上删除</p>
<div class="blog_h3"><span class="graybg">meta</span></div>
<p>设置/删除虚拟机的元数据</p>
<div class="blog_h3"><span class="graybg">diagnostics</span></div>
<p>获取虚拟机诊断信息</p>
<div class="blog_h3"><span class="graybg">evacuate</span></div>
<p>重建失败宿主机上的某个虚拟机</p>
<div class="blog_h3"><span class="graybg">migrate</span></div>
<p>迁移虚拟机</p>
<div class="blog_h3"><span class="graybg">resize</span></div>
<p>修改虚拟机规格，即flavor</p>
<div class="blog_h3"><span class="graybg">resize-confirm</span></div>
<p>确认修改规格操作</p>
<div class="blog_h3"><span class="graybg">resize-revert</span></div>
<p>撤销尚未确认的resize操作，虚拟机恢复原状</p>
<div class="blog_h3"><span class="graybg">live-migration</span></div>
<p>对指定的虚拟机进行在线迁移</p>
<div class="blog_h3"><span class="graybg">live-migration-abort</span></div>
<p>中止正在进行的在线迁移。需要Nova API版本2.24+</p>
<div class="blog_h3"><span class="graybg">live-migration-force-complete</span></div>
<p>强制结束正在进行的在线迁移</p>
<div class="blog_h3"><span class="graybg">server-migration-show</span></div>
<p>显示某次虚拟机迁移的详细信息</p>
<div class="blog_h3"><span class="graybg">server-migration-list</span></div>
<p>列出指定虚拟机的迁移</p>
<div class="blog_h3"><span class="graybg">migration-list</span></div>
<p>列出所有迁移</p>
<div class="blog_h3"><span class="graybg">server-tag-add</span></div>
<p>添加一个或多个tag到虚拟机，类似的server-tag-delete、server-tag-delete-all、server-tag-list、server-tag-set完成相应CRUD操作</p>
<div class="blog_h3"><span class="graybg">flavor-access-add</span></div>
<p>为某个租户增加某个flavor的权限。类似的flavor-access-remove移除某个租户访问某个flavor的权限</p>
<div class="blog_h3"><span class="graybg">flavor-access-list</span></div>
<p>查看某个flavor的访问权限列表</p>
<div class="blog_h3">
<div class="blog_h3"><span class="graybg">flavor-create</span></div>
</div>
<p>创建一个flavor。类似的 flavor-delete、flavor-list、flavor-update、flavor-show命令完成相关CRUD操作。</p>
<div class="blog_h3"><span class="graybg">flavor-key</span></div>
<p>为某个flavor设置或清除extra_spec。</p>
<div class="blog_h3"><span class="graybg">get-rdp-console</span></div>
<p>达到虚拟机的RDP控制台</p>
<div class="blog_h3"><span class="graybg">get-serial-console</span></div>
<p>得到寻机的串口控制台</p>
<div class="blog_h3"><span class="graybg">get-spice-console</span></div>
<p>得到虚拟机的Spice控制台</p>
<div class="blog_h3"><span class="graybg">get-vnc-console</span></div>
<p>得到虚拟机的VNC控制台</p>
<div class="blog_h3"><span class="graybg">host-evacuate</span></div>
<p>重建失败宿主机上的所有实例</p>
<div class="blog_h3"><span class="graybg">host-evacuate-live</span></div>
<p>对指定宿主机上所有虚拟机执行在线迁移操作</p>
<div class="blog_h3"><span class="graybg">host-servers-migrate</span></div>
<p>对指定宿主机上所有虚拟机执行迁移操作</p>
<div class="blog_h3"><span class="graybg">host-meta</span></div>
<p>设置/删除宿主机上所有的虚拟机的元数据</p>
<div class="blog_h3"><span class="graybg">hypervisor-list</span></div>
<p>列出可用的Hypervisor</p>
<div class="blog_h3"><span class="graybg">hypervisor-servers</span></div>
<p>列出基于指定Hypervisor的虚拟机</p>
<div class="blog_h3"><span class="graybg">hypervisor-show</span></div>
<p>查看Hypervisor的详细信息</p>
<div class="blog_h3"><span class="graybg">hypervisor-stats</span></div>
<p>显示所有Hypervisor的总和统计信息</p>
<div class="blog_h3"><span class="graybg">hypervisor-uptime</span></div>
<p>显示指定Hypervisor的已启动时间</p>
<div class="blog_h3"><span class="graybg">image-create</span></div>
<p>通过获取虚拟机快照，来创建新的镜像</p>
<div class="blog_h3"><span class="graybg">instance-action-list</span></div>
<p>列出针对指定虚拟机的操作历史</p>
<div class="blog_h3"><span class="graybg">interface-attach</span></div>
<p>为虚拟机添加一个网络接口（Port）</p>
<div class="blog_h3"><span class="graybg">interface-list</span></div>
<p>列出连接到虚拟机的Port</p>
<div class="blog_h3"><span class="graybg">refresh-network</span></div>
<p>刷新虚拟机的网络信息</p>
<div class="blog_h3"><span class="graybg">reset-network</span></div>
<p>重置虚拟机的网络</p>
<div class="blog_h3"><span class="graybg">volume-attach</span></div>
<p>添加一个卷给虚拟机</p>
<div class="blog_h3"><span class="graybg">volume-detach</span></div>
<p>将某个卷从虚拟机移除</p>
<div class="blog_h3"><span class="graybg">volume-attachments</span></div>
<p>列出所有添加到虚拟机的卷</p>
<div class="blog_h3"><span class="graybg">volume-update</span></div>
<p>将指定的、已经添加到虚拟机的卷的数据，拷贝到另外一个可用（没有被其它虚拟机使用）的卷上，然后将当前挂载的卷换成新的（接收数据拷贝的哪个）</p>
<div class="blog_h3"><span class="graybg">keypair-add</span></div>
<p>添加访问虚拟机的密钥。类似的 keypair-delete、keypair-list、keypair-show命令完成相关CRUD操作。</p>
<div class="blog_h3"><span class="graybg">quota-class-show</span></div>
<p>显示一个quota class的配额信息</p>
<div class="blog_h3"><span class="graybg">quota-class-update</span></div>
<p>更新quota class的配额值</p>
<div class="blog_h3"><span class="graybg">quota-defaults</span></div>
<p>列出租户的默认配额</p>
<div class="blog_h3"><span class="graybg">quota-delete</span></div>
<p>为一个用户/租户删除配额，配额值恢复为默认</p>
<div class="blog_h3"><span class="graybg">quota-show</span></div>
<p>显示指定用户/租户的配额</p>
<div class="blog_h3"><span class="graybg">quota-update</span></div>
<p>为指定用户/租户更新配额</p>
<div class="blog_h3"><span class="graybg">usage</span></div>
<p>显示单个租户的用量信息</p>
<div class="blog_h3"><span class="graybg">usage-list</span></div>
<p>列出所有租户的用量信息</p>
<div class="blog_h3"><span class="graybg">version-list</span></div>
<p>列出所有API 版本</p>
<div class="blog_h3"><span class="graybg">service-delete</span></div>
<p>删除服务</p>
<div class="blog_h3"><span class="graybg">service-disable</span></div>
<p>禁用服务</p>
<div class="blog_h3"><span class="graybg">service-enable</span></div>
<p>启用服务</p>
<div class="blog_h3"><span class="graybg">service-force-down</span></div>
<p>强制停止服务</p>
<div class="blog_h3"><span class="graybg">service-list</span></div>
<p>列出运行中的服务</p>
<div class="blog_h3"><span class="graybg">bash-completion</span></div>
<p>用于bash自动补全。脚本文件位于：/etc/bash_completion.d/nova，可以直接拷贝到其它机器使用</p>
<div class="blog_h2"><span class="graybg">cinder</span></div>
<div class="blog_h3"><span class="graybg">absolute-limits</span></div>
<p>列出针对某个用户的，存储（总计、备份、快照、卷等）用量的硬限制</p>
<div class="blog_h3"><span class="graybg">backup-create</span></div>
<p>创建一个卷的备份</p>
<div class="blog_h3"><span class="graybg">backup-delete</span></div>
<p>删除一个卷备份</p>
<div class="blog_h3"><span class="graybg">backup-export</span></div>
<p>导出备份元数据</p>
<div class="blog_h3"><span class="graybg">backup-import</span></div>
<p>导入备份元数据</p>
<div class="blog_h3"><span class="graybg">backup-list</span></div>
<p>列出所有备份</p>
<div class="blog_h3"><span class="graybg">backup-reset-state</span></div>
<p>显式的更新备份状态</p>
<div class="blog_h3"><span class="graybg">backup-restore</span></div>
<p>从一个备份恢复</p>
<div class="blog_h3"><span class="graybg">backup-show</span></div>
<p>显示备份详细信息</p>
<div class="blog_h3"><span class="graybg">backup-update</span></div>
<p>更新一个备份</p>
<div class="blog_h3"><span class="graybg">create</span></div>
<p>创建一个卷</p>
<div class="blog_h3"><span class="graybg">delete</span></div>
<p>删除一个或多个卷</p>
<div class="blog_h3"><span class="graybg">extend</span></div>
<p>尝试扩展一个卷的尺寸</p>
<div class="blog_h3"><span class="graybg">failover-host</span></div>
<p>进行故障转移，要求卷是replicated</p>
<div class="blog_h3"><span class="graybg">force-delete</span></div>
<p>强制删除卷，不管其状态如何</p>
<div class="blog_h3"><span class="graybg">freeze-host</span></div>
<p>冻结并且禁用指定的卷主机</p>
<div class="blog_h3"><span class="graybg">get-capabilities</span></div>
<p>显示卷的后端的统计信息、属性</p>
<div class="blog_h3"><span class="graybg">get-pools</span></div>
<p>显示卷的池信息：</p>
<pre class="crayon-plain-tag">cinder get-pools 

+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-3@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-4@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-2@lvm#LVM |
+----------+---------------------+
+----------+---------------------+
| Property | Value               |
+----------+---------------------+
| name     | openstack-2@nfs#nfs |
+----------+---------------------+</pre>
<p> 每个主机上的lvm后端，独立作为一个池。但是在各主机上都配置了的、指向同一NFS export的，只显示了一个池</p>
<div class="blog_h3"><span class="graybg">list</span></div>
<p>列出所有卷</p>
<div class="blog_h3"><span class="graybg">migrate</span></div>
<p>将卷迁移到一个新的主机上</p>
<div class="blog_h3"><span class="graybg">qos-associate</span></div>
<p>为指定的卷类型设置QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-create</span></div>
<p>创建一个QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-delete</span></div>
<p>删除一个QoS规格</p>
<div class="blog_h3"><span class="graybg">qos-disassociate</span></div>
<p>解除一个QoS规格和一个卷类型的关联</p>
<div class="blog_h3"><span class="graybg">qos-disassociate-all</span></div>
<p>解除一个QoS规格的所有关联</p>
<div class="blog_h3"><span class="graybg">qos-get-association</span></div>
<p>列出QoS规格的关联</p>
<div class="blog_h3"><span class="graybg">qos-key</span></div>
<p>设置/删除QoS规格的某个属性</p>
<div class="blog_h3"><span class="graybg">qos-list</span></div>
<p>列出所有的QoS规格</p>
<div class="blog_h3"><span class="graybg"> quota-class-show </span></div>
<p>列出一个配额类（quota class）的所有配额属性</p>
<div class="blog_h3"><span class="graybg">quota-class-update </span></div>
<p>更新配额类的属性</p>
<div class="blog_h3"><span class="graybg">quota-defaults</span></div>
<p>列出租户的默认配额</p>
<div class="blog_h3"><span class="graybg"> quota-delete</span></div>
<p>为一个租户删除配额</p>
<div class="blog_h3"><span class="graybg">quota-show </span></div>
<p>显示某个租户的当前配额</p>
<div class="blog_h3"><span class="graybg">quota-update</span></div>
<p>更新一个租户的配额</p>
<div class="blog_h3"><span class="graybg"> quota-usage</span></div>
<p>列出一个租户的配额使用情况</p>
<div class="blog_h3"><span class="graybg"> rate-limits</span></div>
<p>列出一个租户的速率限制</p>
<div class="blog_h3"><span class="graybg">rename</span></div>
<p>重命名卷</p>
<div class="blog_h3"><span class="graybg">reset-state</span></div>
<p>在Cinder数据库中显式的重置卷的状态</p>
<div class="blog_h3"><span class="graybg">retype</span></div>
<p>修改卷的类型</p>
<div class="blog_h3"><span class="graybg">revert-to-snapshot</span></div>
<p>将某个卷回退到某个快照</p>
<div class="blog_h3"><span class="graybg">service-disable</span></div>
<p>禁用一个卷服务：</p>
<pre class="crayon-plain-tag">cinder service-disable  openstack-4@nfs  cinder-volume</pre>
<p>要删除卷服务，需要：</p>
<pre class="crayon-plain-tag">cinder-manage service remove  cinder-volume openstack-4@nfs </pre>
<div class="blog_h3"><span class="graybg"> snapshot-create</span></div>
<p>创建卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-delete </span></div>
<p>删除卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-list</span></div>
<p>列出卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-manage</span></div>
<p>管理卷快照</p>
<div class="blog_h3"><span class="graybg">snapshot-metadata</span></div>
<p>设置或删除快照元数据</p>
<div class="blog_h3"><span class="graybg">snapshot-metadata-show</span></div>
<p>查看快照元数据</p>
<div class="blog_h3"><span class="graybg">snapshot-rename</span></div>
<p>重命名快照</p>
<div class="blog_h3"><span class="graybg">snapshot-reset-state</span></div>
<p>显式的重置快照状态</p>
<div class="blog_h3"><span class="graybg">snapshot-show</span></div>
<p>显示卷快照的信息</p>
<div class="blog_h3"><span class="graybg">snapshot-unmanage</span></div>
<p>停止管理卷快照</p>
<div class="blog_h3"><span class="graybg">transfer-accept</span></div>
<p>接受一个卷转移（volume transfer）</p>
<div class="blog_h3"><span class="graybg">transfer-create</span></div>
<p>创建一个卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-delete</span></div>
<p>撤销一个卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-list</span></div>
<p>列出所有卷转移</p>
<div class="blog_h3"><span class="graybg">transfer-show</span></div>
<p>显示卷转移的信息</p>
<div class="blog_h3"><span class="graybg">type-access-add </span></div>
<p>授予指定项目（租户）访问某个卷类型的权限</p>
<div class="blog_h3"><span class="graybg">type-access-list </span></div>
<p>列出卷类型的访问权限</p>
<div class="blog_h3"><span class="graybg">type-access-remove</span></div>
<p>移除卷类型的访问权限</p>
<div class="blog_h3"><span class="graybg">type-create </span></div>
<p>配置好一个卷后端后，需要使用该命令，创建对应的卷类型，并且将类型关联到后端：</p>
<pre class="crayon-plain-tag">cinder type-create nfs-fast
cinder type-key nfs-fast set volume_backend_name=nfs-fast

cinder type-create nfs-slow
cinder type-key nfs-slow set volume_backend_name=nfs-slow</pre>
<div class="blog_h3"><span class="graybg"> type-default</span></div>
<p>显示默认使用的卷类型：</p>
<pre class="crayon-plain-tag">cinder type-default
+--------------------------------------+-------------+---------------------+-----------+
| ID                                   | Name        | Description         | Is_Public |
+--------------------------------------+-------------+---------------------+-----------+
| 464dc192-cc63-4aab-8466-6d4f41cd0fb4 | __DEFAULT__ | Default Volume Type | True      |
+--------------------------------------+-------------+---------------------+-----------+</pre>
<p>通过type-update修改默认卷类型的名字，会导致出错。你必须同步修改控制节点的cinder.conf：</p>
<pre class="crayon-plain-tag">default_volume_type = lvm</pre>
<div class="blog_h3"><span class="graybg">type-delete</span></div>
<p>删除卷类型</p>
<div class="blog_h3"><span class="graybg">type-key</span></div>
<p>设置/删除卷类型的额外规格（extra_spec）</p>
<div class="blog_h3"><span class="graybg">type-list </span></div>
<p>列出所有卷类型</p>
<div class="blog_h3"><span class="graybg">type-show</span></div>
<p>显示一个卷类型的详细信息</p>
<div class="blog_h3"><span class="graybg">type-update </span></div>
<p>根据ID来更新一个卷类型的名字、描述、是否公开：</p>
<pre class="crayon-plain-tag">cinder type-update --name __DEFAULT__ 464dc192-cc63-4aab-8466-6d4f41cd0fb4</pre>
<div class="blog_h3"><span class="graybg">unmanage</span></div>
<p>停止管理卷</p>
<div class="blog_h3"><span class="graybg">upload-to-image</span></div>
<p>将卷上传到镜像服务，作为镜像使用</p>
<div class="blog_h1"><span class="graybg">样例环境</span></div>
<p>本章按照官网的样例架构，搭建最小化的OpenStack集群。此集群和生产环境架构的不同之处是：</p>
<ol>
<li>网络代理（networking agents）部署在控制器节点上，而非专用的网络节点</li>
<li>自服务网络（self-service networks）的隧道（Overlay）流量，通过管理网络（management network）而非专用网络传递</li>
</ol>
<div class="blog_h2"><span class="graybg">环境需求</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/hwreqs.png"><img class="size-full wp-image-34681 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/hwreqs.png" alt="hwreqs" width="714" height="630" /></a></p>
<div class="blog_h3"><span class="graybg">节点角色</span></div>
<p>此集群包含如下角色的节点：</p>
<ol>
<li>Controller：要求2张NIC
<ol>
<li>部署Identity Service、Image Service、计算服务的管理部分、网络服务的管理部分、Web仪表盘，以及多种网络代理</li>
<li>部署支持性服务，包括SQL数据库、消息代理、NTP</li>
<li>可选的，部署块存储服务、对象存储服务、编排服务、遥测服务的部分组件</li>
</ol>
</li>
<li>Compute：要求2张NIC
<ol>
<li>部署计算服务的Hypervisor部分，能够操控Instance。默认Hypervisor是KVM</li>
<li>部署一个网络代理，用于<span style="background-color: #c0c0c0;">将Instance连接到虚拟网络</span>，并通过安全组为Instance提供防火墙服务</li>
</ol>
</li>
<li>Block Storage：可选的，为Instance提供块存储、共享文件系统服务。在本示例环境中，计算节点和块存储节点之间的流量通过管理网络传输，生产环境下应该有独立的存储网络</li>
<li>Object Storage：可选的，用于对象存储的服务</li>
</ol>
<div class="blog_h2"><span class="graybg">网络布局</span></div>
<p>推荐使用两套物理网络：</p>
<ol>
<li>管理网络（management network，10.1.0.0/16）：通过NAT连接到互联网。绝大部分情况下，节点需要连接到外网（例如安装软件包）时，都应该通过管理网</li>
<li>提供者网络（provider network，10.0.0.0/16）：这是虚拟机的工作负载流量所使用的网络，在：
<ol>
<li>网络选项一（提供者网络）下，虚拟机直接连接到此网络</li>
<li>网络选项二（自服务网络）下，虚拟机连接到自服务网络，然后NAT到此网络以获得外部连接</li>
</ol>
</li>
</ol>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/networklayout.png"><img class="size-full wp-image-34961 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/networklayout.png" alt="networklayout" width="693" height="567" /></a></p>
<div class="blog_h2"><span class="graybg">网络选项</span></div>
<p>此集群可以选用两种虚拟网络之一。</p>
<div class="blog_h3"><span class="graybg">提供者网络</span></div>
<p>Provider networks，也就是外部网络。以最简单的方式部署OpenStack网络服务，通常基于L2（桥接/交换）服务和网络VLAN分段实现。这种选项将<span style="background-color: #c0c0c0;">虚拟网络桥接到物理网络</span>，依赖于物理网络基础设施完成L3服务。</p>
<p>此外，一个DHCP服务用于为Instance提供IP地址。</p>
<p>这种选项不支持一些高级特性，例如LBaaS、FWaaS。</p>
<div class="blog_h3"><span class="graybg">自服务网络</span></div>
<p>所谓自服务，是指非特权账户在不需要管理员介入的情况下，管理虚拟化基础设施 —— 例如网络的能力。</p>
<p>这种选项通过提供L3（路由）服务来增强提供者网络，使用的是类似VXLAN之类的overlay segmentation技术。<span style="background-color: #c0c0c0;">虚拟网络到物理网络的路由通过NAT</span>实现。</p>
<p>OpenStack用户可以在不了解数据网络（data network）底层基础设施的情况下，创建虚拟网络。包括VLAN网络（如果L2插件被相应的配置）。 </p>
<div class="blog_h2"><span class="graybg">客户端安装</span></div>
<div class="blog_h3"><span class="graybg">Ubuntu</span></div>
<p>安装软件：</p>
<pre class="crayon-plain-tag">sudo -H pip install python-openstackclient  --ignore-installed PyYAML
# placement插件
sudo -H pip install osc-placement

# 修复错误，将下面两个文件开头的import queue 改为 import Queue as queue
sudo vim /usr/local/lib/python2.7/dist-packages/openstack/utils.py
sudo vim /usr/local/lib/python2.7/dist-packages/openstack/cloud/openstackcloud.py</pre>
<p>配置Shell自动完成： </p>
<pre class="crayon-plain-tag">openstack complete | sudo tee /etc/bash_completion.d/osc.bash_completion &gt; /dev/null</pre>
<div class="blog_h2"><span class="graybg">组件安装</span></div>
<p>OpenStack由一系列独立安装的、相互协作的组件构成。</p>
<div class="blog_h3"><span class="graybg">源配置</span></div>
<pre class="crayon-plain-tag">dnf -y install centos-release-openstack-ussuri
yum config-manager --set-enabled powertools
dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
dnf -y upgrade</pre>
<div class="blog_h3"><span class="graybg">基础软件</span></div>
<pre class="crayon-plain-tag">dnf -y install telnet htop bridge-utils xterm</pre>
<div class="blog_h2"><span class="graybg">安装时钟同步服务</span></div>
<p>略，参考<a href="/ntp-under-ubuntu">Ubuntu的时钟同步</a></p>
<div class="blog_h2"><span class="graybg">安装数据库</span></div>
<p>Keystone为OpenStack提供身份（Identity）服务。Keystone可以提供四种Token（代表请求者身份），样例环境使用Fernet Token，同时基于Apache HTTP Server来处理请求。</p>
<p>首先，你需要安装一个数据库： </p>
<pre class="crayon-plain-tag"># 安装mariadb模块
dnf -y install mariadb mariadb-server python2-PyMySQL</pre>
<p>创建并修改配置文件：</p>
<pre class="crayon-plain-tag">[mysqld]
bind-address = 10.1.0.10

default-storage-engine = innodb
innodb_file_per_table = on
max_connections = 4096
collation-server = utf8_general_ci
character-set-server = utf8</pre>
<p>启用服务：</p>
<pre class="crayon-plain-tag">systemctl enable mariadb.service
systemctl start mariadb.service</pre>
<p>启用安全配置：</p>
<pre class="crayon-plain-tag">mysql_secure_installation </pre>
<div class="blog_h2"><span class="graybg">安装消息队列</span></div>
<p>安装RabbitMQ：</p>
<pre class="crayon-plain-tag">dnf -y install rabbitmq-server

systemctl enable rabbitmq-server.service
systemctl start rabbitmq-server.service</pre>
<p>创建一个RabbitMQ用户：</p>
<pre class="crayon-plain-tag">rabbitmqctl add_user openstack openstack</pre>
<p>为用户openstack授予配置、读写权限：</p>
<pre class="crayon-plain-tag">rabbitmqctl set_permissions openstack ".*" ".*" ".*"</pre>
<div class="blog_h2"><span class="graybg">安装Memcached</span></div>
<p>Identity Service使用Memcached来缓存Tokens。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install memcached python3-memcached</pre>
<p> 修改配置：</p>
<pre class="crayon-plain-tag">OPTIONS="-l 0.0.0.0"</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable memcached.service
systemctl start memcached.service</pre>
<div class="blog_h2"><span class="graybg">安装Etcd</span></div>
<p>OpenStack组件可能需要使用Etcd来实现分布式键锁定、配置存储、跟踪服务是否存活。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install etcd</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">#[Member]
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://10.1.0.10:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.1.0.10:2379"
ETCD_NAME="openstack"
#[Clustering]
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.1.0.10:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.1.0.10:2379"
ETCD_INITIAL_CLUSTER="openstack=http://10.1.0.10:2380"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster-01"
ETCD_INITIAL_CLUSTER_STATE="new"</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable etcd
systemctl start etcd</pre>
<div class="blog_h2"><span class="graybg">安装Keystone</span></div>
<p>为Keystone创建数据库和用户：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS keystone;

GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost' IDENTIFIED BY 'keystone';

GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'%' IDENTIFIED BY 'keystone';</pre>
<p>然后，安装以下包： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-keystone httpd
dnf -y install python3-mod_wsgi</pre>
<p>编辑Keystone配置文件：</p>
<pre class="crayon-plain-tag">[database]
# ...
connection = mysql+pymysql://keystone:keystone@os.gmem.cc/keystone


[token]
provider = fernet</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "keystone-manage db_sync" keystone</pre>
<p>初始化Fernet密钥存储库：</p>
<pre class="crayon-plain-tag">#                            运行keystone的操作系统用户和组
keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
keystone-manage credential_setup --keystone-user keystone --keystone-group keystone</pre>
<p>启动Identity服务：</p>
<pre class="crayon-plain-tag">keystone-manage bootstrap --bootstrap-password keystone \
  --bootstrap-admin-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-internal-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-public-url http://os.gmem.cc:5000/v3/ \
  --bootstrap-region-id china</pre>
<p>编辑Apache配置文件： </p>
<pre class="crayon-plain-tag">ServerName os.gmem.cc</pre>
<p>将Keystone的配置文件链接到Apache配置目录：</p>
<pre class="crayon-plain-tag">ln -s /usr/share/keystone/wsgi-keystone.conf /etc/httpd/conf.d/</pre>
<p>启动Apache服务：</p>
<pre class="crayon-plain-tag">systemctl enable httpd.service
systemctl start httpd.service</pre>
<p>为了后续使用OpenStack工具时进行身份验证，你需要设置环境变量（密码来自上面的 keystone-manage bootstrap 步骤）：</p>
<pre class="crayon-plain-tag">export OS_USERNAME=admin
export OS_PASSWORD=keystone
export OS_PROJECT_NAME=admin
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_DOMAIN_NAME=Default
export OS_AUTH_URL=http://os.gmem.cc:5000/v3
export OS_IDENTITY_API_VERSION=3 </pre>
<p>现在，我们需要需要创建一些OpenStack对象：</p>
<pre class="crayon-plain-tag"># 创建一个域
# openstack domain create --description "An Example Domain" example

# 创建一个项目
# 示例环境为每个服务创建一个用户，这些用户在此项目中获得授权
openstack project create --domain default --description "Service Project" service


# 常规操作（非管理）应该使用非特权的项目和用户进行
openstack project create --domain default --description "Gmem Project" gmem
openstack user create --domain default --password gmem gmem
openstack role create gmem
openstack role add --project gmem --user gmem gmem </pre>
<div class="blog_h2"><span class="graybg">安装Glance</span></div>
<div class="blog_h3"><span class="graybg">安装组件</span></div>
<p>Glance为OpenStack提供（虚拟机）镜像服务，Glance支持多种后端存储，本样例环境下我们直接存放在文件系统中。</p>
<p>首先，需要为Glance创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS glance;

GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'localhost' IDENTIFIED BY 'glance';
GRANT ALL PRIVILEGES ON glance.* TO 'glance'@'%' IDENTIFIED BY 'glance';</pre>
<p>使用OpenStack命令行来创建Glance的凭证信息，注意需要进行上述环境变量设置： </p>
<pre class="crayon-plain-tag">openstack user create --domain default --password glance glance</pre>
<p>为service项目中的glance用户添加admin角色：</p>
<pre class="crayon-plain-tag">openstack role add --project service --user glance admin</pre>
<p>创建Glance服务： </p>
<pre class="crayon-plain-tag"># 服务的类型
openstack service create --name glance --description "OpenStack Image" image</pre>
<p>为Glance服务添加一个端点：</p>
<pre class="crayon-plain-tag">openstack endpoint create --region china image public http://os.gmem.cc:9292</pre>
<p>下面，需要安装和配置Glance组件。安装软件包：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-glance</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://glance:glance@os.gmem.cc/glance


[keystone_authtoken]
www_authenticate_uri  = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = glance
password = glance


[paste_deploy]
flavor = keystone


[glance_store]
stores = file,http
default_store = file
filesystem_store_datadir = /var/lib/glance/images/</pre>
<p>初始化Glance数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "glance-manage db_sync" glance</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable openstack-glance-api.service
systemctl start openstack-glance-api.service</pre>
<div class="blog_h3"><span class="graybg">下载镜像</span></div>
<p>为了测试OpenStack，建议使用<a href="http://download.cirros-cloud.net/0.5.1/">CirrOS镜像</a>。</p>
<pre class="crayon-plain-tag">wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img

openstack image create --public --disk-format qcow2 --container-format bare \
  --file cirros-0.4.0-x86_64-disk.img cirros-0.4.0-amd64 </pre>
<div class="blog_h2"><span class="graybg">安装Placement</span></div>
<p>在Stein版本之前，placement 的代码在Nova中，服务在compute REST API（nova-api）中。</p>
<p>Placement提供了WSGI脚本placement-api，可以在Apache/Nginx之类的WSGI-capable服务器中使用。取决于你的安装方式，该脚本可能位于/usr/bin或/usr/local/bin下面。</p>
<p>为Placement创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS placement;
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'localhost' IDENTIFIED BY 'placement';
GRANT ALL PRIVILEGES ON placement.* TO 'placement'@'%' IDENTIFIED BY 'placement';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password placement placement

openstack role add --project service --user placement admin

openstack service create --name placement --description "Placement API" placement

openstack endpoint create --region china placement public  http://os.gmem.cc:8778
openstack endpoint create --region china placement internal http://os.gmem.cc:8778
openstack endpoint create --region china placement admin http://os.gmem.cc:8778</pre>
<p>安装和配置Placement组件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-placement-api</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[placement_database]
connection = mysql+pymysql://placement:placement@os.gmem.cc/placement


[api]
auth_strategy = keystone


[keystone_authtoken]
auth_url = http://os.gmem.cc:5000/v3
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = placement
password = placement</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "placement-manage db sync" placement</pre>
<p>修改Apache配置文件（否则可能计算节点nova报 You don't have permission to access this resource）：</p>
<pre class="crayon-plain-tag">&lt;Directory /usr/bin&gt;
  Require all granted
&lt;/Directory&gt;</pre>
<p>重启Apache服务：</p>
<pre class="crayon-plain-tag">systemctl restart httpd</pre>
<div class="blog_h2"><span class="graybg">安装Nova</span></div>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<p>创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS nova_api;
CREATE DATABASE IF NOT EXISTS nova;
CREATE DATABASE IF NOT EXISTS nova_cell0;

GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova_api.* TO 'nova'@'%' IDENTIFIED BY 'nova';

GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'%' IDENTIFIED BY 'nova';

GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'localhost' IDENTIFIED BY 'nova';
GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'%' IDENTIFIED BY 'nova';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password nova nova
openstack role add --project service --user nova admin
openstack service create --name nova  --description "OpenStack Compute" compute

openstack endpoint create --region china compute public   http://os.gmem.cc:8774/v2.1
openstack endpoint create --region china compute internal http://os.gmem.cc:8774/v2.1
openstack endpoint create --region china compute admin    http://os.gmem.cc:8774/v2.1</pre>
<p>安装组件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-nova-api openstack-nova-conductor openstack-nova-novncproxy openstack-nova-scheduler</pre>
<p>修改配置文件： </p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_apis = osapi_compute,metadata
transport_url = rabbit://openstack:openstack@os.gmem.cc:5672/
; 管理网络的IP地址
my_ip = 10.1.0.10

[api_database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova_api

[database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova

[api]
auth_strategy = keystone

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000/
auth_url = http://os.gmem.cc:5000/
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = nova
password = nova

[vnc]
enabled = true
server_listen = $my_ip
server_proxyclient_address = $my_ip

[glance]
api_servers = http://os.gmem.cc:9292

[oslo_concurrency]
lock_path = /var/lib/nova/tmp

[placement]
region_name = china
project_domain_name = Default
project_name = service
auth_type = password
user_domain_name = Default
auth_url = http://os.gmem.cc:5000/v3
username = placement
password = placement

[neutron]
; 参考：安装Neutron

[cinder]
; 参考：安装Cinder</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage api_db sync" nova

su -s /bin/sh -c "nova-manage cell_v2 map_cell0" nova

su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova

su -s /bin/sh -c "nova-manage db sync" nova</pre>
<p>校验一下，确保cell0和cell1正确的注册了： </p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage cell_v2 list_cells" nova</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable \
    openstack-nova-api.service \
    openstack-nova-scheduler.service \
    openstack-nova-conductor.service \
    openstack-nova-novncproxy.service

systemctl start \
    openstack-nova-api.service \
    openstack-nova-scheduler.service \
    openstack-nova-conductor.service \
    openstack-nova-novncproxy.service</pre>
<div class="blog_h3"><span class="graybg">计算节点</span></div>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-nova-compute</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_apis = osapi_compute,metadata
; 替换为该计算节点上，管理网络的IP
my_ip = 10.1.0.10

[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc

[api]
auth_strategy = keystone

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000/
auth_url = http://os.gmem.cc:5000/
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = Default
user_domain_name = Default
project_name = service
username = nova
password = nova

[vnc]
enabled = true
server_listen = 0.0.0.0
server_proxyclient_address = $my_ip
novncproxy_base_url = http://os.gmem.cc:6080/vnc_auto.html

[glance]
api_servers = http://os.gmem.cc:9292

[oslo_concurrency]
lock_path = /var/lib/nova/tmp

[placement]
region_name = china
project_domain_name = Default
project_name = service
auth_type = password
user_domain_name = Default
auth_url = http://os.gmem.cc:5000/v3
username = placement
password = placement

[libvirt]
virt_type = kvm

[neutron]
; 参考：安装Neutron

[cinder]
; 参考：安装Cinder</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable libvirtd.service openstack-nova-compute.service
systemctl start libvirtd.service openstack-nova-compute.service</pre>
<p>如果nova-compute启动失败，检查日志： /var/log/nova/nova-compute.log。</p>
<p>最后，将该节点加入到cell数据库中：</p>
<pre class="crayon-plain-tag"># 检查计算服务列表
openstack compute service list --service nova-compute

# 发现计算服务主机
# 需要在控制节点上执行
su -s /bin/sh -c "nova-manage cell_v2 discover_hosts --verbose" nova

# 如果希望自动发现新的主机，可以配置控制节点的/etc/nova/nova.conf：
# [scheduler]
# discover_hosts_in_cells_interval = 300</pre>
<div class="blog_h2"><span class="graybg">安装Neutron</span></div>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<p>创建数据库：</p>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS neutron;

GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'localhost'  IDENTIFIED BY 'neutron';
GRANT ALL PRIVILEGES ON neutron.* TO 'neutron'@'%' IDENTIFIED BY 'neutron';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password neutron neutron

openstack role add --project service --user neutron admin

openstack service create --name neutron --description "OpenStack Networking" network

openstack endpoint create --region china network public   http://os.gmem.cc:9696
openstack endpoint create --region china network internal http://os.gmem.cc:9696
openstack endpoint create --region china network admin    http://os.gmem.cc:9696</pre>
<p>如果使用提供者网络，也就是直接将VM添加到外部网络，不提供自服务网络（以及路由器、浮动IP等），则需要使用admin或其它特权账户。步骤如下：</p>
<ol>
<li>安装软件：<br />
<pre class="crayon-plain-tag">dnf -y install openstack-neutron openstack-neutron-ml2 openstack-neutron-linuxbridge ebtables </pre>
</li>
<li>配置Neutron：<br />
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://neutron:neutron@os.gmem.cc/neutron

[DEFAULT]
core_plugin = ml2
service_plugins =

transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone

notify_nova_on_port_status_changes = true
notify_nova_on_port_data_changes = true

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[nova]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = nova
password = nova

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
</li>
<li>
<p>配置Modular Layer2（ML2）插件。此插件使用Linux bridge来为虚拟机构建L2 VNI：</p>
<pre class="crayon-plain-tag">[ml2]
; 启用Flat/VLAN网络
type_drivers = flat,vlan
; 禁用自服务网络
tenant_network_types =
; 启用 Linux bridge mechanism 
mechanism_drivers = linuxbridge
; 启用端口安全扩展驱动
extension_drivers = port_security

[ml2_type_flat]
; 配置提供者虚拟网络为Flat网络
flat_networks = provider

[securitygroup]
; 增强安全组规则的性能
enable_ipset = true </pre>
</li>
<li>
<p> 配置Linux Bridge agent，此Agent为VM构建L2 VNI，并处理安全组：
<pre class="crayon-plain-tag">[linux_bridge]
; 这里填写底层的提供者网络的设备名
physical_interface_mappings = provider:eth0

[vxlan]
; 禁用VXLAN
enable_vxlan = false

[securitygroup]
; 启用安全组，配置基于iptables的防火墙驱动
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>确保宿主机内核支持Network bridge filters：
<pre class="crayon-plain-tag"># 二层的网桥在转发包时也会被iptables的FORWARD规则所过滤
net.bridge.bridge-nf-call-iptables  1
net.bridge.bridge-nf-call-ip6tables 1</pre>
<p>此外，为了支持网桥，需要加载内核模块<pre class="crayon-plain-tag">br_netfilter</pre></p>
</li>
<li>
<p> 配置DHCP代理，此代理为虚拟网络提供DHCP服务：</p>
<pre class="crayon-plain-tag">[DEFAULT]
interface_driver = linuxbridge
dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq
enable_isolated_metadata = true</pre>
</li>
</ol>
<p>如果使用自服务网络，不需要特权用户就可以管理网络（包括路由）并在自服务网络和提供者网络之间创建连接，也可以为VM提供浮动IP，以便从外部访问VM。步骤如下：
<ol>
<li>安装软件，同上</li>
<li>配置Neutron，基本同上：<br />
<pre class="crayon-plain-tag">[database]
connection = mysql+pymysql://neutron:neutron@os.gmem.cc/neutron

[DEFAULT]
; 同样使用ML2插件
core_plugin = ml2
; 启用路由服务，允许IP重叠
service_plugins = router
allow_overlapping_ips = true

transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone

notify_nova_on_port_status_changes = true
notify_nova_on_port_data_changes = true

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[nova]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = nova
password = nova

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
</li>
<li>
<p> 配置Modular Layer2（ML2）插件：</p>
<pre class="crayon-plain-tag">[ml2]
; 启用Flat/VLAN/VXLAN
type_drivers = flat,vlan,vxlan
; 启用自服务网络，基于VXLAN
tenant_network_types = vxlan
; 启用Linux网桥，以及Layer-2 Population
mechanism_drivers = linuxbridge,l2population
; 启用端口安全扩展驱动
extension_drivers = port_security

[ml2_type_flat]
; 配置提供者虚拟网络为Flat网络
flat_networks = provider

[ml2_type_vxlan]
; 设置VXLAN网络标识符的范围
vni_ranges = 1:1000

[securitygroup]
; 增强安全组规则的性能
enable_ipset = true</pre>
</li>
<li>配置Linux Bridge agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
; 这里填写底层的提供者网络的设备名
physical_interface_mappings = provider:eth0

[vxlan]
; 启用VXLAN
enable_vxlan = true
; 用于处理Overlay网络的底层网络的本机IP地址
local_ip = 10.1.0.10
l2_population = true

[securitygroup]
; 启用安全组，配置基于iptables的防火墙驱动
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver </pre>
</li>
<li>
<p>确保宿主机内核支持Network bridge filters，同上
</li>
<li>
<p> 配置DHCP，同上</p>
</li>
<li>配置L3代理，此代理为自服务VNI提供路由、NAT：<br />
<pre class="crayon-plain-tag">[DEFAULT]
interface_driver = linuxbridge</pre>
</li>
</ol>
<p>自服务网络是overlay网络，使用VXLAN之类的协议，这些协议具有额外的头，导致实际可能负载减小，如果VM不知道此VNI的特征，会自动设置过大的MTU 1500。Neutron提供的DHCP能自动给VM提供正确的MTU。但是，某些云镜像不使用DHCP，或者忽略DHCP的MTU选项，需要注意。</p>
<p>执行完上述两种网络选项之一后，继续配置元数据代理（metadata agent） ，<span style="background-color: #c0c0c0;">元数据代理代替虚拟机（附加Instance ID、Tenant ID等请求头）访问Nova metadata API</span>，获取虚拟机镜像的配置信息：</p>
<pre class="crayon-plain-tag">[DEFAULT]
nova_metadata_host = os.gmem.cc
; 设置适当的共享密钥
metadata_proxy_shared_secret = openstack</pre>
<p> 配置计算服务（Nova）来使用网络服务：</p>
<pre class="crayon-plain-tag">[neutron]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = neutron
password = neutron
service_metadata_proxy = true
metadata_proxy_shared_secret = openstack</pre>
<p>将ML2配置链接为Neutron插件主配置文件：</p>
<pre class="crayon-plain-tag">ln -s /etc/neutron/plugins/ml2/ml2_conf.ini /etc/neutron/plugin.ini</pre>
<p>初始化数据库： </p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "neutron-db-manage --config-file /etc/neutron/neutron.conf \
  --config-file /etc/neutron/plugins/ml2/ml2_conf.ini upgrade head" neutron</pre>
<p>重启Nova： </p>
<pre class="crayon-plain-tag">systemctl restart openstack-nova-api.service</pre>
<p>启动Neutron：  </p>
<pre class="crayon-plain-tag">systemctl enable neutron-server.service \
  neutron-linuxbridge-agent.service neutron-dhcp-agent.service \
  neutron-metadata-agent.service
systemctl start neutron-server.service \
  neutron-linuxbridge-agent.service neutron-dhcp-agent.service \
  neutron-metadata-agent.service</pre>
<p>如果使用自服务网络选项，还需要启用L3服务： </p>
<pre class="crayon-plain-tag">systemctl enable neutron-l3-agent.service
systemctl start neutron-l3-agent.service</pre>
<div class="blog_h3"><span class="graybg">计算节点</span></div>
<p>安装软件： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-neutron-linuxbridge ebtables ipset</pre>
<p>配置身份验证、消息队列： </p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc

auth_strategy = keystone


[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = neutron
password = neutron

[oslo_concurrency]
lock_path = /var/lib/neutron/tmp</pre>
<p>配置网络选项。如果使用提供者网络：</p>
<ol>
<li> 配置Linux bridge Agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
physical_interface_mappings = provider:eth0

[vxlan]
enable_vxlan = false

[securitygroup]
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>确保内核参数（通常需要保证内核模块br_netfilter已加载 ）：</p>
<pre class="crayon-plain-tag">net.bridge.bridge-nf-call-iptables 1
net.bridge.bridge-nf-call-ip6tables 1</pre>
</li>
</ol>
<p>如果使用自服务网络：
<ol>
<li> 配置Linux bridge Agent：<br />
<pre class="crayon-plain-tag">[linux_bridge]
physical_interface_mappings = provider:eth0

[vxlan]
enable_vxlan = true
local_ip = 10.1.0.11
l2_population = true

[securitygroup]
enable_security_group = true
firewall_driver = neutron.agent.linux.iptables_firewall.IptablesFirewallDriver</pre>
</li>
<li>
<p>同上 </p>
</li>
</ol>
<p>配置计算服务，让它使用网络服务：</p>
<pre class="crayon-plain-tag">[neutron]
auth_url = http://os.gmem.cc:5000
auth_type = password
project_domain_name = default
user_domain_name = default
region_name = china
project_name = service
username = neutron
password = neutron</pre>
<p>重启Nova：<pre class="crayon-plain-tag">systemctl restart openstack-nova-compute.service</pre></p>
<p>启动Linux bridge Agent：</p>
<pre class="crayon-plain-tag">systemctl enable neutron-linuxbridge-agent.service
systemctl start neutron-linuxbridge-agent.service</pre>
<div class="blog_h3"><span class="graybg">操作校验</span></div>
<p>列出已加载的扩展列表，确保Neutron相关进程启动：</p>
<pre class="crayon-plain-tag">openstack extension list --network</pre>
<p>校验Neutron代理都已经启动： </p>
<pre class="crayon-plain-tag">openstack network agent list</pre>
<p>应该启动的代理包括：</p>
<ol>
<li>控制节点的元数据代理、DHCP代理、Linux bridge代理</li>
<li>计算节点的Linux bridge代理</li>
<li>如果使用自服务网络，控制节点还有L3代理</li>
</ol>
<div class="blog_h2"><span class="graybg">安装Cinder</span></div>
<p>样例环境中，使用存储节点上的空白磁盘/dev/sdb ，基于LVM划分初逻辑卷，然后通过iSCSI协议暴露给虚拟机。</p>
<div class="blog_h3"><span class="graybg">控制节点</span></div>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS cinder;

GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'localhost' IDENTIFIED BY 'cinder';
GRANT ALL PRIVILEGES ON cinder.* TO 'cinder'@'%' IDENTIFIED BY 'cinder';</pre>
<p>创建用户和端点：</p>
<pre class="crayon-plain-tag">openstack user create --domain default --password cinder cinder

openstack role add --project service --user cinder admin

openstack service create --name cinderv2 --description "OpenStack Block Storage" volumev2
openstack service create --name cinderv3 --description "OpenStack Block Storage" volumev3

openstack endpoint create --region china volumev2 public http://os.gmem.cc:8776/v2/%\(project_id\)s
openstack endpoint create --region china volumev2 internal http://os.gmem.cc:8776/v2/%\(project_id\)s
openstack endpoint create --region china volumev2 admin http://os.gmem.cc:8776/v2/%\(project_id\)s

openstack endpoint create --region china volumev3 public http://os.gmem.cc:8776/v3/%\(project_id\)s
openstack endpoint create --region china volumev3 internal http://os.gmem.cc:8776/v3/%\(project_id\)s
openstack endpoint create --region china volumev3 admin http://os.gmem.cc:8776/v3/%\(project_id\)s</pre>
<p><span class="graybg">安装组件：</span></p>
<pre class="crayon-plain-tag">dnf -y install openstack-cinder</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc
auth_strategy = keystone
; 管理网络接口的IP地址
my_ip = 10.1.0.10

[database]
connection = mysql+pymysql://cinder:cinder@os.gmem.cc/cinder

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = cinder
password = cinder

[oslo_concurrency]
lock_path = /var/lib/cinder/tmp</pre>
<p>初始化数据库：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "cinder-manage db sync" cinder</pre>
<p>配置Nova来使用存储服务： </p>
<pre class="crayon-plain-tag">[cinder]
os_region_name = china</pre>
<p>重新启动Nova：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-nova-api.service</pre>
<p>启动Cinder服务： </p>
<pre class="crayon-plain-tag">systemctl enable openstack-cinder-api.service openstack-cinder-scheduler.service
systemctl start openstack-cinder-api.service openstack-cinder-scheduler.service</pre>
<div class="blog_h3"><span class="graybg">存储节点</span></div>
<p>在本样例环境下，需要LVM支持：</p>
<pre class="crayon-plain-tag">yum install lvm2 device-mapper-persistent-data

systemctl enable lvm2-lvmetad.service
systemctl start lvm2-lvmetad.service</pre>
<p>在/dev/sdb上创建LVM物理卷，并创建名为cinder-volumes的卷组：</p>
<pre class="crayon-plain-tag">pvcreate /dev/sdb

vgcreate cinder-volumes /dev/sdb</pre>
<p>只有虚拟机实例能够访问块设备卷。但是，存储节点的底层OS负责管理卷关联的块设备。默认情况下，LVM卷扫描工具会扫描/dev/目录来寻找包含卷的块设备。如果某个OpenStack项目使用基于LVM的卷，扫描工具会扫描卷并缓存结果，这可能导致很多问题。因此，你必须重新配置LVM，让它仅仅扫描包含cinder-volumes卷组的设备：</p>
<pre class="crayon-plain-tag">devices {
# a表示允许允许使用的卷，r表示拒绝使用的卷     
#                       拒绝所有其它的卷    
filter = [ "a/sdb/", "r/.*/"]</pre>
<p>安装组件： </p>
<pre class="crayon-plain-tag">dnf -y install openstack-cinder targetcli python3-keystone</pre>
<p>修改配置文件： </p>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@os.gmem.cc
auth_strategy = keystone
; 此节点启用的存储后端
enabled_backends = lvm
; 配置镜像服务的API地址
glance_api_servers = http://os.gmem.cc:9292
; 管理网络接口的IP地址
my_ip = 10.1.0.11

[database]
connection = mysql+pymysql://cinder:cinder@os.gmem.cc/cinder

[keystone_authtoken]
www_authenticate_uri = http://os.gmem.cc:5000
auth_url = http://os.gmem.cc:5000
memcached_servers = os.gmem.cc:11211
auth_type = password
project_domain_name = default
user_domain_name = default
project_name = service
username = cinder
password = cinder

[oslo_concurrency]
lock_path = /var/lib/cinder/tmp

[lvm]
volume_driver = cinder.volume.drivers.lvm.LVMVolumeDriver
volume_group = cinder-volumes
target_protocol = iscsi
target_helper = lioadm</pre>
<p>启动服务：</p>
<pre class="crayon-plain-tag">systemctl enable openstack-cinder-volume.service target.service
systemctl start openstack-cinder-volume.service target.service</pre>
<div class="blog_h2"><span class="graybg">安装Horizon</span></div>
<p>OpenStack Dashboard组件，即Horizon，此组件仅仅依赖于Identity。</p>
<p>安装软件：</p>
<pre class="crayon-plain-tag">dnf -y install openstack-dashboard</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">OPENSTACK_HOST = "os.gmem.cc"

WEBROOT = '/dashboard/'

# 允许哪些主机访问仪表盘
ALLOWED_HOSTS = ['os.gmem.cc']


# 配置基于Memcache的分布式会话存储
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
    'default': {
         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
         'LOCATION': 'os.gmem.cc:11211',
    }
}

# 启用Identity API v3
OPENSTACK_KEYSTONE_URL = "http://%s/identity/v3" % OPENSTACK_HOST

# 确保Domain支持
OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True

# 配置API版本
OPENSTACK_API_VERSIONS = {
    "identity": 3,
    "image": 2,
    "volume": 3,
}

# 默认访问的Domain
OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "Default"

# 通过仪表盘创建的用户的默认角色
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "user"

# 配置网络
OPENSTACK_NEUTRON_NETWORK = {
    # 如果使用提供者网络，需要禁用router
    'enable_router': False,
    'enable_quotas': False,
    'enable_distributed_router': False,
    'enable_ha_router': False,
    'enable_lb': False,
    'enable_firewall': False,
    'enable_vpn': False,
    'enable_fip_topology_check': False,
}

TIME_ZONE = "Asia/Shanghai"</pre>
<p>修改配置文件：</p>
<pre class="crayon-plain-tag">WSGIDaemonProcess dashboard
WSGIProcessGroup dashboard
WSGISocketPrefix run/wsgi

WSGIApplicationGroup %{GLOBAL}

WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
Alias /dashboard/static /usr/share/openstack-dashboard/static

&lt;Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi&gt;
  Options All
  AllowOverride All
  Require all granted
&lt;/Directory&gt;

&lt;Directory /usr/share/openstack-dashboard/static&gt;
  Options All
  AllowOverride All
  Require all granted
&lt;/Directory&gt;</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart httpd.service memcached.service</pre>
<p>此后你应该可以通过：http://os.gmem.cc访问仪表盘。</p>
<div class="blog_h3"><span class="graybg">启用HTTPS</span></div>
<p>修改horizon主配置文件：</p>
<pre class="crayon-plain-tag"># 添加以下配置
USE_SSL = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True</pre>
<p>修改Dashboard的httpd配置：</p>
<pre class="crayon-plain-tag">WSGISocketPrefix run/wsgi

&lt;VirtualHost *:443&gt;
  ServerName os.gmem.cc

  SSLEngine On
  SSLCertificateFile /etc/httpd/ssl/os.gmem.cc.crt
  SSLCACertificateFile /etc/httpd/ssl/os.gmem.cc.crt
  SSLCertificateKeyFile /etc/httpd/ssl/os.gmem.cc.key
  SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown

  Header add Strict-Transport-Security "max-age=15768000"

  WSGIDaemonProcess dashboard
  WSGIProcessGroup dashboard

  WSGIApplicationGroup %{GLOBAL}

  WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
  Alias /dashboard/static /usr/share/openstack-dashboard/static

  &lt;Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi&gt;
    Options All
    AllowOverride All
    Require all granted
  &lt;/Directory&gt;

  &lt;Directory /usr/share/openstack-dashboard/static&gt;
    Options All
    AllowOverride All
    Require all granted
  &lt;/Directory&gt;
&lt;/VirtualHost&gt;</pre>
<p>安装必要的httpd模块：</p>
<pre class="crayon-plain-tag">dnf -y install mod_ssl </pre>
<div class="blog_h2"><span class="graybg">创建网络</span></div>
<div class="blog_h3"><span class="graybg">创建提供者网络</span></div>
<p>在启动实例之前，你需要创建必要的VNI。如果使用网络选项一，则虚拟机通过Provider（External）网络连接到PNI（基于bridging/switching）。</p>
<p>网络架构如下图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-overview.png"><img class="size-full wp-image-34955 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-overview.png" alt="network1-overview" width="630" height="558" /></a></p>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-connectivity.png"><img class="size-full wp-image-34953 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network1-connectivity.png" alt="network1-connectivity" width="817" height="451" /></a></p>
<p>使用下面的命令在OpenStack中创建一个名为provider的网络：</p>
<pre class="crayon-plain-tag"># share 表示允许所有project使用此虚拟网络
# external 表示网络是外部的，默认值internal
openstack network create  --share --external \
# 此虚拟网络所基于的物理网络
# 此名字对应的物理网络接口在linuxbridge_agent.ini中定义
#   physical_interface_mappings = provider:eth0
  --provider-physical-network provider \
# 此虚拟网络的物理实现机制（physical mechanism）
  --provider-network-type flat \
# 网络的名字
  provider</pre>
<p>为上述网络创建一个子网：</p>
<pre class="crayon-plain-tag">openstack subnet create --network provider \
# 子网中，用于分配给虚拟机实例的IP地址范围，此IP地址范围由DHCP agent管理
  --allocation-pool start=10.0.100.1,end=10.0.1.255 \
# DNS服务器地址              底层物理网络的网关地址
  --dns-nameserver 10.0.0.1 --gateway 10.0.0.1 \
# 底层物理网络的CIDR
  --subnet-range 10.0.0.0/16 provider</pre>
<div class="blog_h3"><span class="graybg">创建自服务网络</span></div>
<p>前面我们提到过两个网络选项，如果使用选项1，则参考上一小节的内容创建虚拟网络。如果使用选项2，则<span style="background-color: #c0c0c0;">在创建上述provider网络之后，还需要</span>参考本节内容。</p>
<p>网络架构如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-overview.png"><img class="size-full wp-image-34951 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-overview.png" alt="network2-overview" width="774" height="726" /></a></p>
<p>&nbsp;</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-connectivity.png"><img class="size-full wp-image-34949 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2020/12/network2-connectivity.png" alt="network2-connectivity" width="753" height="898" /></a></p>
<p>使用选项2时，需要创建一个自服务（私有）的虚拟网络，并通过NAT连接到物理网络。此虚拟网络提供DHCP服务器，并且分配IP地址给实例。在这种私有网络中的实例可以直接访问外部网络（NAT），但是外部网络不能直接访问这些实例，除非给实例分配浮动IP。</p>
<p>修改环境变量，使用用户gmem来操作。</p>
<p>创建名为gmem的自服务网络：</p>
<pre class="crayon-plain-tag">openstack network create gmem-net</pre>
<p>非特权用户通常无法为上述命令指定额外的参数。OpenStack会依据下面的配置文件来自动选择参数：</p>
<pre class="crayon-plain-tag">[ml2]
tenant_network_types = vxlan

[ml2_type_vxlan]
vni_ranges = 1:1000</pre>
<p>创建子网： </p>
<pre class="crayon-plain-tag">openstack subnet create --network gmem-net \
#                           子网的网关地址，不是物理网络上的网关地址
  --dns-nameserver 10.1.0.1 --gateway 192.168.100.1 \
# 子网的CIDR
  --subnet-range 192.168.100.0/24 gmem-subnet</pre>
<p>自服务网络通过一个虚拟路由器，连接到提供者网络：</p>
<pre class="crayon-plain-tag">openstack router create gmem-router</pre>
<p>将上述子网添加为此虚拟路由器的一个接口（interface）： </p>
<pre class="crayon-plain-tag">openstack router add subnet gmem-router gmem-subnet</pre>
<p>将在提供者网络上的生成一个“网关“，连接到路由器：</p>
<pre class="crayon-plain-tag">openstack router set gmem-router --external-gateway provider</pre>
<p>到控制节点上进行校验，确保虚拟网络正常工作：</p>
<pre class="crayon-plain-tag"># 检查网络命名空间，应该至少看到：
#   一个qrouter开头的
#   二个qdhcp开头的
ip netns


# 列出路由器的端口，确定提供者网络上的网关地址
openstack port list --router router


# 确认可以从控制节点，或者物理网络上任何节点来访问上述网关地址  </pre>
<div class="blog_h2"><span class="graybg">创建Flavor</span></div>
<p>所谓Flavor就是虚拟机的规格，例如内存多大，CPU几个，磁盘几个，等等。最小的默认Flavor消耗512MB内存，这里我们创建一个仅消耗64MB内存的Flavor：</p>
<pre class="crayon-plain-tag">openstack flavor create --id 0 --vcpus 1 --ram 64 --disk 1 m1.nano</pre>
<div class="blog_h2"><span class="graybg">创建密钥</span></div>
<p>大部分的云镜像支持基于PKI的身份验证，而不是密码。在启动实例之前，我们需要为计算服务创建密钥： </p>
<pre class="crayon-plain-tag">openstack keypair create --public-key /home/alex/Documents/puTTY/gmem.crt default

openstack keypair list</pre>
<div class="blog_h2"><span class="graybg">添加安全组规则</span></div>
<p><span style="background-color: #c0c0c0;">默认的安全组应用到所有的实例， 此安全组禁止对实例的所有远程访问</span>。对于Linux镜像例如CirrOS，我们至少应该允许ICMP（ping）以及SSH：</p>
<pre class="crayon-plain-tag">openstack security group rule create --proto icmp default

openstack security group rule create --proto tcp --dst-port 22 default</pre>
<div class="blog_h2"><span class="graybg">创建虚拟机实例</span></div>
<p>列出可用的基础资源：</p>
<pre class="crayon-plain-tag">openstack flavor list
# +----+---------+-----+------+-----------+-------+-----------+
# | ID | Name    | RAM | Disk | Ephemeral | VCPUs | Is Public |
# +----+---------+-----+------+-----------+-------+-----------+
# | 0  | m1.nano |  64 |    1 |         0 |     1 | True      |
# +----+---------+-----+------+-----------+-------+-----------+

openstack image list
# +--------------------------------------+--------------------+--------+
# | ID                                   | Name               | Status |
# +--------------------------------------+--------------------+--------+
# | 5b337b93-96c6-4803-b0e2-bc0bce0afde9 | cirros-0.5.1-amd64 | active |
# +--------------------------------------+--------------------+--------+

openstack network list
# +--------------------------------------+----------+--------------------------------------+
# | ID                                   | Name     | Subnets                              |
# +--------------------------------------+----------+--------------------------------------+
# | 500cd78a-a05c-4b93-b399-06b26cc108de | provider | 9be1305c-a641-40f0-bd1b-6617e96025e6 |
# +--------------------------------------+----------+--------------------------------------+

openstack security group list
# +--------------------------------------+---------+------------------------+----------------------------------+------+
# | ID                                   | Name    | Description            | Project                          | Tags |
# +--------------------------------------+---------+------------------------+----------------------------------+------+
# | f5b0e967-5903-4b6b-9b9d-8e39a07b52da | default | Default security group | e1c4a3403e1b46cd969e4d626b5cf799 | []   |
# +--------------------------------------+---------+------------------------+----------------------------------+------+</pre>
<div class="blog_h3"><span class="graybg">在提供者网络上</span></div>
<pre class="crayon-plain-tag">openstack server create --flavor m1.nano --image cirros-0.5.1-amd64 \
  --nic net-id=500cd78a-a05c-4b93-b399-06b26cc108de --security-group default \
  --key-name default cirros-amd64-0</pre>
<p>当虚拟机构建完毕后，其状态会从BUILD变为ACTIVE：</p>
<pre class="crayon-plain-tag">openstack server list </pre>
<p>CirrOS的默认用户密码是cirros / gocubsgo，确认可以登陆。</p>
<p>在虚拟机的宿主机上，可以看到OpenStack创建了一个网桥，连接了提供者物理网络和虚拟机（的tap设备）：</p>
<pre class="crayon-plain-tag">brctl show
# bridge name     bridge id               STP enabled     interfaces
# brq1305444e-cf          8000.100000000012       no              eth0
#                                                         tapa87f2880-fc </pre>
<div class="blog_h1"><span class="graybg">Keystone</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Keystone是OpenStack的Identity服务，它负责身份<span style="background-color: #c0c0c0;">验证、授权，以及一系列其它服务</span>。Keystone可以集成到外部的用户管理系统，例如LDAP。</p>
<p>Keystone通常是用户首先与之交互的服务，随后用户使用他的Identity来访问其它OpenStack服务。OpenStack组件也需要访问Keystone来验证用户声明的Identity是否合法。</p>
<p>用户或者服务，可以利用服务目录来得到其它服务（Openstack组件）的位置，<span style="background-color: #c0c0c0;">服务目录（Service catalog）也是Keystone提供</span>的服务之一。</p>
<p>每个服务可以提供1-N个端点，每个端点可以是admin/internal（可能仅限于OpenStack所在主机访问）/public（可能允许Internet访问）三种类型之一。在生产环境中，这些不同类型的端点可能出于安全方面的考虑，存放在不同的网络中，供不同类型的用户访问。</p>
<div class="blog_h2"><span class="graybg">权限模型</span></div>
<div class="blog_h3"><span class="graybg">User</span></div>
<p>代表单个API消费者：</p>
<ol>
<li>用户就是一个有身份验证信息的API消费实体</li>
<li>用户可以属于多个项目/角色</li>
</ol>
<div class="blog_h3"><span class="graybg">Group</span></div>
<p>代表一组User的集合。</p>
<div class="blog_h3"><span class="graybg">Project</span></div>
<p>代表OpenStack中基本的所有权单元 —— OpenStack中各种<span style="background-color: #c0c0c0;">计算资源都是归属于某个特定项目</span>的。而Project则是归属于某个Domain的。</p>
<p><span style="background-color: #c0c0c0;">租户（Tenant）在OpenStack中，就是项目，其目的就是隔离计算资源</span>。</p>
<div class="blog_h3"><span class="graybg">Domin</span></div>
<p>是Project、User、Role、Group的高层次容器，后面三者仅仅属于唯一一个domain。Keystone默认提供一个名为Defulat的domain。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Keystone由三类组件构成。</p>
<div class="blog_h3"><span class="graybg">Server</span></div>
<p>中心化的HTTP服务器，对外提供鉴权的RESTful接口</p>
<div class="blog_h3"><span class="graybg">Drivers</span></div>
<p>集成在Server里面，用于访问存放在OpenStack外部（例如LDAP或MySQL数据库）的身份信息。</p>
<div class="blog_h3"><span class="graybg">Modules</span></div>
<p>运行在使用Identity service的那些OpenStack组件中，负责拦截针对这些组件的请求，抽取用户凭证信息，发送到上述Server执行鉴权。</p>
<p>这些Modules基于Python Web Server Gateway Interface和OpenStack组件集成。</p>
<div class="blog_h2"><span class="graybg">Token选型</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;"><strong>Token类型</strong></td>
<td style="text-align: center;"><strong>UUID</strong></td>
<td style="text-align: center;"><strong>PKI</strong></td>
<td style="text-align: center;"><strong>PKIZ</strong></td>
<td style="text-align: center;"><strong>Fernet</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">大小</td>
<td style="text-align: left;">32 Byte</td>
<td style="text-align: left;">KB 级别</td>
<td style="text-align: left;">KB 级别</td>
<td style="text-align: left;">约 255 Byte</td>
</tr>
<tr>
<td style="text-align: left;">支持本地认证</td>
<td style="text-align: left;">不支持</td>
<td style="text-align: left;">支持</td>
<td style="text-align: left;">支持</td>
<td style="text-align: left;">不支持</td>
</tr>
<tr>
<td style="text-align: left;">Keystone 负载</td>
<td style="text-align: left;">大</td>
<td style="text-align: left;">小</td>
<td style="text-align: left;">小</td>
<td style="text-align: left;">大</td>
</tr>
<tr>
<td style="text-align: left;">存储于数据库</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">否</td>
</tr>
<tr>
<td style="text-align: left;">携带信息</td>
<td style="text-align: left;">无</td>
<td style="text-align: left;">user, catalog 等</td>
<td style="text-align: left;">user, catalog 等</td>
<td style="text-align: left;">user 等</td>
</tr>
<tr>
<td style="text-align: left;">涉及加密方式</td>
<td style="text-align: left;">无</td>
<td style="text-align: left;">非对称加密</td>
<td style="text-align: left;">非对称加密</td>
<td style="text-align: left;">对称加密(AES)</td>
</tr>
<tr>
<td style="text-align: left;">是否压缩</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">否</td>
<td style="text-align: left;">是</td>
<td style="text-align: left;">否</td>
</tr>
<tr>
<td style="text-align: left;">版本支持</td>
<td style="text-align: left;">D</td>
<td style="text-align: left;">G</td>
<td style="text-align: left;">J</td>
<td style="text-align: left;">K</td>
</tr>
</tbody>
</table>
<p>K版本之后，通常选择Fernet Token。</p>
<div class="blog_h1"><span class="graybg">Nova</span></div>
<p>Nova，即OpenStack Compute组件，负责host和管理云主机，是IaaS系统的核心组成部分。可以管理的云主机类型包括虚拟机、物理机（依赖ironic），并对容器提供有限的支持。</p>
<p>Nova和Keystone交互进行身份验证。<span style="background-color: #c0c0c0;">Placement负责计算资源的跟踪和选择</span>（作为实例的宿主），Glance提供虚拟机镜像，这些组件配合就能让实例运行起来。</p>
<p>OpenStack自身不提供虚拟化软件，而是通过“驱动”和底层的Hypervisor进行交互。交互工作主要由Nova完成。</p>
<div class="blog_h2"><span class="graybg">架构</span></div>
<p>Nova由一系列分布式的组件构成，每种组件具有自己的职责。面向用户的是一个REST API。内部组件主要通过RPC消息传递机制通信。</p>
<p>API组件监听到请求后，<span style="background-color: #c0c0c0;">通常会进行数据库读写操作，可选的，会将RPC消息发送给其它Nova组件，然后将REST响应发给客户端</span>。RPC消息是通过 <span style="background-color: #c0c0c0;">oslo.messaging 库完成的，这个库是基于消息队列的抽象</span>。</p>
<p><span style="background-color: #c0c0c0;">大部分组件可以运行在多台宿主机上，并且其中具有一个manager负责监听RPC消息、执行一些周期性工作</span>。一个主要的例外是nova-compute，此组件和它管理的Hypervisor（除了VMware或Ironic驱动）对应，每个Hypervisor对应一个nova-compute。</p>
<p>Nova具有一个逻辑的、中心化的、被所有组件共享的数据库。为了辅助OpenStack升级，<span style="background-color: #c0c0c0;">对数据库的访问基于一个对象层</span>，此对象层确保一个升级后的控制平面，仍然能和低版本的nova-compute进行交互。具体实现上，nova-compute通过中心化的nova-conductor间接的访问数据库，后者提供基于RPC的接口。</p>
<p><img src="https://blog.gmem.cc/wp-content/uploads/2021/01/nova-arch.svg" alt="" width="1038" height="860" /></p>
<div class="blog_h3"><span class="graybg">nova-api服务</span></div>
<p>安装在控制节点上。提供OpenStack Compute API，负责确保一些策略，<span style="background-color: #c0c0c0;">发起大部分编排活动（例如运行实例）</span>。</p>
<div class="blog_h3"><span class="graybg">nova-api-metadata服务</span></div>
<p>安装在控制节点上。处理处理获取实例元数据的请求。</p>
<div class="blog_h3"><span class="graybg">nova-compute服务</span></div>
<p>安装在计算节点上。Worker守护进程，负责调用Hypervisor API，在计算节点上创建、终结虚拟机实例。支持的Hypervisor API包括：</p>
<ol>
<li>XenAPI for XenServer/XCP</li>
<li>libvirt for KVM or QEMU</li>
<li>VMwareAPI for VMware</li>
</ol>
<p>该组件的大概工作流程是：</p>
<ol>
<li>从消息队列里接受Action</li>
<li>调用一系列的系统命令，启动虚拟机</li>
<li>在数据库中更新实例状态</li>
</ol>
<div class="blog_h3"><span class="graybg">nova-scheduler服务</span></div>
<p>从队列中取出虚拟机实例的请求，然后决定将其调度在哪台计算节点上。</p>
<div class="blog_h3"><span class="graybg">nova-conductor模块</span></div>
<p>协调nova-compute s服务和数据库之间的交互。避免nova-compute直接访问数据库。该模块支持水平扩容，<span style="background-color: #c0c0c0;">避免将其部署在nova-compute运行的节点</span>。</p>
<div class="blog_h3"><span class="graybg">nova-novncproxy进程</span></div>
<p>提供一个代理，用于通过VNC协议连接到运行中的实例。</p>
<div class="blog_h3"><span class="graybg">nova-spicehtml5proxy进程</span></div>
<p>提供一个代理，用于通过SPICE协议连接到运行中的实例，支持HTML5客户端。</p>
<div class="blog_h3"><span class="graybg">placenment服务</span></div>
<p>这个服务目前已经独立出去，它负责跟踪资源库存、使用情况。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>这是一个中心化的Hub，用于在不同组件之间传递消息。通常使用RabbitMQ。</p>
<div class="blog_h3"><span class="graybg">数据库</span></div>
<p>存储云基础设施的构建时、运行时状态，包括：</p>
<ol>
<li>可用的实例类型</li>
<li>使用着哦个的实例</li>
<li>可用的网络</li>
<li>项目</li>
<li>……</li>
</ol>
<p>任何SQLAlchemy支持的RDBMS都可以。</p>
<div class="blog_h2"><span class="graybg">Cell简介</span></div>
<p>为了支持Nova部署的水平扩展，Nova引入了分片机制，每个分片叫<a href="https://docs.openstack.org/nova/latest/user/cells.html">Cell</a>。利用Cell：</p>
<ol>
<li>可以在<span style="background-color: #c0c0c0;">单个Region中将计算节点的数量扩容到成千上万</span>。每个Cell具有自己的数据库、消息队列，这是可扩容的关键</li>
<li>实现故障隔离的特性（一个Cell故障，其它Cell还可以正常工作）</li>
<li>作为一种分组机制，可以将类似的硬件放在同一Cell中</li>
</ol>
<div class="blog_h3"><span class="graybg">Cell V1</span></div>
<p>Cell V1的特性：</p>
<ol>
<li>捕获并中继消息给Cell</li>
<li>处理竞态条件</li>
<li>两级调度架构</li>
</ol>
<p>Cell V1的缺点：</p>
<ol>
<li>不支持安全组、主机聚合、可用区等特性</li>
<li>顶级调度功能很弱</li>
</ol>
<div class="blog_h3"><span class="graybg">Cell V2</span></div>
<p>当Nova API接收到针对实例的前请求后，实例的信息将从数据库读取，其中包含实例的宿主机名字。如果需要针对实例进行其它操作（通常都需要），那么宿主机名字将用来计算出消息队列的名字，RPC消息随后被写入消息队列，可以到达正确的计算节点。</p>
<p>引入Cell后，上述逻辑将变成：</p>
<ol>
<li>查找实例的三元组：宿主机名称、数据库连接信息、消息队列连接信息</li>
<li>连接到数据库，获取实例记录</li>
<li>连接到消息队列，并根据宿主机名称，选额消息队列，发送RPC消息</li>
</ol>
<p>引入Cell V2后，不存在没有Cell的部署架构。Cell V2的优势包括：</p>
<ol>
<li>数据库、消息队列的分片，成为Nova的一等特性</li>
<li>不需要在顶级复制Cell数据库，Nova API需要自己的数据库，存放例如实例索引的信息</li>
<li>在gloabl和local数据元素之间划分好了界限。Flavor、Keypair之类的全局性质对象，仅仅需要存储在顶级。这样计算节点更加无状态化，不会被全局数据的修改所干扰</li>
</ol>
<div class="blog_h2"><span class="graybg">Cell部署</span></div>
<p>所有Nova部署中，都需要一个名为API的数据库，一个<span style="background-color: #c0c0c0;">特殊的Cell数据库cell0</span>，1-N个cell的数据库。高层次的跟踪信息存放在API数据库中，哪些<span style="background-color: #c0c0c0;">从未调度成功的实例，存放在cell0</span>。所有成功调度的/运行的实例，都放在其它cell数据库中。</p>
<p>你需要将API数据库的信息配置在nova.conf：</p>
<pre class="crayon-plain-tag">[api_database]
connection = mysql+pymysql://nova:nova@os.gmem.cc/nova_api?charset=utf8</pre>
<div class="blog_h3"><span class="graybg">cell0</span></div>
<p>由于cell数据库数量不定，此外任何部署都至少有cell0和cell1（唯一的Cell使用），因此这些Cell的连接信息，是写在API数据库中（而不是静态编写在文件）的。</p>
<pre class="crayon-plain-tag"># 后续命令需要读写API数据库，因此首先执行api_db sync子命令来初始化schema
su -s /bin/sh -c "nova-manage api_db sync" nova

# 为cell0的数据库创建记录
nova-manage cell_v2 map_cell0 --database_connection \
  mysql+pymysql://nova:nova@os.gmem.cc/nova_cell0
# 如果不指定--database_connection，就像样例环境那样，则自动使用[database]/connection字段
# 中的连接串，但是在结尾添加_cell0后缀</pre>
<p>由于cell0中不会存在任何宿主机，不需要对它进行进一步配置。</p>
<div class="blog_h3"><span class="graybg">常规cell</span></div>
<p>现在，你需要创建第一个常规cell： </p>
<pre class="crayon-plain-tag"># 如果不指定 --database_connection、--transport-url，就像样例环境那样，
# 则自动使用 [database]/connection 和 [DEFAULT]/transport_url
su -s /bin/sh -c "nova-manage cell_v2 create_cell --name=cell1 --verbose" nova

nova-manage cell_v2 create_cell --verbose --name cell1 \
  --database_connection mysql+pymysql://nova:nova@os.gmem.cc/nova
  --transport-url rabbit://openstack:openstack@os.gmem.cc</pre>
<p>如果为cell1准备的数据库是空白的，你需要同步数据库schema：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "nova-manage db sync" nova</pre>
<p>现在，cell1中没有任何宿主机，因此nova-scheduler不会把实例调度到此cell中。</p>
<p>使用下面的命令，可以扫描数据库中计算节点的记录，并将其添加到刚刚创建的cell中。执行命令之前<span style="background-color: #c0c0c0;">，至少需要安装一个计算节点并<strong><span style="background-color: #99cc00;">添加到cell</span></strong></span>。</p>
<pre class="crayon-plain-tag">nova-manage cell_v2 discover_hosts</pre>
<p>上述命令会连接到所有cell数据库，扫描将自己注册到cell的宿主机，然后在API数据库中映射这些宿主机，这样nova-scheduler就可以进行调度了。</p>
<p>任何时候，你加入新的宿主机到cell，都需要调用此命令（或者启用自动发现）。</p>
<div class="blog_h3"><span class="graybg">添加宿主机到cell</span></div>
<p>我们知道计算节点通过消息队列和控制平面通信，也不会直接访问数据库。实际上，计算节点属于哪个cell，就看它通过哪个消息队列连接。你只需要配置计算节点的nova.conf：</p>
<pre class="crayon-plain-tag">[DEFAULT]
; 设置为某个cell的--transport-url 
transport_url = rabbit://openstack:openstack@os.gmem.cc</pre>
<p>然后再启动nova-compute服务，最后验证、确保节点在下面命令的输出中： </p>
<pre class="crayon-plain-tag">nova service-list --binary nova-compute</pre>
<p>就将计算节点<strong><span style="background-color: #99cc00;">添加到cell</span></strong>中了。 </p>
<div class="blog_h3"><span class="graybg">自动发现</span></div>
<p>要实现自动发现添加到cell的计算节点，并通过到API数据库，可以配置所有nova-scheduler节点的nova.conf：</p>
<pre class="crayon-plain-tag">[scheduler]
; 300秒执行一次发现
discover_hosts_in_cells_interval = 300 </pre>
<div class="blog_h2"><span class="graybg">多Cell陷阱</span></div>
<div class="blog_h3"><span class="graybg">跨Cell迁移实例</span></div>
<p>到目前为止（V版），还不支持跨Cell迁移实例。影响的操作包括resize/evacuate/migrate等。</p>
<div class="blog_h3"><span class="graybg">Quota计算</span></div>
<p>如果Cell不可达，那么针对租户的用量统计信息可能不准确。</p>
<p>从T版开始，可以配置在Placement服务+API数据库上进行Quota统计，这样宕掉/性能很差的Cell不会导致用量统计不准确。</p>
<div class="blog_h3"><span class="graybg">列出实例</span></div>
<p>多Cell环境下，列出实例的结果可能没有排序、分页可能不正确。</p>
<div class="blog_h3"><span class="graybg">元数据服务</span></div>
<p>从S版开始，元数据服务可以运行为两种模式之一：全局/PerCell。使用api.local_metadata_per_cell配置项</p>
<div class="blog_h2"><span class="graybg">Metadata</span></div>
<p>Nova将它启动的<span style="background-color: #c0c0c0;">实例的配置信息呈现为元数据</span>。cloud-init之类的助手会在虚拟机初始化时，利用元数据进行配置工作，例如设置虚拟机root密码。</p>
<p>通过元数据服务、或者config drive，元数据变的可（被实例使）用。你还可以通过nova api的user data特性来定制实例的元数据。</p>
<div class="blog_h3"><span class="graybg">元数据类别</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>用户提供</td>
<td>
<p>创建实例的用户可以通过多种方式，将元数据传递给实例：</p>
<ol>
<li>实例nova api的keypairs功能，可以设置宿主机的登陆密钥</li>
<li>使用nova api的<a href="https://docs.openstack.org/nova/latest/user/metadata.html#metadata-userdata">user data</a>特性，可以传递一小块opaque blob</li>
</ol>
</td>
</tr>
<tr>
<td>Nova提供</td>
<td>
<p>Nova自身会添加一些元数据，例如实例所在宿主机名称、实例所在AZ。</p>
<p>Nova提供OpenStack metadata API，以及EC2-compatible API。两者都是以日期来版本化的</p>
</td>
</tr>
<tr>
<td>部署者提供</td>
<td>
<p>对于创建实例的用户来说未知，由OpenStack的部署者提供。通过<a href="https://docs.openstack.org/nova/latest/user/metadata.html#metadata-vendordata">vendordata</a>特性可以实现。用于实现在实例创建后，自动加入AD这样的网络管理类功能</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">元数据代理</span></div>
<p>控制节点上的Neutron元数据代理服务，负责代替虚拟机Nova metadata API。并自动设置正确的Instance ID、Tenant ID等请求头。</p>
<div class="blog_h3"><span class="graybg">元数据服务</span></div>
<p>元数据服务<span style="background-color: #c0c0c0;">为实例提供了一种获取自身元数据</span>的REST API。实例可以通过169.254.169.254或者fe80::a9fe:a9fe访问此REST API，如此奇怪的地址是兼容Amazon EC2的考虑。在Openstack里面，这两个IP通过iptables映射到控制节点。</p>
<p>所有上述三类元数据，都可以通过此REST API访问：</p>
<pre class="crayon-plain-tag"># OpenStack metadata API
curl http://169.254.169.254/openstack
# EC2-compatible API
curl http://169.254.169.254
# 都会显示若干目录（版本信息）
# 2012-08-10
# 2013-04-04
# 2013-10-17
# 2015-10-15
# 2016-06-30
# 2016-10-06
# 2017-02-22
# 2018-08-27
# latest</pre>
<div class="blog_h3"><span class="graybg">config drive</span></div>
<p>Config drive是一种特殊的drive，在实例boot时添加。实例可以挂载此drive，读取其中的文件，从而获取（通常应该从metadata service获取的）信息。</p>
<p>下面的命令示意了如何使用在创建实例时使用config drive：</p>
<pre class="crayon-plain-tag">#                       使用config drive
openstack server create --config-drive true --image my-image-name \
#                               传递一个user data文件
    --flavor 1 --key-name mykey --user-data ./my-user-data.txt \
#   传递两个元数据键值对
    --property role=webservers --property essential=false MYINSTANCE</pre>
<p>如果客户机操作系统支持udev，则可以这样挂载config drive： </p>
<pre class="crayon-plain-tag">mkdir -p /mnt/config
mount /dev/disk/by-label/config-2 /mnt/config</pre>
<p>否则，这样识别config drive对应的块设备：</p>
<pre class="crayon-plain-tag">blkid -t LABEL="config-2" -odevice
# /dev/vdb</pre>
<p>config drive中的文件目录结构，和metadata service的URL结构对应：</p>
<pre class="crayon-plain-tag">cd /mnt/config
find . -maxdepth 2
# .
# EC2兼容的元数据放在这里
# ./ec2
# ./ec2/2009-04-04
# ./ec2/latest
# OpenStack元数据放在这里
# ./openstack
# ./openstack/2012-08-10
# ./openstack/2013-04-04
# ./openstack/2013-10-17
# ./openstack/2015-10-15
# ./openstack/2016-06-30
# ./openstack/2016-10-06
# ./openstack/2017-02-22
# ./openstack/latest</pre>
<div class="blog_h3"><span class="graybg">OpenStack元数据格式 </span></div>
<p>OpenStack元数据基于JSON格式分发： </p>
<ol>
<li>meta_data.json：提供Nova相关的信息</li>
<li> network_data.json：提供从Neutron获取的，网络相关信息</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">// curl http://169.254.169.254/openstack/2018-08-27/meta_data.json
{
   "random_seed": "yu5ZnkqF2CqnDZVAfZgarG...",
   "availability_zone": "nova",
   "keys": [
       {
         "data": "ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n",
         "type": "ssh",
         "name": "mykey"
       }
   ],
   "hostname": "test.novalocal",
   "launch_index": 0,
   "meta": {
      "priority": "low",
      "role": "webserver"
   },
   "devices": [
       {
         "type": "nic",
         "bus": "pci",
         "address": "0000:00:02.0",
         "mac": "00:11:22:33:44:55",
         "tags": ["trusted"]
       },
       {
         "type": "disk",
         "bus": "ide",
         "address": "0:0",
         "serial": "disk-vol-2352423",
         "path": "/dev/sda",
         "tags": ["baz"]
       }
   ],
   "project_id": "f7ac731cc11f40efbc03a9f9e1d1d21f",
   "public_keys": {
       "mykey": "ssh-rsa AAAAB3NzaC1y...== Generated by Nova\n"
   },
   "name": "test"
}


// curl http://169.254.169.254/openstack/2018-08-27/network_data.json
{
    "links": [
        {
            "ethernet_mac_address": "fa:16:3e:9c:bf:3d",
            "id": "tapcd9f6d46-4a",
            "mtu": null,
            "type": "bridge",
            "vif_id": "cd9f6d46-4a3a-43ab-a466-994af9db96fc"
        }
    ],
    "networks": [
        {
            "id": "network0",
            "link": "tapcd9f6d46-4a",
            "network_id": "99e88329-f20d-4741-9593-25bf07847b16",
            "type": "ipv4_dhcp"
        }
    ],
    "services": [
        {
            "address": "8.8.8.8",
            "type": "dns"
        }
    ]
}</pre>
<div class="blog_h3"><span class="graybg">EC2兼容元数据格式</span></div>
<p>兼容Amazon EC2 metadata service 2009-04-04版本。这意味着，为EC2设计的虚拟机镜像，可以和OpenStack一起工作。</p>
<p>EC2 API为每个元数据暴露了独立的URL：</p>
<pre class="crayon-plain-tag"># curl http://169.254.169.254/2009-04-04/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
instance-action
instance-id
instance-type
kernel-id
local-hostname
local-ipv4
placement/
public-hostname
public-ipv4
public-keys/
ramdisk-id
reservation-id
security-groups

# curl http://169.254.169.254/2009-04-04/meta-data/block-device-mapping/
ami

# curl http://169.254.169.254/2009-04-04/meta-data/placement/
availability-zone

# curl http://169.254.169.254/2009-04-04/meta-data/public-keys/
0=mykey

# curl http://169.254.169.254/2009-04-04/meta-data/public-keys/0/openssh-key
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+US\
LGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3B\
ISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated\
by Nova</pre>
<div class="blog_h3"><span class="graybg">user-data</span></div>
<p>就是一小段blob，OpenStack不知道它内容的意义。传递user data： </p>
<pre class="crayon-plain-tag">openstack server create --image ubuntu-cloudimage --flavor 1 \
    --user-data mydata.file TEST</pre>
<p>在客户机里面访问user data： </p>
<pre class="crayon-plain-tag"># OpenStack
http://169.254.169.254/openstack/{version}/user_data
# EC2
http://169.254.169.254/{version}/user-data</pre>
<p>支持cloud-init的镜像，可以利用user data，定制实例的初始化过程。</p>
<div class="blog_h3"><span class="graybg">vendor-data</span></div>
<p>这类数据可以通过metadata service或config drive读取。对于前者： </p>
<pre class="crayon-plain-tag">// curl http://169.254.169.254/openstack/2018-08-27/vendor_data2.json
{
    "testing": {
        "value1": 1
    }
}</pre>
<div class="blog_h2"><span class="graybg">cloud-init</span></div>
<p>cloud-init是一个在主要Linux发行版中都支持的包，用于在云环境（例如OpenStack）中初始化虚拟机实例。cloud-init在虚拟机第一次运行时执行。</p>
<div class="blog_h3"><span class="graybg">启动过程</span></div>
<p>cloud-init集成到系统启动的五个Stage，以发挥作用：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">Stage</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Generator</td>
<td>
<p>基于Systemd启动时，此阶段中有一个Generator用于确认cloud-init.target十分需要包含在Boot Goals中。默认情况下此Generator会启用cloud-init，以下情形之一，则不启用：</p>
<ol>
<li>文件<pre class="crayon-plain-tag">/etc/cloud/cloud-init.disabled</pre>存在</li>
<li>内核命令行参数<pre class="crayon-plain-tag">/proc/cmdline</pre>包含<pre class="crayon-plain-tag">cloud-init=disabled</pre></li>
</ol>
</td>
</tr>
<tr>
<td>Local</td>
<td>
<p>此Stage运行一个Systemd服务cloud-init-local.service。该服务在 / 挂载为读写后立即执行，block尽可能多的Systemd启动单元，必须block网络</p>
<p>该Stage的意图包括：</p>
<ol>
<li>定位到“local”数据源</li>
<li>应用网络配置到系统（包含Fallback）。网络配置的来源可能包括：
<ol>
<li>datasource，云环境通过元数据（Metadata）提供的网络配置信息</li>
<li>fallback，cloud-init的备用网络配置，等价于dhcp on eth0</li>
<li>none，如果/etc/cloud/cloud.cfg包含配置<pre class="crayon-plain-tag">network: {config: disabled}</pre></li>
</ol>
</li>
</ol>
</td>
</tr>
<tr>
<td>Network</td>
<td>
<p>此Stage运行一个Systemd服务cloud-init.service。该服务在Local Stage之后，所有网络启动后运行。包含的模块定义在/etc/cloud/cloud.cfg中</p>
<p>此Stage会处理所有user-data，所谓处理指：</p>
<ol>
<li>递归的读取<pre class="crayon-plain-tag">#include</pre>或<pre class="crayon-plain-tag">#include-once</pre></li>
<li>展开所有压缩内容</li>
<li>运行发现的所有<pre class="crayon-plain-tag">part-handler</pre></li>
</ol>
<p>此Stage会运行disk_setup、mounts模块，从而进行分区、格式化、配置挂载点（/etc/fstab）。这些模块不能运行的更早，韵味可能依赖于需要从网络才能得到的配置输入，例如用户提供的位于网络资源中的user-data</p>
</td>
</tr>
<tr>
<td>Config</td>
<td>
<p>此Stage运行一个Systemd服务cloud-config.service。包含的模块定义在/etc/cloud/cloud.cfg中</p>
<p>对于其他Boot Stage不产生影响的模块运行在此Stage</p>
</td>
</tr>
<tr>
<td>Final</td>
<td>
<p>此Stage运行一个Systemd服务cloud-final.service。它的运行时机相当于rc.local，也就是在启动的最后阶段。可以做的事情包括：</p>
<ol>
<li>安装软件包</li>
<li>执行用户定义的、通过user-data传递的脚本</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">判断首次启动</span></div>
<p>cloud-init需要判断实例是否第一次启动。在第一次启动时，会运行<pre class="crayon-plain-tag">per-instance</pre>的配置；在后续启动时，仅运行<pre class="crayon-plain-tag">per-boot</pre>的配置。</p>
<p>在运行的时候，cloud-init会将内部状态缓存起来，共后续boot读取。缓存存在，意味着两种情况之一：</p>
<ol>
<li>实例不是第一次启动</li>
<li>文件系统被挂载给一个新实例，该实例是第一次启动。将一个OpenStack卷上传为镜像，然后从此镜像启动新实例，就会导致这种情况</li>
</ol>
<p>默认情况下，cloud-init检查缓存中的实例ID，和运行时获取的实例ID，来判断是上述两种情况的哪一种。</p>
<p>使用命令<pre class="crayon-plain-tag">cloud-init clean</pre>可以清空缓存。</p>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>cloud-init的主配置文件位于<pre class="crayon-plain-tag">/etc/cloud/cloud.cfg</pre>，<pre class="crayon-plain-tag">/etc/cloud/cloud.cfg.d</pre>目录中的所有其它文件都会被合并。CentOS 8云镜像的配置文件如下：</p>
<pre class="crayon-plain-tag"># 需要添加到系统中的用户，特殊值default特指system_info.default_user
users:
 - default

# 禁止root登陆
disable_root: 1
# 禁用密码登陆
ssh_pwauth:   0

mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service', '0', '2']
resize_rootfs_tmp: /dev
ssh_deletekeys:   1
ssh_genkeytypes:  ~
syslog_fix_perms: ~
disable_vmware_customization: false

# 每个Stage都包含一系列的模块，可以启用/禁用
cloud_init_modules:
 - disk_setup
 - migrator
 - bootcmd
 - write-files
 - growpart
 - resizefs
 - set_hostname
 - update_hostname
 - update_etc_hosts
 - rsyslog
 - users-groups
 - ssh

cloud_config_modules:
 - mounts
 - locale
 - set-passwords
 - rh_subscription
 - yum-add-repo
 - package-update-upgrade-install
 - timezone
 - puppet
 - chef
 - salt-minion
 - mcollective
 - disable-ec2-metadata
 - runcmd

cloud_final_modules:
 - rightscale_userdata
 - scripts-per-once
 - scripts-per-boot
 - scripts-per-instance
 - scripts-user
 - ssh-authkey-fingerprints
 - keys-to-console
 - phone-home
 - final-message
 - power-state-change

system_info:
  # 默认用户信息
  default_user:
    name: centos
    lock_passwd: true
    gecos: Cloud User
    groups: [adm, systemd-journal]
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
    shell: /bin/bash
  distro: rhel
  paths:
    cloud_dir: /var/lib/cloud
    templates_dir: /etc/cloud/templates
  ssh_svcname: sshd

# vim:syntax=yaml</pre>
<div class="blog_h3"><span class="graybg">user-data</span></div>
<p>用户数据支持多种形式：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">格式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Gzip压缩内容</td>
<td>任何user-data都可以压缩为gzip格式，cloud-init会解压它，然后再处理</td>
</tr>
<tr>
<td>MIME Multi Part Archive</td>
<td>使用此格式，用户可以指定多种类型的数据，例如同时指定一个user-data script和cloud-config。支持的类型包括：<br />
<pre class="crayon-plain-tag"># cloud-init devel make-mime --list-types
cloud-boothook
cloud-config
cloud-config-archive
cloud-config-jsonp
jinja2
part-handler
upstart-job
x-include-once-url
x-include-url
x-shellscript</pre></p>
<p>使用make-mime子命令可以生成MIME multi-part文件：</p>
<p><pre class="crayon-plain-tag">cloud-init devel make-mime -a config.yaml:cloud-config \
                           -a script.sh:x-shellscript &gt; user-data</pre>
</td>
</tr>
<tr>
<td>User-Data Script </td>
<td>就是一段脚本，需要以<pre class="crayon-plain-tag">#!</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/x-shellscript</td>
</tr>
<tr>
<td>Include File</td>
<td>
<p>文件包含一系列URL，每个URL一行，这些URL会被读取，从中获取Gzip压缩内容、MIME Multi Part Archive，或者普通文本</p>
<p>需要以<pre class="crayon-plain-tag">#include</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type： text/x-include-url</p>
</td>
</tr>
<tr>
<td>Cloud Config Data</td>
<td>
<p>这是最简单的通过user-data来实现实例定制的方式。就是提供一个YAML配置文件</p>
<p>需要以<pre class="crayon-plain-tag">#cloud-config</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/cloud-config</p>
<p>定制用户、组的例子：</p>
<pre class="crayon-plain-tag">#cloud-config
# Add groups to the system
# The following example adds the ubuntu group with members 'root' and 'sys'
# and the empty group cloud-users.
groups:
  - ubuntu: [root,sys]
  - cloud-users

# Add users to the system. Users are added after groups are added.
# Note: Most of these configuration options will not be honored if the user
#       already exists. Following options are the exceptions and they are
#       applicable on already-existing users:
#       - 'plain_text_passwd', 'hashed_passwd', 'lock_passwd', 'sudo',
#         'ssh_authorized_keys', 'ssh_redirect_user'.
users:
  - default
  - name: foobar
    gecos: Foo B. Bar
    primary_group: foobar
    groups: users
    selinux_user: staff_u
    expiredate: '2012-09-01'
    ssh_import_id: foobar
    lock_passwd: false
    passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
  - name: barfoo
    gecos: Bar B. Foo
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    ssh_import_id: None
    lock_passwd: true
    ssh_authorized_keys:
      - &lt;ssh pub key 1&gt;
      - &lt;ssh pub key 2&gt;
  - name: cloudy
    gecos: Magic Cloud App Daemon User
    inactive: '5'
    system: true
  - name: fizzbuzz
    sudo: False
    ssh_authorized_keys:
      - &lt;ssh pub key 1&gt;
      - &lt;ssh pub key 2&gt;
  - snapuser: joe@joeuser.io
  - name: nosshlogins
    ssh_redirect_user: true

# Valid Values:
#   name: The user's login name
#   expiredate: Date on which the user's account will be disabled.
#   gecos: The user name's real name, i.e. "Bob B. Smith"
#   homedir: Optional. Set to the local path you want to use. Defaults to
#           /home/&lt;username&gt;
#   primary_group: define the primary group. Defaults to a new group created
#           named after the user.
#   groups:  Optional. Additional groups to add the user to. Defaults to none
#   selinux_user:  Optional. The SELinux user for the user's login, such as
#           "staff_u". When this is omitted the system will select the default
#           SELinux user.
#   lock_passwd: Defaults to true. Lock the password to disable password login
#   inactive: Number of days after password expires until account is disabled
#   passwd: The hash -- not the password itself -- of the password you want
#           to use for this user. You can generate a safe hash via:
#               mkpasswd --method=SHA-512 --rounds=4096
#           (the above command would create from stdin an SHA-512 password hash
#           with 4096 salt rounds)
#
#           Please note: while the use of a hashed password is better than
#               plain text, the use of this feature is not ideal. Also,
#               using a high number of salting rounds will help, but it should
#               not be relied upon.
#
#               To highlight this risk, running John the Ripper against the
#               example hash above, with a readily available wordlist, revealed
#               the true password in 12 seconds on a i7-2620QM.
#
#               In other words, this feature is a potential security risk and is
#               provided for your convenience only. If you do not fully trust the
#               medium over which your cloud-config will be transmitted, then you
#               should use SSH authentication only.
#
#               You have thus been warned.
#   no_create_home: When set to true, do not create home directory.
#   no_user_group: When set to true, do not create a group named after the user.
#   no_log_init: When set to true, do not initialize lastlog and faillog database.
#   ssh_import_id: Optional. Import SSH ids
#   ssh_authorized_keys: Optional. [list] Add keys to user's authorized keys file
#   ssh_redirect_user: Optional. [bool] Set true to block ssh logins for cloud
#       ssh public keys and emit a message redirecting logins to
#       use &lt;default_username&gt; instead. This option only disables cloud
#       provided public-keys. An error will be raised if ssh_authorized_keys
#       or ssh_import_id is provided for the same user.
#
#       ssh_authorized_keys.
#   sudo: Defaults to none. Accepts a sudo rule string, a list of sudo rule
#         strings or False to explicitly deny sudo usage. Examples:
#
#         Allow a user unrestricted sudo access.
#             sudo:  ALL=(ALL) NOPASSWD:ALL
#
#         Adding multiple sudo rule strings.
#             sudo:
#               - ALL=(ALL) NOPASSWD:/bin/mysql
#               - ALL=(ALL) ALL
#
#         Prevent sudo access for a user.
#             sudo: False
#
#         Note: Please double check your syntax and make sure it is valid.
#               cloud-init does not parse/check the syntax of the sudo
#               directive.
#   system: Create the user as a system user. This means no home directory.
#   snapuser: Create a Snappy (Ubuntu-Core) user via the snap create-user
#             command available on Ubuntu systems.  If the user has an account
#             on the Ubuntu SSO, specifying the email will allow snap to
#             request a username and any public ssh keys and will import
#             these into the system with username specifed by SSO account.
#             If 'username' is not set in SSO, then username will be the
#             shortname before the email domain.
#

# Default user creation:
#
# Unless you define users, you will get a 'ubuntu' user on ubuntu systems with the
# legacy permission (no password sudo, locked user, etc). If however, you want
# to have the 'ubuntu' user in addition to other users, you need to instruct
# cloud-init that you also want the default user. To do this use the following
# syntax:
#   users:
#     - default
#     - bob
#     - ....
#  foobar: ...
#
# users[0] (the first user in users) overrides the user directive.
#
# The 'default' user above references the distro's config:
# system_info:
#   default_user:
#     name: Ubuntu
#     plain_text_passwd: 'ubuntu'
#     home: /home/ubuntu
#     shell: /bin/bash
#     lock_passwd: True
#     gecos: Ubuntu
#     groups: [adm, audio, cdrom, dialout, floppy, video, plugdev, dip, netdev]</pre>
<p>写入到文件系统的例子：</p>
<pre class="crayon-plain-tag">#cloud-config
# vim: syntax=yaml
#
# This is the configuration syntax that the write_files module
# will know how to understand. encoding can be given b64 or gzip or (gz+b64).
# The content will be decoded accordingly and then written to the path that is
# provided. 
#
# Note: Content strings here are truncated for example purposes.
write_files:
- encoding: b64
  content: CiMgVGhpcyBmaWxlIGNvbnRyb2xzIHRoZSBzdGF0ZSBvZiBTRUxpbnV4...
  owner: root:root
  path: /etc/sysconfig/selinux
  permissions: '0644'
- content: |
    # My new /etc/sysconfig/samba file

    SMBDOPTIONS="-D"
  path: /etc/sysconfig/samba
- content: !!binary |
    f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAAAAAAAAEAAOAAI
    AEAAHgAdAAYAAAAFAAAAQAAAAAAAAABAAEAAAAAAAEAAQAAAAAAAwAEAAAAAAADAAQAAAAAAAAgA
    AAAAAAAAAwAAAAQAAAAAAgAAAAAAAAACQAAAAAAAAAJAAAAAAAAcAAAAAAAAABwAAAAAAAAAAQAA
    ....
  path: /bin/arch
  permissions: '0555'
- encoding: gzip
  content: !!binary |
    H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA=
  path: /usr/bin/hello
  permissions: '0755'</pre>
<p>添加YUM源：</p>
<pre class="crayon-plain-tag">#cloud-config
# vim: syntax=yaml
#
# Add yum repository configuration to the system
#
# The following example adds the file /etc/yum.repos.d/epel_testing.repo
# which can then subsequently be used by yum for later operations.
yum_repos:
  # The name of the repository
  epel-testing:
    # Any repository configuration options
    # See: man yum.conf
    #
    # This one is required!
    baseurl: http://download.fedoraproject.org/pub/epel/testing/5/$basearch
    enabled: false
    failovermethod: priority
    gpgcheck: true
    gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL
    name: Extra Packages for Enterprise Linux 5 - Testing</pre>
<p>添加APT源：</p>
<pre class="crayon-plain-tag">#cloud-config

# Add primary apt repositories
#
# To add 3rd party repositories, see cloud-config-apt.txt or the
# Additional apt configuration and repositories section.
#
#
# Default: auto select based on cloud metadata
#  in ec2, the default is &lt;region&gt;.archive.ubuntu.com
# apt:
#   primary:
#     - arches [default]
#       uri:
#     use the provided mirror
#       search:
#     search the list for the first mirror.
#     this is currently very limited, only verifying that
#     the mirror is dns resolvable or an IP address
#
# if neither mirror is set (the default)
# then use the mirror provided by the DataSource found.
# In EC2, that means using &lt;region&gt;.ec2.archive.ubuntu.com
#
# if no mirror is provided by the DataSource, but 'search_dns' is
# true, then search for dns names '&lt;distro&gt;-mirror' in each of
# - fqdn of this host per cloud metadata
# - localdomain
# - no domain (which would search domains listed in /etc/resolv.conf)
# If there is a dns entry for &lt;distro&gt;-mirror, then it is assumed that there
# is a distro mirror at http://&lt;distro&gt;-mirror.&lt;domain&gt;/&lt;distro&gt;
#
# That gives the cloud provider the opportunity to set mirrors of a distro
# up and expose them only by creating dns entries.
#
# if none of that is found, then the default distro mirror is used
apt:
  primary:
    - arches: [default]
      uri: http://us.archive.ubuntu.com/ubuntu/
# or
apt:
  primary:
    - arches: [default]
      search:
        - http://local-mirror.mydomain
        - http://archive.ubuntu.com
# or
apt:
  primary:
    - arches: [default]
      search_dns: True</pre>
<p>在首次启动时更新APT数据库：</p>
<pre class="crayon-plain-tag">#cloud-config
# Update apt database on first boot (run 'apt-get update').
# Note, if packages are given, or package_upgrade is true, then
# update will be done independent of this setting.
#
# Default: false
# Aliases: apt_update
package_update: true</pre>
<p>执行YUM/APT Upgrade：</p>
<pre class="crayon-plain-tag">#cloud-config

# Upgrade the instance on first boot
# (ie run apt-get upgrade)
#
# Default: false
# Aliases: apt_upgrade
package_upgrade: true</pre>
<p>配置实例的受信任CA：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# This is an example file to configure an instance's trusted CA certificates
# system-wide for SSL/TLS trust establishment when the instance boots for the
# first time.
#
# Make sure that this file is valid yaml before starting instances.
# It should be passed as user-data when starting the instance.

ca-certs:
  # If present and set to True, the 'remove-defaults' parameter will remove
  # all the default trusted CA certificates that are normally shipped with
  # Ubuntu.
  # This is mainly for paranoid admins - most users will not need this
  # functionality.
  remove-defaults: true

  # If present, the 'trusted' parameter should contain a certificate (or list
  # of certificates) to add to the system as trusted CA certificates.
  # Pay close attention to the YAML multiline list syntax.  The example shown
  # here is for a list of multiline certificates.
  trusted: 
  - |
   -----BEGIN CERTIFICATE-----
   YOUR-ORGS-TRUSTED-CA-CERT-HERE
   -----END CERTIFICATE-----
  - |
   -----BEGIN CERTIFICATE-----
   YOUR-ORGS-TRUSTED-CA-CERT-HERE
   -----END CERTIFICATE-----</pre>
<p>配置DNS：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# This is an example file to automatically configure resolv.conf when the
# instance boots for the first time.
#
# Ensure that your yaml is valid and pass this as user-data when starting
# the instance. Also be sure that your cloud.cfg file includes this
# configuration module in the appropriate section.
#
manage_resolv_conf: true

resolv_conf:
  nameservers: ['8.8.4.4', '8.8.8.8']
  searchdomains:
    - foo.example.com
    - bar.example.com
  domain: example.com
  options:
    rotate: true
    timeout: 1</pre>
<p>在第一次启动时执行命令：</p>
<pre class="crayon-plain-tag">#cloud-config

# boot commands
# default: none
# this is very similar to runcmd, but commands run very early
# in the boot process, only slightly after a 'boothook' would run.
# bootcmd should really only be used for things that could not be
# done later in the boot process.  bootcmd is very much like
# boothook, but possibly with more friendly.
# - bootcmd will run on every boot
# - the INSTANCE_ID variable will be set to the current instance id.
# - you can use 'cloud-init-per' command to help only run once
bootcmd:
  - echo 192.168.1.130 us.archive.ubuntu.com &gt;&gt; /etc/hosts
  - [ cloud-init-per, once, mymkfs, mkfs, /dev/vdb ]</pre><br />
<pre class="crayon-plain-tag">#cloud-config

# run commands
# default: none
# runcmd contains a list of either lists or a string
# each item will be executed in order at rc.local like level with
# output to the console
# - runcmd only runs during the first boot
# - if the item is a list, the items will be properly executed as if
#   passed to execve(3) (with the first arg as the command).
# - if the item is a string, it will be simply written to the file and
#   will be interpreted by 'sh'
#
# Note, that the list has to be proper yaml, so you have to quote
# any characters yaml would eat (':' can be problematic)
runcmd:
 - [ ls, -l, / ]
 - [ sh, -xc, "echo $(date) ': hello world!'" ]
 - [ sh, -c, echo "=========hello world'=========" ]
 - ls -l /root
 # Note: Don't write files to /tmp from cloud-init use /run/somedir instead.
 # Early boot environments can race systemd-tmpfiles-clean LP: #1707222.
 - mkdir /run/mydir
 - [ wget, "http://slashdot.org", -O, /run/mydir/index.html ]</pre>
<p>安装软件包：</p>
<pre class="crayon-plain-tag">#cloud-config

# Install additional packages on first boot
#
# Default: none
#
# if packages are specified, this apt_update will be set to true
#
# packages may be supplied as a single package name or as a list
# with the format [&lt;package&gt;, &lt;version&gt;] wherein the specifc
# package version will be installed.
packages:
 - pwgen
 - pastebinit
 - [libpython2.7, 2.7.3-0ubuntu3.1]</pre>
<p>调整挂载点：</p>
<pre class="crayon-plain-tag">#cloud-config

# set up mount points
# 'mounts' contains a list of lists
#  the inner list are entries for an /etc/fstab line
#  ie : [ fs_spec, fs_file, fs_vfstype, fs_mntops, fs-freq, fs_passno ]
#
# default:
# mounts:
#  - [ ephemeral0, /mnt ]
#  - [ swap, none, swap, sw, 0, 0 ]
#
# in order to remove a previously listed mount (ie, one from defaults)
# list only the fs_spec.  For example, to override the default, of
# mounting swap:
# - [ swap ]
# or
# - [ swap, null ]
#
# - if a device does not exist at the time, an entry will still be
#   written to /etc/fstab.
# - '/dev' can be ommitted for device names that begin with: xvd, sd, hd, vd
# - if an entry does not have all 6 fields, they will be filled in
#   with values from 'mount_default_fields' below.
#
# Note, that you should set 'nofail' (see man fstab) for volumes that may not
# be attached at instance boot (or reboot).
#
mounts:
 - [ ephemeral0, /mnt, auto, "defaults,noexec" ]
 - [ sdc, /opt/data ]
 - [ xvdh, /opt/data, "auto", "defaults,nofail", "0", "0" ]
 - [ dd, /dev/zero ]

# mount_default_fields
# These values are used to fill in any entries in 'mounts' that are not
# complete.  This must be an array, and must have 6 fields.
mount_default_fields: [ None, None, "auto", "defaults,nofail", "0", "2" ]


# swap can also be set up by the 'mounts' module
# default is to not create any swap files, because 'size' is set to 0
swap:
  filename: /swap.img
  size: "auto" # or size in bytes
  maxsize: size in bytes</pre>
<p>cloud-init完毕后重启/关机：</p>
<pre class="crayon-plain-tag">#cloud-config

## poweroff or reboot system after finished
# default: none
#
# power_state can be used to make the system shutdown, reboot or
# halt after boot is finished.  This same thing can be acheived by
# user-data scripts or by runcmd by simply invoking 'shutdown'.
# 
# Doing it this way ensures that cloud-init is entirely finished with
# modules that would be executed, and avoids any error/log messages
# that may go to the console as a result of system services like
# syslog being taken down while cloud-init is running.
#
# If you delay '+5' (5 minutes) and have a timeout of
# 120 (2 minutes), then the max time until shutdown will be 7 minutes.
# cloud-init will invoke 'shutdown +5' after the process finishes, or
# when 'timeout' seconds have elapsed.
#
# delay: form accepted by shutdown.  default is 'now'. other format
#        accepted is '+m' (m in minutes)
# mode: required. must be one of 'poweroff', 'halt', 'reboot'
# message: provided as the message argument to 'shutdown'. default is none.
# timeout: the amount of time to give the cloud-init process to finish
#          before executing shutdown.
# condition: apply state change only if condition is met.
#            May be boolean True (always met), or False (never met),
#            or a command string or list to be executed.
#            command's exit code indicates:
#               0: condition met
#               1: condition not met
#            other exit codes will result in 'not met', but are reserved
#            for future use.
#
power_state:
  delay: "+30"
  mode: poweroff
  message: Bye Bye
  timeout: 30
  condition: True</pre>
<p>配置实例的SSH Keys：</p>
<pre class="crayon-plain-tag">#cloud-config

# add each entry to ~/.ssh/authorized_keys for the configured user or the
# first user defined in the user definition directive.
ssh_authorized_keys:
  - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUUk8EEAnnkhXlukKoUPND/RRClWz2s5TCzIkd3Ou5+Cyz71X0XmazM3l5WgeErvtIwQMyT1KjNoMhoJMrJnWqQPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host
  - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies

# Send pre-generated SSH private keys to the server
# If these are present, they will be written to /etc/ssh and
# new random keys will not be generated
#  in addition to 'rsa' and 'dsa' as shown below, 'ecdsa' is also supported
ssh_keys:
  rsa_private: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIBxwIBAAJhAKD0YSHy73nUgysO13XsJmd4fHiFyQ+00R7VVu2iV9Qcon2LZS/x
    1cydPZ4pQpfjEha6WxZ6o8ci/Ea/w0n+0HGPwaxlEG2Z9inNtj3pgFrYcRztfECb
    1j6HCibZbAzYtwIBIwJgO8h72WjcmvcpZ8OvHSvTwAguO2TkR6mPgHsgSaKy6GJo
    PUJnaZRWuba/HX0KGyhz19nPzLpzG5f0fYahlMJAyc13FV7K6kMBPXTRR6FxgHEg
    L0MPC7cdqAwOVNcPY6A7AjEA1bNaIjOzFN2sfZX0j7OMhQuc4zP7r80zaGc5oy6W
    p58hRAncFKEvnEq2CeL3vtuZAjEAwNBHpbNsBYTRPCHM7rZuG/iBtwp8Rxhc9I5w
    ixvzMgi+HpGLWzUIBS+P/XhekIjPAjA285rVmEP+DR255Ls65QbgYhJmTzIXQ2T9
    luLvcmFBC6l35Uc4gTgg4ALsmXLn71MCMGMpSWspEvuGInayTCL+vEjmNBT+FAdO
    W7D4zCpI43jRS9U06JVOeSc9CDk2lwiA3wIwCTB/6uc8Cq85D9YqpM10FuHjKpnP
    REPPOyrAspdeOAV+6VKRavstea7+2DZmSUgE
    -----END RSA PRIVATE KEY-----

  rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEAoPRhIfLvedSDKw7XdewmZ3h8eIXJD7TRHtVW7aJX1ByifYtlL/HVzJ09nilCl+MSFrpbFnqjxyL8Rr/DSf7QcY/BrGUQbZn2Kc22PemAWthxHO18QJvWPocKJtlsDNi3 smoser@localhost

  dsa_private: |
    -----BEGIN DSA PRIVATE KEY-----
    MIIBuwIBAAKBgQDP2HLu7pTExL89USyM0264RCyWX/CMLmukxX0Jdbm29ax8FBJT
    pLrO8TIXVY5rPAJm1dTHnpuyJhOvU9G7M8tPUABtzSJh4GVSHlwaCfycwcpLv9TX
    DgWIpSj+6EiHCyaRlB1/CBp9RiaB+10QcFbm+lapuET+/Au6vSDp9IRtlQIVAIMR
    8KucvUYbOEI+yv+5LW9u3z/BAoGBAI0q6JP+JvJmwZFaeCMMVxXUbqiSko/P1lsa
    LNNBHZ5/8MOUIm8rB2FC6ziidfueJpqTMqeQmSAlEBCwnwreUnGfRrKoJpyPNENY
    d15MG6N5J+z81sEcHFeprryZ+D3Ge9VjPq3Tf3NhKKwCDQ0240aPezbnjPeFm4mH
    bYxxcZ9GAoGAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI3
    8UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC
    /QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQCFEIsKKWv
    99iziAH0KBMVbxy03Trz
    -----END DSA PRIVATE KEY-----

  dsa_public: ssh-dss AAAAB3NzaC1kc3MAAACBAM/Ycu7ulMTEvz1RLIzTbrhELJZf8Iwua6TFfQl1ubb1rHwUElOkus7xMhdVjms8AmbV1Meem7ImE69T0bszy09QAG3NImHgZVIeXBoJ/JzByku/1NcOBYilKP7oSIcLJpGUHX8IGn1GJoH7XRBwVub6Vqm4RP78C7q9IOn0hG2VAAAAFQCDEfCrnL1GGzhCPsr/uS1vbt8/wQAAAIEAjSrok/4m8mbBkVp4IwxXFdRuqJKSj8/WWxos00Ednn/ww5QibysHYULrOKJ1+54mmpMyp5CZICUQELCfCt5ScZ9GsqgmnI80Q1h3Xkwbo3kn7PzWwRwcV6muvJn4PcZ71WM+rdN/c2EorAINDTbjRo97NueM94WbiYdtjHFxn0YAAACAXmLIFSQgiAPu459rCKxT46tHJtM0QfnNiEnQLbFluefZ/yiI4DI38UzTCOXLhUA7ybmZha+D/csj15Y9/BNFuO7unzVhikCQV9DTeXX46pG4s1o23JKC/QaYWNMZ7kTRv+wWow9MhGiVdML4ZN4XnifuO5krqAybngIy66PMEoQ= smoser@localhost

# By default, the fingerprints of the authorized keys for the users
# cloud-init adds are printed to the console. Setting
# no_ssh_fingerprints to true suppresses this output.
no_ssh_fingerprints: false

# By default, (most) ssh host keys are printed to the console. Setting
# emit_keys_to_console to false suppresses this output.
ssh:
  emit_keys_to_console: false</pre>
<p>初始化磁盘：</p>
<pre class="crayon-plain-tag">#cloud-config
# Cloud-init supports the creation of simple partition tables and file systems
# on devices.

# Default disk definitions for AWS
# --------------------------------
# (Not implemented yet, but provided for future documentation)

disk_setup:
  ephmeral0:
    table_type: 'mbr'
    layout: True
    overwrite: False

fs_setup:
  - label: None,
    filesystem: ext3
    device: ephemeral0
    partition: auto

# Default disk definitions for Microsoft Azure
# ------------------------------------------

device_aliases: {'ephemeral0': '/dev/sdb'}
disk_setup:
  ephemeral0:
    table_type: mbr
    layout: True
    overwrite: False

fs_setup:
  - label: ephemeral0
    filesystem: ext4
    device: ephemeral0.1
    replace_fs: ntfs


# Data disks definitions for Microsoft Azure
# ------------------------------------------

disk_setup:
  /dev/disk/azure/scsi1/lun0:
    table_type: gpt
    layout: True
    overwrite: True

fs_setup:
  - device: /dev/disk/azure/scsi1/lun0
    partition: 1
    filesystem: ext4


# Default disk definitions for SmartOS
# ------------------------------------

device_aliases: {'ephemeral0': '/dev/vdb'}
disk_setup:
  ephemeral0:
    table_type: mbr
    layout: False
    overwrite: False

fs_setup:
  - label: ephemeral0
    filesystem: ext4
    device: ephemeral0.0

# Caveat for SmartOS: if ephemeral disk is not defined, then the disk will
#    not be automatically added to the mounts.


# The default definition is used to make sure that the ephemeral storage is
# setup properly.

# "disk_setup": disk partitioning
# --------------------------------

# The disk_setup directive instructs Cloud-init to partition a disk. The format is:

disk_setup:
  ephmeral0:
    table_type: 'mbr'
    layout: 'auto'
  /dev/xvdh:
    table_type: 'mbr'
    layout:
      - 33
      - [33, 82]
      - 33
    overwrite: True

# The format is a list of dicts of dicts. The first value is the name of the
# device and the subsequent values define how to create and layout the
# partition.
# The general format is:
#   disk_setup:
#     &lt;DEVICE&gt;:
#       table_type: 'mbr'
#       layout: &lt;LAYOUT|BOOL&gt;
#       overwrite: &lt;BOOL&gt;
#
# Where:
#   &lt;DEVICE&gt;: The name of the device. 'ephemeralX' and 'swap' are special
#               values which are specific to the cloud. For these devices
#               Cloud-init will look up what the real devices is and then
#               use it.
#
#               For other devices, the kernel device name is used. At this
#               time only simply kernel devices are supported, meaning
#               that device mapper and other targets may not work.
#
#               Note: At this time, there is no handling or setup of
#               device mapper targets.
#
#   table_type=&lt;TYPE&gt;: Currently the following are supported:
#                   'mbr': default and setups a MS-DOS partition table
#                   'gpt': setups a GPT partition table
#
#               Note: At this time only 'mbr' and 'gpt' partition tables
#                   are allowed. It is anticipated in the future that
#                   we'll also have "RAID" to create a mdadm RAID.
#
#   layout={...}: The device layout. This is a list of values, with the
#               percentage of disk that partition will take.
#               Valid options are:
#                   [&lt;SIZE&gt;, [&lt;SIZE&gt;, &lt;PART_TYPE]]
#
#               Where &lt;SIZE&gt; is the _percentage_ of the disk to use, while
#               &lt;PART_TYPE&gt; is the numerical value of the partition type.
#
#               The following setups two partitions, with the first
#               partition having a swap label, taking 1/3 of the disk space
#               and the remainder being used as the second partition.
#                 /dev/xvdh':
#                   table_type: 'mbr'
#                   layout:
#                     - [33,82]
#                     - 66
#                   overwrite: True
#
#               When layout is "true" it means single partition the entire
#               device.
#
#               When layout is "false" it means don't partition or ignore
#               existing partitioning.
#
#               If layout is set to "true" and overwrite is set to "false",
#               it will skip partitioning the device without a failure.
#
#   overwrite=&lt;BOOL&gt;: This describes whether to ride with saftey's on and
#               everything holstered.
#
#               'false' is the default, which means that:
#                   1. The device will be checked for a partition table
#                   2. The device will be checked for a file system
#                   3. If either a partition of file system is found, then
#                       the operation will be _skipped_.
#
#               'true' is cowboy mode. There are no checks and things are
#                   done blindly. USE with caution, you can do things you
#                   really, really don't want to do.
#
#
# fs_setup: Setup the file system
# -------------------------------
#
# fs_setup describes the how the file systems are supposed to look.

fs_setup:
  - label: ephemeral0
    filesystem: 'ext3'
    device: 'ephemeral0'
    partition: 'auto'
  - label: mylabl2
    filesystem: 'ext4'
    device: '/dev/xvda1'
  - cmd: mkfs -t %(filesystem)s -L %(label)s %(device)s
    label: mylabl3
    filesystem: 'btrfs'
    device: '/dev/xvdh'

# The general format is:
#   fs_setup:
#     - label: &lt;LABEL&gt;
#       filesystem: &lt;FS_TYPE&gt;
#       device: &lt;DEVICE&gt;
#       partition: &lt;PART_VALUE&gt;
#       overwrite: &lt;OVERWRITE&gt;
#       replace_fs: &lt;FS_TYPE&gt;
#
# Where:
#   &lt;LABEL&gt;: The file system label to be used. If set to None, no label is
#     used.
#
#   &lt;FS_TYPE&gt;: The file system type. It is assumed that the there
#     will be a "mkfs.&lt;FS_TYPE&gt;" that behaves likes "mkfs". On a standard
#     Ubuntu Cloud Image, this means that you have the option of ext{2,3,4},
#     and vfat by default.
#
#   &lt;DEVICE&gt;: The device name. Special names of 'ephemeralX' or 'swap'
#     are allowed and the actual device is acquired from the cloud datasource.
#     When using 'ephemeralX' (i.e. ephemeral0), make sure to leave the
#     label as 'ephemeralX' otherwise there may be issues with the mounting
#     of the ephemeral storage layer.
#
#     If you define the device as 'ephemeralX.Y' then Y will be interpetted
#     as a partition value. However, ephermalX.0 is the _same_ as ephemeralX.
#
#   &lt;PART_VALUE&gt;:
#     Partition definitions are overwriten if you use the '&lt;DEVICE&gt;.Y' notation.
#
#     The valid options are:
#     "auto|any": tell cloud-init not to care whether there is a partition
#       or not. Auto will use the first partition that does not contain a
#       file system already. In the absence of a partition table, it will
#       put it directly on the disk.
#
#       "auto": If a file system that matches the specification in terms of
#       label, type and device, then cloud-init will skip the creation of
#       the file system.
#
#       "any": If a file system that matches the file system type and device,
#       then cloud-init will skip the creation of the file system.
#
#       Devices are selected based on first-detected, starting with partitions
#       and then the raw disk. Consider the following:
#           NAME     FSTYPE LABEL
#           xvdb
#           |-xvdb1  ext4
#           |-xvdb2
#           |-xvdb3  btrfs  test
#           \-xvdb4  ext4   test
#
#         If you ask for 'auto', label of 'test, and file system of 'ext4'
#         then cloud-init will select the 2nd partition, even though there
#         is a partition match at the 4th partition.
#
#         If you ask for 'any' and a label of 'test', then cloud-init will
#         select the 1st partition.
#
#         If you ask for 'auto' and don't define label, then cloud-init will
#         select the 1st partition.
#
#         In general, if you have a specific partition configuration in mind,
#         you should define either the device or the partition number. 'auto'
#         and 'any' are specifically intended for formating ephemeral storage or
#         for simple schemes.
#
#       "none": Put the file system directly on the device.
#
#       &lt;NUM&gt;: where NUM is the actual partition number.
#
#   &lt;OVERWRITE&gt;: Defines whether or not to overwrite any existing
#     filesystem.
#
#     "true": Indiscriminately destroy any pre-existing file system. Use at
#         your own peril.
#
#     "false": If an existing file system exists, skip the creation.
#
#   &lt;REPLACE_FS&gt;: This is a special directive, used for Microsoft Azure that
#     instructs cloud-init to replace a file system of &lt;FS_TYPE&gt;. NOTE:
#     unless you define a label, this requires the use of the 'any' partition
#     directive.
#
# Behavior Caveat: The default behavior is to _check_ if the file system exists.
#   If a file system matches the specification, then the operation is a no-op.</pre>
<p>自动增长分区大小：</p>
<pre class="crayon-plain-tag">#cloud-config
#
# growpart entry is a dict, if it is not present at all
# in config, then the default is used ({'mode': 'auto', 'devices': ['/']})
#
#  mode:
#    values:
#     * auto: use any option possible (any available)
#             if none are available, do not warn, but debug.
#     * growpart: use growpart to grow partitions
#             if growpart is not available, this is an error.
#     * off, false
#
# devices:
#   a list of things to resize.
#   items can be filesystem paths or devices (in /dev)
#   examples:
#     devices: [/, /dev/vdb1]
#
# ignore_growroot_disabled:
#   a boolean, default is false.
#   if the file /etc/growroot-disabled exists, then cloud-init will not grow
#   the root partition.  This is to allow a single file to disable both
#   cloud-initramfs-growroot and cloud-init's growroot support.
#
#   true indicates that /etc/growroot-disabled should be ignored
#
growpart:
  mode: auto
  devices: ['/']
  ignore_growroot_disabled: false</pre>
</td>
</tr>
<tr>
<td>Upstart Job</td>
<td>
<p>内容存放为/etc/init/下的一个文件，从而被Upstart调用
<p>需要以<pre class="crayon-plain-tag">#upstart-job</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/upstart-job</p>
</td>
</tr>
<tr>
<td>Cloud Boothook</td>
<td>
<p>存放在/var/lib/cloud并立即执行，没有任何机制保证钩子仅仅执行一次</p>
<p>需要以<pre class="crayon-plain-tag">#cloud-boothook</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/cloud-boothook</p>
</td>
</tr>
<tr>
<td>Part Handler</td>
<td>
<p>一段代码，用于处理新的MIME类型，或者覆盖既有的MIME类型的处理器。以Python编写，包含函数：</p>
<ol>
<li>list_types：本Handler支持的MIME类型列表</li>
<li>handle_part：执行处理</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">#part-handler

def list_types():
    # return a list of mime-types that are handled by this module
    return(["text/plain", "text/go-cubs-go"])

def handle_part(data,ctype,filename,payload):
    # data: the cloudinit object
    # ctype: '__begin__', '__end__', or the specific mime-type of the part
    # filename: the filename for the part, or dynamically generated part if
    #           no filename is given attribute is present
    # payload: the content of the part (empty for begin or end)
    if ctype == "__begin__":
       print "my handler is beginning"
       return
    if ctype == "__end__":
       print "my handler is ending"
       return

    print "==== received ctype=%s filename=%s ====" % (ctype,filename)
    print payload
    print "==== end ctype=%s filename=%s" % (ctype, filename)</pre>
<p>需要以<pre class="crayon-plain-tag">#part-handler</pre>开头，包含在MIME Multi Part Archive中时使用Content-Type：text/part-handler</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">instance-data</span></div>
<p>所谓实例数据，是指cloud-init用来配置实例的所有数据的集合。数据来源包括：</p>
<ol>
<li>云环境的元数据服务（metadata）</li>
<li>用户定制的，提供给实例的config-drive</li>
<li>镜像中的cloud-config seed files</li>
<li>提供文件/元数据服务提供的vendor-data</li>
<li>创建实例时提供的user-data</li>
</ol>
<p>也就是说，上节的user-data是instance-data的一部分。</p>
<div class="blog_h2"><span class="graybg">Flavor</span></div>
<p>在OpenStack中，Flavor定义了实例的规格 —— 计算、内存、存储资源的硬件规格。Flavor也可以用来定义实例可以在哪些宿主机上启动。</p>
<div class="blog_h3"><span class="graybg">规格参数</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">命令行标记</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--vcpus</td>
<td>虚拟CPU数量，必须</td>
</tr>
<tr>
<td>--ram</td>
<td>内存/MB，必须</td>
</tr>
<tr>
<td>--disk</td>
<td>
<p>根磁盘大小/GB，必须</p>
<p>从镜像创建虚拟机时，根磁盘是一个临时（ephemeral）磁盘，虚拟机的镜像会拷贝到该磁盘上。如果从持久化的卷来启动虚拟机，则不会使用这种磁盘</p>
<p>设置为0，其含义是，使用虚拟机镜像的大小作为临时磁盘大小。但是这种情况下会导致filter scheduler不能基于镜像尺寸来选择适当的宿主机。因此，你应该仅仅在从卷启动虚拟机、或者出于测试目的的时候，才将此参数设置为0</p>
<p>要强制必须从0根磁盘大小的Flavor来创建基于卷的虚拟机，设置策略规则：os_compute_api:servers:create:zero_disk_flavor</p>
</td>
</tr>
<tr>
<td>--ephemeral</td>
<td>
<p>额外的临时磁盘/GB，默认0</p>
<p>该参数为虚拟机提供额外的临时分去，虚拟机销毁后，此磁盘消失</p>
</td>
</tr>
<tr>
<td>--swap</td>
<td>交换分区/MB，默认0</td>
</tr>
<tr>
<td>--public<br />--private</td>
<td>
<p>公共标记，默认True</p>
<p>指示该Flavor是不是可以被除了Flavor所在项目（租户）的其它租户使用</p>
</td>
</tr>
<tr>
<td>--property</td>
<td>
<p>额外规格说明，可以指定多次，键值对。高级配置下，用作scheduler的提示信息</p>
<p>&nbsp;</p>
<p>以quota:开头的属性，用于对Flavor进行配额限制，示例：</p>
<pre class="crayon-plain-tag"># 确保虚拟机只能消耗50%的物理CPU能力
openstack flavor set FLAVOR-NAME \
    --property quota:cpu_quota=10000 \
    --property quota:cpu_period=20000

# 限制每秒最多写入10MB到磁盘
openstack flavor set FLAVOR-NAME \
    --property quota:disk_write_bytes_sec=10485760</pre>
<p>所有quota属性列表（前缀省略）：<br />cpu_shares，相对于domain下其它虚拟机的、使用CPU时间片的权重值<br />cpu_shares_level，仅仅用于VMware，custom, high, normal, low<br />cpu_period，设置QEMU/LXC的enforcement interval，在周期内，不得消耗大于cpu_quota的带宽<br />cpu_limit，设置VMware的CPU频率，单位MHZ<br />cpu_reservation，设置VMware可以确保给虚拟机的CPU频率，MHZ<br />cpu_quota，设置最大允许带宽，单位微秒。负数表示无限制<br />memory_limit，内存上限，单位MB<br />memory_reservation，设置VMware最小保证内存，指定数量的内存一定会分配给虚拟机<br />disk_io_limit，设置VMware下每秒磁盘IO的上限<br />disk_io_reservation，设置VMware下保证的IOPS<br />disk_read_bytes_sec，限制读流量<br />disk_read_iops_sec，限制读IOPS<br />disk_write_bytes_sec，限制写流量<br />disk_write_iops_sec，限制写IOPS<br />disk_total_bytes_sec，限制流量<br />disk_total_iops_sec，限制IOPS<br />vif_inbound_average，入站流量平均速度，单位kb<br />vif_inbound_burst，入站流量以peak速度最多连续接收多少kb<br />vif_inbound_peak，入站流量的的最大速度，单位kb<br />vif_outbound_average，出站流量平均速度，单位kb<br />vif_outbound_burst，出站流量以peak速度最多连续接收多少kb<br />vif_outbound_peak，出站流量的的最大速度，单位kb</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Quotas</span></div>
<p>允许使用的资源的配额可以针对项目（租户）或者用户进行设置。</p>
<div class="blog_h3"><span class="graybg">配额类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">配额</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cores</td>
<td>每个项目总计允许分配的核心数</td>
</tr>
<tr>
<td>instances</td>
<td>每个项目总计允许启动的实例数</td>
</tr>
<tr>
<td>key_pairs</td>
<td>每个用户允许的密钥对数量</td>
</tr>
<tr>
<td>metadata_items</td>
<td>每个实例允许的元数据数量</td>
</tr>
<tr>
<td>ram</td>
<td>每个项目总计允许分配的内存MB</td>
</tr>
<tr>
<td>server_groups</td>
<td>每个项目允许的服务器组数量</td>
</tr>
<tr>
<td>server_group_members</td>
<td>每个服务器组中成员的数量</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">默认配额</span></div>
<p>使用下面的命令查看默认配额值：</p>
<pre class="crayon-plain-tag">openstack quota show --default</pre>
<div class="blog_h3"><span class="graybg">用户配额</span></div>
<p>要查看某个用户的配额，执行：</p>
<pre class="crayon-plain-tag">nova quota-show --user USER --tenant PROJECT</pre>
<div class="blog_h2"><span class="graybg">Host Aggregate</span></div>
<p>主机聚合是一种对宿主机进行分区的机制，聚合中的主机常常具有类似的硬件/性能特征。<span style="background-color: #c0c0c0;">一个主机可以属于多个聚合</span>。</p>
<p>引入主机聚合最初是为了使用Xen资源池，但是现在作为一种机制，允许管理员将一系列键值对（属性）同时分配到多台主机。</p>
<p>主机聚合不直接对用户暴露，管理员可以将Flavor映射到主机聚合 —— 只需要<span style="background-color: #c0c0c0;">为聚合设置匹配Flavor的额外规格说明的元数据</span>，即可完成映射。</p>
<p>管理员也可以将一组主机划分为AZ，与主机聚合不同，AZ是面向用户的概念，而且主机仅能属于一个AZ。</p>
<div class="blog_h3"><span class="graybg">启用支持</span></div>
<p>要让Nova调度器支持主机聚合，需要配置：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
enabled_filters=...,AggregateInstanceExtraSpecsFilter</pre>
<div class="blog_h3"><span class="graybg">使用示例</span></div>
<pre class="crayon-plain-tag"># 在nova可用区中创建一个SSD磁盘的主机的聚合（后面假设聚合的ID为1）
openstack aggregate create --zone nova fast-io

# 为聚合设置元数据
openstack aggregate set --property ssd=true 1

# 添加主机到聚合
openstack aggregate add host 1 node1
openstack aggregate add host 1 node2


# 创建一个Flavor
openstack flavor create --id 6 --ram 8192 --disk 80 --vcpus 4 ssd.large

# 映射Flavor到聚合
openstack flavor set \
# 设置scope为aggregate_instance_extra_specs的额外规格，
#                                             键值和聚合元数据一致
    --property aggregate_instance_extra_specs:ssd=true ssd.large</pre>
<div class="blog_h3"><span class="graybg">Placement中的聚合</span></div>
<p>在Placement中，Aggregate表示相关的资源提供者（resource provider）的分组。<span style="background-color: #c0c0c0;">在Placement中，Nova的计算节点就是资源提供者</span>。因此，节点在Placement中也可以被加入到聚合。</p>
<p>使用下面的命令，可以查询计算节点的UUID，并将其加入到Placement聚合中：</p>
<pre class="crayon-plain-tag">openstack --os-compute-api-version=2.53 hypervisor list
# +--------------------------------------+---------------------+-----------------+-----------------+-------+
# | ID                                   | Hypervisor Hostname | Hypervisor Type | Host IP         | State |
# +--------------------------------------+---------------------+-----------------+-----------------+-------+
# | 815a5634-86fb-4e1e-8824-8a631fee3e06 | node1               | QEMU            | 192.168.1.123   | up    |
# +--------------------------------------+---------------------+-----------------+-----------------+-------+

openstack --os-placement-api-version=1.2 resource provider aggregate set \
    --aggregate df4c74f3-d2c4-4991-b461-f1a678e1d161 \
    815a5634-86fb-4e1e-8824-8a631fee3e06</pre>
<p>从Nova 18.0.0开始，添加主机到Host Aggregate中后，会自动修改对应的Placement聚合。不需要手工操作。删除时类似。</p>
<div class="blog_h3"><span class="graybg">基于Placement的租户隔离</span></div>
<p>为了使用Placement来进行租户隔离，必须存在和Host Aggregate在UUID+成员关系上匹配的Placement Aggregate。调度过滤器AggregateMultiTenancyIsolation会使用聚合元数据。</p>
<p>需要设置 scheduler.limit_tenants_to_placement_aggregate  = True才能启用此特性。</p>
<p>配置示例：</p>
<pre class="crayon-plain-tag"># 创建主机聚合
openstack --os-compute-api-version=2.53 aggregate create myagg
# +-------------------+--------------------------------------+
# | Field             | Value                                |
# +-------------------+--------------------------------------+
# | availability_zone | None                                 |
# | created_at        | 2018-03-29T16:22:23.175884           |
# | deleted           | False                                |
# | deleted_at        | None                                 |
# | id                | 4                                    |
# | name              | myagg                                |
# | updated_at        | None                                 |
# | uuid              | 019e2189-31b3-49e1-aff2-b220ebd91c24 |
# +-------------------+--------------------------------------+

# 添加节点到主机聚合
openstack --os-compute-api-version=2.53 aggregate add host myagg node1

# 获取租户ID
openstack project list -f value | grep 'demo'
9691591f913949818a514f95286a6b90 demo

# 设置此聚合仅仅给租户使用
openstack aggregate set --property filter_tenant_id=9691591f913949818a514f95286a6b90 myagg

# 将主机加入到聚合
openstack --os-placement-api-version=1.2 resource provider aggregate set \
    --aggregate 019e2189-31b3-49e1-aff2-b220ebd91c24 \
    815a5634-86fb-4e1e-8824-8a631fee3e06</pre>
<div class="blog_h3"><span class="graybg">为聚合缓存镜像</span> </div>
<pre class="crayon-plain-tag">nova aggregate-cache-images my-aggregate image1 image2</pre>
<div class="blog_h2"><span class="graybg">调度器</span></div>
<p>Nova组件nova-scheduler负责决定在哪台计算节点上创建虚拟机。 在调度的场景下，术语host表示运行了nova-compute服务的哪些节点。</p>
<p>调度器的基本配置项是：</p>
<pre class="crayon-plain-tag">[scheduler]
driver = filter_scheduler

[filter_scheduler]
available_filters = nova.scheduler.filters.all_filters
enabled_filters = AvailabilityZoneFilter, ComputeFilter, ComputeCapabilitiesFilter, ImagePropertiesFilter, ServerGroupAntiAffinityFilter, ServerGroupAffinityFilter</pre>
<p>默认的调度器驱动是 filter_scheduler，在默认配置下，该调度器选择满足以下所有条件的宿主机：</p>
<ol>
<li>AvailabilityZoneFilter：位于请求的AZ中</li>
<li>ComputeFilter：能够服务请求</li>
<li>ComputeCapabilitiesFilter：满足实例类型的extra_specs（来自Flavor）</li>
<li>ImagePropertiesFilter：满足实例镜像属性中的架构、Hypervisor类型、虚拟机模式属性</li>
<li>ServerGroupAntiAffinityFilter：满足服务器组的反亲和设置——和组中的其它虚拟机不再同一宿主机上</li>
<li>ServerGroupAffinityFilter：满足服务器组亲和设置</li>
</ol>
<p>当执行nova evacuate命令重建虚拟机时，调度器服务遵循管理员给出的目标宿主机，如果管理员没有指定目标宿主机，则由调度器来选择适当的宿主机。</p>
<div class="blog_h3"><span class="graybg">预过滤</span></div>
<p>从R版开始，调度器包含一个前置的过滤步骤，其目的时提升效率，<span style="background-color: #c0c0c0;">减少候选的主机</span>。</p>
<p>下面是一些常用的预过滤器：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p>镜像类型支持过滤器：<strong> [scheduler]/query_placement_for_image_type_support=True</strong></p>
<p>过滤掉那些不支持用于启动虚拟机的镜像的格式的计算节点。例如，对于libvrit驱动，当使用Ceph作为临时存储后端时，不支持qcow2镜像格式。在混合使用基于Ceph、不使用Ceph作为存储后端的计算节点时，可以启用此过滤器</p>
</td>
</tr>
<tr>
<td>
<p>禁用状态节点过滤器：强制启用</p>
<p>从T版本开始，此过滤器会排除禁用状态的节点（类似于ComputeFilter）。具有trait COMPUTE_STATUS_DISABLED的（计算节点）资源提供者，将被排除，不作为调度候选</p>
<p>Trait由nova-compute服务管理，应该mirror位于os-services中的计算服务记录的disabled状态。例如，如果<span style="background-color: #c0c0c0;">计算服务的状态是disabled，那么它关联的计算节点资源提供者对象应当具有COMPUTE_STATUS_DISABLED这一trait</span>；当计算服务状态为enabled，对应资源提供者的此trait应该被移除</p>
<p>如果状态改变时计算服务宕了，那么trait将在它重启后同步。如果尝试给对应资源提供者添加/删除trait时出错，则update_available_resource这一定时任务负责重新同步。<strong>[DEFAULT]/update_resources_interval</strong>负责此同步操作的间隔</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Filter scheduler</span></div>
<p>前面我们提到过，nova.scheduler.filter_scheduler.FilterScheduler是默认的调度器（驱动）。它使用过滤器、权重来选择宿主机。</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2021/01/filtering-workflow-1.png"><img class="size-full wp-image-35277 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2021/01/filtering-workflow-1.png" alt="filtering-workflow-1" width="550" height="400" /></a></p>
<p>&nbsp;</p>
<p>配置项<strong> [filter_scheduler]/available_filters</strong> 列出调度器可以使用的过滤器集合，此配置项可以指定多次：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
; 所有自带过滤器
available_filters = nova.scheduler.filters.all_filters
; 加上这个自己编写的过滤器
available_filters = myfilter.MyFilter</pre>
<p>配置项 <strong>[filter_scheduler]/filter_scheduler.enabled_filters</strong> 列出当前nova-scheduler启用的过滤器。</p>
<div class="blog_h3"><span class="graybg">常用过滤器</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">过滤器</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>AggregateImagePropertiesIsolation</td>
<td>
<p>从L版开始，Nova仅会传递标准元数据给此过滤器。如果需要使用所有元数据，考虑过滤器AggregateInstanceExtraSpecsFilter</p>
<p>该过滤器对 <span style="background-color: #c0c0c0;">镜像元数据 和 聚合元数据 进行匹配</span>：</p>
<ol>
<li>如果主机属于聚合，且<span style="background-color: #c0c0c0;">聚合</span>定义了1-N个元数据，这些<span style="background-color: #c0c0c0;">元数据匹配镜像的属性</span>，则主机作为<span style="background-color: #c0c0c0;">从镜像启动的虚拟机</span>的候选宿主</li>
<li>如果宿主机不属于任何聚合，通过此过滤器（即不会被过滤掉）</li>
</ol>
</td>
</tr>
<tr>
<td>AggregateInstanceExtraSpecsFilter</td>
<td>
<p>该过滤器对 实例类型（Flavor）的、Scope为aggregate_instance_extra_specs的extra_specs 和 聚合属性进行匹配</p>
<p>为了向后兼容，没有Scope的Specs也可以用来匹配，但是不推荐 —— 当同时使用ComputeCapabilitiesFilter时会出现冲突</p>
<p>使用此过滤器，可以实现将<span style="background-color: #c0c0c0;">Flavor调度到特定的主机集合</span>中</p>
</td>
</tr>
<tr>
<td>AggregateMultiTenancyIsolation</td>
<td>
<p><span style="background-color: #c0c0c0;">确保租户隔离</span>（tenant-isolated）的主机聚合（即设置了filter_tenant_id的主机聚合）仅仅能被相关的租户（项目）所使用</p>
<p>filter_tenant_id可以用逗号分隔多个租户</p>
<p>如果某个主机属于设置了filter_tenant_id的聚合，那么某个不属于相应租户的用户发起创建虚拟机的请求时，虚拟机不会在此聚合的某个宿主机上创建</p>
<p>主机可以不属于任何设置了filter_tenant_id的聚合，通过此过滤器</p>
</td>
</tr>
<tr>
<td>AggregateNumInstancesFilter</td>
<td>
<p>用于<span style="background-color: #c0c0c0;">限制宿主机上实例的最大数量</span></p>
<p>对于一个聚合，如果设置了max_instances_per_host，那么其中的宿主机上的实例不会超过特定数量</p>
<p>如果主机属于多个设置了max_instances_per_host的聚合，验证此主机实例数量是否到达上限时，使用最小的max_instances_per_host值</p>
</td>
</tr>
<tr>
<td>AggregateTypeAffinityFilter</td>
<td>
<p>用于实现<span style="background-color: #c0c0c0;">实例类型（Flavor）亲和性</span></p>
<p>此过滤器pass（通过）没有设置instance_type的主机，或者所在聚合的元数据instance_type（逗号分隔）包含正在请求创建的虚拟机的instance_type的主机</p>
</td>
</tr>
<tr>
<td>AllHostsFilter</td>
<td>通过所有主机</td>
</tr>
<tr>
<td>AvailabilityZoneFilter</td>
<td>满足调度请求中关于<span style="background-color: #c0c0c0;">可用区的需求</span></td>
</tr>
<tr>
<td>ComputeCapabilitiesFilter</td>
<td>
<p>对Flavor的extra_specs中的属性 和 compute capabilities进行匹配</p>
<p>如果Extra Spec中包含冒号，则：之前的看作命名空间，之后的看作需要匹配的key。如果命名空间不是capabilities，则忽略此Spec</p>
<p>为了向后兼容，没有Namespace的Specs也可以用来匹配，但是不推荐 —— 当同时使用AggregateInstanceExtraSpecsFilter时会出现冲突</p>
<p>某些虚拟化驱动支持报告CPU的trait给placement服务，这种情况下，应该在Flavor中使用trait，而不是使用此过滤器。因为trait提供了CPU特性的一致性命名，而且查询trait的效率更高</p>
</td>
</tr>
<tr>
<td>ComputeFilter</td>
<td>此过滤器pass（通过）所有启用的、可以工作的计算服务（节点）</td>
</tr>
<tr>
<td>DifferentHostFilter</td>
<td>
<p>调度到其它宿主机，排除的宿主机由请求时给出的different_host来确定，该字段是一个实例的列表，排除的是这些实例所在的宿主机：</p>
<pre class="crayon-plain-tag">{
    "server": {
        "name": "server-1",
        "imageRef": "cedef40a-ed67-4d10-800e-17455edce175",
        "flavorRef": "1"
    },
    "os:scheduler_hints": {
        "different_host": [
            "a0cf03a5-d921-4877-bb5c-86d26cf818e1",
            "8c19174f-4220-44f0-824a-cd1eeef10287"
        ]
    }
}</pre>
<p> 使用命令：</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint different_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 \
  # 调度提示
  --hint different_host=8c19174f-4220-44f0-824a-cd1eeef10287 server-1</pre>
</td>
</tr>
<tr>
<td>ImagePropertiesFilter</td>
<td>
<p>根据实例的<span style="background-color: #c0c0c0;">镜像的属性来过滤宿主机</span>，仅仅通过那些支持镜像属性的宿主机
<p>属性包括：<span style="background-color: #c0c0c0;">架构、Hypervisor类型/版本、虚拟机模式：</span></p>
<p style="padding-left: 30px;">hw_architecture，架构：i686, x86_64, arm, ppc64 ...<br />img_hv_type，Hypervisor类型：qemu,hyperv ...<br />img_hv_requested_version，Hypervisor版本</p>
<p>对于QEMU、KVM，Hypervisor类型都是qemu</p>
<pre class="crayon-plain-tag">openstack image set --architecture arm --property img_hv_type=qemu img-uuid</pre>
</td>
</tr>
<tr>
<td>IsolatedHostsFilter</td>
<td>
<p>允许定义一个<span style="background-color: #c0c0c0;">隔离镜像集、隔离宿主机集，两者必须在一起</span>
<p>restrict_isolated_hosts_to_isolated_images 用于限制隔离主机仅仅运行隔离镜像。取值True意味着卷后备的虚拟机不能调度到隔离主机集；取值False则没有任何限制（对比镜像后备的虚拟机）</p>
<p>镜像集、宿主机集合必须配置在nova.conf：</p>
<pre class="crayon-plain-tag">[filter_scheduler]
isolated_hosts = server1, server2
isolated_images = 342b492c-128f-4a42-8d3a-c5088cf27d13, ebd267a6-ca86-4d6c-9a0e-bd132d6b7d09 </pre>
</td>
</tr>
<tr>
<td>IoOpsFilter</td>
<td>
<p><span style="background-color: #c0c0c0;">过滤掉具有太多并发IO实例的宿主机</span>
<p>max_io_ops_per_host指定 单个宿主机上IO敏感的实例的最大数量</p>
</td>
</tr>
<tr>
<td>JsonFilter</td>
<td>
<p>此过滤器默认没有启用，且没有广泛测试</p>
<p>允许用户为调度器提供一个JSON格式的提示。在提示中：</p>
<ol>
<li>支持操作符 = &lt; &gt; in &lt;= &gt;= not or and</li>
<li>支持判断属性   $free_ram_mb  $free_disk_mb  $hypervisor_hostname  $total_usable_ram_mb  $vcpus_total $vcpus_used 等任何<a href="https://opendev.org/openstack/nova/src/branch/master/nova/scheduler/host_manager.py">HostState</a></li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">openstack server create --image 827d564a-e636-4fc4-a376-d36f7ebe1747 \
  --flavor 1 --hint query='["&gt;=","$free_ram_mb",1024]' server1</pre>
</td>
</tr>
<tr>
<td>PciPassthroughFilter</td>
<td>仅仅通过匹配Flavor的extra_specs中设备请求的宿主机</td>
</tr>
<tr>
<td>SameHostFilter</td>
<td>
<p>调度到和<span style="background-color: #c0c0c0;">指定实例（集）相同的宿主机（之一）</span>
<p>实例（集）由提示给出：</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint same_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 \
  --hint same_host=8c19174f-4220-44f0-824a-cd1eeef10287 server-1</pre>
</td>
</tr>
<tr>
<td>ServerGroupAffinityFilter</td>
<td>
<p>确保<span style="background-color: #c0c0c0;">调度到指定的服务器组</span>中，服务器组由提示给出
<pre class="crayon-plain-tag">openstack server group create --policy affinity group-1
openstack server create --image IMAGE_ID --flavor 1 \
  --hint group=SERVER_GROUP_UUID server-1</pre>
</td>
</tr>
<tr>
<td>ServerGroupAntiAffinityFilter</td>
<td>
<p>确保<span style="background-color: #c0c0c0;">不调度到指定的服务器组</span>中，服务器组由提示给出
</td>
</tr>
<tr>
<td>SimpleCIDRAffinityFilter</td>
<td>
<p>根据<span style="background-color: #c0c0c0;">宿主机的IP地址CIDR</span>进行过滤，指定两个提示：</p>
<p style="padding-left: 30px;">build_near_host_ip  CIDR的第一个IP <br />cidr  CIDR的掩码部分</p>
<pre class="crayon-plain-tag">openstack server create --image cedef40a-ed67-4d10-800e-17455edce175 \
  --flavor 1 --hint build_near_host_ip=192.168.1.1 --hint cidr=/24 server-1</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Weights</span></div>
<p>经过过滤后，可能仍然有多个宿主机满足需求。那么，到底选择哪一个？这时需要基于权重来确定。
<p>一个宿主机的<span style="background-color: #c0c0c0;">最初权重由它拥有的硬件资源来确定</span>，<span style="background-color: #c0c0c0;">每当调度新的实例到它上面，宿主机的权重值就变小</span>。 </p>
<p>权重计算算法的参数，配置在nova.conf中：</p>
<pre class="crayon-plain-tag">[DEFAULT]

; 注意：主机聚合可以覆盖这里的权重因子设置
; 内存权重因子
ram_weight_multiplier
; 磁盘权重因子
disk_weight_multiplier
; 处理器权重因子
cpu_weight_multiplier
; IO负载权重因子，负数表示倾向于选择轻负载的宿主机
io_ops_weight_multiplier

; 进行权重判断后，会返回N个最适合主机，然后在随机从中取一个作为最终宿主机
; 该选项决定N的大小
scheduler_host_subset_size

; 默认权重值最大的获胜
scheduler_weight_classes = nova.scheduler.weights.all_weighers

[filter_scheduler]
; 对于主机组进行软亲和时时候
soft_affinity_weight_multiplier
; 对于主机组进行软反亲和时时候
soft_anti_affinity_weight_multiplier
; 对于最近发生创建虚拟机失败的宿主机，设置权重因子
; 设置为负数，则最近失败的主机更加少的机会被选择
build_failure_weight_multiplier
; 在跨Cell移动实例时，使用该权重因子
cross_cell_move_weight_multiplier </pre>
<div class="blog_h3"><span class="graybg">Compute capabilities作为Trait</span></div>
<p>从S版开始，nova-compute会基于计算驱动的capabilities报告为COMPUTE_开头的（资源提供者的）Trait（特性）。</p>
<p>通过配置Flavor，可以指定实例要求、禁止哪些Trait。例如，某个主机聚合支持multi-attach卷，你可以限制某个Flavor仅仅调度到这个主机聚合：</p>
<ol>
<li>为Flavor设置extra_specs：<pre class="crayon-plain-tag">trait:COMPUTE_VOLUME_MULTI_ATTACH=required</pre></li>
<li>按照常规方式限制Flavor到主机聚合</li>
</ol>
<pre class="crayon-plain-tag">openstack --os-compute-api-version=2.53 hypervisor list
# 列出trait
openstack --os-placement-api-version 1.6 resource provider trait list 8fa133f5-a41e-4ef6-a485-cb0d6e167860 </pre>
<p>关于基于计算驱动capabilities定义的Trait，需要注意：</p>
<ol>
<li>计算服务拥有这些COMPUTE_开头的Trait的控制权，nova-compute服务启动后，或者update_available_resource定时任务执行后，会自动添加/擅长Traits</li>
<li>用户自定义的Trait，不会被删除。除非定义的Trait以COMPUTE_开头</li>
<li>如果用户通过命令：<pre class="crayon-plain-tag">openstack resource provider trait delete</pre> 删除某个资源提供者的COMPUTE_*，计算服务会在重启时再次添加</li>
</ol>
<div class="blog_h2"><span class="graybg">可用区</span></div>
<p>在OpenStack中，可用区是一个用户可见的逻辑的云分区。在创建主机时，用户可以指定，创建到哪个可用区。</p>
<p><span style="background-color: #c0c0c0;">可用区没有在数据库中建模，而是定义为主机聚合的元数据</span>。为主机聚合添加特定的元数据，即可将其中的主机加入到某个可用区。</p>
<p>需要注意主机聚合和AZ的不同：</p>
<ol>
<li>宿主机可以属于多个主机聚合，但是只能属于一个AZ</li>
<li><span style="background-color: #c0c0c0;">默认情况下，主机是默认AZ的成员</span>，即使它不属于任何主机聚合</li>
</ol>
<p>其它OpenStack服务，例如网络、块存储服务，也有可用区的概念，但是实现方式各不相同。</p>
<div class="blog_h3"><span class="graybg">默认可用区</span></div>
<p>默认AZ的名字，可以在nova.conf中配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
default_availability_zone = nova</pre>
<p>该配置项指定计算服务（nova-compute组件）的默认可用区的名字。 </p>
<div class="blog_h3"><span class="graybg">配置可用区</span></div>
<p>可用区是在主机聚合上设置的：</p>
<pre class="crayon-plain-tag"># --os-compute-api-version=2.53
openstack aggregate create zircon
openstack aggregate add host zircon centos-11
openstack aggregate add host zircon centos-12
openstack aggregate add host zircon centos-13

# 设置聚合的可用区
openstack aggregate set --property availability_zone=zircon zircon</pre>
<p>将主机聚合关联到可用区的操作，需要提前规划。聚合中任何主机上已经实例，则无法设置可用区。</p>
<div class="blog_h3"><span class="graybg">对迁移的影响</span></div>
<p>导致实例所在宿主机改变的操作包括 evacuate, resize, cold migrate, live migrate 以及 unshelve。其中<span style="background-color: #c0c0c0;">只有evacuate和live migrate可以绕过调度器，强制指定目标主机</span>。</p>
<p>如果满足以下条件之一，迁移后的实例限定在特定的AZ中：</p>
<ol>
<li>创建实例时，指定了availability_zone参数，即指明在特地AZ中创建实例</li>
<li>虽然没有指定availability_zone，但是API service配置了 default_schedule_zone</li>
<li>2.77版本之后，Unshelve实例时，指定了availability_zone</li>
<li>cinder.cross_az_attach设置为False，default_schedule_zone也没有设置，但是实例使用了卷，这样会调度到卷所在的AZ</li>
</ol>
<p>如果实例没有在特定AZ内创建，则它可以被自由的移动到其它AZ， AvailabilityZoneFilter不做任何事情。</p>
<p>需要注意，如果实例在某个AZ内创建的情况下，通过evacuate或者live migrate将其<span style="background-color: #c0c0c0;">移动到另外一个AZ的主机上，是个危险的操作</span>。因为假设后续你又resize实例，调度器会将其转移到原先的AZ。</p>
<div class="blog_h3"><span class="graybg">资源亲和</span></div>
<p>Noava的配置项cinder.cross_az_attach，用于限制实例和它使用的卷在相同的AZ中。如果设置为False，则计算和存储资源会位于相同AZ，如果无法满足，则请求会失败：</p>
<ol>
<li>创建实例时，将一个已存在的卷挂载给它，那么实例将创建到卷所在的AZ</li>
<li>创建实例时，需要创建一个新卷挂载给它，那么Nova将会在实例所在的AZ中创建卷</li>
</ol>
<div class="blog_h2"><span class="graybg">管理卷</span></div>
<p>关于在Nova中使用、创建、管理卷，参考<a href="#server-create">openstack server create</a>。</p>
<div class="blog_h3"><span class="graybg">multi-attach</span></div>
<p>Nova从Q版开始支持Cinder的多重挂载。前提条件：</p>
<ol>
<li>Compute API最小版本是2.6</li>
<li>底层的Hypervisor驱动必须支持将卷挂载到多个客户机。使用libvirt驱动时，<span style="background-color: #c0c0c0;">libvirt必须大于3.10，qemu必须大于2.10</span></li>
<li>不支持swap一个正在使用的multiattach卷</li>
</ol>
<div class="blog_h2"><span class="graybg">远程访问</span></div>
<p>OpenStack支持多种控制台来连接到客户机，包括VNC、SPICE、Serial、RDP、MKS（VMware vSphere）。推荐仅仅部署一种类型的控制台支持，此外需要注意某些Hypervisor不支持某些控制台类型。</p>
<p>为了连接到虚拟机控制台，<span style="background-color: #c0c0c0;">计算节点的5900-5999端口必须开启</span>。</p>
<div class="blog_h3"><span class="graybg">控制台代理</span></div>
<p>不管使用哪种控制台，都需要部署console proxy服务。该服务负责：</p>
<ol>
<li>提供用户所在的公共网络和虚拟机所在的私有网络之间的桥梁</li>
<li>中介Token验证</li>
<li>屏蔽Hypervisor相关的连接细节，给用户一致的体验</li>
</ol>
<p>对于<span style="background-color: #c0c0c0;">某些Hypervisor + Console驱动的组合，控制台代理是Hypervisor/其它外部服务提供</span>的。其它的则由Nova提供控制台代理服务。Nova控制台代理的工作方式如下（以基于noVNC的VNC控制台为例）：</p>
<ol>
<li>用户访问OpenStack API，获取控制台访问URL，例如：http://ip:port/?path=%3Ftoken%3Dxyz</li>
<li>用户在浏览器打开控制台</li>
<li>浏览器连接到代理</li>
<li>代理校验用户的Token，映射Token到私有网络中的、实例的VNC服务器的地址:端口</li>
<li>计算节点在vnc.server_proxyclient_address中指定代理应该如何连接到本机的VNC服务器，代理通过此地址连接到VNC服务器</li>
</ol>
<p>要启用基于noVNC的VNC控制台，OpenStack需要部署以下额外组件：</p>
<ol>
<li>一个或多个 nova-novncproxy服务，以支持<span style="background-color: #c0c0c0;">基于浏览器的noVNC客户端</span>。在简单部署场景中，此服务运行在nova-api所在机器上，因为它是公共、私有网络之间的桥梁</li>
</ol>
<div class="blog_h3"><span class="graybg">基于noVNC的VNC控制台</span></div>
<p>VNC是很多Hypervisor和客户端支持的图形化控制台。noVNC支持通过浏览器访问VNC。</p>
<p>配置nova-novncproxy服务：</p>
<pre class="crayon-plain-tag">[vnc]
novncproxy_host = 0.0.0.0
novncproxy_port = 6082</pre>
<p>配置nova-compute服务： </p>
<pre class="crayon-plain-tag">[vnc]
enabled = True
novncproxy_base_url = http://os.gmem.cc:6082/vnc_auto.html
server_listen = 127.0.0.1
server_proxyclient_address = 127.0.0.1</pre>
<div class="blog_h3"><span class="graybg">串口控制台</span></div>
<p>使用串口控制台（serial console）可以检查虚拟机的内核输出，查看其它虚拟消息。串口控制台在虚拟机的网络连接不可用时特别有效。</p>
<p>从J版开始，OpenStack支持可读写的串口控制台。你需要在计算节点上配置：</p>
<pre class="crayon-plain-tag">[serial_console]
; ...
enabled = true
base_url = ws://os.gmem.cc:6083/
; 监听虚拟控制台请求的地址
listen = 0.0.0.0
; 控制台代理连接到哪个网络接口，通常管理网
proxyclient_address = MANAGEMENT_INTERFACE_IP_ADDRESS</pre>
<p>使用下面的命令后的串口控制台的WS地址：</p>
<pre class="crayon-plain-tag">nova get-serial-proxy INSTANCE_NAME</pre>
<div class="blog_h2"><span class="graybg">注入密码</span></div>
<p>Nova支持注入密码到虚拟机的管理员用户，密码会打印在openstack server create命令的输出中。</p>
<p>默认情况下，仪表盘会显示管理员密码并允许修改。如果希望禁用此特性：</p>
<pre class="crayon-plain-tag">PENSTACK_HYPERVISOR_FEATURES = {
...
    'can_set_password': False,
}</pre>
<p>对于使用libvirt后端的Hypervisor（KVM/QEMU/LXC），管理员密码注入默认禁用，要启用需要修改：</p>
<pre class="crayon-plain-tag">[libvirt]
inject_password=true</pre>
<div class="blog_h2"><span class="graybg">配置文件</span></div>
<pre class="crayon-plain-tag">[libvirt]
; 使用KVM以提升性能
virt_type = kvm
; 创建的实例的CPU模式，参考
; https://blog.gmem.cc/libvirt-study-note#cpu-mode
cpu_mode=host-passthrough</pre>
<div class="blog_h1"><span class="graybg">Placement</span></div>
<div class="blog_h2"><span class="graybg">osc-placement</span></div>
<p>默认情况下，很多placement相关的OpenStackt命令不可用，需要安装osc-placement插件。</p>
<p>默认情况下，使用的Placement API版本是1.0。要使用其它版本，需要指定<pre class="crayon-plain-tag">--os-placement-api-version</pre>命令行标记，或者设置环境变量：</p>
<pre class="crayon-plain-tag">export OS_PLACEMENT_API_VERSION=1.6 </pre>
<div class="blog_h1"><span class="graybg">Neutron</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>OpenStack Networking组件，即Neutron，允许你创建网络接口设备、将网络接口设备Attach到虚拟网络。Neutron基于<span style="background-color: #c0c0c0;">插件化机制</span>设计，用以支持不同的网络设备以及软件。</p>
<p>Neutron负责管理虚拟网络基础设施（Virtual Networking Infrastructure，VNI）的方方面面、以及物理网络基础设施（Physical Networking Infrastructure，PNI）的访问/接入层面。</p>
<p>Neutron支持<span style="background-color: #c0c0c0;">防火墙、VPN等高级网络</span>特性。</p>
<p>Neutron提供<span style="background-color: #c0c0c0;">网络、子网、路由器</span>的抽象，这些抽象模拟相应物理实体的特性。例如，网络包含子网，路由器负责再不同子网/网络之间进行封包路由。</p>
<p>任何给定的网络方案（set up），<span style="background-color: #c0c0c0;">至少包含一个外部网络</span>（External network）。与其它网络不同，外部网络不仅仅是虚拟网络，他是真实物理网络（可以访问OpenStack外部）的一种视图。外部网络上的IP地址，可以从OpenStack外部直接访问到。</p>
<p>除了外部网络之外，任何网络方案<span style="background-color: #c0c0c0;">至少包含一个内部网络</span>（Internal network），内部网络是虚拟（软件定义的）网络，<span style="background-color: #c0c0c0;">直接将VM连接在一起。</span></p>
<p>为了从<span style="background-color: #c0c0c0;">外部访问VM（或者反之），需要添加虚拟路由器</span>。每个路由器<span style="background-color: #c0c0c0;">包含一个网关，通向外部网络；包含1-N个接口，通往内部网络</span>。和物理路由器类似，连接到同一个路由器的<span style="background-color: #c0c0c0;">子网之间可以相互访问</span>。VM可以<span style="background-color: #c0c0c0;">通过路由器的网关访问外部网络</span>。</p>
<p>当有什么连接到一个子网时，那个<span style="background-color: #c0c0c0;">连接点（connection）就称为端口（port）</span>。你将外部网络的IP地址分配到内部网络的（由于VM连接到子网而产生的）端口上，这样，外部实体就能直接访问VM。</p>
<p>Neutron支持安全组（security groups），<span style="background-color: #c0c0c0;">安全组让管理员可以按组来定义防火墙规则</span>。一个VM可以归属1-N个安全组，Neutron根据安全组中的规则，来阻止VM访问端口、端口范围。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<p>一个典型的Netron部署，包含了多个服务（service）和代理（agent），这些组件可能运行在一个或多个节点上。</p>
<div class="blog_h3"><span class="graybg">neutron-server</span></div>
<p>接受API请求，并路由给适当的OpenStack Networking plug-in进行处理。作为访问数据库的中心点。</p>
<div class="blog_h3"><span class="graybg">插件</span></div>
<p>Neutron使用插件化的架构，各种可拔插功能依赖于plugin和agent实现。</p>
<div class="blog_h3"><span class="graybg">L2代理</span></div>
<p>使用通用/厂商特定的技术，来提供网络分段（segmentation）和隔离（isolation），也就是划分出子网（<span style="background-color: #c0c0c0;">network number相同的主机位于同一个子网，子网是个以太网</span>）。</p>
<p>L2代理应当运行在任何需要网络连接、提供虚拟接口安全性的节点上，包括计算、网络节点。</p>
<p>OpenStack自带了Cisco 虚拟/物理交换机、NEC OpenFlow产品、Open vSwitch、Linux Bridging、VMware NSX产品的代理。</p>
<div class="blog_h3"><span class="graybg">L3代理</span></div>
<p>运行在网络节点上，提供东西向、南北向的路由能力，并提供FWaaS、VPNaaS之类的高级特性。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>用于再Neutron服务器和各种代理之间进行消息交换，也用作某些特定插件存储网络状态的数据库。</p>
<div class="blog_h2"><span class="graybg">配置概述</span></div>
<p>主配置文件neutron.conf，neutron-server和各种Agent都会读取。该文件包含用于Neutron内部RPC的oslo.messaging配置，并且会包含一些和主机相关的信息。此配置文件还包括database、nova、keystone的凭证信息。</p>
<p>此外neutron-server可能会加载plugin特定的配置文件。而Agent则不会加载。原因是插件配置主要是全局范围的选项。</p>
<p>每个不同的Agent可能有自己的配置文件，他们应当在主配置之后加载。因此其中的配置项优先级更高。代理配置文件中，可能包含主机特定的配置，例如local_ip。</p>
<div class="blog_h2"><span class="graybg">ML2</span></div>
<p>模块化L2（Modular Layer 2 ，ML2）插件是Neutron的L2框架，它允许你在一个部署中使用多种L2网络技术。ML2的扩展点是两种不同类型的驱动</p>
<div class="blog_h3"><span class="graybg">Type驱动</span></div>
<p>网络类型驱动，定义了OpenStack网络的技术分类，例如VxLAN、flat。</p>
<p>每种实现技术都对应了一个ML2 Type驱动。这些驱动会维护任何需要的、和网络类型相关的状态。它会验证Provider网络上和网络类型有关的信息，并且负责在项目的网络中分配一个空闲的段。</p>
<div class="blog_h3"><span class="graybg">Mechanism驱动</span></div>
<p>网络机制驱动，定义了OpenStack网络的实现技术，例如flat网络可以利用Linux bridge或者OVS来实现。</p>
<p>Mechanism驱动会利用Type驱动所产生的信息，并且确保这些信息并应用。</p>
<p>Mechanism驱动能通过RPC利用L2代理，也能够直接和外部控制器/设备进行交互。</p>
<div class="blog_h3"><span class="graybg">驱动兼容矩阵</span></div>
<p>Mechanism驱动和Type驱动的搭配，不是任意的：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">Mech\Type</td>
<td style="text-align: center;">Flat</td>
<td style="text-align: center;">VLAN</td>
<td style="text-align: center;">VxLAN</td>
<td style="text-align: center;">GRE</td>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Open vSwitch</strong></td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
</tr>
<tr>
<td><strong>Linx Bridge</strong></td>
<td>Y</td>
<td>Y</td>
<td>Y</td>
<td>N</td>
</tr>
<tr>
<td><strong>SRIOV</strong></td>
<td>Y</td>
<td>Y</td>
<td>N</td>
<td>N</td>
</tr>
<tr>
<td><strong>MacVTap</strong></td>
<td>Y</td>
<td>N</td>
<td>N</td>
<td>N</td>
</tr>
<tr>
<td><strong>L2 population</strong></td>
<td>N</td>
<td>N</td>
<td>Y</td>
<td>Y</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Type驱动配置</span></div>
<p>在ML2的主配置文件/etc/neutron/plugins/ml2/ml2_conf.ini中配置：</p>
<pre class="crayon-plain-tag">[ml2]
; 支持的Type驱动列表
type_drivers = flat,vlan,vxlan,gre


; flat相关配置
[ml2_type_flat]
; 可以从中创建flat网络的物理网络（physical_network）的名称
; 设置为*则允许使用任何物理网络名
; 设置为空则禁用flat网络
flat_networks = provider


; vlan相关配置
[ml2_type_vlan]
; 格式 &lt;physical_network&gt;:&lt;vlan_min&gt;:&lt;vlan_max&gt; 或者 &lt;physical_network&gt;
; 指定可用作VLAN provider以及租户网络的物理网络名称，以及可分配的VLAN Tag范围
network_vlan_ranges = provider:0:200


; vxlan相关配置
[ml2_type_vxlan]
; 格式 &lt;vni_min&gt;:&lt;vni_max&gt;,&lt;vni_min&gt;:&lt;vni_max&gt;... 列出VxLAN VNI ID范围
vni_ranges = 
; VxLAN的多播组，如果配置，所有广播流量发送到此组；如果不配置，禁用multicast VxLAN模式
vxlan_group =</pre>
<div class="blog_h3"><span class="graybg">Mech驱动</span></div>
<p>在ML2的主配置文件/etc/neutron/plugins/ml2/ml2_conf.ini中配置：</p>
<pre class="crayon-plain-tag">[ml2]
;支持的Mech驱动列表
mechanism_drivers = ovs,l2pop</pre>
<p>更多的配置查看相关Agent的配置文件。</p>
<div class="blog_h2"><span class="graybg">L2代理</span></div>
<div class="blog_h3"><span class="graybg">Linux Bridge</span></div>
<p>该代理通过配置Linux Bridge来为OpenStack资源实现L2网络。配置文件路径 /etc/neutron/plugins/ml2/linuxbridge_agent.ini</p>
<pre class="crayon-plain-tag">[DEFAULT]
; 是否打开DEBUG级别的日志，默认INFO
debug = False

[agent]
; 每隔多少秒，Agent来轮询本地设备的变化情况
polling_interval = 2
; 封装为外层IP报文时，设置的DSCP值。用于overlay网络。0-63之间的整数
dscp = 
; 如果设置为True，则从内层封包取得DSCP，设置到外层封包上
dscp_inherit = False
; 使用的扩展列表
extensions = 

[linux_bridge]
; 格式：&lt;physical_network&gt;:&lt;physical_interface&gt;,&lt;physical_network&gt;:&lt;physical_interface&gt;...
; 将物理网络名称映射到Agent节点上的物理网络接口名。这样flat和VLAN网络才能利用这些网络
physical_interface_mappings = provider:eth0
; 格式：&lt;physical_network&gt;:&lt;physical_bridge&gt;
bridge_mappings

[securitygroup]
; 此L2代理使用的安全组防火墙驱动
firewall_driver = 
; 是否启用neutron security group API，如果不使用安全组，或者使用nova security group API，则禁用
enable_security_group = True
; 十分启用IPSet提升iptables的性能
enable_ipset = True
; 逗号分隔的，允许的ethertypes。0x开头的16进制形式
permitted_ethertypes = 

[vxlan]
; 此Agent是否支持VxLAN
enable_vxlan = Tre
; VxLAN接口协议包的TTL
ttl =
; VxLAN接口协议包的TOS，已经废弃，使用agent段的dscp选项代替
tos = 
; VxLAN接口的多播组
; 如果指定组地址的范围，必须使用CIDR格式
vxlan_group = 224.0.0.1
; 本地的Overlay(Tunnel)网络端点的IP地址，使用一个IPv4/IPv6的、位于宿主机网络接口上的地址
local_ip = 
; VxLAN包的UDP源地址范围
udp_srcport_min = 
udp_srcport_max = 
; VxLAN的UDP目的地址
udp_dstport = 
; 使用Local ARP responder，它提供本地响应，而非在overlay范围内进行ARP广播
; Local ARP responder和allowed-address-pairs扩展存在兼容性问题
arp_responder = 
; 描述组播地址和VLAN（VNI ID）之间的映射关系
; &lt;multicast address&gt;:&lt;vni_min&gt;:&lt;vni_max&gt; 逗号分隔
multicast_ranges</pre>
<div class="blog_h2"><span class="graybg">QoS</span></div>
<p>参考<a href="#neutron-qos">配额和限速 - Neutron QoS</a></p>
<div class="blog_h2"><span class="graybg">内部DNS</span></div>
<p>OpenStack支持对以下资源配置DNS：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">资源</td>
<td style="text-align: center;">DNS名</td>
<td style="text-align: center;">DNS域</td>
</tr>
</thead>
<tbody>
<tr>
<td>端口/Ports</td>
<td>是</td>
<td>否</td>
</tr>
<tr>
<td>网络/Networks</td>
<td>否</td>
<td>是</td>
</tr>
<tr>
<td>浮动IP</td>
<td>是</td>
<td>是</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">配置内置解析</span></div>
<p>要更改默认的域名后缀openstacklocal，需要修改neutron-server节点的：</p>
<pre class="crayon-plain-tag">[default]
dns_domain = os.gmem.cc
; router插件需要启用，否则报错Extensions not found: ['auto-allocated-topology', 'dns-integration'].
service_plugins = router</pre><br />
<pre class="crayon-plain-tag">[ml2]
# dns即DNS Integration。是dns_domain_ports的子集
# dns_domain_ports，额外允许设置port的dns_domain属性
extension_drivers = port_security,dns</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart neutron-server.service</pre>
<div class="blog_h3"><span class="graybg">配置DNS相关属性</span></div>
<p>你可以为一个Port配置DNS名：</p>
<pre class="crayon-plain-tag">neutron port-create my-net --dns_name my-port</pre>
<p>当Port分配给实例时，实例的主机名+域名后缀，会自动成为Port的FQDN，例如centos8-amd64.os.gmem.cc</p>
<div class="blog_h1"><span class="graybg">designate</span></div>
<p>OpenStack DNS Service，即Designate，是一个支持多租户的OpenStack的DNSaaS服务。它提供集成了keystone身份验证的REST API，能够基于Nova/Neutron动作自动生成DNS记录。Designate支持多种DNS服务器，包括Bind9和PowerDNS 4</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<div class="blog_h3"><span class="graybg">designate-api </span></div>
<p>提供REST API接口。</p>
<div class="blog_h3"><span class="graybg">designate-central</span></div>
<p>编排Zones和RecordSet的创建、删除和更新。</p>
<div class="blog_h3"><span class="graybg">designate-producer</span></div>
<p>编排周期性任务。</p>
<div class="blog_h3"><span class="graybg">designate-worker</span></div>
<p>运行任务，例如Zone的创建/删除/更新，以及来自designate-producer的周期性任务。</p>
<div class="blog_h3"><span class="graybg">designate-mdns</span></div>
<p>一个小的DNS服务器，负责推送DNS Zone信息到面向客户的DNS服务器，也能够拉取Designate基础设施之外的DNS Zone信息。</p>
<div class="blog_h3"><span class="graybg">designate-agent</span></div>
<p>某些DNS服务要求在本地执行命令，此代理负责对接到这些DNS服务。</p>
<div class="blog_h3"><span class="graybg">面向客户的DNS服务</span></div>
<p>为客户提供DNS服务，由designate-worker管理其记录。Bind9、Power DNS 4被支持的很好。</p>
<div class="blog_h2"><span class="graybg">安装配置</span></div>
<div class="blog_h3"><span class="graybg">客户端安装</span></div>
<pre class="crayon-plain-tag">sudo apt install python3-designateclient</pre>
<div class="blog_h3"><span class="graybg">安装软件</span></div>
<p>安装openstack-designate以及相关依赖：</p>
<pre class="crayon-plain-tag">dnf install openstack-designate-* bind bind-utils -y</pre>
<div class="blog_h3"><span class="graybg">创建数据库</span></div>
<pre class="crayon-plain-tag">CREATE DATABASE IF NOT EXISTS designate CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON designate.* TO 'designate'@'localhost' IDENTIFIED BY 'designate';
GRANT ALL PRIVILEGES ON designate.* TO 'designate'@'%' IDENTIFIED BY 'designate'; </pre>
<div class="blog_h3"><span class="graybg">创建OS对象</span></div>
<pre class="crayon-plain-tag">openstack user create --domain default --password designate designate
openstack role add --project service --user designate admin
openstack service create --name designate --description "DNS" dns
openstack endpoint create --region china dns public http://openstack.gmem.cc:9001/</pre>
<p>创建一个RNDC key：</p>
<pre class="crayon-plain-tag">rndc-confgen -a -k designate -c /etc/designate/rndc.key -r /dev/urandom
chown named:named /etc/designate/rndc.key
chmod 644 /etc/rndc.key</pre>
<div class="blog_h3"><span class="graybg">修改named配置文件</span></div>
<pre class="crayon-plain-tag">...
include "/etc/designate/rndc.key";

options {
    ...
    allow-new-zones yes;
    request-ixfr no;
    listen-on port 53 { any; };
    recursion no;
    allow-query { any; };
};

controls {
  inet 127.0.0.1 port 953
    allow { 127.0.0.1; } keys { "designate"; };
};</pre>
<p>改完重启服务：</p>
<pre class="crayon-plain-tag">systemctl enable named.service
systemctl start named.service</pre>
<div class="blog_h3"><span class="graybg">配置OpenStack</span></div>
<pre class="crayon-plain-tag">[DEFAULT]
transport_url = rabbit://openstack:openstack@openstack.gmem.cc

[service:api]
listen = 0.0.0.0:9001
auth_strategy = keystone
enable_api_v2 = True
enable_api_admin = True
enable_host_header = True
enabled_extensions_admin = quotas, reports

[keystone_authtoken]
auth_type = password
username = designate
password = designate
project_name = service
project_domain_name = Default
user_domain_name = Default
www_authenticate_uri = http://openstack.gmem.cc:5000/
auth_url = http://openstack.gmem.cc:5000/
memcached_servers = openstack.gmem.cc:11211

[storage:sqlalchemy]
connection = mysql+pymysql://designate:designate@openstack.gmem.cc/designate</pre>
<div class="blog_h3"><span class="graybg">初始化数据库</span></div>
<pre class="crayon-plain-tag">su -s /bin/sh -c "designate-manage database sync" designate</pre>
<div class="blog_h3"><span class="graybg">创建池</span></div>
<pre class="crayon-plain-tag">- name: default
  # 池的名字，创建后不可改变。除非删除（连同关联的Zones）并重建
  description: default

  attributes: {}

  # 该池负责管理的Zone的NS记录
  # 记录应当在Designate之外创建, 指向控制节点的公共IP
  ns_records:
    - hostname: ns.openstack.gmem.cc.
      priority: 1

  # 该池使用的DNS服务器列表，也就是Bind服务器的地址
  nameservers:
    - host: 127.0.0.1
      port: 53

  # 该池的目标列表，对于Bind来说，每个BIND服务器对应一个条目
  # designate会在每个Bind服务器上运行rndc命令
  targets:
    - type: bind9
      description: bind9 on openstack-1
      # 列出designate-mdns服务地址，Bind服务器向其发送zone transfers (AXFRs)请求
      # 应当是控制节点的IP
      masters:
        - host: 127.0.0.1
          port: 5354

      options:
        host: 127.0.0.1
        port: 53
        rndc_host: 127.0.0.1
        rndc_port: 953
        rndc_key_file: /etc/designate/rndc.key</pre>
<p>执行下面的命令创建池：</p>
<pre class="crayon-plain-tag">su -s /bin/sh -c "designate-manage pool update" designate</pre>
<div class="blog_h3"><span class="graybg">启用服务 </span></div>
<pre class="crayon-plain-tag">systemctl start designate-central designate-api
systemctl enable designate-central designate-api

systemctl start designate-worker designate-producer designate-mdns
systemctl enable designate-worker designate-producer designate-mdns</pre>
<div class="blog_h3"><span class="graybg">验证 </span></div>
<p>查看DNS服务列表：</p>
<pre class="crayon-plain-tag">openstack dns service list
+--------------------------------------+-------------+--------------+--------+-------+--------------+
| id                                   | hostname    | service_name | status | stats | capabilities |
+--------------------------------------+-------------+--------------+--------+-------+--------------+
| a071880d-a2d4-468a-a452-da4e7856a63c | openstack-1 | api          | UP     | -     | -            |
| e7b8a2dd-c92e-41fb-bd23-c342c76de154 | openstack-1 | central      | UP     | -     | -            |
| 502c5b1f-1036-4a84-9b3e-14c7255b20eb | openstack-1 | mdns         | UP     | -     | -            |
| a2f0d9ad-366b-4ec2-8b92-d2f0765f2333 | openstack-1 | producer     | UP     | -     | -            |
| d64fa762-3598-4477-b7bc-a4394b326177 | openstack-1 | worker       | UP     | -     | -            |
+--------------------------------------+-------------+--------------+--------+-------+--------------+</pre>
<p>创建一个DNS Zone：</p>
<pre class="crayon-plain-tag">openstack zone create --email admin@gmem.cc os.gmem.cc.</pre>
<p>确认其状态到达ACTIVE：</p>
<pre class="crayon-plain-tag">openstack zone list
+--------------------------------------+-------------+---------+------------+--------+--------+
| id                                   | name        | type    |     serial | status | action |
+--------------------------------------+-------------+---------+------------+--------+--------+
| 2093169d-15d5-4e9c-8b94-a6569ffac7b8 | os.gmem.cc. | PRIMARY | 1618994404 | ACTIVE | NONE   |
+--------------------------------------+-------------+---------+------------+--------+--------+</pre>
<p>创建一个记录集（RecordSet）：</p>
<pre class="crayon-plain-tag">openstack recordset create --record '10.1.1.1' --type A os.gmem.cc. horizon</pre>
<div class="blog_h2"><span class="graybg"> 外部DNS</span></div>
<p>Neutron支持将域名通过designate暴露给外部。</p>
<div class="blog_h3"><span class="graybg">配置neutron-server</span></div>
<pre class="crayon-plain-tag">[default]
# 决定了配置了dns_domain的port默认所在Zone
dns_domain = os.gmem.cc.
external_dns_driver = designate

[designate]
url = http://openstack.gmem.cc:9001/v2
admin_auth_url = http://openstack.gmem.cc:35357/v2.0
admin_username = neutron
admin_password = neutron
admin_tenant_name = service
allow_reverse_dns_lookup = True
ipv4_ptr_zone_prefix_size = 16
ipv6_ptr_zone_prefix_size = 116</pre><br />
<pre class="crayon-plain-tag">extension_drivers=port_security,dns</pre>
<div class="blog_h3"><span class="graybg">UC1：在外部DNS中发布Port</span></div>
<p>用例说明：用户创建在一个可被外部访问的网络上创建实例，并且希望从集群外部通过域名来访问实例。</p>
<p>参考步骤：</p>
<ol>
<li>前提条件：
<ol>
<li><span style="background-color: #c0c0c0;">不支持使用 --external 创建的网络</span></li>
<li>支持FLAT, VLAN, GRE, VXLAN, GENEVE类型的网络</li>
<li>对于FLAT, VLAN, GRE, VXLAN, GENEVE，其segmentation ID必须位于分配给project networks的范围之外</li>
</ol>
</li>
<li>给网络的dns_domain属性分配一个合法的值：<br />
<pre class="crayon-plain-tag"># neutron net-update 5fdf474c-533a-403d-b7dd-cc7d7e1dfa23 --dns_domain os.gmem.cc.
openstack network set --dns-domain os.gmem.cc. provider</pre>
</li>
<li>
<p>查看此Zone中的记录：</p>
<pre class="crayon-plain-tag">openstack recordset list os.gmem.cc.
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+
| id                                   | name        | type | records                                                             | status | action |
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+
| 22422e08-0c17-4106-a222-35c3e535d37a | os.gmem.cc. | NS   | ns.openstack.gmem.cc.                                               | ACTIVE | NONE   |
| e4683c9e-70c0-4d3b-83e1-bb5530059311 | os.gmem.cc. | SOA  | ns.openstack.gmem.cc. admin.gmem.cc. 1618997152 3586 600 86400 3600 | ACTIVE | NONE   |
+--------------------------------------+-------------+------+---------------------------------------------------------------------+--------+--------+</pre>
</li>
<li>
<p> 创建一个端口，配置DNS名：
<pre class="crayon-plain-tag">openstack port create --dns-name t3m1 --network provider \
    --fixed-ip subnet=provider,ip-address=10.2.0.111 --disable-port-security tcnp3-master-1
# | dns_assignment          | fqdn='t3m1.os.gmem.cc.', hostname='t3m1', ip_address='10.2.0.111'         |
 </pre>
</li>
<li>再次查看Zone，应该发现名为t3m1.os.gmem.cc.的新A记录：<br />
<pre class="crayon-plain-tag">openstack recordset list os.gmem.cc.
# | f81bc3c2-2272-4f21-93d3-2c4fc9e6435d | t3m1.os.gmem.cc. | A    | 10.2.0.111   | ACTIVE | NONE   |</pre>
</li>
</ol>
<p>当把端口授予实例后，<span style="background-color: #c0c0c0;">原先的A记录被替换，前缀改为实例的主机名</span>。
<div class="blog_h1"><span class="graybg">Cinder</span></div>
<p>OpenStack Block Storage service，即Cinder为VM提供块设备。块设备如何产生、如何被消费，取决于<span style="background-color: #c0c0c0;">块设备驱动</span>。如果使用多后端配置，则取决于多个驱动。Cinder具有多种驱动，包括NAS/SAN, NFS, iSCSI, Ceph，等等。</p>
<p>Block Storage API组件和Scheduler服务通常运行在控制节点上。取决于使用的驱动，Volume服务可能运行在控制节点、计算节点，或者独立的存储节点上。</p>
<p>Cinder和Nova交互，从而将卷提供给虚拟机实例。</p>
<div class="blog_h2"><span class="graybg">组件</span></div>
<div class="blog_h3"><span class="graybg">cinder-api</span></div>
<p>接受API请求，路由给cinder-volume处理。</p>
<div class="blog_h3"><span class="graybg">cinder-volume</span></div>
<p>直接（或者通过消息队列）和后端存储服务交互、cinder-scheduler这样的组件交互。</p>
<p>cinder-volume会响应针对后端存储服务的读写请求，从而维持状态。它是通过驱动架构与后端交互的。</p>
<div class="blog_h3"><span class="graybg">cinder-scheduler</span></div>
<p>这是一个守护程序，负责选取一个最优化的、能够提供卷的节点，并由节点创建卷。</p>
<div class="blog_h3"><span class="graybg">cinder-backup</span></div>
<p>这是一个守护程序，负责备份任意类型的卷到某个Backup storage provider。它是通过驱动架构与后端交互的。</p>
<div class="blog_h3"><span class="graybg">消息队列</span></div>
<p>在上述进程之间交换信息。</p>
<div class="blog_h2"><span class="graybg">备份后端配置</span></div>
<div class="blog_h3"><span class="graybg">nfs</span></div>
<pre class="crayon-plain-tag">[DEFAULT]
backup_driver = cinder.backup.drivers.nfs.NFSBackupDriver
backup_share = 10.0.0.1:/slow</pre>
<div class="blog_h2"><span class="graybg">卷后端配置</span></div>
<div class="blog_h3"><span class="graybg">lvm</span></div>
<p>参考上文的样例环境。</p>
<p>需要注意的是，LVM后端可以映射为远程宿主机的磁盘。当卷从位于LVM后端上的快照上创建时，它必须和快照在一个存储节点上，这种情况下，远程映射可能是必须的，需要注意性能问题。不能假设LVM后端是本地磁盘。</p>
<div class="blog_h3"><span class="graybg">nfs</span></div>
<p>添加到已启用后端列表：</p>
<pre class="crayon-plain-tag">[DEFAULT]
enabled_backends = lvm,nfs

[nfs]
volume_driver = cinder.volume.drivers.nfs.NfsDriver
nfs_shares_config = /etc/cinder/nfs_shares
nfs_mount_point_base = /var/lib/cinder/mnt
volume_backend_name = nfs</pre>
<p>配置NFS服务地址，以及使用的NFS导出目录的绝对路径：</p>
<pre class="crayon-plain-tag">10.0.0.1:/cinder</pre>
<p>并修改该配置文件的访问权限：</p>
<pre class="crayon-plain-tag">chown root:cinder /etc/cinder/nfsshares
chmod 0640 /etc/cinder/nfsshares</pre>
<p>重新启动cinder-volume服务：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-cinder-volume.service</pre>
<p>查看cinder服务列表：</p>
<pre class="crayon-plain-tag">cinder service-list

| cinder-volume    | openstack-2@nfs | nova | enabled | up    | 2021-04-15T13:59:30.000000</pre>
<p>创建对应的volume type：</p>
<pre class="crayon-plain-tag">cinder type-create nfs
cinder type-key nfs set volume_backend_name=nfs

openstack volume type show nfs
+--------------------+--------------------------------------+
| Field              | Value                                |
+--------------------+--------------------------------------+
| access_project_ids | None                                 |
| description        | None                                 |
| id                 | c31ec381-c1f7-46c3-9119-bf699413f6b1 |
| is_public          | True                                 |
| name               | nfs                                  |
| properties         | volume_backend_name='nfs'            |
| qos_specs_id       | None                                 |
+--------------------+--------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">ceph</span></div>
<p>参考<a href="#ceph">集成Ceph</a>。</p>
<div class="blog_h1"><span class="graybg">Glance</span></div>
<div class="blog_h2"><span class="graybg">多后端</span></div>
<div class="blog_h3"><span class="graybg">启用RBD后端</span></div>
<p>参考如下配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
# 增加             后端key:后端类型
enabled_backends = rbd:rbd, file:file

[glance_store]
# 增加
default_backend=file
# 注释掉
# stores = file,http,rbd
# default_store = file
filesystem_store_datadir = /var/lib/glance/images/

# 增加
[rbd]
rbd_store_pool = images
rbd_store_user = glance
rbd_store_ceph_conf = /etc/ceph/ceph.conf
rbd_store_chunk_size = 8</pre>
<p>重启服务：</p>
<pre class="crayon-plain-tag">systemctl restart  openstack-glance-api.service</pre>
<p>查看后端列表：</p>
<pre class="crayon-plain-tag">glance stores-info

+----------+----------------------------------------------------+
| Property | Value                                              |
+----------+----------------------------------------------------+
| stores   | [{"id": "rbd"}, {"id": "file", "default": "true"}] |
+----------+----------------------------------------------------+</pre>
<div class="blog_h3"><span class="graybg">向特定后端上传镜像</span></div>
<p>首先创建一个镜像记录：</p>
<pre class="crayon-plain-tag">glance image-create --disk-format raw --container-format bare --visibility public --name bionic-amd64 </pre>
<p>然后向此镜像记录上传镜像：</p>
<pre class="crayon-plain-tag">#                                                           指定后端     镜像记录的ID
glance image-upload --file bionic-server-cloudimg-amd64.img --store rbd d828f193-830f-4e1f-ad28-4fc607474368</pre>
<p>通过Ceph命令可以看到，存储池中出现一个以镜像ID为名字的RBD镜像：</p>
<pre class="crayon-plain-tag">rbd -p images ls
d828f193-830f-4e1f-ad28-4fc607474368</pre>
<div class="blog_h1"><span class="graybg"><a id="ceph"></a>集成Ceph</span></div>
<p>通过libvirt（底层是配置QEMU使用librbd），你可以将Ceph的RBD镜像挂载（Attach）给Openstack实例。</p>
<p>OpenStack有三部分可以用到Ceph存储：</p>
<ol>
<li>镜像：Glance管理的虚拟机镜像（不可变的）可以存放在Ceph中。Openstack将其作为blob并下载使用</li>
<li>卷：虚拟机启动使用的卷，或者后续挂载的额外卷</li>
<li>客户机磁盘：虚拟机启动时使用的系统盘<span style="background-color: #c0c0c0;">，默认情况下，此系统盘表现为Hypervisor的文件系统中的一个文件</span>，通常位于/var/lib/nova/instances/&lt;uuid&gt;/。在Havana版本之前，唯一启动系统盘位于Ceph中的VM方法是，使用boot-from-volume特性。现在，则可以直接启动存储在Ceph中的虚拟机，而<span style="background-color: #c0c0c0;">不需要使用Cinder</span>。这样，在虚拟机热迁移过程中，可以方便的执行维护操作。另外一个好处是，如果Hypervisor宕机，你可以通过<pre class="crayon-plain-tag">nova evacuate</pre>几乎无缝的重新实例化VM，OpenStack会在系统盘对应的RBD镜像上加独占锁以防多个计算节点并发访问之</li>
</ol>
<p>Glance 能够将镜像存储为一个 Ceph 块设备。通过 Cinder使用COW克隆镜像来启动虚拟机。<span style="background-color: #c0c0c0;">Ceph不支持 QCOW2 格式的镜像，必须使用RAW格式的镜像</span>。</p>
<div class="blog_h2"><span class="graybg">准备存储池</span></div>
<pre class="crayon-plain-tag">ceph osd pool create volumes 32; rbd pool init volumes
ceph osd pool create backups 32; rbd pool init backups
ceph osd pool create images 32;  rbd pool init images
ceph osd pool create vms 32;     rbd pool init vms

for i in volumes backups images vms; do ceph osd pool application enable $i rbd; done</pre>
<div class="blog_h2"><span class="graybg">安装软件</span></div>
<p>需要在nova-compute, cinder-backup 和 cinder-volume节点上安装Ceph客户端软件：</p>
<pre class="crayon-plain-tag">dnf install -y ceph-common python-rbd</pre>
<p>glance-api节点只需要安装 python-rbd。</p>
<div class="blog_h2"><span class="graybg">配置Ceph客户端</span></div>
<p>运行glance-api, cinder-volume, nova-compute, cinder-backup的节点，是Ceph客户端，因此首先把/etc/ceph/ceph.conf复制到这些节点。</p>
<p>然后，创建以下Ceph账户：</p>
<pre class="crayon-plain-tag">ceph auth get-or-create client.glance mon 'profile rbd' osd 'profile rbd pool=images' mgr 'profile rbd pool=images'
ceph auth get-or-create client.cinder mon 'profile rbd' osd 'profile rbd pool=volumes, profile rbd pool=vms, profile rbd-read-only pool=images' mgr 'profile rbd pool=volumes, profile rbd pool=vms'
ceph auth get-or-create client.cinder-backup mon 'profile rbd' osd 'profile rbd pool=backups' mgr 'profile rbd pool=backups'</pre>
<p>并且把Keyring分发到相应节点：</p>
<pre class="crayon-plain-tag"># 镜像服务使用client-glance
ceph auth get-or-create client.glance | ssh root@openstack-1 tee /etc/ceph/ceph.client.glance.keyring
ssh root@openstack-1 chown glance:glance /etc/ceph/ceph.client.glance.keyring

# 卷服务、计算节点使用client-cinder
ceph auth get-or-create client.cinder | ssh root@openstack-2 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-2 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-2 tee /tmp/client.cinder.key
ceph auth get-or-create client.cinder | ssh root@openstack-3 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-3 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-3 tee /tmp/client.cinder.key
ceph auth get-or-create client.cinder | ssh root@openstack-4 tee /etc/ceph/ceph.client.cinder.keyring
ssh root@openstack-4 chown cinder:cinder /etc/ceph/ceph.client.cinder.keyring
ceph auth get-key client.cinder | ssh root@openstack-4 tee /tmp/client.cinder.key

# 备份服务使用client.cinder-backup
ceph auth get-or-create client.cinder-backup | ssh root@openstack-1 tee /etc/ceph/ceph.client.cinder-backup.keyring
ssh root@openstack-1 chown cinder:cinder /etc/ceph/ceph.client.cinder-backup.keyring</pre>
<p>准备一个UUID，推荐所有节点使用同一UUID：</p>
<pre class="crayon-plain-tag">uuidgen
b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>在每个节点上执行：</p>
<pre class="crayon-plain-tag">cat &gt; secret.xml &lt;&lt;EOF
&lt;secret ephemeral='no' private='no'&gt;
  &lt;uuid&gt;b3a20e8b-a27a-4623-8ea1-39eb7e34da4c&lt;/uuid&gt;
  &lt;usage type='ceph'&gt;
    &lt;name&gt;client.cinder secret&lt;/name&gt;
  &lt;/usage&gt;
&lt;/secret&gt;
EOF

virsh secret-define --file secret.xml

virsh secret-set-value --secret b3a20e8b-a27a-4623-8ea1-39eb7e34da4c --base64 $(cat /tmp/client.cinder.key)
rm -f secret.xml /tmp/client.cinder.key</pre>
<div class="blog_h2"><span class="graybg">作为镜像存储</span></div>
<p>编辑配置文件：</p>
<pre class="crayon-plain-tag">[glance_store]
stores = file,http,rbd
default_store = file
filesystem_store_datadir = /var/lib/glance/images/
rbd_store_pool = images
rbd_store_user = glance
rbd_store_ceph_conf = /etc/ceph/ceph.conf
rbd_store_chunk_size = 8</pre>
<p>注意，glance支持多个存储后端，上面的配置增加了一个rbd后端。</p>
<p>要启用copy-on-write的镜像克隆，增加配置：</p>
<pre class="crayon-plain-tag">[DEFAULT]
show_image_direct_url = True</pre>
<p>为了防止glance在/var/lib/glance/image-cache/下缓存镜像，配置：</p>
<pre class="crayon-plain-tag">[paste_deploy]
flavor = keystone</pre>
<div class="blog_h2"><span class="graybg">作为备份存储</span></div>
<p>修改备份节点的Cinder配置文件：</p>
<pre class="crayon-plain-tag">backup_driver = cinder.backup.drivers.ceph
backup_ceph_conf = /etc/ceph/ceph.conf
backup_ceph_user = cinder-backup
backup_ceph_chunk_size = 134217728
backup_ceph_pool = backups
backup_ceph_stripe_unit = 0
backup_ceph_stripe_count = 0
restore_discard_excess_bytes = true</pre>
<div class="blog_h2"><span class="graybg">作为卷存储</span></div>
<div class="blog_h3"><span class="graybg">配置Cinder后端</span></div>
<p>配置cinder.conf：</p>
<pre class="crayon-plain-tag">[DEFAULT]
# 增加一个ceph的后端
enabled_backends = lvm,nfs-fast,nfs-slow,ceph
# 要支持多个Cinder后端，需要配置：
glance_api_version = 2

# 后端配置
[ceph]
volume_driver = cinder.volume.drivers.rbd.RBDDriver
volume_backend_name = ceph
rbd_pool = volumes
rbd_ceph_conf = /etc/ceph/ceph.conf
rbd_flatten_volume_from_snapshot = false
rbd_max_clone_depth = 5
rbd_store_chunk_size = 4
rados_connect_timeout = -1
# 如果启用了cephx身份验证
rbd_user = cinder
rbd_secret_uuid = b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>创建卷类型并关联到新建的后端：</p>
<pre class="crayon-plain-tag">cinder type-create ceph
cinder type-key ceph set volume_backend_name=ceph</pre>
<p>到这一步，可以创建Ceph卷了，但是还不能挂载给虚拟机。</p>
<div class="blog_h3"><span class="graybg">配置Nova</span></div>
<p>libvirt进程需要利用client.cinder的keyring来挂载Ceph卷（不管是作为普通块设备还是启动盘）：</p>
<pre class="crayon-plain-tag">[libvirt]
rbd_user = cinder
rbd_secret_uuid = b3a20e8b-a27a-4623-8ea1-39eb7e34da4c</pre>
<p>为了支持直接从Ceph卷启动虚拟机，需要为Nova配置ephemeral后端。</p>
<p>建议在ceph.conf中启用RBD缓存（从Giant版本默认开启）。此外，可以开启client admin socket，以收集指标和辅助故障诊断：</p>
<pre class="crayon-plain-tag">[client]
    rbd cache = true
    rbd cache writethrough until flush = true
    admin socket = /var/run/ceph/guests/$cluster-$type.$id.$pid.$cctid.asok
    log file = /var/log/qemu/qemu-guest-$pid.log
    rbd concurrent management ops = 20</pre><br />
<pre class="crayon-plain-tag">mkdir -p /var/run/ceph/guests/ /var/log/qemu/
chown qemu:libvirt /var/run/ceph/guests /var/log/qemu/</pre>
<div class="blog_h2"><span class="graybg">从Ceph卷启动</span></div>
<p>使用下面的命令可以将镜像转换为raw格式：</p>
<pre class="crayon-plain-tag">qemu-img convert -f {source-format} -O {output-format} {source-filename} {output-filename}
qemu-img convert -f qcow2 -O raw precise-cloudimg.img precise-cloudimg.raw</pre>
<p>从raw格式的、存放在Ceph后端的镜像创建卷：</p>
<pre class="crayon-plain-tag">cinder create --image-id {id of image} --display-name {name of volume} {size of volume}</pre>
<p>当Glance和Cinder同时使用Ceph后端时，镜像使用copy-on-write方式克隆，因此新卷的很快。</p>
<p>如果通过OpenStack Dashboard操作，参考步骤：</p>
<ol>
<li>新建一个实例</li>
<li>选择一个关联到copy-on-write克隆的镜像的卷。也就是存放在Ceph中的，创建自位于Ceph中的镜像的卷</li>
<li>选择从卷启动，并使用上一步的卷</li>
</ol>
<div class="blog_h1"><span class="graybg">配额和限速</span></div>
<div class="blog_h2"><span class="graybg">配额</span></div>
<p>所谓配额，是指限制某个项目（租户）或者类（class）能够使用的各种云资源的量。例如CPU核心个数、固定IP个数、实例个数、密钥对个数、内存量、存储量、备份存储量、备份个数、每个卷最大存储量、网络数、子网数，等等。</p>
<p>默认的admin项目的配额比较小，可以用<pre class="crayon-plain-tag">openstack quota set</pre>修改配额。要查看配额，可以：</p>
<pre class="crayon-plain-tag"># 显示项目的各种配额
openstack limits show --absolute --project admin

# 显示计算配额
openstack quota list --compute --project admin</pre>
<div class="blog_h2"><span class="graybg">Flavor限速</span></div>
<p>每个Flavor都可以被分配额外的属性（Extra Specs），来限制它的CPU、IO、网络的速度。</p>
<p>CPU限速基于cgroups实现。</p>
<p>IO限流由QEMU处理（通过libvirt的blkdeviotune），尽管libvirt提供了基于cgroups的blkiotune特性，但是Nova并没有使用它。</p>
<p>流量塑形（Traffic shaping）也就是出入站带宽限制，基于tc实现。<span style="background-color: #c0c0c0;">Libvirt以网络接口为级别进行流量塑形，如果它基于cgroups进行塑形，则客户机的所有网络接口被一起限制，因为cgroups运行在进程级别</span>。</p>
<div class="blog_h3"><span class="graybg">IO限速</span></div>
<p>可以使用以下Extra Specs键：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">键</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>disk_write_bytes_sec</td>
<td>限制字节数/秒，写</td>
</tr>
<tr>
<td>disk_read_bytes_sec</td>
<td>限制字节数/秒，读</td>
</tr>
<tr>
<td>disk_read_iops_sec</td>
<td>限制IOPS，读</td>
</tr>
<tr>
<td>disk_write_iops_sec</td>
<td>限制IOPS，写</td>
</tr>
<tr>
<td>disk_total_bytes_sec</td>
<td>限制字节数/秒，读写</td>
</tr>
<tr>
<td>disk_total_iops_sec</td>
<td>限制IOPS，读写</td>
</tr>
</tbody>
</table>
<p>配置示例：</p>
<pre class="crayon-plain-tag"># 通过nova-manage命令
nova-manage flavor set_key --name m1.small  --key quota:disk_read_bytes_sec --value 10240000

# 通过nova命令，需要admin凭证
nova flavor-key m1.small  set quota:disk_read_bytes_sec=10240000

# 通过openstack命令，property可以指定多个
openstack flavor set m1.small --property quota:disk_read_bytes_sec=10000</pre>
<div class="blog_h3"><span class="graybg">CPU限速</span></div>
<p>可以使用以下Extra Specs键：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">键</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cpu_shares</td>
<td>占用CPU时间的权重，不是绝对值。一个设置了2048的VM，会比1024的多用一倍的CPU时间</td>
</tr>
<tr>
<td>cpu_period</td>
<td rowspan="2">单位微秒，限制在指定周期（period）内占用CPU带宽（时间）最大值（quota）</td>
</tr>
<tr>
<td>cpu_quota</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络限速</span></div>
<p>可以使用以下Extra Specs键：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">键</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>vif_inbound_average</td>
<td rowspan="6">
<p>inbound表示入站流量限制，outbound表示出站流量限制</p>
<p>average: 期望的平均带宽KB/s<br />peak：最大的峰值带宽KB/s<br />burst：以峰值带宽最多连续发送多少KB</p>
</td>
</tr>
<tr>
<td>vif_outbound_average</td>
</tr>
<tr>
<td>vif_inbound_peak</td>
</tr>
<tr>
<td>vif_outbound_peak</td>
</tr>
<tr>
<td>vif_inbound_burst</td>
</tr>
<tr>
<td>vif_outbound_burst</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="neutron-qos"></a>Neutron QoS</span></div>
<p>所谓<span style="background-color: #c0c0c0;">QoS</span>，是指能够确保一定的网络需求的能力，这些网络需求包括<span style="background-color: #c0c0c0;">带宽、延迟、抖动、可靠性</span>。</p>
<p>交换机、路由器之类的网络设备，能够给流量打标记，并以更高的优先级来处理这些流量，以满足应用提供者和终端用户之间的SLA（Service Level Agreement ）。</p>
<p>在Neutron中，qos是一个高级的插件，它将QoS从OpenStaack网络代码的其他部分解耦出来，它从多个网络级别进行QoS控制，可以通过ml2扩展驱动的方式来使用。目前qos仅仅支持ml2（的SR-IOV, Open vSwitch, linuxbridge驱动）</p>
<div class="blog_h3"><span class="graybg">配置</span></div>
<p>在控制节点，配置Neutron：</p>
<pre class="crayon-plain-tag">service_plugins = router,metering,qos</pre><br />
<pre class="crayon-plain-tag">[ml2]
extension_drivers = port_security,qos</pre>
<p>修改你所使用的网络代理的配置，配置文件位于 /etc/neutron/plugins/ml2/&lt;agent_name&gt;_agent.ini。例如：</p>
<pre class="crayon-plain-tag">[agent]
extensions = qos</pre>
<p>在计算/网络节点，修改网络代理配置：</p>
<pre class="crayon-plain-tag">[agent]
extensions = qos</pre>
<p>如果希望给浮动IP配置QoS，需要配置L3代理：</p>
<pre class="crayon-plain-tag">[agent]
;                    如果需要支持路由器网关地址的QoS
extensions = fip_qos,gateway_ip_qos

; 使用Open vSwitch时，由于rate limit对于OVS内部端口不工作，作为变通方法，添加配置
ovs_use_veth = True</pre>
<p>Differentiated Services（DS，DiffServ）是一种在IP网络中分类、管理网络流量并提供QoS的机制。DS在保障关键流量（例如VoIP、视频流）低延迟的同时，为非关键服务（例如Web流量）提供best-effort服务。</p>
<p>DS在8bit的IP头字段differentiated services（DS）字段（用于替换过时的IPv4 TOS字段）中，写入6bit的DSCP（Differentiated Services Code Point）。<a href="https://tools.ietf.org/html/rfc4594">RFC 4594</a>对各个DSCP值由规定，例如广播食品CS3的DSCP为24。</p>
<p>当使用VxLAN之类的Overlay网络时，DHCP标记仅仅应用在内层IP头上。在封装期间，DSCP标记不会自动拷贝到外层包头。为了自动在外层包头设置DSCP，需要配置网络代理：</p>
<pre class="crayon-plain-tag">[agent]
dscp = 8
dscp_inherit = true</pre>
<div class="blog_h3"><span class="graybg">实现</span></div>
<p>OpenStack会将QoS规则映射为底层代理的配置。如果使用Linux Bridge作为网络代理：</p>
<pre class="crayon-plain-tag"># 入站
tc qdisc show dev tap2e939f9e-9e
# qdisc tbf 8002: root refcnt 2 rate 512Kbit burst 16Kb lat 50.0ms

# 出站
tc filter show dev tap2e939f9e-9e parent ffff:
# filter protocol all pref 49 basic chain 0
# filter protocol all pref 49 basic chain 0 handle 0x1
#   police 0x1 rate 1024Kbit burst 32Kb mtu 64Kb action drop overhead 0b
#     ref 1 bind 1

# DSCP
iptables -t mangle -nL neutron-linuxbri-qos-o2e939f
Chain neutron-linuxbri-qos-o2e939f (1 references)
target prot opt source destination
DSCP all -- 0.0.0.0/0 0.0.0.0/0 DSCP set 0x10</pre>
<div class="blog_h3"><span class="graybg">使用</span></div>
<p>使用默认的policy.json时，仅仅管理员能够创建QoS策略。 下面是个例子：</p>
<pre class="crayon-plain-tag"># 创建一个规则
openstack network qos policy create bw-limiter
# 设置出站带宽限制
openstack network qos rule create --type bandwidth-limit --max-kbps 3000
  --max-burst-kbits 2400 --egress bw-limiter


# 创建一个DSCP标记规则
openstack network qos policy create dscp-marking
# 设置DSCP值
openstack network qos rule create --type dscp-marking --dscp-mark 26 \
    dscp-marking</pre>
<p>注意：对于OVS和Linux Bridge来说，QoS实现需要burst值才能确保正确的带宽限制行为。设置合理的burst值非常重要，如果仅设置bandwidth-limit，即使它的值是合理的，带宽也会被throttled。对于TCP流量来说，<span style="background-color: #c0c0c0;">推荐burst为bandwidth-limit的80%</span>，如果burst设置的过低，则实际获得的带宽可能小于预期。</p>
<p>为某个网络端口配置/解除QoS策略：</p>
<pre class="crayon-plain-tag"># 设置
openstack port set --qos-policy bw-limiter 88101e57-76fa-4d12-b0e0-4fc7634b874a
# 解除
openstack port unset --qos-policy 88101e57-76fa-4d12-b0e0-4fc7634b874a

# 创建端口时可以直接指定QoS策略
openstack port create --qos-policy bw-limiter --network private port1</pre>
<p>你也可以将浮动IP、网络关联到QoS策略：</p>
<pre class="crayon-plain-tag"># 关联浮动IP到QoS策略
openstack floating ip create --qos-policy bw-limiter public
openstack floating ip set --qos-policy bw-limiter d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
# 解除关联
openstack floating ip set --no-qos-policy d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
openstack floating ip unset --qos-policy d0ed7491-3eb7-4c4f-a0f0-df04f10a067c
# 关联整个网络到QoS策略
openstack network set --qos-policy bw-limiter private</pre>
<p>关联到浮动IP的QoS策略，将在浮动IP挂载到某个端口的时候生效。</p>
<p>每个项目可以配置一个默认QoS策略：</p>
<pre class="crayon-plain-tag">openstack network qos policy create --default bw-limiter</pre>
<p>你可以在<span style="background-color: #c0c0c0;">运行时动态修改QoS策略的规则，这些修改会传播到每个Attach到策略的端口</span>：</p>
<pre class="crayon-plain-tag"># 修改规则
openstack network qos rule set --max-kbps 2000 --max-burst-kbits 1600 \
  --ingress bw-limiter 92ceb52f-170f-49d0-9528-976e2fee2d6f</pre>
<div class="blog_h1"><span class="graybg">诊断和调试</span></div>
<div class="blog_h2"><span class="graybg">日志级别</span></div>
<p>要调整Nova日志级别，修改配置：</p>
<pre class="crayon-plain-tag">log_config_append=/etc/nova/logging.conf</pre>
<p>在/etc/nova/logging.conf中配置日志级别： </p>
<pre class="crayon-plain-tag">[logger_nova]
level = INFO
handlers = stderr
qualname = nova</pre>
<div class="blog_h1"><span class="graybg">新特性</span></div>
<div class="blog_h2"><span class="graybg">12.Queens</span></div>
<div class="blog_h3"><span class="graybg">Cinder Multi-Attach</span></div>
<p>此功能使用户能够将单个块存储卷挂载到多个服务器，以及从多个服务器访问单个块存储卷。此功能的用例包括 active-active 和 hot-standby 场景——有多台服务器需要访问卷上的数据，以在出现故障时快速恢复或能够处理系统中增加的负载。在 Queens 发行版中，有三个驱动程序支持 multi-attach ：LVM、NetApp / SolidFire 和 Oracle ZFSSA</p>
<div class="blog_h3"><span class="graybg">vGPU支持</span></div>
<p>在 Nova 中，对 vGPU 的支持让云管理员能够定义 flavor 以请求 vGPU 的特定资源和分辨率。最终用户可以启动具有 vGPU 的虚拟机，这对于图像密集型工作负载以及人工智能和机器学习工作负载来说是一项重要的功能</p>
<div class="blog_h3"><span class="graybg">Cyborg</span></div>
<p>Cyborg是用于管理硬件和软件加速资源（如 GPU，FPGA，CryptoCards 和 DPDK/SPDK）的架构，对NFV工作负载的电信公司而言，加速是一项必备的功能。通过Cyborg，运维人员可以列出、识别和发现加速器，将加速器连接到实例并将其分离、安装和卸载驱动器。它可以单独使用，或与 Nova 或 Ironic 结合使用</p>
<div class="blog_h3"><span class="graybg"> Ironic 救援模式</span></div>
<p>之前在 Nova 中可以实现虚拟机实例修复，现在 Ironic 中可以实现裸机实例修复。运维人员可以对错误配置的裸机节点进行故障排除，或从诸如丢失的SSH密钥等问题中恢复</p>
<div class="blog_h3"><span class="graybg">Kuryr CNI 守护进程</span></div>
<p>OpenStack是在私有云中部署容器的首选平台，Queens版本扩展了微服务功能。Kuryr 增加了一个 CNI 守护进程来增加 Kubernetes 操作的可扩展性。为了支持高可用（HA），CNI 守护进程能够监控 Pod 事件，不需要为每个事件等待 Kubernetes API。即便控制器宕机了，也可以创建 Pod。</p>
<div class="blog_h3"><span class="graybg">Zun容器服务</span></div>
<p>Zun 是一个新 的OpenStack 项目，它允许用户无需管理服务器或集群即可快速启动和运行容器。它通过与 Neutron，Cinder，Keystone 和其他核心 OpenStack 服务集成，无缝地将先进的企业网络、存储和身份验证功能添加到容器中</p>
<div class="blog_h3"><span class="graybg"> OpenStack-Helm</span></div>
<p>提供了一系列 Helm 图表和工具，用于在 Kubernetes之 上管理 OpenStack的生命周期，并将 OpenStack 作为独立服务运行</p>
<div class="blog_h3"><span class="graybg">LOCI</span></div>
<p>OCI 生成兼容 Open Container Initiative 的 OpenStack 服务镜像，可以放入像 OpenStack-Helm 这样的重量级部署工具，或者单独使用来交付像 Cinder 块存储这样的独立服务。 LOCI 提供了现有 OpenStack Kolla 项目的一种替代方案（为每个容器镜像提供一个更完整的打包方法）。LOCI 采取的方法更符合 Kubernetes 运行镜像的方式，其中容器本身非常小，管理位于容器外部</p>
<div class="blog_h2"><span class="graybg">14.Stein</span></div>
<div class="blog_h3"><span class="graybg">容器功能的强化</span></div>
<p>提供运行容器所需的裸机和网络功能：</p>
<ol>
<li>OpenStack Magnum，经过认证的Kubernetes安装程序，显著提升了Kubernetes集群的启动时间—无论节点数量多少，每个节点从10-12分钟降至5分钟</li>
<li>通过OpenStack云供应商，您现在可以在Manila、Cinder和Keystone服务的支持下启动完全集成的Kubernetes集群，从而充分利用其底层的OpenStack云平台</li>
<li>Neutron，OpenStack网络服务，针对在组中创建端口的容器用例，更快速的创建批量端口</li>
<li>Ironic，裸机配置服务，持续改进部署模板，以便于独立用户请求分配裸机节点并提交配置数据，而不需要预先配置驱动器</li>
</ol>
<div class="blog_h3"><span class="graybg">网络功能强化</span></div>
<ol>
<li>
<p>Neutron，网段范围管理，云管理员可通过新的扩展API动态管理网段范围，而不是采用之前编辑配置文件的方法。StarlingX和边缘用例将得益于此，更易于管理</p>
</li>
<li>
<p>对于网络密集型应用程序，拥有最小可用网络带宽至关重要。在Rocky周期中开始工作，提供基于最小带宽需求的调度，该功能已在Stein中交付。作为强化功能的一部分，Neutron将带宽视为一种资源，并与OpenStack Nova计算服务协作，将实例调度到满足其带宽需求的主机上</p>
</li>
<li>
<p>对API的改进增加了OpenStack体系结构和部署的灵活性，增加了对服务质量（QoS）策略规则aliases的支持，使调用者能够更高效地执行删除、显示和更新QoS规则等请求</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">增强资源管理</span></div>
<ol>
<li>
<p>Blazar，资源预留服务，引进了新的资源分配API，运营商可查询其云计算资源的保留状态</p>
</li>
<li>
<p>Placement是引入Stein版本的一个新项目，是从Nova项目中分离出来的。可定位候选资源供应商，简化了为工作负载迁移指定主机的任务。对于常见的调度操作，API性能提升了50%。Train版本中将删除Nova中的Placement服务，其后安装Nova需要使用单独的Placement服务</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">15.Train</span></div>
<div class="blog_h3"><span class="graybg">增强安全性</span></div>
<ol>
<li>支持软件RAID：具有Ironic 裸机服务可保护服务免受磁盘故障的影响。欧洲核研究组织欧洲核子研究组织（CERN）领导了此功能的上游开发，并且已经将该功能在超过1000个节点上投入生产</li>
<li>基于硬件的加密：Nova 是OpenStack的计算功能， 其新框架支持对guest存储器进行基于硬件的加密，以保护用户免遭攻击者或流氓管理员在使用libvirt计算驱动程序时窥探其工作负载。此功能对于多租户环境和具有可公开访问的硬件的环境很有用</li>
<li>数据保护流程：Karbor 为检查，还原，计划和触发操作添加了事件通知。此功能允许用户使用位于根磁盘上的新添加的数据备份映像引导服务器</li>
</ol>
<div class="blog_h2"><span class="graybg">16.Ussuri</span></div>
<p>要求CentOS 8</p>
<div class="blog_h3"><span class="graybg">增强AI支持</span></div>
<p>完成了Nova Cyborg Interaction功能，使两者在某些方面进行紧密的联系，用于启动和管理具有GPU等加速器的实例</p>
<div class="blog_h3"><span class="graybg">增强安全性</span></div>
<ol>
<li>
<p>Nova API策略引入了具有scope_type功能的新默认角色</p>
</li>
<li>
<p>Ironic及其远程代理之间的交互身份验证得到了补充</p>
</li>
<li>
<p>Kolla 添加了对后端API服务的TLS加密的初始支持</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">优化用户体验</span></div>
<ol>
<li>
<p>Glance 优化了对Multiple Strores的操作，单次操作，后台同步</p>
</li>
<li>
<p>Keystone对创建应用程序凭据和信任关系的用户体验进行了极大改善</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Cinder增强</span></div>
<ol>
<li>
<p>为volume-type设置最大和最小的Size</p>
</li>
<li>
<p>使用时间比较运算符过滤卷列表的能力</p>
</li>
<li>
<p>将卷上载到Image Service时，支持Glance multistore和镜像数据托管</p>
</li>
<li>
<p>添加了一些新的后端驱动程序，并且许多当前的驱动程序都增加了对更多功能的支持</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Cyborg增强</span></div>
<ol>
<li>
<p>随着Nova-Cyborg集成的完成，用户现在可以使用由Cyborg管理的加速器启动实例</p>
</li>
<li>
<p>实现了新的API，用以列出由Cyborg管理的设备，可以查看和管理加速器的列表</p>
</li>
<li>
<p>Cyborg通过在v2 API中采用microversions的模式，旨在将来版本中提供向后兼容的办法</p>
</li>
<li>
<p>Cyborg客户端现在基于OpenStack SDK，并支持大多数Version 2 API</p>
</li>
<li>
<p>通过增加更多的单元/功能测试并减少技术负担来提高总体质量</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Glance增强</span></div>
<ol>
<li>
<p>增强了Multiple Stores功能，用户现在可以向多个Stores导入单个镜像，在多个Stores中复制现有的imgae，并从单个Store中删除镜像</p>
</li>
<li>
<p>新导入了插件以解压镜像</p>
</li>
<li>
<p>再次为glance-store引入了S3 driver</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Horizon增强</span></div>
<ol>
<li>
<p>支持范围内省的规则，该规则允许每个节点子集具有（并保留）规则，例如不同的硬件交付</p>
</li>
<li>
<p>支持硬件退役工作流程，以实现托管云中硬件退役的自动化</p>
</li>
<li>
<p>非管理员使用Ironic可以使用多租户概念和其他策略选项</p>
</li>
<li>
<p>Ironic及其远程代理之间的交互身份验证得到了补充，从而可以在不受信任的网络上进行部署</p>
</li>
<li>
<p>UEFI和设备选择现在可用于软件RAID</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Keystone增强</span></div>
<ol>
<li>
<p>使用联合身份验证方法时，用于创建应用程序凭据和信任关系的用户体验已得到极大改善。角色分配来自映射的组成员身份的联盟用户将在令牌过期后将这些组成员身份保留为可配置的TTL，在此期间其应用程序凭证将保持有效</p>
</li>
<li>
<p>现在，可以通过在Keystone中直接创建联盟用户并将其链接到其身份提供者，而无需依赖于映射API，就可以为联盟用户指定具体的角色分配</p>
</li>
<li>
<p>当引导新的Keystone部署时，管理员角色现在默认设置为“ immutable”选项，这可以防止意外删除或修改它，除非有意删除了“ immutable”选项</p>
</li>
<li>
<p>Keystonemiddleware不再支持Identity v2.0 API，该身份在先前的发行周期中已从keystone中删除</p>
</li>
<li>
<p>恢复资源驱动程序的可配置性，因此，如果内置sql驱动程序不满足业务要求，现在可以创建自定义资源驱动程序</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Kolla/容器化部署</span></div>
<ol>
<li>
<p>所有镜像，脚本和Ansible剧本都使用Python 3，并且对Python 2的支持也已删除</p>
</li>
<li>
<p>添加了对CentOS 8主机和镜像的支持</p>
</li>
<li>
<p>添加了对后端API服务的TLS加密的初始支持，从而提供了API流量的端到端加密。目前支持Keystone</p>
</li>
<li>
<p>增加了对开放虚拟网络（OVN）部署以及与Neutron集成的支持</p>
</li>
<li>
<p>增加了对部署Zun CNI（容器网络接口）组件的支持，从而使带有容器的Docker可以支持Zun capsules(pods)</p>
</li>
<li>
<p>添加了对Elasticsearch Curator的支持，以帮助管理集群日志数据</p>
</li>
<li>
<p>添加了将Mellanox网络设备与Neutron一起使用所必需的组件</p>
</li>
<li>
<p>简化了外部Ceph集成的配置，可以轻松地从Ceph-Ansible部署的Ceph集群过渡到在OpenStack中启用它</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Kuryr连接到容器网络</span></div>
<ol>
<li>
<p>支持IPv6</p>
</li>
<li>
<p>DPDK支持嵌套设置以及其他各种与DPDK和SR-IOV相关功能的改进</p>
</li>
<li>
<p>与NetworkPolicy支持相关的多个修复程序</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Manila共享文件系统</span></div>
<ol>
<li>
<p>共享组已从试验性功能逐渐发展成熟。从API版本2.55开始，不再需要X-OpenStack-Manila-API-Experimental标头来创建/更新/删除共享组类型，组规范，组配额和共享组本身</p>
</li>
<li>
<p>兼容时，可以从跨存储池的快照创建共享。这项新功能可以通过分散先前局限于托管快照的后端的工作负载来更好地利用后端资源</p>
</li>
<li>
<p>引入了新的配额控制机制，以限制项目及其用户可创建的共享副本的数量和大小</p>
</li>
<li>
<p>现在可以按时间间隔查询异步用户消息</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Neutron增强</span></div>
<ol>
<li>
<p>OVN驱动程序现在已合并到Neutron存储库中，并且是核心 Neutron ML2 drivers之一，例如linuxbridge或openvswitch。与openvswitch驱动程序相比，OVN驱动程序的优点包括具有分布式SNAT流量的DVR，分布式DHCP以及无需网络节点即可运行的可能性。当然其他ML2驱动程序仍然受到完全支持。当前默认代理还是openvswitch，但计划是使用OVN驱动程序成为将来的默认选择</p>
</li>
<li>
<p>添加了对无状态安全组的支持。用户现在可以将安全组集创建为无状态，这意味着conntrack将不会用于该组中的任何规则。一个端口只能使用无状态或有状态安全组。在某些用例中，无状态安全组将允许操作员选择优化的数据路径性能，而有状态安全组会在系统上施加额外的处理</p>
</li>
<li>
<p>已添加用于地址范围和子网池的基于角色的访问控制（RBAC）。地址范围和子通常由运营商定义并向用户公开。此更改使操作员可以在地址范围和子网池上使用更精细的访问控制</p>
</li>
<li>
<p>Neutron API中添加了对创建过程中标记资源的支持。用户现在可以设置资源标签，例如直接在POST请求中移植端口。这将大大提高kubernetes网络操作的性能。API调用的数量，例如Kuryr已发送给Neutron的邮件大大减少</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Nova增强</span></div>
<ol>
<li>
<p>Nova不再支持Python 2，Python 3.6和3.7则受支持</p>
</li>
<li>
<p>支持在Nova cells间进行冷迁移和重新调整虚拟机大小</p>
</li>
<li>
<p>支持precaching glance image到计算节点</p>
</li>
<li>
<p>支持在创建虚拟机时通过Cyborg来附加加速设备</p>
</li>
<li>
<p>进一步支持QOS最小的带宽功能(拓展了以下操作evacuate、live migrate、unshelve)</p>
</li>
<li>
<p>支持nova-manage placement auditCLI，以查找和清理孤立的资源分配</p>
</li>
<li>
<p>Nova API策略引入了具有scope_type功能的新默认角色。这些新更改提高了安全级别和可管理性。在处理具有“读取”和“写入”角色的系统和项目级别令牌的访问权限方面，新策略更加丰富</p>
</li>
<li>
<p>从卷启动的虚拟机能够使用Rescue操作，允许将稳定的磁盘设备连接到救援实例</p>
</li>
<li>
<p>计算节点支持多种虚拟GPU类型</p>
</li>
<li>
<p>移除os-consoles和os-networksREST APIs</p>
</li>
<li>
<p>移除nova-dhcpbridge、nova-console、nova-xvpvncproxy服务</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Octavia增强</span></div>
<p>Octavia提供负载均衡服务</p>
<ol>
<li>
<p>Octavia现在支持在特定可用性区域中部署负载平衡器。这允许将负载平衡功能部署到边缘环境</p>
</li>
<li>
<p>Octavia amphora驱动程序已添加了一项技术预览功能，可以改善控制平面的弹性。如果控制平面主机在负载均衡器配置操作期间发生故障，备用控制器可以恢复进行中的配置并完成请求</p>
</li>
<li>
<p>用户现在可以指定侦听器和池可接受的TLS密码。这允许负载平衡器强制执行安全合规性要求</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Placement增强</span></div>
<p>Placement 放置服务，通过使分配重试计数可配置，提高了常见的并发分配写入次数（例如繁忙的群集管理程序）情况下的鲁棒性</p>
<div class="blog_h3"><span class="graybg">Swift增强</span></div>
<ol>
<li>
<p>为Swift容器和对象添加了新的系统命名空间</p>
</li>
<li>
<p>使用新的名称空间添加了新的Swift对象版本API</p>
</li>
<li>
<p>添加了对使用新API的S3版本控制的支持</p>
</li>
<li>
<p>添加了使用SIGUSR1执行“无缝”重载的功能，其中WSGI服务器套接字从不停止接受连接</p>
</li>
</ol>
<div class="blog_h2"><span class="graybg">17.Victoria</span></div>
<div class="blog_h3"><span class="graybg">Cinder增强</span></div>
<ol>
<li>改进了对配置的默认卷类型的处理，并使用microversion 3.62添加了新的块存储API调用，可以为单个项目设置项目级别的默认卷类型</li>
<li>添加了一些新的后端驱动程序，同时当前的大部分驱动程序都添加了对更多功能的支持。例如，NFS驱动程序现在支持卷加密</li>
<li>使用流行的Zstandard压缩算法，增加了对cinder备份的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Cyborg增强</span></div>
<ol>
<li>自Ussuri发行以来，用户可以使用由Cyborg管理的加速器启动实例，该发行版还支持两项操作Rebuild and Evacuate</li>
<li>Cyborg支持新的加速器驱动程序（Intel QAT和Inspur FPGA），并达成协议，希望实施新驱动程序的供应商至少应提供完整的驱动程序报告结果</li>
<li>支持Program API，现在，用户可以在给定预加载的bitstream的情况下对FPGA进行编程</li>
<li>在此版本中，部分实施了针对cyborg的策略刷新（带有作用域的RBAC）（设备配置文件API），我们在基本策略和device_profile策略中实现了新的默认规则，并为所有策略添加了基本测试框架。对于向后兼容性，将旧规则保留为不推荐使用的规则，并使用与当前相同的默认值，以便现有部署将保持原样运行。实施所有功能后，我们将为用户提供两个周期的过渡期</li>
</ol>
<div class="blog_h3"><span class="graybg">Glance增强</span></div>
<ol>
<li>增强了多个商店功能，管理员现在可以设置策略以允许用户复制其他租户拥有的镜像</li>
<li>概览允许配置多cinder存储</li>
<li>一目了然的RBD和Filesystem驱动程序现在支持稀疏镜像上传</li>
<li>增强了RBD驱动程序块上传镜像</li>
</ol>
<div class="blog_h3"><span class="graybg">Ironic增强</span></div>
<ol>
<li>部署步骤工作将基本部署操作分解为多个步骤，现在还可以包括部署时支持的RAID和BIOS接口的步骤</li>
<li>一个agent电源接口支持在没有基板管理控制器的情况下进行资源调配操作</li>
<li>现在可以将Ironic配置为进行HTTP Basic身份验证，而无需其他服务</li>
<li>添加了对Redfish虚拟介质的基于DHCP的部署的初始支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Kolla增强</span></div>
<ol>
<li>添加了对Ubuntu Focal 20.04的支持</li>
<li>添加了对后端API服务的TLS加密的附加支持，从而提供了API流量的端到端加密</li>
<li>增加了对核心OpenStack服务的容器健康检查的支持</li>
<li>添加了对etcd的TLS加密的支持</li>
<li>改善了Ansible Playbook的性能和可伸缩性</li>
<li>增加了对将Neutron与Mellanox InfiniBand集成的支持</li>
<li>为Kayobe添加了对在neutron上部署自定义容器的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Kuryr增强</span></div>
<ol>
<li>Kuryr将不再使用注释在k8s api中存储关于OpenStack对象的数据。而是创建了一个相应的CRD，即KuryrPort、KuryrLoadBalancer和KuryrNetworkPolicy</li>
<li>增加了在嵌套设置中自动检测虚拟机桥接接口的支持</li>
</ol>
<div class="blog_h3"><span class="graybg">Manila增强</span></div>
<ol>
<li>租户驱动的共享复制，数据保护，灾难恢复和高可用性的自助服务现已普遍可用并得到完全支持</li>
<li>共享服务器迁移现在作为一个实验性功能提供。共享服务器通过隔离网络路径中的共享文件系统来提供多租户保证。在这个版本中，云管理员能够将共享服务器移动到不同的后端或共享网络</li>
</ol>
<div class="blog_h3"><span class="graybg">Neutron增强</span></div>
<ol>
<li>现在可以通过IPv6使用元数据服务。用户现在可以在仅IPv6的网络中使用不带配置驱动器的元数据服务。</li>
<li>flat已为分布式虚拟路由器（DVR）添加了对网络的支持。</li>
<li>OVN后端增加了对浮动IP端口转发的支持。现在，在Neutron中使用OVN后端时，用户可以为浮动IP创建端口转发。</li>
<li>在OVN中增加了对路由器可用区域的支持。OVN驱动程序现在可以从路由器的Availability_zone_hints字段中读取，并使用给定的可用区域相应地调度路由器端口</li>
</ol>
<div class="blog_h3"><span class="graybg">Nova增强</span></div>
<ol>
<li>Nova支持在同一nova服务器中混合使用绑定和浮动CPU</li>
<li>Nova支持通过提供程序配置文件来自定义计算节点的放置资源清单</li>
<li>即使使用Glance多存储 配置，Nova也支持 从Ceph RBD群集快速克隆Glance镜像 </li>
<li>Nova支持使用虚拟TPM设备创建服务器</li>
</ol>
<div class="blog_h3"><span class="graybg">Octavia增强</span></div>
<ol>
<li>用户现在可以指定侦听器和池接受的TLS版本。用户现在还可以设置其部署可接受的最低TLS版本</li>
<li>Octavia现在使用新的侦听器应用程序层协议协商（ALPN）配置选项来支持TLS上的HTTP/2</li>
<li>现在可以将负载均衡器统计信息同时报告给多个统计信息驱动程序，并支持增量指标。这样可以更轻松地集成到外部度量系统中，例如时间序列数据库</li>
<li>用于amphora驱动程序的Octavia flavors现在支持将glance image标记指定为flavor的一部分。这允许用户定义Octavia flavor来引导备用的amphora镜像</li>
<li>负载平衡器池现在支持PROXY协议的版本2。使用TCP协议时，这允许将客户端信息传递到成员服务器。PROXYV2提高了使用PROXY协议与成员服务器建立新连接的性能，尤其是在侦听器使用IPv6的情况下</li>
</ol>
<div class="blog_h3"><span class="graybg">Swift增强</span></div>
<ol>
<li>改进了读取纠错码数据时的第一字节延迟</li>
<li>当使用单独的复制网络运行时，后台守护程序和代理服务器之间的隔离度增加</li>
<li>我们开始看到生产集群从python2下运行Swift过渡到python3 </li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">元数据相关</span></div>
<div class="blog_h3"><span class="graybg">The remote metadata server responded with Forbidden</span></div>
<p>Neutron元数据代理日志（/var/log/neutron/metadata-agent.log）报错：The remote metadata server responded with Forbidden. This response usually occurs when shared secrets do not match.</p>
<p>可能原因：元数据代理配置文件metadata_agent.ini中的metadata_proxy_shared_secret和nova.conf中配置的不一样。</p>
<div class="blog_h2"><span class="graybg">仪表盘相关</span></div>
<div class="blog_h3"><span class="graybg"> You have offline compression enabled but</span></div>
<p>报错示例：compressor.exceptions.OfflineGenerationError: You have offline compression enabled but key "xxx" is missing from offline manifest. You may need to run "python manage.py compress". Here is the original content</p>
<p>解决办法：</p>
<pre class="crayon-plain-tag">COMPRESS_OFFLINE = False</pre>
<p>然后重启httpd</p>
<div class="blog_h2"><span class="graybg">网络相关</span></div>
<div class="blog_h3"><span class="graybg">pyroute2.netlink.exceptions.NetlinkError: (13, 'Permission denied')</span></div>
<p>禁用IPv6可以消除此错误：</p>
<pre class="crayon-plain-tag">net.ipv6.conf.all.disable_ipv6 = 1</pre>
<p>linux-bridge从设计上禁用了IPv6，如果物理网卡上配置了IPv6地址，则neutron/root没有权限将IPv6地址从物理网卡移动到linux-bridge上。</p>
<p>尽管如此，linux-bridge仍然会将IPv6的L2帧转发，你仍然可以在客户机上使用IPv6。</p>
<div class="blog_h3"><span class="graybg">实例的真实IP和Nova中的不一致</span></div>
<p>使用provider网络时，底层网络中存在DHCP服务器时出现此情况。实例向底层网络中的DHCP请求了IP地址，而非DHCP Agent。</p>
<p>建议：使用底层网络，但是分配独立的网段。</p>
<div class="blog_h2"><span class="graybg">镜像相关</span></div>
<div class="blog_h3"><span class="graybg">Image virtual size is 128GB and doesn't fit in a volume of size 12GB</span></div>
<p>需要修改镜像虚拟尺寸，首先检查一下镜像信息：</p>
<pre class="crayon-plain-tag"># 进入镜像存储目录
/var/lib/glance/images/


# 得到镜像ID
openstack image list
# | 379caead-7878-4d82-847d-502feea5b8ed | centos8-amd64-prepared | active |


# 查看镜像信息
qemu-img info 379caead-7878-4d82-847d-502feea5b8ed
# image: 379caead-7878-4d82-847d-502feea5b8ed
# file format: qcow2
# virtual size: 128 GiB (137438953472 bytes)  虚拟尺寸
# disk size: 1.42 GiB
# cluster_size: 65536
# Format specific information:
#     compat: 1.1
#     compression type: zlib
#     lazy refcounts: false
#     refcount bits: 16
#     corrupt: false

# 挂载到虚拟文件系统
export LIBGUESTFS_BACKEND=direct
virt-filesystems --long -h --all -a 379caead-7878-4d82-847d-502feea5b8ed
# Name       Type        VFS  Label  MBR  Size  Parent
# /dev/sda1  filesystem  xfs  -      -    128G  -
# /dev/sda1  partition   -    -      83   128G  /dev/sda
# /dev/sda   device      -    -      -    128G  
# 检查实际用量
virt-df  379caead-7878-4d82-847d-502feea5b8ed 
# Filesystem                           1K-blocks       Used  Available  Use%
# 379caead-7878-4d82-847d-502feea5b8ed:/dev/sda1
#                                      134206444    1678740  132527704    2%</pre>
<p>可以看到这是一个128G的XFS分区，由于XFS分区不支持Shrink，我们可以考虑将其备份，然后还原到一个较小的卷上。</p>
<pre class="crayon-plain-tag"># 从上述镜像创建一个卷，附到/dev/vdb
openstack volume create --size 130 --image centos8-amd64-prepared centos8-amd64 
# 创建一个新的2GB的目标卷，附到/dev/vdc

# 挂载
mount -t xfs /dev/vdb1 /tmp/vdb1                                                                                                                                                      
# mount: /tmp/vdb1: wrong fs type, bad option, bad superblock on /dev/vdb1, 
# missing codepage or helper program, or other error.
# 上述报错的原因是 vda vdb的UUID重复
uuidgen
xfs_admin -U 9e58a8e1-6962-4342-af59-ba10c53626cc /dev/vdb1
mount -t xfs /dev/vdb1 /tmp/vdb1     

# 导出
xfsdump -l 0 -f vdb /dev/vdb1 

# 导入
mkfs.xfs /dev/vdc1
mount /dev/vdc1 /tmp/vdc1
xfsrestore -f vdb /tmp/vdc1</pre>
<p>把这个2G的卷保存为镜像即可。</p>
<div class="blog_h3"><span class="graybg">IOError: 32 Corrupt image download</span></div>
<p>镜像被外部程序修改，建议重新上传镜像。</p>
<div class="blog_h3"><span class="graybg">Force upload to image is disabled, Force option will be ignored.</span></div>
<p>上传卷为镜像时报此错误：Invalid volume: Volume 8d2747cf-b50e-4d36-a90a-33dd7b56cad4 status must be available</p>
<p>解决方案，配置Cinder：</p>
<pre class="crayon-plain-tag">enable_force_upload = true</pre>
<p>重启控制节点服务：</p>
<pre class="crayon-plain-tag">systemctl restart openstack-cinder-volume
systemctl restart openstack-cinder-api
systemctl restart openstack-cinder-scheduler</pre>
<div class="blog_h3"><span class="graybg">Glance metadata cannot be updated, key signature_verified exists for volume</span></div>
<p>从镜像创建卷失败，报如上错误。移除镜像的signature_verified属性即可解决：</p>
<pre class="crayon-plain-tag">openstack image unset --property signature_verified centos8-amd64-prepared </pre>
<div class="blog_h2"><span class="graybg">存储相关</span></div>
<div class="blog_h3"><span class="graybg">Update driver status failed: (config name lvm) is uninitialized.</span></div>
<p><pre class="crayon-plain-tag">openstack volume service list</pre>报告某些节点的cinder-volume处于down状态：</p>
<pre class="crayon-plain-tag">openstack volume service list 
# +------------------+---------------+------+---------+-------+----------------------------+
# | Binary           | Host          | Zone | Status  | State | Updated At                 |
# +------------------+---------------+------+---------+-------+----------------------------+
# | cinder-volume    | centos-11@lvm | nova | enabled | down  | 2021-01-14T10:44:12.000000 |
# | cinder-volume    | centos-13@lvm | nova | enabled | down  | 2021-01-14T10:44:26.000000 |
# | cinder-volume    | centos-12@lvm | nova | enabled | down  | 2021-01-14T10:44:23.000000 |
# | cinder-scheduler | centos-10     | nova | enabled | up    | 2021-01-16T10:14:23.000000 |
# +------------------+---------------+------+---------+-------+----------------------------+</pre>
<p>查看对应节点的/var/log/cinder/volume.log，出现上述报错。</p>
<p>原因：LVM卷组cinder-volumes没有初始化。 </p>
<div class="blog_h2"><span class="graybg">快照相关</span></div>
<div class="blog_h3"><span class="graybg">卡在error_deleting如何强制删除</span></div>
<p>首先重置状态：</p>
<pre class="crayon-plain-tag">cinder snapshot-reset-state --state error  7f5154ca-9413-4071-b99f-ec60626a9efe</pre>
<p>然后登陆数据库删除记录：</p>
<pre class="crayon-plain-tag">use cinder;

update snapshots set deleted=1,status='deleted',deleted_at=now(),updated_at=now() where deleted=0 and id='7f5154ca-9413-4071-b99f-ec60626a9efe';</pre>
<div class="blog_h2"><span class="graybg">Flaver相关</span></div>
<div class="blog_h3"><span class="graybg">设置Flaver限速导致新实例报错 Illegal "rate"</span></div>
<p>报错内容：libvirt.libvirtError: internal error: Child process (tc filter add dev tapaf7dae14-ef parent ffff: protocol all u32 match u32 0 0 police rate 10485760kbps burst 10485760kb mtu 64kb drop flowid :1) unexpected exit status 1: Illegal "rate"</p>
<p>可能原因：限速的值太大了。</p>
<div class="blog_h2"><span class="graybg">环境变量相关</span></div>
<div class="blog_h3"><span class="graybg">nova命令环境变量</span></div>
<pre class="crayon-plain-tag">export OS_USERNAME=admin
export OS_PASSWORD=111111
export OS_TENANT_NAME=admin
export OS_AUTH_URL=http://192.168.101.250:35357/v3
export OS_USER_DOMAIN_NAME=Default                                                                                                                                                  
export OS_PROJECT_DOMAIN_NAME=Default</pre>
<div class="blog_h3"><span class="graybg">openstack命令环境变量</span></div>
<p>如果遇到报错：</p>
<p style="padding-left: 30px;">Ignoring domain related config project_domain_name because identity API version is 2.0</p>
<p style="padding-left: 30px;">Expecting to find domain in project. The server could not comply with the request since it is either malformed or otherwise incorrect. The client is assumed to be in error. (HTTP 400) (Request-ID: req-047e4968-2c06-4501-8635-0dd27093d5d8)</p>
<p>需要增加环境变量：</p>
<pre class="crayon-plain-tag">export OS_IDENTITY_API_VERSION=3</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/openstack-study-note">OpenStack学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/openstack-study-note/feed</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>IaaS知识集锦</title>
		<link>https://blog.gmem.cc/iaas-faq</link>
		<comments>https://blog.gmem.cc/iaas-faq#comments</comments>
		<pubDate>Wed, 01 Nov 2017 08:09:38 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[IaaS]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=29647</guid>
		<description><![CDATA[<p>云的分类 公有云 公有云（Public Cloud）所有硬件、软件和其他支持性基础设施均为云提供商所拥有和管理。在公有云中，你与其他组织或云租户共享相同的硬件、存储和网络设备。 公有云的优势： 成本更低 — 无需购买硬件或软件，仅对使用的服务付费 无需维护 — 维护由服务提供商提供 近乎无限制的缩放性 — 提供按需资源，可满足业务需求 高可靠性 — 具备众多服务器，确保免受故障影响 私有云 私有云（Private Cloud）由专供一个企业或组织使用的云计算资源构成。私有云可在物理上位于组织的现场数据中心，也可由第三方服务提供商托管。但是，在私有云中，服务和基础设施始终在私有网络上进行维护，硬件和软件专供组织使用。这样，私有云可使组织更加方便地自定义资源，从而满足特定的 IT 需求。私有云的使用对象通常为政府机构、金融机构以及其他具备业务关键性运营且希望对环境拥有更大控制权的中型到大型组织。 <a class="read-more" href="https://blog.gmem.cc/iaas-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/iaas-faq">IaaS知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">云的分类</span></div>
<div class="blog_h2"><span class="graybg">公有云</span></div>
<p>公有云（Public Cloud）所有硬件、软件和其他支持性基础设施均为云提供商所拥有和管理。在公有云中，你与其他组织或云租户共享相同的硬件、存储和网络设备。</p>
<p>公有云的优势：</p>
<ol>
<li>成本更低 — 无需购买硬件或软件，仅对使用的服务付费</li>
<li>无需维护 — 维护由服务提供商提供</li>
<li>近乎无限制的缩放性 — 提供按需资源，可满足业务需求</li>
<li>高可靠性 — 具备众多服务器，确保免受故障影响</li>
</ol>
<div class="blog_h2"><span class="graybg">私有云</span></div>
<p>私有云（Private Cloud）由专供一个企业或组织使用的云计算资源构成。<span style="background-color: #c0c0c0;">私有云可在物理上位于组织的现场数据中心，也可由第三方服务提供商托管</span>。但是，在私有云中，服务和基础设施始终在<span style="background-color: #c0c0c0;">私有网络</span>上进行维护，硬件和软件专供组织使用。这样，私有云可使组织更加方便地自定义资源，从而满足特定的 IT 需求。私有云的使用对象通常为政府机构、金融机构以及其他具备业务关键性运营且希望对环境拥有更大控制权的中型到大型组织。</p>
<p>私有云的优势：</p>
<ol>
<li>灵活性更高 — 组织可自定义云环境以满足特定业务需求</li>
<li>安全性更高 — 资源不与其他组织共享，从而可实现更高控制性和安全性级别</li>
<li>缩放性更高 — 私有云仍然具有公有云的缩放性和效率</li>
</ol>
<div class="blog_h2"><span class="graybg">混合云</span></div>
<p>混合云（Hybrid Cloud）将<span style="background-color: #c0c0c0;">本地基础设施或私有云与公有云相结合</span>，组织可利用这两者的优势。在混合云中，数据和应用程序可在私有云和公有云之间移动，从而可提供更大灵活性和更多部署选项。例如，对于基于 Web 的电子邮件等大批量和低安全性需求可使用公有云，对于财务报表等敏感性和业务关键型运作可使用私有云（或其他本地基础设施）。在混合云中，还可选择“<span style="background-color: #c0c0c0;">云爆发</span>”。应用程序或资源在私有云中运行出现需求峰值（例如网络购物或报税等季节性事件）时可选择“云爆发”，此时组织可“冲破”至公有云以使用其他计算资源。</p>
<p>混合云的优势：</p>
<ol>
<li>控制性 — 组织可针对敏感资产维持私有基础结构</li>
<li>灵活性 — 需要时可利用公有云中的其他资源</li>
<li>成本效益 — 具备扩展至公有云的能力，因此可仅在需要时支付额外的计算能力</li>
<li>容易轻松 — 无需费时费力即可转换至云，因为可根据时间按工作负荷逐步迁移</li>
</ol>
<p>本地基础设施/私有云可以通过VPN或者专线连接到公有云。</p>
<div class="blog_h2"><span class="graybg">专有云</span></div>
<p>云服务商提供的，针对每个用户进行资源完全物理隔离的云。</p>
<div class="blog_h2"><span class="graybg">虚拟私有云</span></div>
<p>Virtual Private Cloud（VPC）是AWS 2009年提出的概念。它是一种运行在公有云上，将一部分<span style="background-color: #c0c0c0;">公有云资源为某个用户隔离</span>出来，给这个用户<span style="background-color: #c0c0c0;">私有使用的资源的集合</span>。</p>
<p>VPC硬件租用模式（Hardware Tenancy）有两种：</p>
<ol>
<li>共享（shared）：VPC中的<span style="background-color: #c0c0c0;">虚拟机运行在共享的硬件资源上</span>，不同VPC中的虚拟机通过虚拟网络技术进行隔离</li>
<li>专属（dedicated）：VPC中的虚拟机运行在<span style="background-color: #c0c0c0;">专属的硬件</span>资源上。专属模式相当于用户直接向公有云服务商租用物理主机，适合那些对于数据安全比较敏感的用户</li>
</ol>
<p>在<span style="background-color: #c0c0c0;">VPC内</span>，云服务提供商通常支持<span style="background-color: #c0c0c0;">安全组、子网、ACL、路由表等网络资源</span>。</p>
<div class="blog_h3"><span class="graybg">专有网络</span></div>
<p>从技术角度来说，VPC的关键之处是用户<span style="background-color: #c0c0c0;">专属的一个二层网络</span>。</p>
<p>经典的公有云网络是多用户共享的二层网络，除非有特定的防火墙规则去限制，租户之间是可以通信的，很不安全。VPC解决了这种安全问题。</p>
<p>由于每个租户都需要独立的二层网络，构建这些虚拟二层网络的主要技术包括：</p>
<ol>
<li>VLAN：支持4000个网络</li>
<li>VXLAN：支持1600万个网络</li>
</ol>
<p>由于公有云租户数量极大，因此基于VLAN技术，只能多个租户共享单个二层网络，需要做好IP分配、防火墙规则问题。而使用VXLAN（2014年标准出来）则基本可以保证每个Region中每个租户都有独立网络，也不需要考虑IP重叠问题。</p>
<div class="blog_h3"><span class="graybg">Overlay技术</span></div>
<p>VXLAN是一种Overlay技术，属于软件定义网络（SDN）的一种，租户原始的二层帧，并封装在IP报文中，发送到目的地后，解包还原。这意味着VPC内的主机所依托的物理主机可以不再二层网络中，可以在任意数据中心中分布。</p>
<p>VLAN这样的技术则做不到这一点。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/iaas-faq">IaaS知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/iaas-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
