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
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,用下面图表示:
这是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的核心。
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/下的文件来说明:
我们能看到有两种类型的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任务等。
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源码结构的重要结构做了阐述,更详细的还需要大家自己去阅读源码进行学习。