這篇博客的目的是通過分析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 )寫成,基本覆蓋了比較典型的邏輯和語法。詳情請參照在線文檔
原創文章,作者:renjin,如若轉載,請注明出處:http://www.www58058.com/81757