foreman源码结构

foreman是Ruby on Rails写的,可以依照我们在前面学过的Ruby on Rails基础来查看源码。

foreman 目录:

.tx
bundler.d
config
db
doc
extras
lib
locale
log
man
public
script
test
vendor/assets
.gitignore
.rubocop.yml
.rubocop_todo.yml
CHANGELOG
Contributors
Gemfile
LICENSE
README.md
Rakefile
Rakefile.dist
VERSION
config.ru

其中有一些目录是Rails(Rails3,Rails4有点变化)的默认的目录结构:

app
config
db
doc
lib
log
public
script
test
vendor
Gemfile
README.md
Rakefile
config.ru

而其他的非默认目录结构,为foreman自己添加的扩展,比如bundler.d, 可以从Gemfile文件中看出来:


Dir["#{File.dirname(FOREMAN_GEMFILE)}/bundler.d/*.rb"].each do |bundle|
  self.instance_eval(Bundler.read_file(bundle))
end

这个文件夹是附属于Gemfile文件,把不同的group下面的gem依赖分离到不同的文件中去了。

还有man目录,里面用asciidoc格式(类似于Markdown)记录了foreman扩展的一些命令:

foreman-rake -T
foreman-rake db:migrate
foreman-tail
foreman-debug
...

这些命令是foreman提供的为了方便用户调试,实际上这些命令都是包装了rails的一些命令,这些命令实际是在script/目录下被定义的,比如:

# foreman/script/foreman-rake

RAKE_CMD=/usr/bin/rake
...
CMD="$BUNDLER_CMD $RAKE_CMD $*"
if [ $USERNAME = foreman ]; then
  RAILS_ENV=production $CMD
else
  su - foreman -s /bin/bash -c "RAILS_ENV=production $CMD"
fi

Gemfile

我们先根据Gemfile来查看一下foreman引入了哪些外部依赖(gem形式)。

gem 'rails', '3.2.21'  # 默认
gem 'json', '~> 1.5' #json解析
gem 'rest-client', '~> 1.6.0', :require => 'rest_client'  # http客户端
gem 'audited-activerecord', '3.0.0'   #是一个ORM扩展,记录了model的所有改变日志
gem 'will_paginate', '~> 3.0' #分页插件
gem 'ancestry', '~> 2.0'          # 可以组织tree结构
gem 'scoped_search', '~> 3.0'            #搜索插件
gem 'ldap_fluff', '>= 0.3.5', '< 1.0'     #用于查询LDAP的gem
gem 'net-ldap', '>= 0.8.0'         #LDAP网络库
gem 'apipie-rails', '~> 0.2.5'         #rails的api文档生成器
gem 'rabl', '~> 0.11'                    #json模板
gem 'oauth', '~> 0.4'                  #oauth gem
gem 'deep_cloneable', '~> 2.0'             #支持ActiveRecord的深层拷贝,包括表关系
gem 'foreigner', '~> 1.4'           #提供了外键相关的一些helper方法
gem 'validates_lengths_from_database',  '~> 0.2' #数据库字段长度验证插件
gem 'friendly_id', '~> 4.0'            #提供了对SEO友好的id
gem 'secure_headers', '~> 1.3'          # 安全设置http头
gem 'safemode', '~> 1.2'            #配合erb使用,让在erb里执行Ruby代码更安全
gem 'fast_gettext', '~> 0.8'        #I18n工具
gem 'gettext_i18n_rails', '~> 1.0'  #I18n工具
gem 'i18n', '~> 0.6.4'               #I18n工具
gem 'rails-i18n', '~> 3.0.0'         #I18n工具
gem 'turbolinks', '~> 2.5'                 #加速页面加载
gem 'logging', '>= 1.8.0', '< 3.0.0'        #日志输出

可以通过后面的注释看到这些gem的作用。 如果你想了解详细介绍和用法,可以通过http://rubygems.org去搜索。

当然除了这些gem,也别忘记那些被分组存放到bunder.d目录下的那些gem依赖。

比如foreman/bundler.d/console.rb文件中:

group :console do
  gem 'wirb', '~> 1.0'
  gem 'hirb-unicode', '~> 0.0.5'
  gem 'awesome_print', '~> 1.0', :require => 'ap'

  # minitest - workaround until Rails 4.0 (#2650)
  gem 'minitest', '~> 4.7', :require => 'minitest/unit'
end

大家可以根据上述方法来查找相关gem的应用介绍。

group的作用是方便部署到生产环境的时候,指定相关的group依赖,不相关的将不会安装到生产环境。

Config

看完Gemfile,我们来看看Config目录下面有什么东西。 github源码:https://github.com/theforeman/foreman/tree/develop/config

foreman

environments 目录

environments目录下包含了不同的开发环境下Rails的不同配置

比如开发环境,我们可以设置 config.cache_classes = false, 这样我们就不需要在每次修改了controller都需要重启服务器了(当然,有些修改是必须得重启,比如你加了新的字段)。

initializers目录

此目录下,都是Rails启动时候自动加载的文件。如果你有需要在Rails启动时候加载的文件,也可以放到此目录。 而Foreman就在这个目录下放了不少文件。

比如我们刚才在Gemfile里提到的gem: secure_headers,foreman在此目录下对它做了配置,随着Rails启动而加载:

::SecureHeaders::Configuration.configure do |config|
  config.hsts = {
    :max_age            => 20.years.to_i,
    :include_subdomains => true
  }
  config.x_frame_options = 'SAMEORIGIN'
  config.x_content_type_options = "nosniff"
  config.x_xss_protection = {
    :value => 1,
    :mode  => 'block'
  }
  config.csp = {
    :enforce     => true,
    :default_src => 'self',
    :frame_src   => 'self',
    :connect_src => 'self ws: wss:',
    :style_src   => 'inline self',
    :script_src  => 'eval inline self',
    :img_src     => ['self', '*.gravatar.com']
  }
end

再比如config/initializers/inflections.rb文件,这个是Rails默认提供的,是用于Rails单复数约定的配置文件,比如User对应于users。 因为Rails自身并不是包含全部的单复数规则,或者用户也想配置其他规则,所以Rails提供了此文件。

在此文件中,我们也看到了foreman添加了一些自己的规则:

ActiveSupport::Inflector.inflections do |inflect|
  # inflect.plural /^(ox)$/i, '\1en'
  # inflect.singular /^(ox)en/i, '\1'
  # inflect.irregular 'person', 'people'
  # inflect.uncountable %w( fish sheep )
  inflect.singular /^puppetclass$/, 'puppetclass'
  inflect.singular /^Puppetclass$/, 'Puppetclass'
  inflect.singular /^HostClass$/, 'HostClass'
  inflect.singular /^host_class$/, 'host_class'
  inflect.singular /^HostgroupClass$/, 'HostgroupClass'
  inflect.singular /^hostgroup_class$/, 'hostgroup_class'
end

我们看到,foreman设置了puppetclass等单词的单复数规则为其自身。

大家也可以自己看看此目录下的其他文件。

routes目录

Rails默认是没有这个文件目录的,原本只有一个routes.rb,是用来配置路由的。具体什么是路由,可以参考第二章里Restful部分。我在后面也会提到相关部分。

这里的routes目录,跟我们在上面将的bundler.d目录类似,也是为了把路由分组到别的文件而设置的。 可以看到routes目录下面有个api目录.

然后看routes.rb文件,结果没有发现引入routes里面文件的代码。然后再去看application.rb文件, application.rb文件是对Rails项目的配置中心。

 config.paths["config/routes"] += Dir[Rails.root.join("config/routes/**/*.rb")]
 #...

在其中,我们发现了上述代码,这行代码的作用就是把config/routes下面的文件加到Rails的加载文件路径中。

然后在routes.rb中,也看到了下面的代码:

require 'api_constraints'

而api_constraints文件在app/services/目录下:

class ApiConstraints
  def initialize(options)
    @version = options[:version]
    @default = options.has_key?(:default) ? options[:default] : false
  end

  def matches?(req)
    route_match       = req.fullpath.match(%r{/api/v(\d+)}) if req.fullpath
    header_match      = req.accept.match(%r{version=(\d+)}) if req.accept

    return (@version.to_s == route_match[1]) if route_match
    return (@version.to_s == header_match[1]) if header_match
    # if version is not specified in route or header, then it returns true only if :default => true in routes file v1.rb or v2.rb
    @default
  end
end

这个是Rails提供的路由规则的高级用法,用来匹配api 版本的,也就是说,你可以把api版本加到路径里,或者加到头里,都可以匹配。 这样就使我们的api版本号的设置相对比较灵活。

application.rb

此文件为Rails为当前应用提供的全局配置文件。 也是Rails启动过程中不可或缺的文件。

module Foreman
  class Application < Rails::Application
    config.paths["config/routes"] += Dir[Rails.root.join("config/routes/**/*.rb")]

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Custom directories with classes and modules you want to be autoloadable.
    # config.autoload_paths += %W(#{config.root}/extras)
    config.autoload_paths += Dir["#{config.root}/lib"]

    ...

    config.cache_store = :file_store, Rails.root.join("tmp", "cache")

    # enables JSONP support in the Rack middleware
    config.middleware.use Rack::JSONP if SETTINGS[:support_jsonp]

    # Enable the asset pipeline
    config.assets.enabled = true

    # Version of your assets, change this if you want to expire all your assets
    config.assets.version = '1.0'

    # Catching Invalid JSON Parse Errors with Rack Middleware
    config.middleware.insert_before ActionDispatch::ParamsParser, "Middleware::CatchJsonParseErrors"

    # Add apidoc hash in headers for smarter caching
    config.middleware.use "Apipie::Middleware::ChecksumInHeaders"

    ...
    ...
  end
end

可以看到,你可以通过config.xxx这样的方法来配置一些加载目录,以及中间件,甚至一些组件的开关。 与此文件对应相关的是我们上面介绍过的environments目录下面的三个不同环境下的配置文件, application.rb对于那三个文件来说,其实是个公共配置。

当然Foreman在此文件中也加了不少启动设置,比如:

# For standalone CR bundler groups, check that all dependencies are laoded
SETTINGS[:libvirt]   = !!(defined?(::Fog::Libvirt) && defined?(::Libvirt))
SETTINGS[:gce]       = !!(defined?(::Fog::Google) && defined?(::Google::APIClient::VERSION))
SETTINGS[:ec2]       = !!defined?(::Fog::AWS)

SETTINGS.merge! :openstack => false, :ovirt => false, :rackspace => false, :vmware => false

# CRs in fog core with extra dependencies will have those deps loaded, so then
# load the corresponding bit of fog
if defined?(::OVIRT)
  require 'fog/ovirt'
  SETTINGS[:ovirt] = Fog::Compute.providers.include?(:ovirt)
end

if defined?(::RbVmomi)
  require 'fog/vsphere'
  SETTINGS[:vmware] = Fog::Compute.providers.include?(:vsphere)
end

...

路由与资源

我们在Rails基础部分学过Restful,Rails也是个Restful框架, 它的路由设置,完全体现出了这点:

resources :hosts do
  member do
    get 'clone'
    ...
  end
  collection do
    get 'multiple_actions'
    ...
  end
end

...

拿hosts来说,每个hosts就是一个资源,而

GET /hosts

代表的就是获取hosts资源集合,其对应app/controllers/hosts_controller.rb中的index action,用下面图表示:

foreman

这是Rails默认提供的restful支持,而 routes里面定义的,则是用户自己指定的规则,比如:

resources :hosts do
  member do
    get 'clone'
  end
end

这个路由代表,GET /hosts/:id/clone ,对应的action为 app/controllers/hosts_controller.rb中的 clone。 路由中的member,代表资源集合中的单个资源,而collection,则代表资源集合。

了解这些规则以后,当你看foreman API接口的时候,你大脑中也应该有上述的映射,http://theforeman.org/api/1.8/index.html。

app目录

app目录下面放的基本上是整个foreman的核心。

foreman

assets 目录

此目录放的都是静态文件: 图片、css、javascript。 Rails通过Assets Pipeline来处理这些静态文件。

controllers目录

此目录为Rails MVC中的C,用于处理每个请求。当请求过来,会经过路由,到达每个控制器的action,然后经过每个action来处理请求,返回响应。

controllers目录下面分为三部分: api目录、concerns目录、散落的controller文件。

我们学过Rails基础可以知道,Rails根据这些目录的命名来查找对应的文件,这是一种约定。 所以,api目录下面放的是跟api相关的控制器代码。 而concerns目录是Rails默认提供的,此目录是让我们放置一些公共的方法,体现的是DRY原则。 而那些散落的controller文件,则是foreman除api部分相关的控制器。

继续拿hosts_controller.rb作为例子:


class HostsController < ApplicationController
  include Foreman::Controller::HostDetails
  include Foreman::Controller::AutoCompleteSearch
  include Foreman::Controller::TaxonomyMultiple
  include Foreman::Controller::SmartProxyAuth

  PUPPETMASTER_ACTIONS=[ :externalNodes, :lookup ]
  SEARCHABLE_ACTIONS= %w[index active errors out_of_sync pending disabled ]
  AJAX_REQUESTS=%w{compute_resource_selected hostgroup_or_environment_selected current_parameters puppetclass_parameters process_hostgroup process_taxonomy review_before_build}
  BOOT_DEVICES={ :disk => N_('Disk'), :cdrom => N_('CDROM'), :pxe => N_('PXE'), :bios => N_('BIOS') }
  MULTIPLE_ACTIONS = %w(multiple_parameters update_multiple_parameters  select_multiple_hostgroup
                        update_multiple_hostgroup select_multiple_environment update_multiple_environment
                        multiple_destroy submit_multiple_destroy multiple_build
                        submit_multiple_build multiple_disable submit_multiple_disable
                        multiple_enable submit_multiple_enable multiple_puppetrun
                        update_multiple_puppetrun multiple_disassociate update_multiple_disassociate)

  add_smart_proxy_filters PUPPETMASTER_ACTIONS, :features => ['Puppet']

  before_filter :ajax_request, :only => AJAX_REQUESTS
  before_filter :find_resource, :only => [:show, :clone, :edit, :update, :destroy, :puppetrun, :review_before_build,
                                          :setBuild, :cancelBuild, :power, :overview, :bmc, :vm,
                                          :runtime, :resources, :templates, :nics, :ipmi_boot, :console,
                                          :toggle_manage, :pxe_config, :storeconfig_klasses, :disassociate]

  before_filter :taxonomy_scope, :only => [:new, :edit] + AJAX_REQUESTS
  before_filter :set_host_type, :only => [:update]
  before_filter :find_multiple, :only => MULTIPLE_ACTIONS
  before_filter :cleanup_passwords, :only => :update
  helper :hosts, :reports, :interfaces

  def index(title = nil)
    begin
      search = resource_base.search_for(params[:search], :order => params[:order])
    rescue => e
      error e.to_s
      search = resource_base.search_for ''
    end
    respond_to do |format|
      format.html do
        @hosts = search.includes(included_associations).paginate(:page => params[:page])
        # SQL optimizations queries
        @last_reports = Report.where(:host_id => @hosts.map(&:id)).group(:host_id).maximum(:id)
        # rendering index page for non index page requests (out of sync hosts etc)
        @hostgroup_authorizer = Authorizer.new(User.current, :collection => @hosts.map(&:hostgroup_id).compact.uniq)
        render :index if title and (@title = title)
      end
      format.yaml { render :text => search.all(:select => "hosts.name").map(&:name).to_yaml }
      format.json
    end
  end
  ...
  private

  def resource_base
    @resource_base ||= Host.authorized(current_permission, Host)
  end

  ...

end

HostsController中include部分,是在concerns里面定义的公共方法,此处需要include来使用。

brefore_filter为前置过滤器,就是在每个action之前所做的动作。后面也可以用only、except来指定具体的action。

helper是用来引入定义在helpers目录下面的helper模块,比如helper :hosts,就是引入helper/hosts_helper.rb里的方法。

而def index则为控制器的action,当有来自GET /hosts的请求过来,会经由路由交给此action进行处理。

最后,置于private下面的方法,则为私有方法,这意味着,它不能暴露给外面使用,也就是说,它不是个action,只供本controller里面内部使用。

api

api的实现跟普通的controller没什么区别,只是action中返回了json,然后views模板使用了rabl来生成最终返回的json。

值得注意的是,api部分代码中大量使用了类似于下面的代码:

api :GET, "/bookmarks/", N_("List all bookmarks")

这些在阅读源码的时候需要注意,这只是为了生成api文档而存在的。

helpers目录

这个目录下的文件,与controllers相对应,主要定义了在Views(页面/Json)中使用的方法。

mailers 目录

邮件相关,此处非重点,就不讲解了,具体规则可以参考:http://guides.rubyonrails.org/action_mailer_basics.html

models目录

这个是重点。 因为model里定义的都是与数据库相关的ORM模型。

foreman支持三种类型数据库(SQLite、MySql、PostGreSQL), 其中SQLite为默认数据库。其实这也是Rails支持的的三种数据库。

此文件夹下面的model文件,都遵循Rails的单复数规则来匹配对应的数据库。

同样,我们拿host.rb来举例:

# host相关的组织结构:
host.rb
host/
  base.rb
  hostmix.rb
  managed.rb

一般来说,我们只需要一个文件就够用了,比如host.rb,但是对于业务比较复杂的情况,有时候需要根据业务逻辑相关对model文件进行拆分, 而foreman选择了上面这种拆分方式。

module Host
  class Base < ActiveRecord::Base
    include Foreman::STI
    ...

    self.table_name = :hosts
    extend FriendlyId
    friendly_id :name
    OWNER_TYPES = %w(User Usergroup)

    validates_lengths_from_database
    belongs_to :model, :counter_cache => :hosts_count
    has_many :fact_values, :dependent => :destroy, :foreign_key => :host_id
    ...
    scope :no_location,     -> { where(:location_id => nil) }
    ...
    delegate :ip, :mac,
    ...

    def self.attributes_protected_by_default
      super - [ inheritance_column ]
    end

    def self.import_host_and_facts(json)
      # noop, overridden by STI descendants
      [self, true]
    end
    ...
  end
end

上面例子为host/base.rb

首先,需要使用module Host来作为命名空间,这个是必须的,这样Rails才能找到此文件,这是一种约定。

其次, class Base < ActiveRecord::Base, 继承自ActiveRecord::Base就代表此类有一个与之对应的数据库表文件, 其实我们可以在foreman源码的db文件(专门存放与数据库操作相关migration和schema文件)中找到hosts表:

  create_table :hosts do |t|
      t.column :name, :string, :null => false
      t.column :ip, :string
      t.column :environment, :text
      t.column :last_compile, :datetime
      t.column :last_freshcheck, :datetime
      t.column :last_report, :datetime
      #Use updated_at to automatically add timestamp on save.
      t.column :updated_at, :datetime
      t.column :source_file_id, :integer
      t.column :created_at, :datetime
    end
    add_index :hosts, :source_file_id
    add_index :hosts, :name
  end

但是为什么model的类名是Base,而表名是hosts呢? 那是因为这句代码: self.table_name = :hosts

上面的叫migration文件,是Rails提供的DSL方法,用来管理数据库表的创建,非常方便。

上面model代码中你看到的 has_many和belongs_to是设置表间关系,比如:

has_many :fact_values, :dependent => :destroy, :foreign_key => :host_id

这代表hosts和fact_values是一对多关系,一个host,会有多个fact value。 has_many跟belongs_to一般是配套使用,不信可以去fact_value.rb(注意这里不是复数)去看看:

class FactValue < ActiveRecord::Base
  ...
  belongs_to_host
  ...
end

而belongs_to_host是在app/models/host/hostmix.rb中定义的:

module Host
  module Hostmix
    def has_many_hosts(options = {})
      has_many :hosts, {:class_name => "Host::Managed"}.merge(options)
    end

    def belongs_to_host(options = {})
      belongs_to :host, {:class_name => "Host::Managed", :foreign_key => :host_id}.merge(options)
    end
  end
end

为什么可以在FactValue中直接使用呢? 我们发现在config/initializers/目录下面有一个active_record_extensions文件:

class ActiveRecord::Base
  extend Host::Hostmix
  include HasManyCommon
  include StripWhitespace
  include Parameterizable::ById
end

可以看到上面第一行就是在ActiveRecord::Base中extend了Host::Hostmix模块。 这就意味着,每个继承了ActiveRecord::Base模块的model 都可以使用belongs_to_host方法,没错,它是个类方法。

回到host/base.rb中,我们还发现一行代码: include Foreman::STI,这是干嘛的呢?

STI模块是被定义在models/concerns/foreman/sti.rb文件中,此文件为STImodel提供了一些方法。

STI是Rails提供的单表继承。通过一个type字段,来把同一个表映射为多个model,来满足一些业务需求。这样看来,hosts也是个用了STI。

我们来看看host/managed.rb文件:

class Host::Managed < Host::Base
  include ReportCommon
  include Hostext::Search
  PROVISION_METHODS = %w[build image]

  has_many :host_classes, :foreign_key => :host_id
  ...
end

我们发现,Host::Managed 继承自 Host::Base类,而且跟host_classes的has_many 关系指定外键是host_id,同时, 我也在db/migrations文件中发现:


class AddStiToHosts < ActiveRecord::Migration
  def up
    add_column :hosts, :type, :string
    execute "UPDATE hosts set type='Host::Managed'"
    add_index :hosts, :type
  end

  def down
    remove_column :hosts, :type
  end
end

hosts里增加了type字段。 这足以证明, Host::Managed类为hosts表的一个单表继承model。

observers目录

此目录不是Rails的默认提供的,这个目录里的文件是foreman为了监视hosts model变化而进行的一些行为:

class HostObserver < ActiveRecord::Observer
  observe Host::Base

  # Sets and expire provisioning tokens
  # this has to happen post validation and before the orchesration queue is starting to
  # process, as the token value is required within the tftp config file manipulations
  def after_validation(host)
    return unless SETTINGS[:unattended]
    # new server in build mode
    host.set_token if host.new_record? && host.build?
    # existing server change build mode
    if host.respond_to?(:old) and host.old and host.build? != host.old.build?
      host.build? ? host.set_token : host.expire_token
    end
  end
end

ActiveRecord::Observer是Rails3提供的观察者,可以让你在Model的生命周期的某个阶段来做一些动作。 上面的代码看来,只是在观察hosts,在验证之后设置新host的token,以及改变老host的token

services目录

此目录也非Rails默认提供。此目录存放的是一些经过抽离的业务逻辑相关的代码,比如fact导入解析、权限、sso等模块。

这是一种架构思想,models中只保留跟数据操作相关的代码,比如models/host/base.rb中:


   def import_facts(facts)
      # we are not importing facts for hosts in build state (e.g. waiting for a re-installation)
      raise ::Foreman::Exception.new('Host is pending for Build') if build?

      time = facts[:_timestamp]
      time = time.to_time if time.is_a?(String)

      # we are not doing anything we already processed this fact (or a newer one)
      if time
        return true unless last_compile.nil? or (last_compile + 1.minute < time)
        self.last_compile = time
      end

      type = facts.delete(:_type) || 'puppet'
      importer = FactImporter.importer_for(type).new(self, facts)
      importer.import!

      save(:validate => false)
      populate_fields_from_facts(facts, type)
      set_taxonomies(facts)

      # we are saving here with no validations, as we want this process to be as fast
      # as possible, assuming we already have all the right settings in Foreman.
      # If we don't (e.g. we never install the server via Foreman, we populate the fields from facts
      # TODO: if it was installed by Foreman and there is a mismatch,
      # we should probably send out an alert.
      save(:validate => false)
    end

跟facts导入相关的逻辑都抽离在一行代码中:FactImporter.importer_for(type).new(self, facts),而剩下的只是对数据的存取。

validators目录

此目录存放这一些model的验证规则,比如:app/validators/mac_address_validator.rb文件:

class AlphanumericValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors.add(attribute, _("is alphanumeric and cannot contain spaces")) unless value =~ /\A\w+\Z/
  end
end

继承自 ActiveModel::EachValidator,实现了def validate_each(record, attribute, value)方法,来自定义一个验证规则,然后在model中:

validates :name, :uniqueness => true, :presence => true, :alphanumeric => true

validates方法,会在你往数据库插入数据的时候对name字段的数据做验证,如果不满足验证要求,则会回滚数据库。

views目录

此目录与controllers目录里的文件相对应,比如:

views/hosts/ 对应于 controllers/hosts_controller.rb views/hosts/index.html.erb 对应于 HostsController#index action

现在拿views/hosts/下的文件来说明:

foreman

我们能看到有两种类型的erb文件: 带下划线前缀的跟无前缀的erb文件。

# index.html.erb
<% if authorized? %>
  <% title_actions multiple_actions_select, button_group(link_to_if_authorized _("New Host"), hash_for_new_host_path) %>
<% end %>
<%= render 'list', :hosts => @hosts, :header => @title || _("Hosts")  %>

在erb文件中,我们在<% %>中写Ruby代码,在<%= %>中为页面上输入值。

 <%= render 'list', :hosts => @hosts, :header => @title || _("Hosts")  %>

这句代码是使用了_list.html.erb模板,在Rails中叫partial,使用partial可以复用模板,复用公共的部分。

而title_actions和button_group正是被定义在app/helpers/layout_helper.rb中的方法:

module LayoutHelper
  def title(page_title, page_header = nil)
    content_for(:title, page_title.to_s)
    @page_header       ||= page_header || @content_for_title || page_title.to_s
  end

  def title_actions(*elements)
    content_for(:title_actions) { elements.join(" ").html_safe }
  end

  def button_group(*elements)
    content_tag(:div,:class=>"btn-group") { elements.join(" ").html_safe }
  end

  ...
end

我们前面讲过,helpers里的方法,就是为了views里使用的。

lib目录

我们讲完了重要的app目录,下来看另一个重要文件夹lib。 lib里存放的是对本应用项目的一些扩展。比如一些第三方代码、rake任务等。

foreman

lib文件的结构组成也遵循下面的规则:

net/
net.rb

foreman/
foreman.rb

proxy_api/
proxy_api.rb

其他独立的文件夹或文件

net.rb,模拟activerecord自定义了一个虚对象(意思就是没有对应的数据表)来执行dhcp或dns相关的操作:

require "net/validations"

module Net
  class Record
    include Net::Validations
    attr_accessor :hostname, :proxy, :logger

    def initialize(opts = {})
      # set all attributes
      opts.each do |k,v|
        self.send("#{k}=",v) if self.respond_to?("#{k}=")
      end if opts

      self.logger ||= Rails.logger
      raise "Must define a proxy" if proxy.nil?
    end
   ...
 end

 # net/dhcp/record.rb
 module Net::DHCP
  class Record < Net::Record
    attr_accessor :ip, :mac, :network, :nextServer, :filename

    def initialize(opts = { })
      super(opts)
      self.mac     = validate_mac self.mac
      self.network = validate_network self.network
      self.ip      = validate_ip self.ip
    end

    def to_s
      "#{hostname}-#{mac}/#{ip}"
    end

    ...
  end
end

而lib下最重要的一个文件夹应该算proxy_api了,因为foreman是通过API跟smart_proxy交互的。这个文件夹下面的代码组织也是非常有规律的:

module ProxyAPI
  class Resource
    attr_reader :url

    def initialize(args)
      raise("Must provide a protocol and host when initialising a smart-proxy connection") unless (url =~ /^http/)

      @connect_params = {:timeout => Setting[:proxy_request_timeout], :open_timeout => 10, :headers => { :accept => :json },
                        :user => args[:user], :password => args[:password]}

      # We authenticate only if we are using SSL
      if url.match(/^https/i)
        cert         = Setting[:ssl_certificate]
        ca_cert      = Setting[:ssl_ca_file]
        hostprivkey  = Setting[:ssl_priv_key]

        @connect_params.merge!(
          :ssl_client_cert  =>  OpenSSL::X509::Certificate.new(File.read(cert)),
          :ssl_client_key   =>  OpenSSL::PKey::RSA.new(File.read(hostprivkey)),
          :ssl_ca_file      =>  ca_cert,
          :verify_ssl       =>  OpenSSL::SSL::VERIFY_PEER
        ) unless Rails.env == "test"
      end
    end

    protected

    attr_reader :connect_params

    def resource
      # Required in order to ability to mock the resource
      @resource ||= RestClient::Resource.new(url, connect_params)
    end

    # Sets the credentials in the connection parameters, creates new resource when called
    # Since there is now other way to set the credential
    def set_credentials(username, password)
      @connect_params[:user]     = username
      @connect_params[:password] = password
      @resource                  = nil
    end

    def logger; Rails.logger; end

    ...

    def parse(response)
      if response and response.code >= 200 and response.code < 300
        return response.body.present? ? JSON.parse(response.body) : true
      else
        false
      end
    rescue => e
      logger.warn "Failed to parse response: #{response} -> #{e}"
      false
    end

    # Perform GET operation on the supplied path
    def get(path = nil, payload = {})
      # This ensures that an extra "/" is not generated
      if path
        resource[URI.escape(path)].get payload
      else
        resource.get payload
      end
    end

    # Perform POST operation with the supplied payload on the supplied path
    def post(payload, path = "")
      resource[path].post payload
    end

    # Perform PUT operation with the supplied payload on the supplied path
    def put(payload, path = "")
      resource[path].put payload
    end

    ...

  end
end

定义了resource.rb文件,把smart_proxy支持的几种服务的公共部分做了抽象,提供了api访问的基本方法。

然后具体的服务类下面就只有相关的业务逻辑了:


module ProxyAPI
  class DHCP < ProxyAPI::Resource
    def initialize(args)
      @url  = args[:url] + "/dhcp"
      super args
    end

    # Retrieve the Server's subnets
    # Returns: Array of Hashes or false
    # Example [{"network":"192.168.11.0","netmask":"255.255.255.0"},{"network":"192.168.122.0","netmask":"255.255.255.0"}]
    def subnets
      parse get
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to retrieve DHCP subnets"))
    end

    def subnet(subnet)
      parse get(subnet)
    rescue => e
      raise ProxyException.new(url, e, N_("Unable to retrieve DHCP subnet"))
    end

    ...

  end
end

我们可以通过proxy_api下面的文件可以了解,foreman支持了bmc、dhcp、puppet、puppet ca、realm、tftp这几种服务。

另外一个需要说明的就是 ws_proxy.rb文件, 此文件提供了websocket支持。


require 'open3'
require 'socket'
require 'timeout'

class PortInUse < StandardError; end

class WsProxy
  attr_accessor :host, :host_port, :password, :timeout, :idle_timeout, :ssl_target
  attr_reader :proxy_port

  # Allowed ports to communicate with our web sockets proxy
  PORTS = 5910..5930

  def initialize(attributes)
    # setup all attributes.
    defaults.merge(attributes).each do |k, v|
      self.send("#{k}=", v) if self.respond_to?("#{k}=")
    end
  end

  ...
  private

  def ws_proxy
    "#{Rails.root}/extras/noVNC/websockify.py"
  end

  def defaults
    {
      :timeout      => 120,
      :idle_timeout => 120,
      :host_port    => 5900,
      :host         => "0.0.0.0",
    }
  end

  ...

end

我们注意到了,ws支持使用了python的扩展,也就是项目根目录下extras文件夹中提供的。

我们可以通过搜索发现,Foreman在compute_resource的Libvirt和Ovirt类中使用了websocket。

rake任务

lib文件夹下面另外一个重要目录是tasks。你可以在那里看到很多.rake的任务。

rake任务的编写也是需要遵循一定的规则,具体可以参考这个链接:http://guides.rubyonrails.org/command_line.html

小结

本章节内容对Foreman源码结构的重要结构做了阐述,更详细的还需要大家自己去阅读源码进行学习。