Puppet 如何向 OS 发送命令?
How does puppet send commands to the OS?
我是 Puppet 的新手,但对这些概念非常了解。 Puppet 清单调用 Puppet 模块,模块执行实际任务。
我想了解 Puppet 模块层发生了什么。命令实际上是如何执行的?以下面的例子,实际上传递给操作系统的命令是什么?另外,那是在哪里定义的?
package { 'ntp':
ensure => installed,
}
这取决于你linux的口味。
首先检查软件包 ntp
是否已安装。
如果没有,它将被安装。
CentOS 示例:
yum list installed ntp
如果没有安装
yum install ntp
Debian 示例:
dpkg -s ntp
如果没有安装
apt-get install ntp
这一切都由您选择的 Linux 的包裹提供商处理。
总结: Puppet 根据系统的 facts
和 Puppet 本身的配置确定需要 运行 的命令。
因此,当 Puppet 在其系统上将目录编译为 运行 时,它看起来如下所示:
"I need to install a Pacakge resource, called ntp. I am CentOS system, of the RedHat family. By default on RedHat, I use the yum command. So I need to run yum install ntp
"
更长的解释
Puppet 知道 运行 的命令以及如何 运行 它们的方式被称为 资源抽象层 。
归根结底,Puppet 并没有做任何神奇的事情:系统上 运行 的命令与人工操作员 运行 的命令相同。
也许 Puppet 已经想出了一个聪明的方法,并考虑了您所在平台的隐蔽错误和陷阱,或者由于您尝试执行的操作包含拼写错误而引发错误或类似的。
但最终,必须使用系统的实际应用程序和工具实际执行该操作。
这就是 RAL 真正发挥作用的地方。它是 Puppet 中最大的抽象层:将与基础系统的所有交互转变为一致的接口。
在您给出的示例中,包相当简单。至少在过去的二十年里,安装软件包的概念(大部分)与几乎所有操作系统都相同:
packagesystemtool keywordforinstall packagename
一般情况下,install关键字是install,但也有少数例外。例如,BSD 的 pkg
使用 pkg add
。
但是:可以在该包中管理的实际属性可能有很大差异:
- 可以指定版本吗?
- 你能降级那个版本吗?
- 如果软件包已经安装,是否需要指定不同的命令来升级它?
大量其他可选参数,例如代理信息、错误日志记录级别。
RAL 允许用户以一致的方式定义资源的特性,而不管实现如何:
type { 'title':
attribute => 'value',
}
每个资源都遵循相同的语法:
- 资源类型(例如用户、包、服务、文件)
- 花括号定义资源块。
- 一个标题,与资源的 body 用冒号分隔 A body 由属性和值对组成
所以我们的包声明如下所示:
package {'tree':
ensure => 'present',
}
RAL 可以在每个已定义的平台上处理该行为,并支持可用的不同包功能,所有这些都以 well-defined 方式,默认情况下对用户隐藏。
我听过的关于RAL最好的比喻是天鹅在湖面上滑翔:
When you look at a swan on a body of water, it looks elegant and
graceful, gliding along. It barely looks like it's working at all.
眼睛看不见的是 activity 水面下发生的事情。那只天鹅踢着它的蹼足,不像它向上看那样优雅:Puppet 的实际命令是 运行ning 是在水下踢腿。
好的,背景知识已经足够了,您可能会问...
它实际上是如何工作的?
RAL 将系统上的所有资源拆分为两个元素:
- 类型: High-level 资源有效属性的模型
- 提供商:Platform-specific 类型的实现
这使您能够以适用于任何系统的方式描述资源。每一种资源,不管它是什么,都有一个或多个提供者。提供程序是底层 OS 和资源类型之间的接口。
一般来说,一个类型会有一个默认的提供者,但如果需要,您可以指定一个特定的提供者。
对于软件包,默认提供程序将是系统的默认软件包提供程序:yum
用于 RHEL,apt
用于 Debian,pkg
用于 BSD 等。这是确定的通过 a,它从系统中获取事实。
例如,yum
提供商具有以下内容:
defaultfor :osfamily => :redhat
但是您可能想要安装一个 pip 包,或者 gem。为此,您将指定提供程序,因此它将使用不同的命令安装它:
package {'tree':
ensure => 'present',
provider => 'pip',
}
这意味着我们对 RAL 说:"Hey, I know yum is the default to install a package, but this is a python package I need, so I'm telling you to use pip instead"
无论实际实现有何不同,属性类型的最重要资源在概念上通常在各个操作系统中都是相同的。
正如我们所说,大多数软件包将安装 package installer install packagename
因此,资源的描述可以从其实现中抽象出来:
Puppet 使用 RAL 读取和修改系统资源的状态。由于它是一个声明式系统,Puppet 从了解资源应具有的状态开始。
为了同步资源,它使用 RAL 查询当前状态,将其与所需状态进行比较,然后然后再次使用 RAL 进行任何必要的更改。它使用工具来获取系统的当前状态,然后找出需要做什么才能将该状态更改为资源定义的状态。
当 Puppet 应用包含资源的目录时,它将读取目标系统上资源的实际状态,将实际状态与所需状态进行比较,并在必要时更改系统以强制执行所需状态.
让我们看看 RAL 将如何处理:
- 我们将类型指定为包。
- 包裹的title/name是
ntp
- 我运行在 RHEL7 系统上安装它,所以默认提供程序是 yum。
- yum 是一个 "child" rpm 提供程序:它使用 RPM 命令检查系统上是否安装了软件包。 (这比 运行ning "yum info" 快很多,因为它不进行任何互联网呼叫,并且如果 yumrepo 失败也不会失败)
- 但是,安装命令将是
yum install
所以之前我们讨论了 Puppet 如何使用 RAL 来读取和修改系统上的资源状态。
RAL的"getter"是provider中的self.instances方法
根据资源类型,这通常通过以下两种方式之一完成:
- 读取磁盘上的文件,遍历文件中的行并将其转化为资源
- 运行 终端上的一个命令,将 stdout 分成几行,将它们转换成成为资源的哈希值
rpm 实例步骤属于后者。它 运行s rpm -qa
带有一些给定的标志来检查系统上有哪些包:
def self.instances
packages = []
# list out all of the packages
begin
execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process|
# now turn each returned line into a package object
process.each_line { |line|
hash = nevra_to_hash(line)
packages << new(hash) unless hash.empty?
}
}
rescue Puppet::ExecutionFailure
raise Puppet::Error, "Failed to list packages", $!.backtrace
end
packages
end
所以它是 运行ning /usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n'
,然后从该命令中获取标准输出,循环遍历输出的每一行,并使用 nevra_to_hash
方法转换行STDOUT 把它变成一个散列。
self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$}
self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch]
private
# @param line [String] one line of rpm package query information
# @return [Hash] of NEVRA_FIELDS strings parsed from package info
# or an empty hash if we failed to parse
# @api private
def self.nevra_to_hash(line)
line.strip!
hash = {}
if match = self::NEVRA_REGEX.match(line)
self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v }
hash[:provider] = self.name
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
hash[:ensure].prepend("#{hash[:epoch]}:") if hash[:epoch] != '0'
else
Puppet.debug("Failed to match rpm line #{line}")
end
return hash
end
所以基本上它是输出上的正则表达式,然后将正则表达式中的那些位转换为给定的字段。
这些哈希成为资源的当前状态。
我们可以 运行 --debug 以查看实际效果:
Debug: Prefetching yum resources for package
Debug: Executing: '/usr/bin/rpm --version'
Debug: Executing '/usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n''
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n'
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n --whatprovides'
因此它使用 RAL 来获取当前状态。 Puppet 正在执行以下操作:
- 嗯,这是一个在 RHEL 系统上名为 'ntp' 的包资源,所以我应该使用 RPM
- 让我们获取安装的 RPM 包的当前状态(例如实例方法
ntp
不在这里...
- 所以我们需要
ntp
安装
- 然后 Yum 提供程序指定安装所需的命令。
这里有很多逻辑:
def install
wanted = @resource[:name]
error_level = self.class.error_level
update_command = self.class.update_command
# If not allowing virtual packages, do a query to ensure a real package exists
unless @resource.allow_virtual?
execute([command(:cmd), '-d', '0', '-e', error_level, '-y', install_options, :list, wanted].compact)
end
should = @resource.should(:ensure)
self.debug "Ensuring => #{should}"
operation = :install
case should
when :latest
current_package = self.query
if current_package && !current_package[:ensure].to_s.empty?
operation = update_command
self.debug "Ensuring latest, so using #{operation}"
else
self.debug "Ensuring latest, but package is absent, so using #{:install}"
operation = :install
end
should = nil
when true, false, Symbol
# pass
should = nil
else
# Add the package version
wanted += "-#{should}"
if wanted.scan(ARCH_REGEX)
self.debug "Detected Arch argument in package! - Moving arch to end of version string"
wanted.gsub!(/(.+)(#{ARCH_REGEX})(.+)/,'')
end
current_package = self.query
if current_package
if rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) < 0
self.debug "Downgrading package #{@resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = :downgrade
elsif rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) > 0
self.debug "Upgrading package #{@resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = update_command
end
end
end
# Yum on el-4 and el-5 returns exit status 0 when trying to install a package it doesn't recognize;
# ensure we capture output to check for errors.
no_debug = if Facter.value(:operatingsystemmajrelease).to_i > 5 then ["-d", "0"] else [] end
command = [command(:cmd)] + no_debug + ["-e", error_level, "-y", install_options, operation, wanted].compact
output = execute(command)
if output =~ /^No package #{wanted} available\.$/
raise Puppet::Error, "Could not find package #{wanted}"
end
# If a version was specified, query again to see if it is a matching version
if should
is = self.query
raise Puppet::Error, "Could not find package #{self.name}" unless is
# FIXME: Should we raise an exception even if should == :latest
# and yum updated us to a version other than @param_hash[:ensure] ?
vercmp_result = rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(is[:ensure]))
raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if vercmp_result != 0
end
end
这是严重的天鹅腿踢。这里有很多逻辑,用于 Yum 上的软件包的更复杂用例,但要确保它适用于可用的各种版本的 Yum,包括 RHEL 4 和 5。
逻辑是这样分解的:我们没有指定版本,所以我们不需要检查安装什么版本。只需 运行 yum install tree 并指定默认选项
Debug: Package[tree](provider=yum): Ensuring => present
Debug: Executing: '/usr/bin/yum -d 0 -e 0 -y install tree'
Notice: /Stage[main]/Main/Package[tree]/ensure: created
Ta-dah,已安装。
我是 Puppet 的新手,但对这些概念非常了解。 Puppet 清单调用 Puppet 模块,模块执行实际任务。
我想了解 Puppet 模块层发生了什么。命令实际上是如何执行的?以下面的例子,实际上传递给操作系统的命令是什么?另外,那是在哪里定义的?
package { 'ntp': ensure => installed, }
这取决于你linux的口味。
首先检查软件包 ntp
是否已安装。
如果没有,它将被安装。
CentOS 示例:
yum list installed ntp
如果没有安装
yum install ntp
Debian 示例:
dpkg -s ntp
如果没有安装
apt-get install ntp
这一切都由您选择的 Linux 的包裹提供商处理。
总结: Puppet 根据系统的 facts
和 Puppet 本身的配置确定需要 运行 的命令。
因此,当 Puppet 在其系统上将目录编译为 运行 时,它看起来如下所示:
"I need to install a Pacakge resource, called ntp. I am CentOS system, of the RedHat family. By default on RedHat, I use the yum command. So I need to run yum install ntp
"
更长的解释
Puppet 知道 运行 的命令以及如何 运行 它们的方式被称为 资源抽象层 。
归根结底,Puppet 并没有做任何神奇的事情:系统上 运行 的命令与人工操作员 运行 的命令相同。
也许 Puppet 已经想出了一个聪明的方法,并考虑了您所在平台的隐蔽错误和陷阱,或者由于您尝试执行的操作包含拼写错误而引发错误或类似的。
但最终,必须使用系统的实际应用程序和工具实际执行该操作。
这就是 RAL 真正发挥作用的地方。它是 Puppet 中最大的抽象层:将与基础系统的所有交互转变为一致的接口。
在您给出的示例中,包相当简单。至少在过去的二十年里,安装软件包的概念(大部分)与几乎所有操作系统都相同:
packagesystemtool keywordforinstall packagename
一般情况下,install关键字是install,但也有少数例外。例如,BSD 的 pkg
使用 pkg add
。
但是:可以在该包中管理的实际属性可能有很大差异:
- 可以指定版本吗?
- 你能降级那个版本吗?
- 如果软件包已经安装,是否需要指定不同的命令来升级它?
大量其他可选参数,例如代理信息、错误日志记录级别。
RAL 允许用户以一致的方式定义资源的特性,而不管实现如何:
type { 'title':
attribute => 'value',
}
每个资源都遵循相同的语法:
- 资源类型(例如用户、包、服务、文件)
- 花括号定义资源块。
- 一个标题,与资源的 body 用冒号分隔 A body 由属性和值对组成
所以我们的包声明如下所示:
package {'tree':
ensure => 'present',
}
RAL 可以在每个已定义的平台上处理该行为,并支持可用的不同包功能,所有这些都以 well-defined 方式,默认情况下对用户隐藏。
我听过的关于RAL最好的比喻是天鹅在湖面上滑翔:
When you look at a swan on a body of water, it looks elegant and graceful, gliding along. It barely looks like it's working at all.
眼睛看不见的是 activity 水面下发生的事情。那只天鹅踢着它的蹼足,不像它向上看那样优雅:Puppet 的实际命令是 运行ning 是在水下踢腿。
好的,背景知识已经足够了,您可能会问...
它实际上是如何工作的? RAL 将系统上的所有资源拆分为两个元素:
- 类型: High-level 资源有效属性的模型
- 提供商:Platform-specific 类型的实现
这使您能够以适用于任何系统的方式描述资源。每一种资源,不管它是什么,都有一个或多个提供者。提供程序是底层 OS 和资源类型之间的接口。
一般来说,一个类型会有一个默认的提供者,但如果需要,您可以指定一个特定的提供者。
对于软件包,默认提供程序将是系统的默认软件包提供程序:yum
用于 RHEL,apt
用于 Debian,pkg
用于 BSD 等。这是确定的通过 a,它从系统中获取事实。
例如,yum
提供商具有以下内容:
defaultfor :osfamily => :redhat
但是您可能想要安装一个 pip 包,或者 gem。为此,您将指定提供程序,因此它将使用不同的命令安装它:
package {'tree':
ensure => 'present',
provider => 'pip',
}
这意味着我们对 RAL 说:"Hey, I know yum is the default to install a package, but this is a python package I need, so I'm telling you to use pip instead"
无论实际实现有何不同,属性类型的最重要资源在概念上通常在各个操作系统中都是相同的。
正如我们所说,大多数软件包将安装 package installer install packagename
因此,资源的描述可以从其实现中抽象出来:
Puppet 使用 RAL 读取和修改系统资源的状态。由于它是一个声明式系统,Puppet 从了解资源应具有的状态开始。
为了同步资源,它使用 RAL 查询当前状态,将其与所需状态进行比较,然后然后再次使用 RAL 进行任何必要的更改。它使用工具来获取系统的当前状态,然后找出需要做什么才能将该状态更改为资源定义的状态。
当 Puppet 应用包含资源的目录时,它将读取目标系统上资源的实际状态,将实际状态与所需状态进行比较,并在必要时更改系统以强制执行所需状态.
让我们看看 RAL 将如何处理:
- 我们将类型指定为包。
- 包裹的title/name是
ntp
- 我运行在 RHEL7 系统上安装它,所以默认提供程序是 yum。
- yum 是一个 "child" rpm 提供程序:它使用 RPM 命令检查系统上是否安装了软件包。 (这比 运行ning "yum info" 快很多,因为它不进行任何互联网呼叫,并且如果 yumrepo 失败也不会失败)
- 但是,安装命令将是
yum install
所以之前我们讨论了 Puppet 如何使用 RAL 来读取和修改系统上的资源状态。
RAL的"getter"是provider中的self.instances方法
根据资源类型,这通常通过以下两种方式之一完成:
- 读取磁盘上的文件,遍历文件中的行并将其转化为资源
- 运行 终端上的一个命令,将 stdout 分成几行,将它们转换成成为资源的哈希值
rpm 实例步骤属于后者。它 运行s rpm -qa
带有一些给定的标志来检查系统上有哪些包:
def self.instances
packages = []
# list out all of the packages
begin
execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process|
# now turn each returned line into a package object
process.each_line { |line|
hash = nevra_to_hash(line)
packages << new(hash) unless hash.empty?
}
}
rescue Puppet::ExecutionFailure
raise Puppet::Error, "Failed to list packages", $!.backtrace
end
packages
end
所以它是 运行ning /usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n'
,然后从该命令中获取标准输出,循环遍历输出的每一行,并使用 nevra_to_hash
方法转换行STDOUT 把它变成一个散列。
self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$}
self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch]
private
# @param line [String] one line of rpm package query information
# @return [Hash] of NEVRA_FIELDS strings parsed from package info
# or an empty hash if we failed to parse
# @api private
def self.nevra_to_hash(line)
line.strip!
hash = {}
if match = self::NEVRA_REGEX.match(line)
self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v }
hash[:provider] = self.name
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
hash[:ensure].prepend("#{hash[:epoch]}:") if hash[:epoch] != '0'
else
Puppet.debug("Failed to match rpm line #{line}")
end
return hash
end
所以基本上它是输出上的正则表达式,然后将正则表达式中的那些位转换为给定的字段。
这些哈希成为资源的当前状态。
我们可以 运行 --debug 以查看实际效果:
Debug: Prefetching yum resources for package
Debug: Executing: '/usr/bin/rpm --version'
Debug: Executing '/usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n''
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n'
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n --whatprovides'
因此它使用 RAL 来获取当前状态。 Puppet 正在执行以下操作:
- 嗯,这是一个在 RHEL 系统上名为 'ntp' 的包资源,所以我应该使用 RPM
- 让我们获取安装的 RPM 包的当前状态(例如实例方法
ntp
不在这里...- 所以我们需要
ntp
安装 - 然后 Yum 提供程序指定安装所需的命令。
这里有很多逻辑:
def install
wanted = @resource[:name]
error_level = self.class.error_level
update_command = self.class.update_command
# If not allowing virtual packages, do a query to ensure a real package exists
unless @resource.allow_virtual?
execute([command(:cmd), '-d', '0', '-e', error_level, '-y', install_options, :list, wanted].compact)
end
should = @resource.should(:ensure)
self.debug "Ensuring => #{should}"
operation = :install
case should
when :latest
current_package = self.query
if current_package && !current_package[:ensure].to_s.empty?
operation = update_command
self.debug "Ensuring latest, so using #{operation}"
else
self.debug "Ensuring latest, but package is absent, so using #{:install}"
operation = :install
end
should = nil
when true, false, Symbol
# pass
should = nil
else
# Add the package version
wanted += "-#{should}"
if wanted.scan(ARCH_REGEX)
self.debug "Detected Arch argument in package! - Moving arch to end of version string"
wanted.gsub!(/(.+)(#{ARCH_REGEX})(.+)/,'')
end
current_package = self.query
if current_package
if rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) < 0
self.debug "Downgrading package #{@resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = :downgrade
elsif rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) > 0
self.debug "Upgrading package #{@resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = update_command
end
end
end
# Yum on el-4 and el-5 returns exit status 0 when trying to install a package it doesn't recognize;
# ensure we capture output to check for errors.
no_debug = if Facter.value(:operatingsystemmajrelease).to_i > 5 then ["-d", "0"] else [] end
command = [command(:cmd)] + no_debug + ["-e", error_level, "-y", install_options, operation, wanted].compact
output = execute(command)
if output =~ /^No package #{wanted} available\.$/
raise Puppet::Error, "Could not find package #{wanted}"
end
# If a version was specified, query again to see if it is a matching version
if should
is = self.query
raise Puppet::Error, "Could not find package #{self.name}" unless is
# FIXME: Should we raise an exception even if should == :latest
# and yum updated us to a version other than @param_hash[:ensure] ?
vercmp_result = rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(is[:ensure]))
raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if vercmp_result != 0
end
end
这是严重的天鹅腿踢。这里有很多逻辑,用于 Yum 上的软件包的更复杂用例,但要确保它适用于可用的各种版本的 Yum,包括 RHEL 4 和 5。
逻辑是这样分解的:我们没有指定版本,所以我们不需要检查安装什么版本。只需 运行 yum install tree 并指定默认选项
Debug: Package[tree](provider=yum): Ensuring => present
Debug: Executing: '/usr/bin/yum -d 0 -e 0 -y install tree'
Notice: /Stage[main]/Main/Package[tree]/ensure: created
Ta-dah,已安装。