Chef源码组织

我们这里讲的Chef源码,是指opscode发布在github的那个名为chef的项目 chef in github: https://github.com/opscode/chef

chef本身就是一个gem,它的源码组织结构,首先是一个gem的组织结构:

组织结构

chef-github

基本上Chef的组织结构是一个标准的gem包。

一个gem包的主要元素:

  • lib/

    lib下面,是这个gem的核心代码

  • .gemspec文件

    这里是这个gem的规格文件,里面包含了gem的作者信息、lib的目录位置、bin目录位置、运行时依赖包以及开发时依赖包等

  • bin/

    如果你的gem有终端命令,那么就放到这个目录下面吧

  • tasks/

    放置Rack任务的目录。

  • spec/

    Rspec测试目录

  • Gemfile

    bunlder的包管理文件。

回到我们的Chef源码目录里面:

chef.gemspec

这是chef这个gem的规格文件,

Gem::Specification.new do |s|

  s.name = 'chef'
  s.version = Chef::VERSION
  s.platform = Gem::Platform::RUBY

  #...

  s.required_ruby_version = ">= 1.9.3"

  s.add_dependency "mixlib-config", "~> 2.0"
  s.add_dependency "mixlib-cli", "~> 1.4"
  s.add_dependency "mixlib-log", "~> 1.3"
  s.add_dependency "mixlib-authentication", "~> 1.3"
  s.add_dependency "mixlib-shellout", ">= 2.0.0.rc.0", "< 3.0"
  s.add_dependency "ohai", ">= 7.6.0.rc.0"
  #...

   s.bindir       = "bin"

   #...

   s.require_path = 'lib'

   #...
end

一个gemspec文件,必须是按照这种格式来写。实际上是初始化了一个Gem::Specification类的对象。你也可以理解为,chef这个gem,就是一个gem对象。这个文件指定了chef这个gem的各种属性。

bin

我们再来看下bin目录。 bin里面放置的是chef这个gem所包含的命令:

chef-bin

我们看到chef常用的几个命令了:chef-client、knife、chef-shell、chef-solo等。我们打开chef-client文件看看:

require 'rubygems'
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
require 'chef'
require 'chef/application/client'

Chef::Application::Client.new.run

再打开knife文件看看:

require 'rubygems'
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
require 'chef/application/knife'

Chef::Application::Knife.new.run
require 'rubygems'
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
require 'chef/application/apply'

Chef::Application::Apply.new.run

我们依次打开这个几个bin文件看了下,除了chef-shell和shef文件之外,结构基本都差不多, 都和上面的代码类似,都是加载了rubygems, 在加载路径里面添加了lib目录, 然后又把lib/chef/applications/下面的相关文件加载,最后用了几乎统一的方法:

Chef::Application::Xxx.new.run

我们学过Ruby的基础知识,可以看得出来,Chef::Application::Xxx肯定是被定义在 chef/application/目录下面的Xxx类。

而chef-shell和shef文件,我们打开后,惊异的发现,这俩文件中代码几乎一模一样:

chef-shell:

begin
  require "rubygems"
rescue LoadError
end

require "irb"
require "irb/completion"
require 'irb/ext/save-history'

$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))

require "chef/shell"

# On Windows only, enable irb --noreadline because of input problems --
# See CHEF-3284.
IRB.conf[:USE_READLINE] = false if Chef::Platform::windows?
Shell.start

shef:

begin
  require "rubygems"
rescue LoadError
end

require "irb"
require "irb/completion"
require 'irb/ext/save-history'

$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))

require "chef/shell"

Chef::Log.warn("DEPRECATED: The 'shef' program is renamed to 'chef-shell'")
Shell.start

当我们看到shef中倒数第二行代码的时候:

Chef::Log.warn("DEPRECATED: The 'shef' program is renamed to 'chef-shell'")

可以看得出来,Chef::Log.warn ,应该是Chef的日志输出,warn代表一个警告,里面的字符串意思是:过期声明: 这个shef程序已经被重命名为chef-shell了。

好了,我们已经破案了。 只需要看chef-shell就好了。

chef-shell代码中,require了irb及其组件,并且也加载了chef/shell文件,最后启动了Shell.start命令。

chef-shell命令,实际上是开启了一个加载了chef环境的irb交互shell界面。我们在安装了chef的终端输入这个命令就知道了:


$ chef-shell

loading configuration: none (standalone session)
Session type: standalone
Loading.....done.

This is the chef-shell.
 Chef Version: 11.16.0
 http://www.opscode.com/chef
 http://docs.opscode.com/

run `help' for help, `exit' or ^D to quit.

Ohai2u vagrant@chef-node!
chef >

你可以输入help查看帮助。

lib

我们通过查看bin下面的文件,了解到,一个chef命令的执行,必须先加载lib/chef下面的相关文件,那么我们就去lib目录下面看看。

lib目录下面的文件组织也是有规范的:

chef-lib

这样的组织结构,意味着,当我们使用这个gem的时候,直接:


require 'chef'

就可以了。 require 'chef', 这个命令会直接加载chef gem lib下面的chef.rb,我们看看chef.rb中的代码:


require 'chef/version'
require 'chef/nil_argument'
require 'chef/mash'
require 'chef/exceptions'
require 'chef/log'
require 'chef/config'
require 'chef/providers'
require 'chef/resources'
require 'chef/shell_out'

require 'chef/daemon'

require 'chef/run_status'
require 'chef/handler'
require 'chef/handler/json_file'

require 'chef/monkey_patches/tempfile'
require 'chef/monkey_patches/string'
require 'chef/monkey_patches/numeric'
require 'chef/monkey_patches/object'
require 'chef/monkey_patches/file'
require 'chef/monkey_patches/uri'

chef.rb中就是一堆require, 加载了lib/chef目录下的全部文件。

所以,chef.rb,算是一个入口。核心的代码还在chef目录下面。

但是我们从chef.rb文件中加载的文件名称中,也可以看出chef整体架构的一个大概。

chef分为四部分,由空行来分隔:

#第一部分:
require 'chef/version'
require 'chef/nil_argument'
require 'chef/mash'
require 'chef/exceptions'
require 'chef/log'
require 'chef/config'
require 'chef/providers'
require 'chef/resources'
require 'chef/shell_out'

这部分似乎定义了Chef的版本、配置、日志、resources、providers、shell等应用层面的东西。

# 第二部分
require 'chef/daemon'

这一部分,似乎是定义了chef daemon的功能代码。我们知道,chef-client可以以daemon模式运行。

# 第三部分
require 'chef/run_status'
require 'chef/handler'
require 'chef/handler/json_file'

这一部分,似乎是定义了chef的运行状态、handler的信息, 用于处理chef的内部状态信息。


require 'chef/monkey_patches/tempfile'
require 'chef/monkey_patches/string'
require 'chef/monkey_patches/numeric'
require 'chef/monkey_patches/object'
require 'chef/monkey_patches/file'
require 'chef/monkey_patches/uri'

这一部分,我们看到了monkey_patches 以及熟悉的Ruby内部类同名的文件,几乎可以肯定,这是chef自己对于Ruby提供的类进行了monkey patch,添加了自己要用的方法。 我们在前面讲类与模块那一节,也用这个代码举过例子。

当然,以上都是目测, 让我们继续查看lib/chef下面的文件吧。

lib/chef

我们打开lib/chef目录,发现里面很多文件和文件夹, 跟我们上面根据chef.rb的猜想有点差别。那么我们把chef.rb中require的文件一个个看一下吧,看完后发现,并没有囊括完chef目录中的所有文件。

那这些文件都是干嘛的呢?

先来剧透一下吧。

如果你看过chef-server-webui,应该可以知道,在chef-server-webui里面也用到了chef,主要是用到了chef/rest这部分文件。

还有上面说过的bin目录下的的命令,也需要用到chef/application.rb以及 chef/application/, chef/api_client/等下面的各种文件。

lib/chef下面还有一部分文件是用于测试的,那些我们就暂时不关心了。

我们继续看看chef最重要的东西。

Knife

knife是chef中不可或缺的工具。 我们通过knife命令和chef server、node交互。那么让我们来看下knife的工作机制:

相关文件

knife 相关的最主要的文件是:

  • lib/chef/knife.rb
  • lib/chef/knife/*.rb

我们打开lib/chef/knife/目录,可以看到里面定义了很多rb文件, 这里几乎定义了全部的knife的命令实现,以及辅助方法。

如果想深入了解的话,具体看代码吧。

Chef Client

chef client的所有行为都在lib/chef/client.rb中。

Chef中的DSL方法

构成recipe的那些chef resource,都在chef/resource.rb和chef/resource/目录下面被定义。

尤其是在chef/resource/目录下面,定义了我们常用的resource:

  • file
  • template
  • execute
  • directory
  • template
  • user

等等。具体可以再深入研究源码。