这篇博客的目的是通过分析Forge上的Puppet模块来加深一些概念的理解,同时了解一些常用用法。
今天的例子是jfryman-nginx模块,它是原puppetlabs-nginx模块的升级版本,依赖3个Puppet公共模块:puppetlabs-apt,puppetlabs-stdlib和puppetlabs-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(作用域)的限制。
这张图中有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