运维学习笔记-看看别人家的Puppet代码

这篇博客的目的是通过分析Forge上的Puppet模块来加深一些概念的理解,同时了解一些常用用法。

今天的例子是jfryman-nginx模块,它是原puppetlabs-nginx模块的升级版本,依赖3个Puppet公共模块:puppetlabs-aptpuppetlabs-stdlibpuppetlabs-concat。安装非常方便,puppet module install会自动为你安装所依赖的模块。

> puppet module install jfryman-nginx
/etc/puppet/modules
└─┬ jfryman-nginx (v0.3.0)
     ├── puppetlabs-apt (v2.2.2)                        #Puppet公共模块,提供Debain/Ubuntu下安装包管理功能
     ├── puppetlabs-stdlib (v4.12.0)                   #Puppet公共模块,提供对各种数据类型的操作函数,比如检查,转换之类
     └── puppetlabs-concat (v2.1.0)                  #Puppet公共模块,可以将分散在不同文件中的内容组合到一个目标文件中

模块的目录结构就不在这里累述了,tree /etc/puppet/modules/nginx命令可以显示详细的结构,需要注意的是模块的manifests目录中有以下5个文件:

├── manifests
│   ├── init.pp                             #模块自装载起始文件
│   ├── params.pp                      #参数类定义文件,用于给其他各类的参数变量提供默认值
│   ├── config.pp                        #配置类的定义文件
│   ├── package.pp                    #软件包安装类的定义文件
│   ├── service.pp                      #服务类的定义文件
│   └── ...

任何Forge中的模块,基本都会包含上面这几个文件。这已经成为了一种标准。其好处之一是可以清晰划分从软件包安装,配置到服务启动的各阶段功能;另一方面代码中可以利用类来定义各阶段的依赖关系,因为每个阶段通常都包含多个资源,所以用类定义依赖关系比用资源更加灵活实用。


下面我们看看模块的init.pp

代码片段1:类定义


class nginx (                                                      #nginx类定义
...
  $package_name      = $::nginx::params::package_name,    #用nginx::params类中的变量$package_name给nginx类的$package_name赋默认值
  $package_source    = 'nginx',                           #设置$package_source默认值为'nginx'
  $package_flavor    = undef,                             #设置$package_source默认值undef
...
) inherits ::nginx::params {                                  #nginx类继承nginx::params类

以上是nginx的类定义。不禁要问几个问题

nginx类为什么要定义参数

主要是为了使nginx类更加灵活的适应各种不同的应用场景。调用者可以通过nginx类的参数变量来传递不同的值,从而影响nginx中资源的属性和行为。

nginx为什么要继承nginx::params类

在Puppet中,类继承一般只用于2个目的

    * 重用基类中大部分代码和逻辑,只重写一小部分代码以实现新的功能

    * 利用基类中的预定义变量值给子类的类参数赋值。

这里是第2种情况。nginx会用到的大多数默认值都预定义在nginx::params类中。nginx继承nginx::params类来为自己的参数赋默认值。这也是最常用的赋默认值的方式。

这里专门定义了nginx::params类是因为某些默认值很可能会随目标主机的OS或其他Facts有所差异,所以将相关逻辑隔离在nginx::params类中可以更容易管理代码。比如下面的代码片段是nginx::params类中根据Facts $::osfamily设置hash变量$_module_os_overrides

  case $::osfamily {
    'ArchLinux': {
      $_module_os_overrides = {
        'pid'         => false,
        'daemon_user' => 'http',
      }
    }
    'FreeBSD': {
      $_module_os_overrides = {
        'conf_dir'    => '/usr/local/etc/nginx',
        'daemon_user' => 'www',
        'root_group'  => 'wheel',
      }
    }
...
}

很容易联想到的另一个场景是为类的参数赋初值(注意不是赋默认值),它需要在声明类的时候(注意不是定义类的时候使用resource-like声明方式声明类。比如

  class { '::nginx::service':
    configtest_enable => $configtest_enable,            #用变量$configtest_enable为类参数赋初值
....
  }

类的名字和继承有什么关系?

答案是没有关系。确定继承关系的唯一标准是看类定义时是否使用了inherits关键字。类的名字只和其manifest文件在模块目录结构中的位置有关。比如: 详情请看模块的目录结构

/etc/puppet/modules/nginx/manifests
├── init.pp                                                #nginx类
├── package.pp                                       #nginx::package类
├── package
│   ├── debian.pp                                     #nginx::package::debian类
│   └── redhat.pp                                     #nginx::package::redhat类
...

什么时候应该使用变量的长名字,什么时候使用短名字?

使用短名字访问变量受scope(作用域)的限制。

scope-euler-diagram.png

这张图中有4层作用域

    a. 顶层作用域是top scope,包含Facts和Agent/Master内置变量,以及所有site.pp中节点定义以外的所有变量,表达式,资源类型等等内容

    b. 下面一层是node scope, 也就是site.pp中节点定义所包含的所有内容

    c. 再下面是各种类的定义,在图中包括example:parent,example:four和exmaple:other.这3个类在同一层,但每个类自己是一个单独的作用域。

    d. 最下面一层是example:child,它是example:parent的子类,自己是一个作用域。

底层作用域中的代码可以用变量的短名字访问上层作用域的变量。比如,top scope 有个变量$var, example:child可以直接在自己的代码中用短名字$var使用它,同样方法也可以用来访问node scope和它的父类example:parent中的变量。

注意:这么做的前提条件是本地作用域没有同名的变量。在上面的代码段中,虽然nginx是nginx:params的子类,但因为都定义了同名的变量$package_nam,需要用长名字$::nginx:params::package_name指代nginx:params类中的$package_name变量。

除此之外,只要是访问其他作用域里的变量,都必须用长名字。比如访问同一个模块中的其他类的变量,或者另外一个模块里的的类变量。来看几个例子:   

    a. example::four和example:child在一个模块中,但它既不是example:child的父类,也不是node scope或者top scope, 如果想访问其中的变量,需要这样写

include ::exmaple::four
$myvar=$::example::four::var1。

  b. 我想访问apache模块中的apache::php类里的变量$var1。需要这样写

include ::apache::php
$myvar=$::apache::ph::var1。

在实际的编码中,为了清晰,一般都会在名字左边加::,表示从顶层作用域开始唯一标识一个类或者变量。这就就如同文件路径中的绝对路径,不会造成任何混淆。

代码片段2:依赖关系定义


Class['::nginx::package'] -> Class['::nginx::config'] ~> Class['::nginx::service']

这段代码的意思是nginx::package类必须在nginx::config类之前执行,而nginx::service类必须在nginx::config类之后执行。而且nginx::config类中任何资源的状态发生变化,比如文件内容,nginx::service类中的所有资源都会收到refresh事件。

值得注意的几点是

为什么需要描述依赖关系?

Puppet语言是声明性语言,不描述流程,所以Puppet代码执行的顺序和代码写的顺序经常是不一致的,这就是Puppet中经常提到的 independent of evaluation-order 。当资源或者类有执行的先后顺序时,就需要显性的描述依赖关系。

还有什么方式可以描述依赖关系?

类和资源都可以使用->和~>描述依赖关系(资源与资源,类与类,资源与类之间)。

另一种方法是用元参数require/before/notify/subscribe。这种方法可以在资源声明时说明依赖关系,既可以用于资源,也适用于resource-like声明的类。

还有一种方法是使用require函数(注意不是元参数require)描述类之间的依赖关系。详情请见在线文档

为什么Class首字母大写?

因为是引用(reference),也就是在定义和声明之外的任何场景使用类或者资源时,Class和资源类型关键字的首字母都要大写。

常见的引用场景包括

    a. 类或资源声明时,使用require/before/notify/subscribe来描述依赖关系,比如

file { '/etc/nginx.conf'
require=> Package['nginx']                                #Package首字母大写
}

    b. 资源声明时,引用另外一个资源的属性,这个例子中引用另外一个文件资源的mode属性

file { "/etc/second.conf":
ensure => file,
mode   => File["/etc/first.conf"]["mode"]                #File首字母大写
}

    c. 资源声明后,需要设置或者修改资源的某个属性,比如

File['/etc/nginx.conf'] {                                 #File首字母大写
content => template('nginx/sample.conf'),
}

在引用资源时,如果被引用的资源类型是长名字时(一般是自定义资源类型),所有::分隔的命名空间的首字母都要大写,比如。

Nginx::Resource::Location["${name}-default"] {           #Nginx::Resource::Location各段首字母大写
    location_cfg_prepend => $location_cfg_prepend
}

引用的对象是谁?

类,资源及属性可以被引用。变量不适用。

可以在类或者资源声明前引用他们吗?

答案是可以,可以在类或者资源声明前引用他们。此外,由于在同一个catalog范围的所有资源(标题)和类(名字)必须是唯一的,所以可以在代码的任何部分引用任何类和资源,不受作用域影响。

哪些资源可以处理refresh事件

service, mount和exec资源可以处理refresh事件

service的默认行为是使用init脚本(redhat linux上,脚本在/etc/rc.d/init.d/中)重新启动服务,如果你想避免重启服务, 可以设置restart属性

service {"sshd":
restart=>"service reload sshd"                #收到refresh时,运行reload而不是restart
}

mount的默认行为是重新挂载(umount再mount)

exec的默认行为是重新运行命令。它有两个相关的属性

exec { "/bin/ls ":
refresh   => "ls -l"                             #refresh发生时,运行refresh属性指定的另外一个命令
refreshonly => true,                          #exec只在refresh事件发生时才运行命令
}

代码片段3:调用所需的类


class { '::nginx::service':
    configtest_enable => $configtest_enable,
    service_ensure    => $service_ensure,
    service_restart   => $service_restart,
    service_name      => $service_name,
    service_flags     => $service_flags,
}

这段代码是用resource-like方式声明类。

Puppet支持两种类的声明方式

  include-like方式:使用include, require, contain或者hiera_include关键字,后面跟类的名字来声明类

  resource-like方式:像声明资源一样声明类。

定义和声明有什么区别?

以类举例,定义一般是说明类看起来是什么样子,含有那些资源,声明是设定类参数从而确定类中资源的属性和行为,说明类的依赖关系,然后告诉Puppet在catalog中加入这个类的一个实例。

除了类,自定义的资源类型,函数,Facts,Provider等等也需要先定义,然后声明。

注意:变量不需要定义或者声明,直接赋值就可以了

inlcude-like和resource-like声明类有什么区别?

相比inlcude-like,resource-like最大优势是可以为类赋值,这也是在类声明时为类传递参数的唯一方法。在上面的例子中,=>左边是类参数,右边是参数值。

同时,resource-like也有一个缺点,就是只能声明一次。相比之下,include-like可以声明任意多次。在一个catalog范围内,允许把个resource-like(一次)和include-like(多次)混合使用。

为什么resource-like声明只允许使用一次呢?

OOP中的类的通常有下面4个特性。Puppet中的类不一样,只有前3个特性,且只支持从一个父类继承,不支持从多个父类继承。

            抽象

            封装

            继承

            多态性

原因是Puppet中的类只是一般OOP中的的单实例类(singleton)。

当用include-like方式声明类时,虽然声明了多次,但是在catalog中只会有一个类的实例,Puppet也只会执行这个实例一次。这就是所谓的“可以多次声明,但只应用一次”。因为include-like不传入任何参数,所以这个单实例可满足所有调用者的要求。

resource-like声明可以传入参数,理论上讲,传入不同的参数也就创建出不同的实例。 所以为了保证一个实例,resource-like声明只允许使用一次,只生成一个实例。

在include-like声明方式中,include, require, contain和hiera_include有什么区别吗?

include关键是最简单的声明方式,就是告诉Puppet在catalog中生成一个类的实例。

require关键字除了include的功能,还表明的类的依赖关系

hiera_include关键字是在当Master和Hirea集成时使用,它可以通过Hirea获取类的信息并声明。

contain用在比较特殊的场合。我们会在下面和anchor一起解释。

代码片段4:使用anchor


anchor{ 'nginx::begin':
    before => Class['::nginx::package'],
    notify => Class['::nginx::service'],
}
anchor { 'nginx::end':
    require => Class['::nginx::service'],
}

anchor是Puppet的内置资源类型。上面的代码声明了两个anchor资源,分别是nginx::begin和nginx::end,他们将类nginx::package,nginx::config和nginx::service夹在中间(这3个类在上面已经用->/~>设好了依赖关系),作用是强制这3个类在nginx类开始后执行,并且必须在nginx类退出前全部执行完。

上面的代码等价于

anchor{ 'nginx::begin': }
anchor{ 'nginx::end': }
Anchor['nginx::begin']->Class['::nginx::package']->Class['::nginx::config']->Class['::nginx::service']->Anchor['nginx::end']

为什么需要用anchor呢?

如果代码中只有2层类,比如类A包含类A1和类A2,A1和A2都直接包含资源,且A2依赖A1,那么执行A时Puppet会按照定义好的顺序先执行A1内的资源,再执行A2里的资源。

当类的层次增加时,情况就不同了。比如这个例子

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
 include mysql
}
# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
 include apache
}
# /etc/puppetlabs/puppet/modules/roles/webstack.pp
class roles::ecommerce_app {
 include profiles::dbserver
 include profiles::webserver
 Class['profiles::dbserver'] -> Class['profiles::webserver']
}
#/etc/puppetpabs/puppet/manifests/site.pp
node 'webapp01.puppetlabs.com' {
 include roles::ecommerce_app
}

大多数人的第一感觉上是Puppet会先运行mysql类,然后是apache类,实际结果却不一定,经常会看到相反的顺序,这是因为默认情况下,下层的类(mysql类和apache类)之间不会继承上层类(profiles::dbserver类和profiles::webserver类)之间的依赖关系。

为了使下层的类也能够遵循上层类的顺序执行,需要使用contain或者anchor

a. 使用contain

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
  contain mysql                                 #把include换成contain
}
# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
  contain apache                                #把include换成contain
}

b.使用anchor

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
  anchor{'before_mysql:'} -> class{'mysql':} -> anchor{'after_mysql':}   #声明anchor并把mysql夹在中间
}
# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
  anchor{'before_apache:'} -> class{'apache':} -> anchor{'after_apache':}#声明anchor并把apache夹在中间
}

anchor和contain有什么区别呢?

contain和anchor的效果完全相同。Puppet也同时支持这两种方法。

区别是contian是在Puppet Enterprise 3.2.0 (Puppet 3.4.0)之后才出现的,在此之前只能用anchor. 而且contain后面只能直接跟类的名字,不能跟resource-like类声明。

除此之外,还可以看到一些常用的用法

代码片段5:调用stdlib函数


  validate_string($multi_accept)                        #检查输入字符串是否合法
...
  validate_array($proxy_set_header)                #检查输入数组是否合法
...
  validate_bool($confd_purge)                         #检查输入bool是否合法
...

上面的代码是调用puppetlabs-stdlib模块中携带的函数做各种检查。

puppetlabs-stdlib是Forge上的一个公共模块,提供了很多自定义资源类型,函数,Facts,极大的方便了编程。在编程中也非常常用到。

在使用时,需要先确认puppetlabs-stdlib已经安装在你的系统中(可以用puppet module list检查),如果没有安装,运行puppet module install puppetlabs-stdlib进行安装。

一般情况下,无需显性声明stdlib(include stdlib),可以直接调用其中功能,和使用内置的资源类型,函数,Facts没有区别。

代码片段6:调用concat函数


concat { $config_file:                                    #声明concat资源,指定目标文件,这里是$config_file指代的文件
    owner  => $owner,
    group  => $group,
    mode   => $mode,
    notify => Class['::nginx::service'],
}

concat::fragment { "${name_sanitized}-header":           #声明concat::fragment资源,然后将所需内容写入目标文件。
    target  => $config_file,                             #目标文件是$config_file指代的文件
    content => template('nginx/vhost/vhost_header.erb'), #写入的内容来自于模板'nginx/vhost/vhost_header.erb'
    order   => '001',                                    #说明当前内容在目标文件中的位置。这个数字越小,写入的内容越排在前面
}

concat::fragment { "${name_sanitized}-footer":            #声明另外一个concat::fragment资源,然后将所需内容写入目标文件。  
    target  => $config_file,                              #目标文件是$config_file指代的文件
    content => template('nginx/vhost/vhost_footer.erb'),  #写入的内容来自于模板'nginx/vhost/vhost_footer.erb'
    order   => '699',                                     #说明当前内容在目标文件中的位置。699大于001,所以写在vhost_header.erb写在vhost_header.erb
}

以上代码是将使用puppetlabs-concat模块将vhost_header.erb和vhost_footer.erb的结果输出的$config_file指定的文件中。

puppetlabs-concat也是一个很常用的公共模块,它的作用是将不同的文件的内容排序后输出到一个新的目标文件中。使用puppetlabs-concat与puppetlabs-stdlib方法一致,不再累述。

代码片段7:日志信息函数


warning('$worker_processes must be an integer or have value "auto".')                #输出一条警告信息

除了notify资源,Puppet还有很多内置的函数可以输出不同级别的日志信息,非常方便。

        emerg             #emergency level

        crit                  #critical level

        alert                #alert level

        err                  #error level

        warning          #warning level

        info                #information level

        debug            # debug level

        notice            # notice level

代码片段8:模板


<%- if @listen_ip.is_a?(Array) then -%>                                 #判断listen_ip是不是一个列表(array),listen_ip是外部的变量,所以有@
   <%- @listen_ip.each do |ip| -%>                                         #遍历listen_ip列表。ip代表列表中的当前项,是内部变量,所以他没有@
 listen       <%= ip %>:<%= @listen_port %><% if @listen_options %> <%= @listen_options %><% end %>;    #将内部变量ip, 外部变量listen_port 和listen_options组成一行,写入文件。
   <%- end -%>
 <%- else -%>                                                                        #如果listen_ip只有一个值,不是列表,就不再遍历
 listen       <%= @listen_ip %>:<%= @listen_port %><% if @listen_options %> <%= @listen_options %><% end %>; ##将外部变量listen_ip,listen_port 和listen_options组成一行,写入文件。
   <%- end -%>
<%- end -%>

上面是模板中的一段代码,由ERB(embedded ruby )写成,基本覆盖了比较典型的逻辑和语法。详情请参照在线文档

原创文章,作者:MVP,如若转载,请注明出处:http://www.178linux.com/18103

(0)
MVPMVP
上一篇 2016-06-23
下一篇 2016-06-23

相关推荐

  • CentOS7中nmcli网络管理及使用详解

    一、网络接口配置工具    在CentOS7系统中,强烈推荐使用nmcli管理网卡。下面记录的是nmcli的使用详解。    网络接口配置工具NetworkManager(简称为nmcli),该命令的作用是:可以查询网络连接的状态,也可以用来管理网络(设置系统每个网卡的特性)。该命令如何使用呢,其实可以用"n…

    Linux干货 2016-09-11
  • ​iptables简单介绍

    iptables简单介绍 什么是防火墙? 防火墙是工作在主机或网络边缘,能够对其所匹配到的报文根据事先定义好的规则作出相应处理的组件,可以是软件,也可以是硬件,还能软硬结合实现。 UNIX/Linux中对防火墙的实现 早期在openBSD中通过内核中的ipfw实现简单的数据报过滤功能、后来在Linux 2.2内核中使用ipchains来取代,意为链、后来在L…

    Linux干货 2016-03-31
  • LAMP在三台服务器上部署

    准备:centos7.3.3172.16.254.28 CentOS7.3.1 ip172.16.251.109  centos7.3.2 ip172.16.252.81  部署:         centos7.3.1部署mariadb         centos7.3.2 中部署httpd         centos7.3.3 中部署php-fpm …

    2017-06-08
  • LVM的创建与管理

    一、基本概念     1、LVM是逻辑盘卷管理(LogicalVolumeManager)的简称,它是Linux环境下对磁盘分区进行管理的一种机制,LVM是建立在硬盘和分区之上的一个逻辑层,来提高磁盘分区管理的灵活性。     2、why:通常在安装Linux系统的时候对磁盘进行一盘的分区,一旦日后分区或整个磁盘的空间…

    Linux干货 2016-06-22
  • Select、Case

    select循环与菜单 select循环主要用于创建菜单,按数字排序list指定的顺序排序,并列出在标准输出,利用PS3列出提示符进行输入选择 用法: select VARIABLE in list  do     循环体命令 done PS3提示语定义: 在脚本中脚本代码的第一…

    Linux干货 2016-08-21
  • 第八周作业

    shell脚本简用

    Linux干货 2017-11-27