Book

原文档:http://docs.puppetlabs.com/guides/language_tutorial.html

语法教程

设计Puppet语言的目的是为了让你能够更轻松的管理你手头上的电脑。
通过介绍一些基本的概念,这篇教程会向你展示这门语言的运作原理。理解Puppet语言是关键,因为你要通过它来驱动Puppet,从而管理你的电脑。
看完这篇教程后,你或许会希望了解更多关于Puppet的信息,那么,请看扩展语言教程。

准备好了吗?

对比于许多其他的编程语言,Poppet语言会相对简单的多。当你在阅读这篇教程的同时,多看看别人写的模块,这会对你有很大帮助。一些完整的现实生活中的例子可以作为很好的Puppet的介绍。在模块页面可以了解到更多的信息,你也可以在那里找到参与Puppet开发的社区列表。

资源

Puppet语言机构化编程的基本单元是资源。资源描述了系统的某些方面的属性,这些方面包括文件,服务,软件包甚至是你自定义的一个资源。我们会在后面向您展示如何用“类”和“定义”来将各种资源整合在一起,如何利用“模块“来组织你的配置。不过我们还是先得从资源开始学起。

每个资源都包含有类型(type),标题(title)和一些其他属性的列表。Puppet中的资源支持各种各样的属性,不过我们已经为其中很多属性设置了合理的默认值,你不需要对每一个都做定义。

这里有两个全面的文档:Type GuidesReferences,你可以在里面找到所有被支持的资源类型,以及它们的有效属性。guides部分提供了详细的列子和注释,而references则是由计算机自动生成的(对于旧版本的Puppet同样适用)。我们推荐从guides开始阅读。

现在正式开始我们的教程。下面是Puppet中一个关于资源的简单的例子,它描述了一个文件的权限控制和所有权:

file { "/etc/passwd":
owner => root,
group => root,
mode => 644,
}

任何执行了这段代码的系统会确保其passwd文件的属性与代码中所定义的保持一致。冒号前的那一部分(也就是例子中的”/etc /passwd”)就是资源的标题(title),通过它,我们可以调用Puppet配置文件中其他部分的资源。

对于简单的资源,一个名字(name)就已经足够了。但是万一同样的文件在不同的系统中有不同的文件名呢,怎么办?为了处理这种情况,除了标题(titile)之外,Puppet允许你定义本地名字(local name):

file { "sshdconfig":
name => $operatingsystem ? {
solaris => "/usr/local/etc/ssh/sshd_config",
default => "/etc/ssh/sshd_config",
},
owner => root,
group => root,
mode  => 644,
}

通过标题(title),我们可以很轻松的调用当前配置里已定义好的各种资源,而不需要每次使用时都对其定义一次。

举个例子,我们可以添加一个与文件关联的服务:

service { "sshd":
subscribe => File[sshdconfig],
}

如果sshdconfig文件发生改变,sshd服务就会重启。你可能注意到,当我们引用一个资源的时候将它的首字母进行了大写,比如File[sshdconfig]。大写的资源类型永远是一个引用,而小写的是一个声明。由于资源只能被声明一次,所以重复两次相同的声明会导致错误。这是Puppet保证你的配置模块化的重要特性之一。

如果我们的资源与多个资源相关联怎么办?从Puppet 0.24.6开始你可以像下面这样定义多从关系:

service { "sshd":
    require => File["sshdconfig", "sshconfig", "authorized_keys"]

很重要的一点是,只有标题(title)能够用来标识资源。即使这些资源看起来在概念上指向相同的实体,它们是否相同也由标题(title)决定。下面这种情况在Puppet中虽然被允许但应该尽量避免,因为一旦被发送到客户端,可能会导致错误。
file { "sshdconfig":
    name  => "/usr/local/etc/ssh/sshd_config",
    owner => "root",
}

file { "/usr/local/etc/ssh/sshd_config":
    owner => "sshd",
}

元参数

除了每个资源类型的属性外,Puppet同时具有称作元参数(Metaparameters)的全局属性。元参数可以与任何资源类型协作。

在上一节的例子中我们使用了两个用于在资源之间构建联系的元参数,subscribe和require。你可以在Metaparameter Reference找到全部的元参数列表,同时我们也会在接下来的教程中指出我们额外使用的元参数。

资源的默认值

有时你需要为一组资源指定一个默认的参数值;Puppet使用一个首字母大写并且没有标题(title)的资源格式来实现这个功能。例如,在下面的例子中,我们将为所有的命令设定一个默认路径:

Exec { path => "/usr/bin:/bin:/usr/sbin:/sbin" }
exec { "echo this works": }

这个代码段中的第一个声明为可执行(exec)资源提供了一个默认值;资源Exec需要一个绝对路径或者能够找到可执行程序的路径。这样减少了代码量,同时,在需要时这个路径能被单独的资源覆盖。通过这种方法,你可以为整个配置文件指定一个默认路径,然后在需要的时候覆盖掉这个值。

在Puppet中,资源的默认值对任何资源均生效。

默认值并不是全局的——它们只影响当前及之后的作用域。如果你需要一个在全部配置文件中起作用的默认值,目前唯一的办法就是在所有的类(class)之外指定它们。我们将在下一节介绍类。

资源集合

聚合是Puppet中一个强有力的概念。有两种方法能够将多个资源合并为一个易用的资源:类(classes)和定义(definitions)。类模型化了节点(node)的基本方面,例如:“这个节点一个WEB服务器”或者“这个节点是这些节点中的一个”。在编程术语中,类是独一无二的——在每个节点中,它们只被求值一次。

另一方面,在相同节点中,定义可以被重用很多次。在本质上,它们就像你用Puppet语言创造的属于自己的类型。伴随着不同的输入,定义会被求值多次。这意味着你可以将不同的变量传递给它们。

类和定义都是非常有用的特性。在搭建puppet的基础结构时,你应该充分利用它们。

类的关键字是class,它的内容由一对大括号包裹。下面这个例子创建了一个用于管理两个独立文件的类:

class unix {
    file {
        "/etc/passwd": 
            owner => "root", 
            group => "root", 
            mode  => 644;
        "/etc/shadow": 
            owner => "root", 
            group => "root", 
            mode  => 440;
    }
}

你可能注意到了我们使用了一些缩写方法。上面的例子等同于:

class unix {
    file { "/etc/password":
         owner => "root", 
         group => "root", 
         mode  => 644;
    }
    file { "/etc/shadow":
         owner => "root", 
         group => "root", 
         mode  => 440;
    }
}

类同样支持简单形式上的对象继承。对于不熟悉编程用语的人来说,这意味着我们可以在不复制/粘贴整个代码的前提下扩展前面定义的类的功能。继承同时允许子类覆盖父类定义的资源。一个类只能继承于另一个类,而不能继承于多个类。在编程用语中,这叫做“单继承”。

class freebsd inherits unix {
    File["/etc/passwd"] { group => wheel }
    File["/etc/shadow"] { group => wheel }
}

如果我们想撤销父类上的一些设定,可以使用undef,比如:

class freebsd inherits unix {
    File["/etc/passwd"] { group => undef }
}

在上面的例子中,包含类unix的节点的password文件的组名将被设置为“wheel”,而包含类freebsd的节点的password文件的组名则不会被设置(既保持原来的值,不去修改)。

在Puppet 0.24.6及以上版本中,你可以同时覆盖多个值,比如:

class freebsd inherits unix {
    File["/etc/passwd","/etc/shadow"] { group => wheel }
}

还有其他方法能够使用继承。在Puppet 0.23.1和更高的版本中,可以使用操作符‘+>’(‘再赋值’)来追加资源的参数:

class apache {
    service { "apache": require => Package["httpd"] }
}

class apache-ssl inherits apache {
    # host certificate is required for SSL to function
    Service[apache] { require +> File["apache.pem"] }
}

上面的例子中使第二个类依赖了所有第一个类所依赖的包,同时增加对包'apache.pem'的依赖。

当追加多个依赖时,使用中括号和逗号:

class apache {
    service { "apache": require => Package["httpd"] }
}

class apache-ssl inherits apache {
    Service[apache] { require +> [ File["apache.pem"], File["/etc/httpd/conf/httpd.conf"] ] }
}

在上面例子中类apache-ssl的require参数等价于:

[Package["httpd"], File["apache.pem"], File["/etc/httpd/conf/httpd.conf"]]

像资源一样,你同样可以在类之间使用‘require’创建依赖关系,比如:

class apache {
    service { "apache": require => Class["squid"] }
}

上面的例子使用元参数require来使类apache依赖于类squid。

在Puppet 0.24.6及更高版本,你同样能指定多重依赖,比如:

class apache {
    service { "apache":
                  require => Class["squid", "xml", "jakarta"]

使用require引用一个类多次不会有任何风险。类是使用include函数(将在下面提到)来进行求值的。如果一个类已经被求值过,那么include不会再做任何事情。

类的嵌套
为了实现模块化和区域化编码,Puppet允许你在一个类里面嵌套的定义其他的类。比如:

class myclass {
class nested {
    file { "/etc/passwd": 
    owner => "root", 
    group => "root", 
    mode  => 644;
    }
}
}

class anotherclass {
include myclass::nested
}

在这个例子中,在外层类中的嵌套类可以通过在名为anotherclass的类中以myclass::nested包括进来。在这里顺序很重要,如果要让这个例子正确的运行的话,myclass类一定要在anotherclass类被求值之前被求值。

定义

定义使用和类相同的基本形式,不同的是它们使用关键字define(而不是class),并且定义支持参数但不支持继承。就像之前所提到的,定义可以接收参数并在相同的系统上多次重用。比如我们可能会在一个系统内创建多个版本库,这里可以使用定义而不是类。下面是一个例子:

define svn_repo($path) {
    exec { "/usr/bin/svnadmin create $path/$title":
        unless => "/bin/test -d $path",
    }
}

svn_repo { puppet_repo: path => "/var/svn_puppet" }
svn_repo { other_repo:  path => "/var/svn_other" }

注意变量是怎么在定义中使用的。我们使用$符号表示变量。注意上面的变量$title。这里有一点专业知识,在Puppet 0.22.3及以后的版本中,定义可以分别使用变量$title和$name表示标题和名字。默认情况下,$title和$name被设置成相同值,不过你可以设置一个标题值,同时将一个不同的名字值作为参数进行传递。$title和$name只在定义中生效,类或其他资源均不生效。

之前我们提到“元参数”是所有资源类型的属性。定义同样能使用元参数,比如我们可以在定义中使用“require”。我们同样可以引用那些使用内建变量的元参数的值。下面是一个例子:

define svn_repo($path) {
    exec {"create_repo_${name}": 
        command => "/usr/bin/svnadmin create $path/$title",
        unless => "/bin/test -d $path",
    }
    if $require {
        Exec["create_repo_${name}"]{
            require +> $require,
        }
    }
}

svn_repo { puppet: 
   path => "/var/svn",
   require => Package[subversion],
}

上面的例子可能并不合适,因为大多数的时候我们知道subversion是依赖svn checkouts的。不过你同样能从上面的例子看到如何在定义中使用require和其他元参数。

类 VS 定义

类和定义的创建过程都很相似(虽然类不接收参数),不过他们使用起来非常不同。

定义是用来定义在一个主机上包含多个实例的可重用对象的,所以定义不能包含任何只能有一个实例的资源。比如,多次使用同一个定义不能创建相同的文件。

另一方面,类是独一无二的——无论你包含它们多少次,你只会得到资源的一个副本。

大多数时候,服务会被定义成类,服务的包,配置文件以及正在运行的服务都会被定义在类中,因为通常在每一个主机上它们都只有一个副本。(这些有时被惯称为“服务——包——文件”)。

定义是被用来管理类似虚拟主机这样可以有许多的资源,或者使用一个可重用的封装来包装一些简单的信息,以此来减少代码量。

模块

你可以(并且应该)将一个集合的类,定义和资源合并到一个模块中。模块是可移植的配置文件的集合,比如一个模块可以包含所有配置Postfix或者Apache所需要的资源。你可以在Modules Page找到更多模块的信息。

节点

在学习了资源,类,定义以及模块的知识后,你已经了解了Puppet的大部分内容了。节点是余下的一个很简单的内容,我们使用节点来将我们的定义(“那是一个web服务器”)映射到需要运行这些指令的机器。

节点的定义和类相似,同样支持继承,它们特别的地方在于,当一个节点(一个运行puppetd的被管理机)连接至Puppet master daemon的时候,它的名字会在已定义的节点列表中被查找。找到的信息会被求值然后发送至这个节点。

节点的名字可以是短的主机名或者完全合格的域名(FQDN)。一些名字,尤其是完全的域名,需要被引号括起来,所以最好的办法是用引号括起所有的名字。下面是一个例子:

node "www.testing.com" {
   include common 
   include apache, squid
}

上面的节点定义创建了一个叫做www.testing.com的节点,然后包含了类common,apache和suqid。

同样可以使用逗号一次定义多个含有相同配置的节点:

node "www.testing.com", "www2.testing.com", "www3.testing.com" {
   include common 
   include apache, squid
}

上面的例子创建了三个相同的节点:www.testing.com,www2.testing.com,www3.testing.com。

使用正则表达式匹配节点

在Puppet 0.25.0及以后版本中,节点可以使用正则表达式进行匹配,这比逐个列出它们要方便的多:

node /^www\d+$/ {
    include common
}

上面的例子会匹配所有以www开头并且以一个或多个数字结尾的主机。下面是另一例子:

node /^(foo|bar)\.testing\.com$/ {
    include common
}

上面的例子会匹配主机foo.testing.com或者bar.testing.com。

如果一个文件中有多个正则表达式或者节点定义会发生什么呢?

  • 如果有一个没有使用正则表达式的节点匹配当前连接的客户端,这个节点会被优先使用。
  • 否则使用第一个匹配的正则表达式。

节点继承

节点支持有限的继承模式。和类一样,节点只能继承于另一节点:

node "www2.testing.com" inherits "www.testing.com" {
    include loadbalancer
}

在这个节点定义中www2.testing.com继承了节点www.testing.com中定义的所有配置,同时增加了类loadbalancer。换句话说,它会做“www.testing.com”做的所有事情,同时增加了一些额外的功能。

默认节点

如果不能找到任何匹配的节点,则名字为default的节点的配置会被默认使用。

外部节点

有时你可能已经有了一个外部的包括机器及它们的角色的列表。这个列表可以在LDAP,版本控制或者数据库中。你可能也需要传递一些变量给这些节点。

这这种情况下,可以使用外部节点脚本来取代本来的节点定义。更多信息见External Nodes

待续

扩展语言教程可以提供更多Puppet的语言特性,包括变量作用域,条件表达式以及其他特性的细节。同样你也可以通过浏览其他人编写的Puppet模块来学习puppet语言。

除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License