使用Salt进行基础设施管理的Python开发-Salt被忽视的一面以及最佳实践

我喜欢的一条(还有很多)关于Salt的事情是它本身并没有一种模糊的语言:正如我一直想提到的,只需要YAML和Jinja,每个3条规则,你就可以开始自动化。例如,当你需要一个简单迭代时,并不需要阅读全部文档并查看“在列表中迭代的特定指令”,仅仅是一个简单而直接的Jinja循环,{%- for element in list %}。然而,在某些特殊情况下,Jinja本身可能是不够的,或者是它可能会变得过于复杂和不可读。当我需要处理一个复杂的任务时,我有时会觉得“我最好用Python来写这个,而不是Jinja(或者两者的结合)”。

众所周知,Salt是一个非常灵活的框架,这是因为它的内部结构简单:一个密集的核心,允许添加可插拔的接口。比如说,如果您查看GitHub上的官方存储库,你会注意到一个相当长的目录列表(还有许多其他的目录):acl,auth,engines,beacons,netapi,states等等。它们都只是针对不同子系统的可插拔接口,只是随着功能的增强,这些子系统已经被添加进了系统。

纯Python渲染器

其中一个可插入的接口称为渲染器:这是一个促进低级别输入-输出交互的子系统。对于Salt来说,如果你的文件结构是YAML、JSON或TOML等等,那么这些数据都是用Python对象来表示:您使用的是数据,而不是文本块!

多亏了这个聪明的方法,(大约6年前)添加一个纯Python渲染器当然是最自然的事情了。毫不夸张的说,这就是为什么你今天能够用纯Python来写东西。但是不要随便相信我的话,耐心等待,我将会证明的。

Python支柱

如果您是Salt新手,SLS(SaLtStack)是Salt所使用的文件格式。默认情况下,SLS是Jinja+YAML的结合(首先渲染Jinja),然后生成的YAML被翻译成Python对象,并加载到内存中。让我们考虑以下SLS示例:

在这个简单的例子中,我们先定义一个IP地址列表,就像普通的YAML一样。但是,如果我们希望有一个更长的地址列表,并且我们想要避免手动输入每个地址,会发生什么呢?如前所述,SLS默认是YAML和Jinja的巧妙组合,因此您可以重写等价的SLS,如下所述:

尽管在这个特殊例子中,人们可能会争辩说它似乎并没有大规模地缩小尺寸,但在处理大量数据时确实如此。

请记住:您可以在任何SLS文件中应用完全相同的逻辑,无论是支柱、状态、反应堆还是top.sls文件,等等。

在继续之前,我想澄清的是,Jinja和YAML都属于我之前提到的渲染器界面。换句话说,SLS在默认情况下使用这两个渲染器。

您可以选择任何对您有效的渲染器组合,并且可以轻易地做到,只需在文件的顶部添加一个释伴(#!),并命名您想要使用的渲染器,由管道符(|)分隔。有了这些,默认的标题是#!jinja|yaml。

需要选择Python渲染器的释伴是!#py。在这里惟一的约束条件是,你需要一个名为run的函数来返回所需的数据。例如,上面的例子的等价SLS如下:

/etc/salt/pillar/ip_addresses.sls

就是看上去那样简单:我们编写的是纯Python,可以使用它作为我们系统的输入数据。然后让我们这样做:将这些内容保存到/etc/salt/pillar/ip_addresses.sls,并在柱顶文件(/etc/salt/pillar/top.sls-正如在pillar_roots中主目录中配置的),以这样一种方式,任何被控端都可以读取内容(正是由于*-想获得更多细节请查看顶部文件文档):

/etc/salt/pillar/top.sls

在刷新支柱后(使用saltutil.refresh_pillar执行功能),我们可以验证数据是否可用:

但是等一下:我将柱顶文件定义为默认的SLS文件,纯YAML格式。即使在这个微不足道的示例中,它也有点太过了,如果想要顶部文件可以根据需要动态地生成,是有很好的生产案例,因此我们就有可能动态地将支柱绑定到被控端(或公式,用于状态头部文件):

/etc/salt/pillar/top.sls

通过以上演示,我们已经确认了完全能够只用Python将数据引入到系统中。尽管我目前提供的示例是很微小的,但它们完全可以扩展到更复杂的实现,就像解决问题所需要的一样。

我最喜欢举的一个例子是从外部系统加载支柱的数据,从HTTP API访问https://example.com/api.json(提供数据格式化为JSON):

/etc/salt/pillar/example.sls

通过5行SLS使用Python渲染器,我们可以直接将数据从远程端点引入到Salt中。当然,在实现之前,您需要评估安全性和其他注意事项。此外,当需要处理非常复杂的问题时,你需要从多个角度来看待问题,应该考虑使用外部支柱或外部顶部系统,因为它们是处理输入数据的另一种很好的方式。而且,它们还提供了在常规支柱之前或之后加载的灵活性(使用的主配置选项)。这里没有一个统一的要求:每个特定的案例都必须进行独立分析。在您自己的环境中为外部支柱子系统编写扩展模块同样也是非常容易的。

Python SLS公式

类似地,我们可以完全用Python来编写SLS公式。例如,以下状态SLS(从napalm-ntp-formula中提取):

可以使用py渲染器来重写:

同样,在这个特殊的例子中和它的默认行为相比,Python渲染器并没有能够带来更多的好处,但是它是一个很好的机会,可以看到__salt__(双下划线),它是所有可用的执行函数的散列。当我们的状态需要非常复杂的决策时,编写Python公式被证明是极其有用的。

但是,我更喜欢(而且推荐)将复杂性转移到执行函数中,就像在编写执行模块部分中所描述的那样。

Python模板

是的,你看的没错,不必过分热心。同样在有些情况下,Jinja或其他模板引擎是不够的。比如,当我需要生成包含unicode字符的文本时,我使用它是因为Jinja表现真的很差劲,或者说过于复杂,而在Python中,它就像在文件的顶部添加# -*- coding: utf-8 -*-一样简单。

如果考虑一下,在一天结束时,给定一组输入变量,模板引擎却只返回一组纯文本。使用Python实现这个几乎是毫无价值的。还有另一种特定于Saltpy的渲染器是需要注意的:在哪里找到输入变量。Salt注入一个名为context的全局变量,它是一个包含您发送给模板的所有变量的字典。

考虑下面的模板(文件扩展名实际上并不重要,但是最好保持一致,这样您和您的文本编辑器就可以知道文件格式):

/etc/salt/templates/example.py

这个Python模板可以作为任何其他的模板使用:我们只需要告诉Salt通过py引擎生成文本。我们可以使用net.load_template执行功能从命令行中验证和加载生成的内容(minion1是瞻博网络设备):

或通过状态系统:

/etc/salt/states/example.sls

当执行example.sls公式,它将生成/tmp/ntp_peers.cfg文本文件,处理salt://templates/example.py模板(请记住,salt://指向Salt文件服务器的位置,在本例中是/etc/salt,通过file_roots配置)通过py接口。执行$ sudo salt "minion1" state.sls example,它将生成以下内容:

编写执行模块

执行模块是Salt中最灵活的子系统,这并不是秘密,因为它们允许您重用代码,正是由于它们可以在其他不同的子系统中使用,包括:渲染器(以及隐式的、模板)、状态模块、引擎、重图器、指向标等等。基本上,一旦你编写了一个执行模块,它就可以立即在Salt的命名部分中找到。

编写一个执行模块所需的内容全部都是关于Python的基本知识,通过阅读文档来了解Salt文件系统是如何工作的以及保存文件的位置。假设我们有以下file_roots配置:

然后将一个文件保存在一个名为的目录下,在file_roots中引用的路径中,例如,/etc/salt/_modules:

/etc/salt/_modules/ip_addresses.py

提示

有大量的辅助函数库可以重用。可以在utils目录中找到。花一些时间浏览这个目录和它的文件。别担心,以我的经验来看,要想避免重新发明车轮,你可能需要花上几个月或几年的时间来了解你需要的确切功能。去到那里,完成任务,得到一件“车轮再发明者”t恤。:-)

此外,请记住,您可以从其他执行函数调用执行函数,如下所述。

要让Salt知道还有另一个执行模块要加载,您必须运行saltutil.sync_modules,只要执行新定义的函数(语法是.;在我们的例子中,模块名是文件保存、ip地址和函数名的名称):

注意在最后一个示例中,键值参数长度是从命令行传递到生成函数,之前我们在Python模块中定义了这个名称。

请注意

默认情况下,执行模块的名称只是Python模块(文件)的名称。要使用不同的名称,可以使用。这是一种根据特殊情况而重载的好方法,这是Salt的一种独特能力。更多细节请参考此页。

到这里,我们有了一个为我们自己的环境定义的新函数。它可以从命令行调用,如我们所见,但也可以在不同的区域中使用,如下所述。

在模板中调用执行模块

新ip_addresses.generate执行函数可以从Salt支持的任何模板语言中调用,例如Jinja:

上面的模板将被呈现为:

在支柱SLS中调用执行模块

使用ip_addresses.generate函数,我们可将上面例子支柱重写/etc/salt/pillar/ip_addresses.sls。

可以完全相同的方式在公式中调用执行模块。

从其他Salt模块调用执行模块

一般来说,我们可以使用__salt__(双下划线)来从不同的Salt模块中执行一个功能。

例如,我们可以定义下面的执行模块,该模块将调用ip_address.generate功能:

/etc/salt/_modules/ixp_interfaces.py

注意,在上面的例子中,扩展参数不再是键值对,在执行该函数时,我们总是需要传递一个值:

类似地,我们可以重用在其他子系统中生成的ip_addresses.generate代码,如指向标、引擎、跑者或支柱等。

这样做的另一个好处是您可以控制更简单的各种参数。例如,在我们设计生成函数的方式中,它从10.10.10.0网络返回IP地址;

假设在某一时刻,我们决定从172.17.19.0网络生成地址,我们只有一个地方可以进行调整。进一步说,如果这经常发生变化,我们可以将基础移动到另一个键值对,或者在一个配置选项中:

IP网络作为形参:

/etc/salt/_modules/ip_addresses.py

IP网络作为配置选项:

在第二种方法中,选择__opts__(双下划线)是拥有被控端配置选项的字典(从配置文件中读取/etc/salt/minion,用于普通的被控端,或用于代理被控端的/etc/salt/proxy),并与支柱和谷物数据合并。

如前所述,在您的系统中传播一个变更,只需要调整(代理)被控端配置文件,例如:

(引用)

请注意

在定义您自己的配置选项之前,检查它是否已被定义,避免最终的冲突:配置Salt被控端。

结论

一如既往,在Salt里没有“最佳规则”:Salt是非常灵活的,你的环境决定了什么对你最有意义。Salt不仅向你展示了Python的强大功能,而且从这个角度来看,它也表现得更像Python,并为你提供了以各种方式解决任何问题的方法,而且没有严格的限制。

这就是为什么你总是要评估和决定哪种方法最适合你。

我的建议是尝试将复杂性转移到执行模块中。是的,在你自己的环境中编写更多的扩展模块(对于社区来说,开源那些与您的业务逻辑耦合不高的源代码也是非常不错的)。使用执行函数简化复杂的Jinja模板。为你的团队多写辅助类。保持SLS文件简明扼要。

当SLS(或SLS的逻辑部分),或这模板的长度超过5-10行,这时应该开始产生疑问,并找到优化和使代码可重用的方法。

但是,当您不能将复杂性分解开,或是没有必要转移到其他地方时,你就会再次被覆盖掉,你可以通过使用常规的Python渲染器的强大功能来避免复杂的yaml/jinja(顺便说一下,这无疑是我最爱的接口)。

英文原文:https://mirceaulinic.net/2017-12-19-salt-pure-python/

译者:任宇は神様

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180108A039PT00?refer=cp_1026

同媒体快讯

相关快讯

扫码关注云+社区