Browse files

Merge branch 'next'

  • Loading branch information...
2 parents 308dcbb + 5acb5b2 commit 4c484f2a6a6f627d4b8926404ca179ed373cb9cc Paul Berry committed Nov 17, 2010
Showing with 519 additions and 206 deletions.
  1. +1 −0 .gitignore
  2. +9 −3 README.markdown
  3. +10 −0 app/controllers/application_controller.rb
  4. +13 −1 app/controllers/nodes_controller.rb
  5. +1 −9 app/models/node.rb
  6. +2 −0 app/models/node_class.rb
  7. +2 −0 app/models/node_group.rb
  8. +8 −7 app/models/status.rb
  9. +1 −1 app/views/node_groups/show.html.haml
  10. +25 −24 app/views/nodes/_form.html.haml
  11. +2 −2 app/views/nodes/_nodes.html.haml
  12. +9 −10 app/views/nodes/show.html.haml
  13. +1 −1 app/views/pages/home.html.haml
  14. +1 −1 app/views/shared/_classes.html.haml
  15. +7 −6 app/views/shared/_global_nav.html.haml
  16. +1 −1 app/views/shared/_groups.html.haml
  17. +3 −2 app/views/shared/_node_manager_sidebar.html.haml
  18. +3 −3 app/views/statuses/_run_failure.html.haml
  19. +10 −5 bin/external_node
  20. +1 −0 config/environment.rb
  21. +2 −2 config/initializers/time_formats.rb
  22. +17 −1 config/{settings-sample.yml → settings.yml.example}
  23. +4 −0 db/migrate/20101109001012_add_status_to_reports.rb
  24. +2 −0 lib/node_classification_disabled_error.rb
  25. +22 −29 lib/settings_reader.rb
  26. +1 −1 lib/tasks/mail_patches.rake
  27. +25 −0 spec/controllers/application_controller_spec.rb
  28. +2 −0 spec/controllers/node_classes_controller_spec.rb
  29. +2 −0 spec/controllers/node_groups_controller_spec.rb
  30. +88 −36 spec/controllers/nodes_controller_spec.rb
  31. +42 −0 spec/lib/settings_reader_spec.rb
  32. +16 −0 spec/lib/settings_spec.rb
  33. +3 −2 spec/models/node_spec.rb
  34. +55 −3 spec/models/status_spec.rb
  35. +56 −52 spec/shared_behaviors/controller_mixins.rb
  36. +12 −0 spec/shared_behaviors/sorted_index.rb
  37. +29 −4 spec/views/nodes/edit.html.haml_spec.rb
  38. +31 −0 spec/views/statuses/_run_failure.html.haml_spec.rb
View
1 .gitignore
@@ -7,6 +7,7 @@
Makefile
README.markdown.html
config/database.yml
+config/settings.yml
coverage.info
coverage/*
db/*.db
View
12 README.markdown
@@ -296,11 +296,17 @@ The Puppet Dashboard can act as an external node classification tool, which will
node_terminus = exec
external_nodes = /opt/dashboard/bin/external_node
- *NOTE:* Set the `external_nodes` value to the absolute path of the Puppet Dashboard's `bin/external_node` program. If the Puppet Dashboard is running on a different computer, you should copy this file to the Puppet Master to a local directory like `/etc/puppet` and specify the path to it.
+ Set the `external_nodes` value to the absolute path of the Puppet Dashboard's `bin/external_node` program. If the Puppet Dashboard is running on a different computer, you should copy this file to the Puppet Master to a local directory like `/etc/puppet` and specify the path to it.
- *NOTE:* The `bin/external_node` program connects to the Puppet Dashboard at `localhost` on port `3000`. If your Puppet Dashboard is running on a different host or node, please modify this file.
+ The `bin/external_node` program connects to the Puppet Dashboard at `localhost` on port `3000`. If your Puppet Dashboard is running on a different host or node, please modify this file.
+
+ If you have Dashboard set up to use HTTPS, change the DASHBOARD_URL in `external_node` to the `https` prefix and the correct port number (443, by default). You may also need to change the CERT_PATH, PKEY_PATH, and CA_PATH variables if your puppet master's hostname is not `puppet` or if your ssldir is not `/etc/puppet/ssl`.
+
+ If you would prefer not to edit the external_node script, you may override these settings using environment variables: PUPPET_DASHBOARD_URL, PUPPET_CERT_PATH, PUPPET_PKEY_PATH, PUPPET_CA_PATH. For example:
+ [puppetmasterd]
+ node_terminus = exec
+ external_nodes = /usr/bin/env PUPPET_DASHBOARD_URL=http://dashboard.localdomain:8000 /opt/dashboard/bin/external_node
- *NOTE:* If you have Dashboard set up to use HTTPS, change the DASHBOARD_URL in `external_node` to the `https` prefix and the correct port number (443, by default). You may also need to change the CERT_PATH, and PKEY_PATH variables if your puppet master's hostname is not `puppet` or if your ssldir is not `/etc/puppet/ssl`.
2. Restart the `puppetmasterd` process.
View
10 app/controllers/application_controller.rb
@@ -13,8 +13,18 @@ class ApplicationController < ActionController::Base
helper_method :current_user_session, :current_user
+ before_filter :set_timezone
+
private
+ def set_timezone
+ if SETTINGS.time_zone
+ time_zone_obj = ActiveSupport::TimeZone.new(SETTINGS.time_zone)
+ raise Exception.new("Invalid timezone #{SETTINGS.time_zone.inspect}") unless time_zone_obj
+ Time.zone = time_zone_obj
+ end
+ end
+
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
View
14 app/controllers/nodes_controller.rb
@@ -6,7 +6,10 @@ class NodesController < InheritedResources::Base
layout lambda {|c| c.request.xhr? ? false : 'application' }
def index
+ raise NodeClassificationDisabledError.new if !SETTINGS.use_external_node_classification and request.format == :yaml
scoped_index
+ rescue NodeClassificationDisabledError => e
+ render :text => "Node classification has been disabled", :content_type => 'text/plain', :status => 403
end
def successful
@@ -39,6 +42,7 @@ def search
def show
begin
+ raise NodeClassificationDisabledError.new if !SETTINGS.use_external_node_classification and request.format == :yaml
show!
rescue ActiveRecord::RecordNotFound => e
raise e unless request.format == :yaml
@@ -47,6 +51,8 @@ def show
rescue ParameterConflictError => e
raise e unless request.format == :yaml
render :text => "Node \"#{resource.name}\" has conflicting parameter(s): #{resource.errors.on(:parameters).to_a.to_sentence}", :content_type => 'text/plain', :status => 500
+ rescue NodeClassificationDisabledError => e
+ render :text => "Node classification has been disabled", :content_type => 'text/plain', :status => 403
end
end
@@ -77,7 +83,13 @@ def reports
protected
def resource
- get_resource_ivar || set_resource_ivar(end_of_association_chain.find_by_name!(params[:id]))
+ node = get_resource_ivar
+ return node if node
+
+ node ||= end_of_association_chain.find(params[:id]) rescue nil
+ node ||= end_of_association_chain.find_by_name!(params[:id])
+
+ set_resource_ivar(node)
end
# Render the index using the +scope_name+ (e.g. :successful for Node.successful).
View
10 app/models/node.rb
@@ -60,12 +60,8 @@ def self.per_page; 20 end # Pagination
# Return nodes that have never reported.
named_scope :unreported, :conditions => {:reported_at => nil}
- # Seconds in the past since a node's last report for a node to be considered no longer reporting.
- # Defaults to twice the default puppet run period to prevent timing errors.
- NO_LONGER_REPORTING_CUTOFF = 1.hour
-
# Return nodes that haven't reported recently.
- named_scope :no_longer_reporting, :conditions => ['reported_at < ?', NO_LONGER_REPORTING_CUTOFF.ago]
+ named_scope :no_longer_reporting, lambda{{:conditions => ['reported_at < ?', SETTINGS.no_longer_reporting_cutoff.seconds.ago] }}
def self.count_by_currentness_and_successfulness(currentness, successfulness)
operator = successfulness ? '!=' : '='
@@ -100,10 +96,6 @@ def self.find_from_inventory_search(search_params)
nodes.concat matches.reject {|match| found.include? match.downcase}.map {|match| Node.create!(:name => match)}
end
- def to_param
- name.to_s
- end
-
def configuration
{ 'name' => name, 'classes' => all_node_classes.collect(&:name), 'parameters' => parameter_list }
end
View
2 app/models/node_class.rb
@@ -14,6 +14,8 @@ def self.per_page; 50 end # Pagination
validates_format_of :name, :with => /\A([a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*\Z/, :message => "must contain a valid Puppet class name, e.g. 'foo' or 'foo::bar'"
validates_uniqueness_of :name
+ default_scope :order => 'name ASC'
+
named_scope :search, lambda{|q| q.blank? ? {} : {:conditions => ['name LIKE ?', "%#{q}%"]} }
named_scope :with_nodes_count,
View
2 app/models/node_group.rb
@@ -24,6 +24,8 @@ def self.per_page; 50 end # Pagination
validates_presence_of :name
validates_uniqueness_of :name
+ default_scope :order => 'name ASC'
+
named_scope :search, lambda{|q| q.blank? ? {} : {:conditions => ['name LIKE ?', "%#{q}%"]} }
named_scope :with_nodes_count,
View
15 app/models/status.rb
@@ -5,9 +5,9 @@ def initialize(datum)
@unchanged = datum["unchanged"].to_i
@failed = datum["failed"].to_i
@total = datum["total"].to_i
- @start = datum["start"].to_time
+ @start = Time.zone.parse(datum["start"])
end
-
+
def self.latest(options={})
by_interval(options.merge(:limit => 1)).first
end
@@ -16,9 +16,6 @@ def self.recent(options={})
by_interval options.merge(:start => 1.hour.ago)
end
- # Default time in seconds for the interval
- INTERVAL_CUTOFF = 30.days
-
# Returns an array of Statuses by date for either a :node, or :nodes or all nodes in the system.
#
# Options:
@@ -37,7 +34,7 @@ def self.by_interval(options={})
# WARNING: This uses the local server time, regardless of what is set in the Rails config.
# This should be changed once we have a user-friendly settings file, or can get the browser
# time zone to this method.
- offset = Time.now.utc_offset
+ offset = Time.zone.now.utc_offset
offset_timestamp = "UNIX_TIMESTAMP(time) + #{offset}"
date = "DATE(FROM_UNIXTIME(#{offset_timestamp}))"
@@ -52,7 +49,7 @@ def self.by_interval(options={})
SQL
sql << "WHERE " if has_where
- sql << "time >= \"#{options[:start].to_s(:db)}\"\n" if options[:start]
+ sql << "time >= \"#{options[:start].getutc.to_s(:db)}\"\n" if options[:start]
sql << "AND " if has_and
sql << "node_id = #{options[:node].id} " if options[:node]
sql << "node_id IN (#{options[:nodes].map(&:id).join(',')})\n" if options[:nodes].present?
@@ -63,6 +60,10 @@ def self.by_interval(options={})
return execute(sql)
end
+ def self.within_daily_run_history(options={})
+ self.by_interval( options.merge( :start => SETTINGS.daily_run_history_length.days.ago, :limit => SETTINGS.daily_run_history_length ) )
+ end
+
def self.runtime
Report.all(:limit => 20, :order => 'time DESC').map{|r| r.metrics[:time][:total]}
end
View
2 app/views/node_groups/show.html.haml
@@ -22,7 +22,7 @@
%th Group
%th Source
%tbody
- - @node_group.node_group_children_with_sources.each do |group,sources|
+ - @node_group.node_group_children_with_sources.sort.each do |group,sources|
%tr
%td
%strong= link_to(group.name,group)
View
49 app/views/nodes/_form.html.haml
@@ -13,27 +13,28 @@
= form.label :description
= form.text_area :description, :rows => 4, :placeholder => "Enter a description for this host here..."
- .element
- %h3 Parameters
- %table#parameters.inspector
- %tbody
- - if form.object.parameters.blank?
- = render :partial => 'parameters/parameter_input', :object => form.object.parameters.build
- - else
- = render :partial => 'parameters/parameter_input', :collection => form.object.parameters
- %tfoot
- %tr
- %td{:colspan => 2}
- = link_to_function("Add parameter", :class => "add button") { |page| page.insert_html :bottom, 'parameters', :partial => 'parameters/parameter_input', :object => form.object.parameters.build }
-
- .element.node_classes
- = label_tag 'Classes'
- = text_field_tag 'node[node_class_ids][]', '', :id => 'node_class_ids'
-
- .element.node_groups
- = label_tag 'Groups'
- = text_field_tag 'node[node_group_ids][]', '', :id => 'node_group_ids'
-
- - class_data = {:class => '#node_class_ids', :data_source => node_classes_path(:format => :json), :objects => form.object.node_classes}
- - group_data = {:class => '#node_group_ids', :data_source => node_groups_path(:format => :json), :objects => form.object.node_groups}
- %script#tokenizer{:type => "text/javascript"}= tokenize_input_class(class_data, group_data)
+ - if SETTINGS.use_external_node_classification
+ .element
+ %h3 Parameters
+ %table#parameters.inspector
+ %tbody
+ - if form.object.parameters.blank?
+ = render :partial => 'parameters/parameter_input', :object => form.object.parameters.build
+ - else
+ = render :partial => 'parameters/parameter_input', :collection => form.object.parameters
+ %tfoot
+ %tr
+ %td{:colspan => 2}
+ = link_to_function("Add parameter", :class => "add button") { |page| page.insert_html :bottom, 'parameters', :partial => 'parameters/parameter_input', :object => form.object.parameters.build }
+
+ .element.node_classes
+ = label_tag 'Classes'
+ = text_field_tag 'node[node_class_ids][]', '', :id => 'node_class_ids'
+
+ .element.node_groups
+ = label_tag 'Groups'
+ = text_field_tag 'node[node_group_ids][]', '', :id => 'node_group_ids'
+
+ - class_data = {:class => '#node_class_ids', :data_source => node_classes_path(:format => :json), :objects => form.object.node_classes}
+ - group_data = {:class => '#node_group_ids', :data_source => node_groups_path(:format => :json), :objects => form.object.node_groups}
+ %script#tokenizer{:type => "text/javascript"}= tokenize_input_class(class_data, group_data)
View
4 app/views/nodes/_nodes.html.haml
@@ -14,7 +14,7 @@
&darr; Latest report
%tbody
- if nodes.present?
- - nodes.each do |node|
+ - nodes.sort {|a,b| (b.reported_at || Time.at(0)) <=> (a.reported_at || Time.at(0))}.each do |node|
- sources = container.nodes_with_sources[node] unless container.nil?
%tr[node]{:class => "#{'active' if node == @node}"}
%td.status{:class => node.status_class}
@@ -32,5 +32,5 @@
= node.last_report ? node.last_report.time : "Has not reported"
= pagination_for nodes, more_link
- else
- %td.empty{:colspan => 3}
+ %td.empty{:colspan => container.nil? ? 3 : 4}
= describe_no_matches_for :nodes
View
19 app/views/nodes/show.html.haml
@@ -12,9 +12,10 @@
- unless @node.description.blank?
.description= simple_format h(@node.description)
- = render 'shared/parameters', :resource => @node
- = render 'shared/groups', :resource => @node
- = render 'shared/classes', :resource => @node
+ - if SETTINGS.use_external_node_classification
+ = render 'shared/parameters', :resource => @node
+ = render 'shared/groups', :resource => @node
+ = render 'shared/classes', :resource => @node
%br.clear
@@ -57,14 +58,12 @@
%td= report.total_resources
%td= report.failed_resources
%td= report.total_time
- %tfoot
- %tr
- %td{:colspan => @node ? 8 : 7}
- .actionbar
- - if @node.reports.count > reports_limit
+ - if @node.reports.count > reports_limit
+ %tfoot
+ %tr
+ %td{:colspan => 5}
+ .actionbar
= link_to "More &raquo;", reports_node_path(@node), :class => 'button'
- - else
- &nbsp;
.section
%h3 Dashboard activity
View
2 app/views/pages/home.html.haml
@@ -18,7 +18,7 @@
%p
= pluralize @no_longer_reporting_nodes.length, 'node'
= @no_longer_reporting_nodes.length == 1 ? 'has' : 'have'
- not reported in the last #{time_ago_in_words Node::NO_LONGER_REPORTING_CUTOFF.ago}:
+ not reported in the last #{time_ago_in_words SETTINGS.no_longer_reporting_cutoff.seconds.ago}:
= succeed '.' do
= truncated_node_sentence(@no_longer_reporting_nodes, :more_link => no_longer_reporting_nodes_path)
View
2 app/views/shared/_classes.html.haml
@@ -7,7 +7,7 @@
%th.name Class
%th Source
%tbody
- - resource.node_classes_with_sources.each do |node_class,sources|
+ - resource.node_classes_with_sources.sort.each do |node_class,sources|
%tr
%td.name
%strong= link_to(node_class.name,node_class)
View
13 app/views/shared/_global_nav.html.haml
@@ -9,12 +9,13 @@
%li &bull;
%li{:class => active_if(controller_name == "nodes")}
= link_to "Nodes", nodes_path
- %li &bull;
- %li{:class => active_if(controller_name == "node_groups")}
- = link_to "Groups", node_groups_path
- %li &bull;
- %li{:class => active_if(controller_name == "node_classes")}
- = link_to "Classes", node_classes_path
+ - if SETTINGS.use_external_node_classification
+ %li &bull;
+ %li{:class => active_if(controller_name == "node_groups")}
+ = link_to "Groups", node_groups_path
+ %li &bull;
+ %li{:class => active_if(controller_name == "node_classes")}
+ = link_to "Classes", node_classes_path
%li &bull;
%li{:class => active_if(controller_name == "reports")}
= link_to "Reports", reports_path
View
2 app/views/shared/_groups.html.haml
@@ -7,7 +7,7 @@
%th Group
%th Source
%tbody
- - resource.node_groups_with_sources.each do |group,sources|
+ - resource.node_groups_with_sources.sort.each do |group,sources|
%tr
%td
%strong= link_to(group.name,group)
View
5 app/views/shared/_node_manager_sidebar.html.haml
@@ -23,5 +23,6 @@
.footer.actionbar
= link_to "Add node", new_node_path, :class => 'button'
-- for type in [NodeClass, NodeGroup]
- = render "shared/node_manager_sidebar_for_type", :type => type
+- if SETTINGS.use_external_node_classification
+ - for type in [NodeClass, NodeGroup]
+ = render "shared/node_manager_sidebar_for_type", :type => type
View
6 app/views/statuses/_run_failure.html.haml
@@ -1,12 +1,12 @@
- nodes = local_assigns[:nodes]
- node = local_assigns[:node]
-- args = {:start => Status::INTERVAL_CUTOFF.ago, :limit => Status::INTERVAL_CUTOFF/1.day}
+- args = {}
- args[:node] = node if node
- args[:nodes] = nodes if nodes
-- statuses = Status.by_interval(args)
+- statuses = Status.within_daily_run_history(args)
%h3 Daily run status
-%p.legend Number and status of runs during the <strong>last #{Status::INTERVAL_CUTOFF/1.day} days</strong>:
+%p.legend Number and status of runs during the <strong>last #{SETTINGS.daily_run_history_length} days</strong>:
- if statuses.present?
%table.inspector.data.status.bar
View
15 bin/external_node
@@ -12,26 +12,31 @@ require 'yaml'
require 'uri'
require 'net/http'
-DASHBOARD_URL="http://dashboard:3000"
+DASHBOARD_URL = "http://dashboard:3000"
# These settings are only used when connecting to dashboard over https (SSL)
CERT_PATH = "/etc/puppet/ssl/certs/puppet.pem"
PKEY_PATH = "/etc/puppet/ssl/private_keys/puppet.pem"
CA_PATH = "/etc/puppet/ssl/certs/ca.pem"
+cert_path = ENV['PUPPET_CERT_PATH'] || CERT_PATH
+pkey_path = ENV['PUPPET_PKEY_PATH'] || PKEY_PATH
+ca_path = ENV['PUPPET_CA_PATH'] || CA_PATH
+
NODE = ARGV.first
-uri = URI.parse("#{DASHBOARD_URL}/nodes/#{NODE}")
+url = ENV['PUPPET_DASHBOARD_URL'] || DASHBOARD_URL
+uri = URI.parse("#{url}/nodes/#{NODE}")
require 'net/https' if uri.scheme == 'https'
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == 'https'
- cert = File.read(CERT_PATH)
- pkey = File.read(PKEY_PATH)
+ cert = File.read(cert_path)
+ pkey = File.read(pkey_path)
http.use_ssl = true
http.cert = OpenSSL::X509::Certificate.new(cert)
http.key = OpenSSL::PKey::RSA.new(pkey)
- http.ca_file = CA_PATH
+ http.ca_file = ca_path
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
result = http.start { http.request_get(uri.path, 'Accept' => 'text/yaml') }
View
1 config/environment.rb
@@ -44,6 +44,7 @@
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names.
+ # The user can override this in config/settings.yml.
config.time_zone = 'UTC'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
View
4 config/initializers/time_formats.rb
@@ -1,8 +1,8 @@
# TODO make format customizable through an external configuration file.
Time::DATE_FORMATS.update(
# :default => lambda{|time| time.strftime('%m/%d/%y ') + time.strftime('%I:%M%p').downcase }
- :default => lambda{|time| time.in_time_zone.strftime('%Y-%m-%d %H:%M %Z') },
- :date => lambda{|time| time.in_time_zone.strftime('%Y-%m-%d') },
+ :default => lambda{|time| time.in_time_zone.strftime(SETTINGS.datetime_format) },
+ :date => lambda{|time| time.in_time_zone.strftime(SETTINGS.date_format) },
:time => lambda{|time| time.in_time_zone.strftime('%I:%M%p') }
)
View
18 config/settings-sample.yml → config/settings.yml.example
@@ -3,7 +3,7 @@
# This file is meant for storing setting information that is never
# published or committed to a revision control system.
#
-# Do not modify this "config/settings-sample.yml" file directly -- you
+# Do not modify this "config/settings.yml.example" file directly -- you
# should copy it to "config/settings.yml" and customize it there.
#
#---[ Values ]----------------------------------------------------------
@@ -37,4 +37,20 @@ inventory_server: 'puppet'
# Port for the inventory server.
inventory_port: 8140
+# Amount of time in seconds since last report before a node is considered no longer reporting
+no_longer_reporting_cutoff: 3600
+
+# How many days of history to display on the "Daily Run Status" graph
+daily_run_history_length: 30
+
+use_external_node_classification: true
+
+# Uncomment the following line to set a local time zone. Run
+# "rake time:zones:local" for the name of your local time zone.
+#time_zone: 'Pacific Time (US & Canada)'
+
+# Look at http://ruby-doc.org/core/classes/Time.html#M000298 for the strftime formatting
+datetime_format: '%Y-%m-%d %H:%M %Z'
+date_format: '%Y-%m-%d'
+
#===[ fin ]=============================================================
View
4 db/migrate/20101109001012_add_status_to_reports.rb
@@ -12,6 +12,10 @@ def report
end
end
+ class Node < ActiveRecord::Base
+ belongs_to :last_report, :class_name => 'AddStatusToReports::Report'
+ end
+
def self.up
add_column :reports, :status, :string
add_index :reports, [:time, :node_id, :status]
View
2 lib/node_classification_disabled_error.rb
@@ -0,0 +1,2 @@
+class NodeClassificationDisabledError < StandardError
+end
View
51 lib/settings_reader.rb
@@ -10,50 +10,43 @@
# Reads settings from an ERB-parsed YAML file and returns an OpenStruct object.
#
# Examples:
-# # Read from default "config/settings.yml" and "config/settings-sample.yml" files:
+# # Read from default "config/settings.yml" and "config/settings.yml.example" files:
# SETTINGS = SettingsReader.read
-#
-# # Read a specific file:
-# SETTINGS = SettingsReader.read("myfile.yml")
-#
class SettingsReader
# Return an OpenStruct object with setting information. The settings are read
# from an ERB-parsed YAML file.
- #
- # Arguments:
- # * Filename to read settings from. Optional, if not given will try
- # "config/setting.yml" and "config/setting-sample.yml".
- #
- # Options:
- # * :verbose => Print status to screen on error. Defaults to true.
- def self.read(*args)
- opts = args.extract_options!
- verbose = opts[:verbose] != false
- given_file = args.first
-
+ def self.read
normal_file = "config/settings.yml"
- sample_file = "config/settings-sample.yml"
+ sample_file = "config/settings.yml.example"
rails_root = RAILS_ROOT rescue File.dirname(File.dirname(__FILE__))
message = "** SettingsReader - "
- if object = self.filename_to_ostruct(given_file)
- message << "loaded '#{given_file}'"
- elsif object = self.filename_to_ostruct(File.join(rails_root, normal_file))
- message << "loaded '#{normal_file}'"
- elsif object = self.filename_to_ostruct(File.join(rails_root, sample_file))
- message << "loaded '#{sample_file}'"
- else
- raise Errno::ENOENT, "Couldn't find '#{normal_file}'"
+ message << "Loading settings from file #{normal_file}. "
+
+ settings = self.filename_to_hash(File.join(rails_root, normal_file))
+ defaults = self.filename_to_hash(File.join(rails_root, sample_file))
+
+ unspecified_keys = []
+
+ defaults.each do |key,value|
+ unless settings.key?(key)
+ unspecified_keys << key
+ settings[key] = value
+ end
+ end
+
+ if unspecified_keys.present?
+ message << "Using default values for unspecified settings " << unspecified_keys.sort.map(&:inspect).to_sentence
end
RAILS_DEFAULT_LOGGER.info(message) rescue nil
- return object
+ return OpenStruct.new(settings)
end
# Return an OpenStruct object by reading the +filename+ and parsing it with ERB and YAML.
- def self.filename_to_ostruct(filename)
- return OpenStruct.new(YAML.load(ERB.new(File.read(filename)).result)) rescue nil
+ def self.filename_to_hash(filename)
+ return YAML.load(ERB.new(File.read(filename)).result) rescue {}
end
end
View
2 lib/tasks/mail_patches.rake
@@ -25,7 +25,7 @@ task :mail_patches do
files = Dir.glob("00*.patch")
files.each do |file|
contents = File.read(file)
- contents.sub!(/^---$/, "#{additional_info}---")
+ contents.sub!(/^---\n/, "---\n#{additional_info}")
File.open(file, 'w') do |file_handle|
file_handle.print contents
end
View
25 spec/controllers/application_controller_spec.rb
@@ -0,0 +1,25 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+class InheritedFromApplicationController < ApplicationController
+ def generic_action
+ @time_zone = Time.zone
+ end
+end
+
+describe InheritedFromApplicationController do
+ before :each do
+ Time.zone = 'UTC'
+ end
+
+ it "should set the timezone to whatever is in SETTINGS.time_zone" do
+ SETTINGS.stubs(:time_zone).returns('Pacific Time (US & Canada)')
+ get :generic_action
+ Time.zone.name.should == "Pacific Time (US & Canada)"
+ end
+
+ it "should raise if SETTINGS.time_zone is set to something invalid" do
+ SETTINGS.stubs(:time_zone).returns('invalid')
+ lambda { get :generic_action }.should raise_error
+ Time.zone.name.should == "UTC"
+ end
+end
View
2 spec/controllers/node_classes_controller_spec.rb
@@ -1,10 +1,12 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require 'shared_behaviors/controller_mixins'
+require 'shared_behaviors/sorted_index'
describe NodeClassesController do
def model; NodeClass end
it_should_behave_like "without JSON pagination"
it_should_behave_like "with search by q and tag"
+ it_should_behave_like "sorted index"
end
View
2 spec/controllers/node_groups_controller_spec.rb
@@ -1,10 +1,12 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require 'shared_behaviors/controller_mixins'
+require 'shared_behaviors/sorted_index'
describe NodeGroupsController do
def model; NodeGroup end
it_should_behave_like "without JSON pagination"
it_should_behave_like "with search by q and tag"
+ it_should_behave_like "sorted index"
end
View
124 spec/controllers/nodes_controller_spec.rb
@@ -24,18 +24,35 @@
end
context "as YAML" do
- it "should return YAML when the nodes are valid" do
- get :index, :format => "yaml"
+ context "when using node classification" do
+ before :each do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
+ end
- response.should be_success
- struct = yaml_from_response_body
- struct.size.should == 1
- struct.first["name"].should == @node.name
+ it "should return YAML when the nodes are valid" do
+ get :index, :format => "yaml"
+
+ response.should be_success
+ struct = yaml_from_response_body
+ struct.size.should == 1
+ struct.first["name"].should == @node.name
+ end
+
+ it "should propagate errors encountered when a node is invalid" do
+ Node.any_instance.stubs(:compiled_parameters).raises ParameterConflictError
+ lambda {get :index, :format => "yaml"}.should raise_error(ParameterConflictError)
+ end
end
- it "should propagate errors encountered when a node is invalid" do
- Node.any_instance.stubs(:compiled_parameters).raises ParameterConflictError
- lambda {get :index, :format => "yaml"}.should raise_error(ParameterConflictError)
+ context "when not using node classification" do
+ it "should raise an error and respond 403" do
+ SETTINGS.stubs(:use_external_node_classification).returns(false)
+ get :index, :format => "yaml"
+
+ response.body.should =~ /Node classification has been disabled/
+ response.should_not be_success
+ response.response_code.should == 403
+ end
end
end
end
@@ -82,28 +99,45 @@
end
context "as YAML" do
- it "should return YAML when the node is valid" do
- get :show, :id => @node.name, :format => "yaml"
+ context "when using node classification" do
+ before :each do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
+ end
- response.should be_success
- struct = yaml_from_response_body
- struct["name"].should == @node.name
- end
+ it "should return YAML when the node is valid" do
+ get :show, :id => @node.name, :format => "yaml"
+
+ response.should be_success
+ struct = yaml_from_response_body
+ struct["name"].should == @node.name
+ end
+
+ it "should explain errors encountered when the node is invalid" do
+ Node.any_instance.stubs(:compiled_parameters).raises ParameterConflictError
+ get :show, :id => @node.name, :format => "yaml"
- it "should explain errors encountered when the node is invalid" do
- Node.any_instance.stubs(:compiled_parameters).raises ParameterConflictError
- get :show, :id => @node.name, :format => "yaml"
+ response.should_not be_success
+ response.body.should =~ /has conflicting parameter\(s\)/
+ end
+
+ it "should return YAML for an empty node when the node is not found" do
+ get :show, :id => "nonexistent", :format => "yaml"
- response.should_not be_success
- response.body.should =~ /has conflicting parameter\(s\)/
+ response.should be_success
+ struct = yaml_from_response_body
+ struct.should == {'classes' => []}
+ end
end
- it "should return YAML for an empty node when the node is not found" do
- get :show, :id => "nonexistent", :format => "yaml"
+ context "when not using node classification" do
+ it "should raise an error and respond 403" do
+ SETTINGS.stubs(:use_external_node_classification).returns(false)
+ get :show, :id => @node.name, :format => "yaml"
- response.should be_success
- struct = yaml_from_response_body
- struct.should == {'classes' => []}
+ response.body.should =~ /Node classification has been disabled/
+ response.should_not be_success
+ response.response_code.should == 403
+ end
end
end
end
@@ -114,7 +148,7 @@
end
def do_get
- get :edit, :id => @node.name
+ get :edit, :id => @node.id
end
it 'should make the requested node available to the view' do
@@ -126,12 +160,18 @@ def do_get
do_get
response.should render_template('edit')
end
+
+ it 'should work when given a node name' do
+ get :edit, :id => @node.name
+
+ assigns[:node].should == @node
+ end
end
describe '#update' do
before :each do
@node = Node.generate!
- @params = { :id => @node.name, :node => @node.attributes }
+ @params = { :id => @node.id, :node => @node.attributes }
end
def do_put
@@ -143,6 +183,13 @@ def do_put
lambda { do_put }.should raise_error(ActiveRecord::RecordNotFound)
end
+ it 'should work when given a node name' do
+ @params.merge!({:id => @node.name})
+
+ do_put
+ assigns[:node].should == @node
+ end
+
describe 'when a valid node id is given' do
describe 'and the data provided would make the node invalid' do
@@ -367,15 +414,20 @@ def do_get
end
context "as YAML" do
- before { get action, action_params.merge(:format => "yaml") }
-
- it_should_behave_like "a successful scoped_index rendering"
- it_should_behave_like "an un-paginated nodes collection"
-
- it "should return YAML" do
- struct = yaml_from_response_body
- struct.size.should == 1
- struct.first["name"].should == action
+ context "when using node classification" do
+ before :each do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
+ get action, action_params.merge(:format => "yaml")
+ end
+
+ it_should_behave_like "a successful scoped_index rendering"
+ it_should_behave_like "an un-paginated nodes collection"
+
+ it "should return YAML" do
+ struct = yaml_from_response_body
+ struct.size.should == 1
+ struct.first["name"].should == action
+ end
end
end
View
42 spec/lib/settings_reader_spec.rb
@@ -0,0 +1,42 @@
+require File.expand_path(File.join(File.dirname(__FILE__), *%w[.. spec_helper]))
+
+describe SettingsReader do
+ before :each do
+ @settings_file = <<FILE
+foo: bar
+FILE
+
+ @sample_file = <<FILE
+foo: bob
+bat: baz
+FILE
+
+ @settings_file_all = <<FILE
+foo: bar
+bat: man
+FILE
+ end
+
+ it "should use values from settings.yml if specified, and values from settings.yml.example if not" do
+ File.stubs(:read).with {|filename| File.basename(filename) == "settings.yml"}.returns(@settings_file)
+ File.stubs(:read).with {|filename| File.basename(filename) == "settings.yml.example"}.returns(@sample_file)
+ RAILS_DEFAULT_LOGGER.expects(:info).with {|msg| msg =~ /Using default values for unspecified settings "bat"/}
+
+ SettingsReader.read.should == OpenStruct.new("foo" => "bar", "bat" => "baz")
+ end
+
+ it "should use values from settings.yml.example if settings.yml does not exist" do
+ File.stubs(:read).with {|filename| File.basename(filename) == "settings.yml.example"}.returns(@sample_file)
+ RAILS_DEFAULT_LOGGER.expects(:info).with {|msg| msg =~ /Using default values for unspecified settings "bat" and "foo"/}
+
+ SettingsReader.read.should == OpenStruct.new("foo" => "bob", "bat" => "baz")
+ end
+
+ it "should not output a warning if settings.yml defines all settings" do
+ File.stubs(:read).with {|filename| File.basename(filename) == "settings.yml"}.returns(@settings_file_all)
+ File.stubs(:read).with {|filename| File.basename(filename) == "settings.yml.example"}.returns(@sample_file)
+ RAILS_DEFAULT_LOGGER.expects(:info).with {|msg| msg !~ /Using default values/}
+
+ SettingsReader.read.should == OpenStruct.new("foo" => "bar", "bat" => "man")
+ end
+end
View
16 spec/lib/settings_spec.rb
@@ -0,0 +1,16 @@
+require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
+
+describe "SETTINGS" do
+ it "should allow you to set the datetime_format" do
+ Time.zone = "UTC"
+ SETTINGS.stubs(:datetime_format).returns('%d-%m-%Y')
+ Time.utc("2010", "11", "12", 12, 15, 00).to_s.should == '12-11-2010'
+ end
+
+ it "should allow you to set the date_format" do
+ Time.zone = "UTC"
+ SETTINGS.stubs(:date_format).returns('%A %B %d, %Y')
+ Time.utc("2010", "11", "12", 12, 15, 00).to_s(:date).should == 'Friday November 12, 2010'
+ end
+end
+
View
5 spec/models/node_spec.rb
@@ -167,8 +167,9 @@
describe "no_longer_reporting" do
it "should return all nodes whose latest report is more than 1 hour ago" do
- old = node = Node.generate(:reported_at => 2.hours.ago)
- new = node = Node.generate(:reported_at => 10.minutes.ago)
+ SETTINGS.expects(:no_longer_reporting_cutoff).at_least_once.returns(1.hour.to_i)
+ old = node = Node.generate(:reported_at => 2.hours.ago, :name => "old")
+ new = node = Node.generate(:reported_at => 10.minutes.ago, :name => "new")
Node.no_longer_reporting.should include(old)
Node.no_longer_reporting.should_not include(new)
View
58 spec/models/status_spec.rb
@@ -6,16 +6,68 @@
describe ".by_interval" do
context "when the interval is 1 day" do
before :each do
- time = Time.now.beginning_of_day + 1.hours
+ Time.zone = 'Pacific Time (US & Canada)'
+
+ time = Time.zone.parse("2009-11-12 00:01 PST")
+ Report.generate!(:report => report_yaml_with(:time => time))
+
+ time = Time.zone.parse("2009-11-11 23:59 PST")
+ Report.generate!(:report => report_yaml_with(:time => time))
+
+ time = Time.zone.parse("2009-11-10 23:59 PST")
+ Report.generate!(:report => report_yaml_with(:time => time))
+ Report.generate!(:report => report_yaml_with(:time => time - 10))
+ end
+
+ it "should return reports for the correct day only" do
+ Status.by_interval(:limit => 1, :start => Time.zone.parse("2009-11-11 00:00 PST")).
+ map(&:start).should == [Time.zone.parse("2009-11-11 00:00 PST")]
+
+ Status.by_interval(:limit => 1, :start => Time.zone.parse("2009-11-12 00:00 PST")).
+ map(&:start).should == [Time.zone.parse("2009-11-12 00:00 PST")]
+
+ Status.by_interval(:limit => 1, :start => Time.zone.parse("2009-11-10 00:00 PST")).map(&:total).should == [2]
+ end
+
+ it "should return reports after the start time" do
+ Status.by_interval(:start => Time.zone.parse("2009-11-11 00:00 PST")).
+ map(&:start).should == ["2009-11-11 00:00 PST", "2009-11-12 00:00 PST"].map {|time| Time.zone.parse time}
+ end
+ end
+ end
+
+ describe ".within_daily_run_history" do
+ context "when the daily_run_history_length is 1 day" do
+ before :each do
+ time = Time.zone.now.beginning_of_day + 1.hours
Report.generate!(:report => report_yaml_with(:time => time))
- time = Time.now.beginning_of_day - 1.hours
+ time = Time.zone.now.beginning_of_day - 1.hours
Report.generate!(:report => report_yaml_with(:time => time))
+
+ SETTINGS.stubs(:daily_run_history_length).returns(1)
end
it "should return reports for the correct day only" do
- Status.by_interval(:limit => 1).first.total.should == 1
+ Status.within_daily_run_history.first.total.should == 1
+ end
+ end
+
+ context "when the daily_run_history_length is 0 days" do
+ before :each do
+ time = Time.zone.now.beginning_of_day + 1.hours
+ Report.generate!(:report => report_yaml_with(:time => time))
+
+ time = Time.zone.now.beginning_of_day - 1.hours
+ Report.generate!(:report => report_yaml_with(:time => time))
+
+ SETTINGS.stubs(:daily_run_history_length).returns(0)
+ end
+
+ it "should not return any history" do
+ Status.within_daily_run_history.should == []
end
end
end
+
end
View
108 spec/shared_behaviors/controller_mixins.rb
@@ -2,79 +2,83 @@
# ActiveRecord model class to use for describing this behavior.
describe "with search by q and tag", :shared => true do
- before :each do
- @for_tag = model.generate(:name => 'for_tag')
- @for_q = model.generate(:name => 'for_q')
- model.generate(:name => 'without_search')
- end
+ describe "when searching" do
+ before :each do
+ @for_tag = model.generate(:name => 'for_tag')
+ @for_q = model.generate(:name => 'for_q')
+ model.generate(:name => 'without_search')
+ end
- describe "without a search" do
- before { get 'index' }
- subject { assigns[model.name.tableize] }
+ describe "without a search" do
+ before { get 'index' }
+ subject { assigns[model.name.tableize] }
- it "returns all node groupes" do
- should == model.all
+ it "returns all node groupes" do
+ should == model.all
+ end
end
- end
- describe "with a 'tag' search" do
- before { get 'index', :tag => 'for_tag' }
- subject { assigns[model.name.tableize] }
+ describe "with a 'tag' search" do
+ before { get 'index', :tag => 'for_tag' }
+ subject { assigns[model.name.tableize] }
- it "returns node groupes whose name contains the term" do
- subject.all?{|item| item.name.include?('for_tag')}.should be_true
- end
+ it "returns node groupes whose name contains the term" do
+ subject.all?{|item| item.name.include?('for_tag')}.should be_true
+ end
- it "does not return node groups whose name does not contain the term" do
- subject.any?{|item| item.name.include?('for_q')}.should be_false
+ it "does not return node groups whose name does not contain the term" do
+ subject.any?{|item| item.name.include?('for_q')}.should be_false
+ end
end
- end
- describe "with a 'q' search" do
- before { get 'index', :q => 'for_q' }
- subject { assigns[model.name.tableize] }
+ describe "with a 'q' search" do
+ before { get 'index', :q => 'for_q' }
+ subject { assigns[model.name.tableize] }
- it "returns node groupes whose name contains the term" do
- subject.all?{|item| item.name.include?('for_q')}.should be_true
- end
+ it "returns node groupes whose name contains the term" do
+ subject.all?{|item| item.name.include?('for_q')}.should be_true
+ end
- it "does not return node groups whose name does not contain the term" do
- subject.any?{|item| item.name.include?('for_tag')}.should be_false
+ it "does not return node groups whose name does not contain the term" do
+ subject.any?{|item| item.name.include?('for_tag')}.should be_false
+ end
end
end
end
describe "without JSON pagination", :shared => true do
- describe "GET index" do
- describe "as HTML" do
- before { get 'index', :format => 'html' }
- subject { assigns[model.name.tableize] }
-
- # NOTE: Once upon a time, the collection was paginated until it was realized that this broke the charts.
- # it "paginates by the page parameter" do
- # should be_a_kind_of(WillPaginate::Collection)
- # end
-
- it "will paginate" do
- should be_a_kind_of(WillPaginate::Collection)
+ describe "without JSON pagination" do
+ describe "GET index" do
+ describe "as HTML" do
+ before { get 'index', :format => 'html' }
+ subject { assigns[model.name.tableize] }
+
+ # NOTE: Once upon a time, the collection was paginated until it was realized that this broke the charts.
+ # it "paginates by the page parameter" do
+ # should be_a_kind_of(WillPaginate::Collection)
+ # end
+
+ it "will paginate" do
+ should be_a_kind_of(WillPaginate::Collection)
+ end
end
- end
- describe "as JSON" do
- before { get 'index', :format => 'json' }
- subject { assigns[model.name.tableize] }
+ describe "as JSON" do
+ before { get 'index', :format => 'json' }
+ subject { assigns[model.name.tableize] }
- it "does not paginate" do
- should_not be_a_kind_of(WillPaginate::Collection)
+ it "does not paginate" do
+ should_not be_a_kind_of(WillPaginate::Collection)
+ end
end
- end
- describe "as YAML" do
- before { get 'index', :format => 'yaml' }
- subject { assigns[model.name.tableize] }
+ describe "as YAML" do
+ before { get 'index', :format => 'yaml' }
+ subject { assigns[model.name.tableize] }
- it "does not paginate" do
- should_not be_a_kind_of(WillPaginate::Collection)
+ it "does not paginate" do
+ should_not be_a_kind_of(WillPaginate::Collection)
+ end
end
end
end
View
12 spec/shared_behaviors/sorted_index.rb
@@ -0,0 +1,12 @@
+describe "sorted index", :shared => true do
+ describe "when retrieving" do
+ it "should be sorted by default" do
+ c = model.generate! :name => "c"
+ b = model.generate! :name => "b"
+ d = model.generate! :name => "d"
+ a = model.generate! :name => "a"
+
+ model.all.should == [a,b,c,d]
+ end
+ end
+end
View
33 spec/views/nodes/edit.html.haml_spec.rb
@@ -3,7 +3,7 @@
describe '/nodes/edit' do
before :each do
assigns[:node] = @node = Node.generate!
- params[:id] = @node.name
+ params[:id] = @node.id
end
def do_render
@@ -82,12 +82,22 @@ def do_render
@node.node_classes << @classes[0..2]
end
- it 'should provide a means to edit the associated classes' do
+ it 'should provide a means to edit the associated classes when using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
+
do_render
response.should have_tag('input#node_class_ids')
end
- it 'should show the associated classes' do
+ it 'should not provide a means to edit the associated classes when not using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(false)
+
+ do_render
+ response.should_not have_tag('input#node_class_ids')
+ end
+
+ it 'should show the associated classes when using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
do_render
response.should have_tag('#tokenizer') do
@@ -100,6 +110,13 @@ def do_render
end
end
+ it 'should not show the associated classes when not using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(false)
+ do_render
+
+ response.should_not have_tag('#node_class_ids')
+ end
+
it 'should provide a remove link for each associated class'
it 'should show the classes available to be associated'
it 'should show non-associated classes in the classes available to be associated section'
@@ -112,7 +129,8 @@ def do_render
@node.node_groups << @groups[0..3]
end
- it 'should show the associated groups' do
+ it 'should show the associated groups when using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(true)
do_render
response.should have_tag('#tokenizer') do
@@ -125,6 +143,13 @@ def do_render
end
end
+ it 'should not show associated groups when not using node classification' do
+ SETTINGS.stubs(:use_external_node_classification).returns(false)
+ do_render
+
+ response.should_not have_tag('#node_group_ids')
+ end
+
it 'should provide a remove link for each associated group'
it 'should show the groups available to be associated'
it 'should show non-associated groups in the groups available to be associated section'
View
31 spec/views/statuses/_run_failure.html.haml_spec.rb
@@ -0,0 +1,31 @@
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
+
+describe "/statuses/_run_failure.html.haml" do
+
+ describe "successful render" do
+ specify do
+ render
+ response.should be_success
+ end
+
+ it "should display the specified number of days of data" do
+ @node = Node.create!(:name => "node")
+
+ 32.times do |n|
+ report = Puppet::Transaction::Report.new
+ report.stubs(:failed?).returns(false)
+ report.stubs(:time).returns n.days.ago
+ report.stubs(:host).returns "node"
+
+ @node.reports.create!(:report => report)
+ end
+
+ SETTINGS.stubs(:daily_run_history_length).returns(20)
+
+ assigns[:node] = @node
+ render
+
+ response.should have_tag("tr.labels th", :count => 20)
+ end
+ end
+end

0 comments on commit 4c484f2

Please sign in to comment.