; puppet代码分析 | Linux运维部落

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 )写成,基本覆盖了比较典型的逻辑和语法。详情请参照在线文档


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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

联系我们

400-080-6560

在线咨询:点击这里给我发消息

邮件:1660809109@qq.com

工作时间:周一至周五,9:30-18:30,节假日同时也值班