diff options
353 files changed, 10671 insertions, 3054 deletions
diff --git a/.gitignore b/.gitignore index ee6b386eef..9401920165 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ examples/openid-cstore examples/openid-db chef/pkg chef-server/pkg -chef-server-slice/pkg +chef-server-webui/pkg +chef-server-api/pkg chef/log chef-server/log log @@ -18,3 +19,5 @@ couchdb.stdout features/data/tmp/** *.swp features/data/cookbooks/**/metadata.json +features/data/solr/** +erl_crash.dump @@ -1,4 +1,4 @@ -gems = %w[chef chef-server-slice chef-server] +gems = %w[chef chef-server-api chef-server-webui chef-server chef-solr] require 'rubygems' require 'cucumber/rake/task' @@ -26,71 +26,111 @@ end desc "Run the rspec tests" task :spec do Dir.chdir("chef") { sh "rake spec" } + Dir.chdir("chef-solr") { sh "rake spec" } end task :default => :spec -def start_dev_environment(type="normal") - @couchdb_server_pid = nil - @chef_server_pid = nil - @chef_indexer_pid = nil - @stompserver_pid = nil - - ccid = fork - if ccid - @couchdb_server_pid = ccid +def start_couchdb(type="normal") + @couchdb_server_pid = nil + cid = fork + if cid + @couchdb_server_pid = cid else exec("couchdb") end +end - scid = fork - if scid - @stompserver_pid = scid +def start_rabbitmq(type="normal") + @rabbitmq_server_pid = nil + cid = fork + if cid + @rabbitmq_server_pid = cid else - exec("stompserver") + exec("rabbitmq-server") end +end - mcid = fork - if mcid # parent - @chef_indexer_pid = mcid - else # child +def start_chef_solr(type="normal") + @chef_solr_pid = nil + cid = fork + if cid + @chef_solr_pid = cid + else case type when "normal" - exec("chef-indexer -l debug") + exec("./chef-solr/bin/chef-solr -l debug") when "features" - exec("chef-indexer -c #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug") + exec("./chef-solr/bin/chef-solr -c #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug") end end +end +def start_chef_solr_indexer(type="normal") + @chef_solr_indexer = nil + cid = fork + if cid + @chef_solr_indexer_pid = cid + else + case type + when "normal" + exec("./chef-solr/bin/chef-solr-indexer -l debug") + when "features" + exec("./chef-solr/bin/chef-solr-indexer -c #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug") + end + end +end + +def start_chef_server(type="normal") + @chef_server_pid = nil mcid = fork if mcid # parent @chef_server_pid = mcid else # child case type when "normal" - exec("chef-server -l debug -N -c 2") + exec("./chef-server/bin/chef-server -a thin -l debug -N") when "features" - exec("chef-server -C #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug -N -c 2") - + exec("./chef-server/bin/chef-server -a thin -C #{File.join(File.dirname(__FILE__), "features", "data", "config", "server.rb")} -l debug -N") end end +end - puts "Running Chef at #{@chef_server_pid}" - puts "Running Chef Indexer at #{@chef_indexer_pid}" +def start_dev_environment(type="normal") + start_couchdb(type) + start_rabbitmq(type) + start_chef_solr(type) + start_chef_solr_indexer(type) + start_chef_server(type) puts "Running CouchDB at #{@couchdb_server_pid}" - puts "Running Stompserver at #{@stompserver_pid}" + puts "Running RabbitMQ at #{@rabbitmq_server_pid}" + puts "Running Chef Solr at #{@chef_solr_pid}" + puts "Running Chef Solr Indexer at #{@chef_solr_indexer_pid}" + puts "Running Chef at #{@chef_server_pid}" end def stop_dev_environment - puts "Stopping CouchDB" - Process.kill("KILL", @couchdb_server_pid) - puts "Stopping Stomp server" - Process.kill("KILL", @stompserver_pid) - puts "Stopping Chef Server" - Process.kill("INT", @chef_server_pid) - puts "Stopping Chef Indexer" - Process.kill("INT", @chef_indexer_pid) - puts "\nCouchDB, Stomp, Chef Server and Chef Indexer killed - have a nice day!" + if @chef_server_pid + puts "Stopping Chef" + Process.kill("KILL", @chef_server_pid) + end + if @chef_solr_pid + puts "Stopping Chef Solr" + Process.kill("INT", @chef_solr_pid) + end + if @chef_solr_indexer_pid + puts "Stopping Chef Solr Indexer" + Process.kill("INT", @chef_solr_indexer_pid) + end + if @couchdb_server_pid + puts "Stopping CouchDB" + Process.kill("KILL", @couchdb_server_pid) + end + if @rabbitmq_server_pid + puts "Stopping RabbitMQ" + Process.kill("KILL", @rabbitmq_server_pid) + end + puts "Have a nice day!" end def wait_for_ctrlc @@ -106,7 +146,7 @@ def wait_for_ctrlc end desc "Run a Devel instance of Chef" -task :dev => "dev:install" do +task :dev do start_dev_environment wait_for_ctrlc end @@ -114,12 +154,77 @@ end namespace :dev do desc "Install a test instance of Chef for doing features against" task :features do - gems.each do |dir| - Dir.chdir(dir) { sh "rake install" } - end start_dev_environment("features") wait_for_ctrlc end + + namespace :features do + + namespace :start do + desc "Start CouchDB for testing" + task :couchdb do + start_couchdb("features") + wait_for_ctrlc + end + + desc "Start RabbitMQ for testing" + task :rabbitmq do + start_rabbitmq("features") + wait_for_ctrlc + end + + desc "Start Chef Solr for testing" + task :chef_solr do + start_chef_solr("features") + wait_for_ctrlc + end + + desc "Start Chef Solr Indexer for testing" + task :chef_solr_indexer do + start_chef_solr_indexer("features") + wait_for_ctrlc + end + + desc "Start Chef Server for testing" + task :chef_server do + start_chef_server("features") + wait_for_ctrlc + end + + end + end + + namespace :start do + desc "Start CouchDB" + task :couchdb do + start_couchdb + wait_for_ctrlc + end + + desc "Start RabbitMQ" + task :rabbitmq do + start_rabbitmq + wait_for_ctrlc + end + + desc "Start Chef Solr" + task :chef_solr do + start_chef_solr + wait_for_ctrlc + end + + desc "Start Chef Solr Indexer" + task :chef_solr_indexer do + start_chef_solr_indexer + wait_for_ctrlc + end + + desc "Start Chef Server" + task :chef_server do + start_chef_server + wait_for_ctrlc + end + end end Cucumber::Rake::Task.new(:features) do |t| @@ -127,12 +232,13 @@ Cucumber::Rake::Task.new(:features) do |t| end namespace :features do + desc "Run cucumber tests for the REST API" Cucumber::Rake::Task.new(:api) do |t| t.profile = "api" end namespace :api do - [ :nodes, :roles].each do |api| + [ :nodes, :roles ].each do |api| Cucumber::Rake::Task.new(api) do |apitask| apitask.profile = "api_#{api.to_s}" end @@ -144,8 +250,52 @@ namespace :features do end end end + + namespace :cookbooks do + desc "Run cucumber tests for the cookbooks portion of the REST API" + Cucumber::Rake::Task.new(:cookbooks) do |t| + t.profile = "api_cookbooks" + end + + Cucumber::Rake::Task.new(:cookbook_tarballs) do |t| + t.profile = "api_cookbooks_tarballs" + end + end + + namespace :data do + desc "Run cucumber tests for the data portion of the REST API" + Cucumber::Rake::Task.new(:data) do |t| + t.profile = "api_data" + end + + desc "Run cucumber tests for deleting data via the REST API" + Cucumber::Rake::Task.new(:delete) do |t| + t.profile = "api_data_delete" + end + desc "Run cucumber tests for adding items via the REST API" + Cucumber::Rake::Task.new(:item) do |t| + t.profile = "api_data_item" + end + end + + namespace :search do + desc "Run cucumber tests for searching via the REST API" + Cucumber::Rake::Task.new(:search) do |t| + t.profile = "api_search" + end + + desc "Run cucumber tests for listing search endpoints via the REST API" + Cucumber::Rake::Task.new(:list) do |t| + t.profile = "api_search_list" + end + desc "Run cucumber tests for searching via the REST API" + Cucumber::Rake::Task.new(:show) do |t| + t.profile = "api_search_show" + end + end end + desc "Run cucumber tests for the chef client" Cucumber::Rake::Task.new(:client) do |t| t.profile = "client" end @@ -156,6 +306,17 @@ namespace :features do end end + desc "Run cucumber tests for the cookbooks" + Cucumber::Rake::Task.new(:cookbooks) do |t| + t.profile = "cookbooks" + end + + desc "Run cucumber tests for the recipe language" + Cucumber::Rake::Task.new(:language) do |t| + t.profile = "language" + end + + desc "Run cucumber tests for searching in recipes" Cucumber::Rake::Task.new(:search) do |t| t.profile = "search" end @@ -170,12 +331,34 @@ namespace :features do end end + desc "Run cucumber tests for providers" + Cucumber::Rake::Task.new(:provider) do |t| + t.profile = "provider" + end + namespace :provider do + desc "Run cucumber tests for directory resources" + Cucumber::Rake::Task.new(:directory) do |t| + t.profile = "provider_directory" + end + + desc "Run cucumber tests for execute resources" + Cucumber::Rake::Task.new(:execute) do |t| + t.profile = "provider_execute" + end + + desc "Run cucumber tests for file resources" + Cucumber::Rake::Task.new(:execute) do |t| + t.profile = "provider_file" + end + + desc "Run cucumber tests for remote_file resources" Cucumber::Rake::Task.new(:remote_file) do |t| t.profile = "provider_remote_file" end namespace :package do + desc "Run cucumber tests for macports packages" Cucumber::Rake::Task.new(:macports) do |t| t.profile = "provider_package_macports" end diff --git a/chef-server-slice/LICENSE b/chef-server-api/LICENSE index 11069edd79..11069edd79 100644 --- a/chef-server-slice/LICENSE +++ b/chef-server-api/LICENSE diff --git a/chef-server-api/README.rdoc b/chef-server-api/README.rdoc new file mode 100644 index 0000000000..a7a4ea8dea --- /dev/null +++ b/chef-server-api/README.rdoc @@ -0,0 +1,94 @@ += chef + +* http://www.opscode.com/chef + +== DESCRIPTION: + +Chef is a systems management framework masquerading as a configuration management tool. + +I'm in ur netwerk, cookin up yer servers. :) + +== REQUIREMENTS: + +chef: + +* ruby-openid +* json +* erubis +* extlib +* stomp +* ohai + +chef-server and the chefserverslice (merb slice), same requires as chef above, plus: + +* stompserver +* ferret +* merb-core +* merb-haml +* mongrel +* haml +* ruby-openid +* syntax + +Interim Note: + +Once the {chef,chef-server,chefserverslice}.gem set is installed, do the following: + + > cd /Library/Ruby/Gems/1.8/gems/chef-server<xxx> # or wherever your gems land + > sudo rake slices:chefserverslice:install + +This installs the chefserverslice into the chef-server merb application + +External Servers: + +* stompserver (for easy stomp mq testing) +* CouchDB + +== INSTALL: + +Install all of the above. To fire up a develpment environment, do the following: + + * Start CouchDB with 'couchdb' + * Start stompserver with 'stompserver' + * Start chef-indexer with: + + chef-indexer -l debug + + * Start chef-server: + + chef-server -N -c 2 + + * Test run chef to begin node registration: + + sudo ./bin/chef-client + + * Validate the node registration: + + Visit http://localhost:4000 + Login, enter an openid URL (see http://openid.net/get/) + Registrations, click Validate + + * Test run chef with: + + chef-client + +== LICENSE: + +Chef - A configuration management system + +Author:: Adam Jacob (<adam@opscode.com>) +Copyright:: Copyright (c) 2008 Opscode, Inc. +License:: Apache License, Version 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/chef-server-slice/Rakefile b/chef-server-api/Rakefile index 2457cfd0a4..3ae75760ff 100644 --- a/chef-server-slice/Rakefile +++ b/chef-server-api/Rakefile @@ -1,15 +1,11 @@ require 'rubygems' require 'rake/gempackagetask' -begin - require 'merb-core' - require 'merb-core/tasks/merb' -rescue LoadError - nil -end +require 'merb-core' +require 'merb-core/tasks/merb' -GEM_NAME = "chef-server-slice" -CHEF_SERVER_VERSION="0.7.9" +GEM_NAME = "chef-server-api" +CHEF_SERVER_VERSION="0.8.0" AUTHOR = "Opscode" EMAIL = "chef@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" @@ -28,13 +24,22 @@ spec = Gem::Specification.new do |s| s.email = EMAIL s.homepage = HOMEPAGE - %w{stomp stompserver ferret - merb-core merb-haml merb-assets - merb-helpers mongrel haml - ruby-openid json coderay}.each { |gem| s.add_dependency gem } - + ["merb-slices", + "stomp", + "stompserver", + "ferret", + "merb-core", + "merb-haml", + "merb-assets", + "merb-helpers", + "mongrel", + "haml", + "ruby-openid", + "json", + "syntax",].each { |g| s.add_dependency g} + s.require_path = 'lib' - s.files = %w(LICENSE README.rdoc) + Dir.glob("{app,config,lib,public}/**/*") + s.files = %w(LICENSE README.rdoc Rakefile) + Dir.glob("{config,lib,spec,app,public,stubs}/**/*") end Rake::GemPackageTask.new(spec) do |pkg| diff --git a/chef-server-api/app/controllers/application.rb b/chef-server-api/app/controllers/application.rb new file mode 100644 index 0000000000..b461b2098b --- /dev/null +++ b/chef-server-api/app/controllers/application.rb @@ -0,0 +1,287 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rubygems' +require "chef" / "mixin" / "checksum" +require "chef" / "cookbook_loader" + +class ChefServerApi::Application < Merb::Controller + include AuthenticateEvery + include Chef::Mixin::Checksum + + include Mixlib::Auth::AuthHelper + include Merb::ChefServerApi::ApplicationHelper::AuthenticateEvery + + controller_for_slice + + # Generate the absolute url for a slice - takes the slice's :path_prefix into account. + # + # @param slice_name<Symbol> + # The name of the slice - in identifier_sym format (underscored). + # @param *args<Array[Symbol,Hash]> + # There are several possibilities regarding arguments: + # - when passing a Hash only, the :default route of the current + # slice will be used + # - when a Symbol is passed, it's used as the route name + # - a Hash with additional params can optionally be passed + # + # @return <String> A uri based on the requested slice. + # + # @example absolute_slice_url(:awesome, :format => 'html') + # @example absolute_slice_url(:forum, :posts, :format => 'xml') + def absolute_slice_url(slice_name, *args) + options = {} + if args.length == 1 && args[0].respond_to?(:keys) + options = args[0] + else + options = extract_options_from_args!(args) || {} + end + protocol = options.delete(:protocol) || request.protocol + host = options.delete(:host) || request.host + + protocol + "://" + host + slice_url(slice_name, *args) + end + + def fix_up_node_id + if params.has_key?(:id) + params[:id].gsub!(/_/, '.') + end + end + + def escape_node_id(arg=nil) + unless arg + arg = params[:id] if params.has_key?(:id) + end + arg.gsub(/\./, '_') + end + + def login_required + if session[:openid] + return session[:openid] + else + self.store_location + throw(:halt, :access_denied) + end + end + + def authorized_node + if session[:level] == :admin + Chef::Log.debug("Authorized as Administrator") + true + elsif session[:level] == :node + Chef::Log.debug("Authorized as node") + if session[:node_name] == params[:id].gsub(/\./, '_') + true + else + raise( + Unauthorized, + "You are not the correct node for this action: #{session[:node_name]} instead of #{params[:id]}" + ) + end + else + Chef::Log.debug("Unauthorized") + raise Unauthorized, "You are not allowed to take this action." + end + end + + # Store the URI of the current request in the session. + # + # We can return to this location by calling #redirect_back_or_default. + def store_location + session[:return_to] = request.uri + end + + # Redirect to the URI stored by the most recent store_location call or + # to the passed default. + def redirect_back_or_default(default) + loc = session[:return_to] || default + session[:return_to] = nil + redirect loc + end + + def access_denied + case content_type + when :html + store_location + redirect slice_url(:openid_consumer), :message => { :error => "You don't have access to that, please login."} + else + raise Unauthorized, "You must authenticate first!" + end + end + + # Load a cookbook and return a hash with a list of all the files of a + # given segment (attributes, recipes, definitions, libraries) + # + # === Parameters + # cookbook_id<String>:: The cookbook to load + # segment<Symbol>:: :attributes, :recipes, :definitions, :libraries + # + # === Returns + # <Hash>:: A hash consisting of the short name of the file in :name, and the full path + # to the file in :file. + def load_cookbook_segment(cookbook, segment) + files_list = segment_files(segment, cookbook) + + files = Hash.new + files_list.each do |f| + full = File.expand_path(f) + name = File.basename(full) + files[name] = { + :name => name, + :file => full, + } + end + files + end + + def segment_files(segment, cookbook) + files_list = nil + case segment + when :attributes + files_list = cookbook.attribute_files + when :recipes + files_list = cookbook.recipe_files + when :definitions + files_list = cookbook.definition_files + when :libraries + files_list = cookbook.lib_files + when :files + files_list = cookbook.remote_files + when :templates + files_list = cookbook.template_files + else + raise ArgumentError, "segment must be one of :attributes, :recipes, :definitions, :remote_files, :template_files or :libraries" + end + files_list + end + + def specific_cookbooks(node_name, cl) + valid_cookbooks = Hash.new + begin + node = Chef::Node.load(node_name, @couchdb) + recipes, default_attrs, override_attrs = node.run_list.expand('couchdb', @couchdb) + rescue Net::HTTPServerException + recipes = [] + end + recipes.each do |recipe| + valid_cookbooks = expand_cookbook_deps(valid_cookbooks, cl, recipe) + end + valid_cookbooks + end + + def expand_cookbook_deps(valid_cookbooks, cl, recipe) + cookbook = recipe + if recipe =~ /^(.+)::/ + cookbook = $1 + end + Chef::Log.debug("Node requires #{cookbook}") + valid_cookbooks[cookbook] = true + cl.metadata[cookbook.to_sym].dependencies.each do |dep, versions| + expand_cookbook_deps(valid_cookbooks, cl, dep) unless valid_cookbooks[dep] + end + valid_cookbooks + end + + def load_cookbook_files(cookbook) + response = { + :recipes => Array.new, + :definitions => Array.new, + :libraries => Array.new, + :attributes => Array.new, + :files => Array.new, + :templates => Array.new + } + [ :recipes, :definitions, :libraries, :attributes, :files, :templates ].each do |segment| + segment_files(segment, cookbook).each do |sf| + next if File.directory?(sf) + file_name = nil + file_url = nil + file_specificity = nil + + if segment == :templates || segment == :files + mo = sf.match("#{Merb::Config.cookbook_cache_path}/[^/]+/#{cookbook.name}/#{segment}/(.+?)/(.+)") + specificity = mo[1] + file_name = mo[2] + url_options = { :cookbook_id => cookbook.name.to_s, :segment => segment, :id => file_name, :organization_id => @organization_id } + + case specificity + when "default" + when /^host-(.+)$/ + url_options[:fqdn] = $1 + when /^(.+)-(.+)$/ + url_options[:platform] = $1 + url_options[:version] = $2 + when /^(.+)$/ + url_options[:platform] = $1 + end + + file_specificity = specificity + file_url = absolute_slice_url(:organization_cookbook_segment, url_options) + else + mo = sf.match("#{Merb::Config.cookbook_cache_path}/[^/]+/#{cookbook.name}/#{segment}/(.+)") + file_name = mo[1] + url_options = { :cookbook_id => cookbook.name.to_s, :segment => segment, :id => file_name, :organization_id => @organization_id } + file_url = absolute_slice_url(:organization_cookbook_segment, url_options) + end + rs = { + :name => file_name, + :uri => file_url, + :checksum => checksum(sf) + } + rs[:specificity] = file_specificity if file_specificity + response[segment] << rs + end + end + response + end + + def load_all_files(node_name=nil) + latest_cookbooks = get_all_latest_cookbooks + Merb.logger.debug "Retrieved latest cookbooks: #{latest_cookbooks.inspect}" + policies = latest_cookbooks.map{|doc| Chef::CookbookPolicy.new(doc['display_name'], doc['_id']) } + cl = Chef::CachedCookbookLoader.new(policies) + + valid_cookbooks = node_name ? specific_cookbooks(node_name, cl) : {} + cookbook_list = Hash.new + cl.each do |cookbook| + if node_name + next unless valid_cookbooks[cookbook.name.to_s] + end + cookbook_list[cookbook.name.to_s] = load_cookbook_files(cookbook) + end + cookbook_list + end + + def get_available_recipes + cl = Chef::CookbookLoader.new + available_recipes = cl.sort{ |a,b| a.name.to_s <=> b.name.to_s }.inject([]) do |result, element| + element.recipes.sort.each do |r| + if r =~ /^(.+)::default$/ + result << $1 + else + result << r + end + end + result + end + available_recipes + end + +end diff --git a/chef-server-api/app/controllers/cookbooks.rb b/chef-server-api/app/controllers/cookbooks.rb new file mode 100644 index 0000000000..d372699c05 --- /dev/null +++ b/chef-server-api/app/controllers/cookbooks.rb @@ -0,0 +1,123 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'cookbook_loader' +require 'chef' / 'cookbook' / 'metadata' + +class ChefServerApi::Cookbooks < ChefServerApi::Application + + provides :json + + before :authenticate_every + + include Chef::Mixin::Checksum + + def index + cl = Chef::CookbookLoader.new + cookbook_list = Hash.new + cl.each do |cookbook| + cookbook_list[cookbook.name] = absolute_slice_url(:cookbook, :id => cookbook.name.to_s) + end + display cookbook_list + end + + def show + cl = Chef::CookbookLoader.new + cookbook = cl[params[:id]] + raise NotFound unless cookbook + results = load_cookbook_files(cookbook) + results[:name] = cookbook.name.to_s + results[:metadata] = cl.metadata[cookbook.name.to_sym] + display results + end + + def show_segment + cl = Chef::CookbookLoader.new + cookbook = cl[params[:cookbook_id]] + raise NotFound unless cookbook + cookbook_files = load_cookbook_files(cookbook) + raise NotFound unless cookbook_files.has_key?(params[:segment].to_sym) + + if params[:id] + case params[:segment] + when "templates","files" + serve_segment_preferred(cookbook, params[:segment], cookbook_files[params[:segment].to_sym]) + else + serve_segment_file(cookbook, params[:segment], cookbook_files[params[:segment].to_sym]) + end + else + display cookbook_files[params[:segment].to_sym] + end + end + + def serve_segment_preferred(cookbook, segment, files) + + to_send = nil + + preferences = [ + "host-#{params[:fqdn]}", + "#{params[:platform]}-#{params[:version]}", + "#{params[:platform]}", + "default" + ] + + preferences.each do |pref| + unless to_send + to_send = files.detect { |file| Chef::Log.debug("#{pref.inspect} #{file.inspect}"); file[:name] == params[:id] && file[:specificity] == pref } + end + end + + raise NotFound, "Cannot find a suitable #{segment} file!" unless to_send + current_checksum = to_send[:checksum] + Chef::Log.debug("#{to_send[:name]} Client Checksum: #{params[:checksum]}, Server Checksum: #{current_checksum}") + if current_checksum == params[:checksum] + raise NotModified, "File #{to_send[:name]} has not changed" + else + file_name = nil + segment_files(segment.to_sym, cookbook).each do |f| + if f =~ /#{to_send[:specificity]}\/#{to_send[:name]}$/ + file_name = File.expand_path(f) + break + end + end + raise NotFound, "Cannot find the real file for #{to_send[:specificity]} #{to_send[:name]} - this is a 42 error (shouldn't ever happen)" unless file_name + send_file(file_name) + end + end + + def serve_segment_file(cookbook, segment, files) + to_send = files.detect { |f| f[:name] == params[:id] } + raise NotFound, "Cannot find a suitable #{segment} file!" unless to_send + current_checksum = to_send[:checksum] + Chef::Log.debug("#{to_send[:name]} Client Checksum: #{params[:checksum]}, Server Checksum: #{current_checksum}") + if current_checksum == params[:checksum] + raise NotModified, "File #{to_send[:name]} has not changed" + else + file_name = nil + segment_files(segment.to_sym, cookbook).each do |f| + next unless File.basename(f) == to_send[:name] + file_name = File.expand_path(f) + end + raise NotFound, "Cannot find the real file for #{to_send[:name]} - this is a 42 error (shouldn't ever happen)" unless file_name + send_file(file_name) + end + end + +end + diff --git a/chef-server-api/app/controllers/data.rb b/chef-server-api/app/controllers/data.rb new file mode 100644 index 0000000000..732e6d6c1d --- /dev/null +++ b/chef-server-api/app/controllers/data.rb @@ -0,0 +1,73 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/data_bag' + +class ChefServerApi::Data < ChefServerApi::Application + + provides :json + + before :authenticate_every + + def index + @bag_list = Chef::DataBag.list(false) + display(@bag_list.collect { |b| absolute_slice_url(:organization_datum, :id => b, :organization_id => @organization_id) }) + end + + def show + begin + @data_bag = Chef::DataBag.load(params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load data bag #{params[:id]}" + end + display(@data_bag.list.collect { |i| absolute_slice_url(:organization_data_bag_item, :data_bag_id => @data_bag.name, :id => i) }) + end + + def create + @data_bag = nil + if params.has_key?("inflated_object") + @data_bag = params["inflated_object"] + else + @data_bag = Chef::DataBag.new + @data_bag.name(params["name"]) + end + exists = true + begin + Chef::DataBag.load(@data_bag.name) + rescue Chef::Exceptions::CouchDBNotFound + exists = false + end + raise Forbidden, "Data bag already exists" if exists + self.status = 201 + @data_bag.save + display({ :uri => absolute_slice_url(:organization_datum, :id => @data_bag.name, :organization_id => @organization_id) }) + end + + def destroy + begin + @data_bag = Chef::DataBag.load(params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load data bag #{params[:id]}" + end + @data_bag.destroy + @data_bag.couchdb_rev = nil + display @data_bag + end + +end diff --git a/chef-server-api/app/controllers/data_item.rb b/chef-server-api/app/controllers/data_item.rb new file mode 100644 index 0000000000..7ac388b2a1 --- /dev/null +++ b/chef-server-api/app/controllers/data_item.rb @@ -0,0 +1,81 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/data_bag' +require 'chef/data_bag_item' + +class ChefServerApi::DataItem < ChefServerApi::Application + + provides :json + + before :populate_data_bag + before :authenticate_every + + def populate_data_bag + begin + @data_bag = Chef::DataBag.load(params[:data_bag_id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load data bag #{params[:data_bag_id]}" + end + end + + def show + begin + @data_bag_item = Chef::DataBagItem.load(params[:data_bag_id], params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load data bag #{params[:data_bag_id]} item #{params[:id]}" + end + display @data_bag_item.raw_data + end + + def create + raw_data = nil + if params.has_key?("inflated_object") + raw_data = params["inflated_object"].raw_data + else + raw_data = params + raw_data.delete(:organization_id) + raw_data.delete(:action) + raw_data.delete(:controller) + raw_data.delete(:data_bag_id) + end + @data_bag_item = nil + begin + @data_bag_item = Chef::DataBagItem.load(@data_bag.name, params[:id]) + rescue Chef::Exceptions::CouchDBNotFound + @data_bag_item = Chef::DataBagItem.new + @data_bag_item.data_bag(@data_bag.name) + end + @data_bag_item.raw_data = raw_data + @data_bag_item.save + display @data_bag_item.raw_data + end + + def destroy + begin + @data_bag_item = Chef::DataBagItem.load(params[:data_bag_id], params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load data bag #{params[:data_bag_id]} item #{params[:id]}" + end + @data_bag_item.destroy + @data_bag_item.couchdb_rev = nil + display @data_bag_item + end + +end diff --git a/chef-server-slice/app/controllers/exceptions.rb b/chef-server-api/app/controllers/exceptions.rb index f3f43d25da..76428895f2 100644 --- a/chef-server-slice/app/controllers/exceptions.rb +++ b/chef-server-api/app/controllers/exceptions.rb @@ -17,12 +17,19 @@ # limitations under the License. # -class ChefServerSlice::Exceptions < ChefServerSlice::Application - - provides :html, :json +class Exceptions < ChefServerApi::Application + provides :json + + def not_acceptable + if request.accept =~ /application\/json/ + display({ "error" => request.exceptions }) + else + render + end + end + def standard_error - Merb.logger.warn(request.content_type) if request.accept =~ /application\/json/ display({ "error" => request.exceptions }) else diff --git a/chef-server-api/app/controllers/main.rb b/chef-server-api/app/controllers/main.rb new file mode 100644 index 0000000000..ecd11347dc --- /dev/null +++ b/chef-server-api/app/controllers/main.rb @@ -0,0 +1,18 @@ +class ChefServerApi::Main < ChefServerApi::Application + + before :authenticate_every + provides :html, :json + + def index + display( + { + absolute_slice_url(:nodes) => "Manage Nodes", + absolute_slice_url(:roles) => "Manage Roles", + absolute_slice_url(:cookbooks) => "Manage Cookbooks", + absolute_slice_url(:data) => "Manage Data Bags", + absolute_slice_url(:search) => "Search" + } + ) + end + +end diff --git a/chef-server-api/app/controllers/nodes.rb b/chef-server-api/app/controllers/nodes.rb new file mode 100644 index 0000000000..eb8eb3b0c7 --- /dev/null +++ b/chef-server-api/app/controllers/nodes.rb @@ -0,0 +1,95 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'node' + +class ChefServerApi::Nodes < ChefServerApi::Application + + provides :json + + before :authenticate_every + before :fix_up_node_id + + def index + @node_list = Chef::Node.list + display(@node_list.collect { |n| absolute_slice_url(:node, escape_node_id(n)) }) + end + + def show + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + @node.couchdb_rev = nil + display @node + end + + def create + @node = params["inflated_object"] + exists = true + begin + Chef::Node.load(@node.name) + rescue Net::HTTPServerException + exists = false + end + raise Forbidden, "Node already exists" if exists + self.status = 201 + @node.save + display({ :uri => absolute_slice_url(:node, escape_node_id(@node.name)) }) + end + + def update + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + + updated = params['inflated_object'] + @node.run_list.reset(updated.run_list) + @node.attribute = updated.attribute + @node.save + @node.couchdb_rev = nil + display(@node) + end + + def destroy + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + @node.destroy + @node.couchdb_rev = nil + display @node + end + + def cookbooks + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + + display(load_all_files(params[:id])) + end + +end + diff --git a/chef-server-api/app/controllers/roles.rb b/chef-server-api/app/controllers/roles.rb new file mode 100644 index 0000000000..08661c4d23 --- /dev/null +++ b/chef-server-api/app/controllers/roles.rb @@ -0,0 +1,71 @@ +require 'chef/role' + +class ChefServerApi::Roles < ChefServerApi::Application + provides :json + + before :authenticate_every + + # GET /roles + def index + @role_list = Chef::Role.list(true) + display(@role_list.collect { |r| absolute_slice_url(:organization_role, :id => r.name, :organization_id => @organization_id) }) + end + + # GET /roles/:id + def show + begin + @role = Chef::Role.load(params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load role #{params[:id]}" + end + @role.couchdb_rev = nil + display @role + end + + # POST /roles + def create + @role = params["inflated_object"] + exists = true + begin + Chef::Role.load(@role.name) + rescue Chef::Exceptions::CouchDBNotFound + exists = false + end + raise Forbidden, "Role already exists" if exists + + @role.save + + self.status = 201 + display({ :uri => absolute_slice_url(:organization_role, :id => @role.name, :organization_id => @organization_id) }) + end + + # PUT /roles/:id + def update + begin + @role = Chef::Role.load(params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load role #{params[:id]}" + end + + @role.description(params["inflated_object"].description) + @role.recipes(params["inflated_object"].recipes) + @role.default_attributes(params["inflated_object"].default_attributes) + @role.override_attributes(params["inflated_object"].override_attributes) + @role.save + self.status = 200 + @role.couchdb_rev = nil + display(@role) + end + + # DELETE /roles/:id + def destroy + begin + @role = Chef::Role.load(params[:id]) + rescue Chef::Exceptions::CouchDBNotFound => e + raise NotFound, "Cannot load role #{params[:id]}" + end + @role.destroy + display @role + end + +end diff --git a/chef-server-api/app/controllers/search.rb b/chef-server-api/app/controllers/search.rb new file mode 100644 index 0000000000..269a326e6d --- /dev/null +++ b/chef-server-api/app/controllers/search.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +require 'chef/solr/query' + +class ChefServerApi::Search < ChefServerApi::Application + provides :json + + before :authenticate_every + + def index + indexes = valid_indexes + display(indexes.collect { |i| absolute_slice_url(:organization_search, :id => i, :organization_id => @organization_id) }) + end + + def valid_indexes + indexes = Chef::DataBag.list(false) + indexes << "role" + indexes << "node" + end + + def show + unless valid_indexes.include?(params[:id]) + raise NotFound, "I don't know how to search for #{params[:id]} data objects." + end + + query = Chef::Solr::Query.new(Chef::Config[:solr_url], Chef::Config[:couchdb_database]) + params[:q] ||= "*:*" + params[:sort] ||= nil + params[:start] ||= 0 + params[:rows] ||= 20 + objects, start, total = query.search(params[:id], params[:q], params[:sort], params[:start], params[:rows]) + display({ + "rows" => objects, + "start" => start, + "total" => total + }) + end + +end diff --git a/chef-server-api/app/helpers/application_helper.rb b/chef-server-api/app/helpers/application_helper.rb new file mode 100644 index 0000000000..e71bdbd4d0 --- /dev/null +++ b/chef-server-api/app/helpers/application_helper.rb @@ -0,0 +1,217 @@ +module Merb + module ChefServerApi + module ApplicationHelper + + # Generate the absolute url for a slice - takes the slice's :path_prefix into account. + # + # @param slice_name<Symbol> + # The name of the slice - in identifier_sym format (underscored). + # @param *args<Array[Symbol,Hash]> + # There are several possibilities regarding arguments: + # - when passing a Hash only, the :default route of the current + # slice will be used + # - when a Symbol is passed, it's used as the route name + # - a Hash with additional params can optionally be passed + # + # @return <String> A uri based on the requested slice. + # + # @example absolute_slice_url(:awesome, :format => 'html') + # @example absolute_slice_url(:forum, :posts, :format => 'xml') + def absolute_slice_url(slice_name, *args) + options = extract_options_from_args!(args) || {} + protocol = options.delete(:protocol) || request.protocol + host = options.delete(:host) || request.host + + protocol + "://" + host + slice_url(slice_name,*args) + end + + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path relative to the public directory, with added segments. + def image_path(*segments) + public_path_for(:image, *segments) + end + + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path relative to the public directory, with added segments. + def javascript_path(*segments) + public_path_for(:javascript, *segments) + end + + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path relative to the public directory, with added segments. + def stylesheet_path(*segments) + public_path_for(:stylesheet, *segments) + end + + # Construct a path relative to the public directory + # + # @param <Symbol> The type of component. + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path relative to the public directory, with added segments. + def public_path_for(type, *segments) + ::ChefServerApi.public_path_for(type, *segments) + end + + # Construct an app-level path. + # + # @param <Symbol> The type of component. + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path within the host application, with added segments. + def app_path_for(type, *segments) + ::ChefServerApi.app_path_for(type, *segments) + end + + # Construct a slice-level path. + # + # @param <Symbol> The type of component. + # @param *segments<Array[#to_s]> Path segments to append. + # + # @return <String> + # A path within the slice source (Gem), with added segments. + def slice_path_for(type, *segments) + ::ChefServerApi.slice_path_for(type, *segments) + end + + def build_tree(name, node, default={}, override={}) + node = Chef::Mixin::DeepMerge.merge(default, node) + node = Chef::Mixin::DeepMerge.merge(node, override) + html = "<table id='#{name}' class='tree table'>" + html << "<tr><th class='first'>Attribute</th><th class='last'>Value</th></tr>" + count = 0 + parent = 0 + append_tree(name, html, node, count, parent, override) + html << "</table>" + html + end + + def append_tree(name, html, node, count, parent, override) + node.sort{ |a,b| a[0] <=> b[0] }.each do |key, value| + to_send = Array.new + count += 1 + is_parent = false + local_html = "" + local_html << "<tr id='#{name}-#{count}' class='collapsed #{name}" + if parent != 0 + local_html << " child-of-#{name}-#{parent}' style='display: none;'>" + else + local_html << "'>" + end + local_html << "<td class='table-key'><span toggle='#{name}-#{count}'/>#{key}</td>" + case value + when Hash + is_parent = true + local_html << "<td></td>" + p = count + to_send << Proc.new { append_tree(name, html, value, count, p, override) } + when Array + is_parent = true + local_html << "<td></td>" + as_hash = {} + value.each_index { |i| as_hash[i] = value[i] } + p = count + to_send << Proc.new { append_tree(name, html, as_hash, count, p, override) } + when String,Symbol + local_html << "<td><div class='json-attr'>#{value}</div></td>" + else + local_html << "<td>#{JSON.pretty_generate(value)}</td>" + end + local_html << "</tr>" + local_html.sub!(/class='collapsed/, 'class=\'collapsed parent') if is_parent + local_html.sub!(/<span/, "<span class='expander'") if is_parent + html << local_html + to_send.each { |s| count = s.call } + count += to_send.length + end + count + end + + # Recursively build a tree of lists. + #def build_tree(node) + # list = "<dl>" + # list << "\n<!-- Beginning of Tree -->" + # walk = lambda do |key,value| + # case value + # when Hash, Array + # list << "\n<!-- Beginning of Enumerable obj -->" + # list << "\n<dt>#{key}</dt>" + # list << "<dd>" + # list << "\t<dl>\n" + # value.each(&walk) + # list << "\t</dl>\n" + # list << "</dd>" + # list << "\n<!-- End of Enumerable obj -->" + # + # else + # list << "\n<dt>#{key}</dt>" + # list << "<dd>#{value}</dd>" + # end + # end + # node.sort{ |a,b| a[0] <=> b[0] }.each(&walk) + # list << "</dl>" + #end + + + module AuthenticateEvery + require 'rubygems' + require 'mixlib/auth/signatureverification' + require 'extlib' + + class << self + def authenticator + @authenticator ||= Mixlib::Auth::SignatureVerification.new + end + end + + + protected + + # This is the main method to use as a before filter. + # It will perform the user lookup, header signing and signature comparison + # A failed login will result in an Unauthorized exception being raised. + # + # ====Parameters + # + def authenticate_every + auth = begin + + Merb.logger.debug("Raw request: #{request.inspect}") + headers = request.env.inject({ }) { |memo, kv| memo[$2.downcase.to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo } + username = headers[:x_ops_userid].chomp + orgname = params[:organization_id] + Merb.logger.debug "I have #{headers.inspect}" + + user = begin + User.find(username) + rescue ArgumentError + if orgname + cr = database_from_orgname(orgname) + Client.on(cr).by_clientname(:key=>username).first + end + end + + actor = user_to_actor(user.id) + params[:requesting_actor_id] = actor.auth_object_id + user_key = OpenSSL::PKey::RSA.new(user.public_key) + Merb.logger.debug "authenticating:\n #{user.inspect}\n" + AuthenticateEvery::authenticator.authenticate_user_request(request, user_key) + rescue StandardError => se + Merb.logger.debug "authenticate every failed: #{se}, #{se.backtrace}" + nil + end + raise Merb::ControllerExceptions::Unauthorized, "Failed authorization" unless auth + auth + end + end + end + end +end diff --git a/chef-server-slice/app/helpers/exceptions_helper.rb b/chef-server-api/app/helpers/exceptions_helper.rb index 86501d650d..18a41965b6 100644 --- a/chef-server-slice/app/helpers/exceptions_helper.rb +++ b/chef-server-api/app/helpers/exceptions_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerApi module ExceptionsHelper end end diff --git a/chef-server-api/app/helpers/global_helpers.rb b/chef-server-api/app/helpers/global_helpers.rb new file mode 100644 index 0000000000..b5627e92b4 --- /dev/null +++ b/chef-server-api/app/helpers/global_helpers.rb @@ -0,0 +1,30 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Merb + module ChefServerApi + + module GlobalHelpers + # helpers defined here available to all views. + def node_escape(node) + node.gsub(/\./, '_') + end + + end + end +end diff --git a/chef-server-api/app/helpers/nodes_helper.rb b/chef-server-api/app/helpers/nodes_helper.rb new file mode 100644 index 0000000000..0204baba8e --- /dev/null +++ b/chef-server-api/app/helpers/nodes_helper.rb @@ -0,0 +1,26 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Merb + module ChefServerApi + module NodesHelper + + end + end +end diff --git a/chef-server-slice/app/helpers/roles_helper.rb b/chef-server-api/app/helpers/roles_helper.rb index b6d46cdcd1..b6d46cdcd1 100644 --- a/chef-server-slice/app/helpers/roles_helper.rb +++ b/chef-server-api/app/helpers/roles_helper.rb diff --git a/chef-server-slice/app/views/exceptions/bad_request.json.erb b/chef-server-api/app/views/exceptions/bad_request.json.erb index f266cf99b9..f266cf99b9 100644 --- a/chef-server-slice/app/views/exceptions/bad_request.json.erb +++ b/chef-server-api/app/views/exceptions/bad_request.json.erb diff --git a/chef-server-slice/app/views/exceptions/internal_server_error.html.erb b/chef-server-api/app/views/exceptions/internal_server_error.html.erb index aadbfad350..aadbfad350 100644 --- a/chef-server-slice/app/views/exceptions/internal_server_error.html.erb +++ b/chef-server-api/app/views/exceptions/internal_server_error.html.erb diff --git a/chef-server-api/app/views/exceptions/not_acceptable.html.haml b/chef-server-api/app/views/exceptions/not_acceptable.html.haml new file mode 100644 index 0000000000..b3e5b966c1 --- /dev/null +++ b/chef-server-api/app/views/exceptions/not_acceptable.html.haml @@ -0,0 +1,5 @@ +.block#block-text + .content + %h2.title Content-Type Unacceptable. + .inner + The Chef Server's REST API only responds to requests for JSON data. Set the 'Accept' header on your request to 'application/json' and try again. diff --git a/chef-server-slice/app/views/exceptions/not_found.html.erb b/chef-server-api/app/views/exceptions/not_found.html.erb index 388c72c31d..388c72c31d 100644 --- a/chef-server-slice/app/views/exceptions/not_found.html.erb +++ b/chef-server-api/app/views/exceptions/not_found.html.erb diff --git a/chef-server-slice/app/views/exceptions/standard_error.html.erb b/chef-server-api/app/views/exceptions/standard_error.html.erb index edb45ddf90..edb45ddf90 100644 --- a/chef-server-slice/app/views/exceptions/standard_error.html.erb +++ b/chef-server-api/app/views/exceptions/standard_error.html.erb diff --git a/chef-server-api/app/views/layout/chef_server_api.html.haml b/chef-server-api/app/views/layout/chef_server_api.html.haml new file mode 100644 index 0000000000..c7913fc491 --- /dev/null +++ b/chef-server-api/app/views/layout/chef_server_api.html.haml @@ -0,0 +1,23 @@ +!!! XML +!!! +%html + %head + %meta{ "http-equiv" => "content-type", :content => "text/html; charset=utf-8" } + %title Chef Server + = css_include_tag "base", "themes/djime-cerulean/style", "chef" + %body + #container + #header + %h1= link_to "Chef REST API", absolute_slice_url(:top) + #main-navigation + .clear + #wrapper + #main + = catch_content :for_layout + #footer + .block + %p Copyright © 2009 Opscode, Inc. + #sidebar + .block.notice#sidebar_block_notice= catch_content :sidebar_block_notice + .block#sidebar_block= catch_content :sidebar_block + .clear diff --git a/chef-server-api/app/views/main/index.html.haml b/chef-server-api/app/views/main/index.html.haml new file mode 100644 index 0000000000..4697d483f0 --- /dev/null +++ b/chef-server-api/app/views/main/index.html.haml @@ -0,0 +1,5 @@ +.block#block-text + .content + %h2.title The REST API + .inner + This is the top of the Chef Server REST API. It's not really explorable via a web browser. diff --git a/chef-server-api/config/init.rb b/chef-server-api/config/init.rb new file mode 100644 index 0000000000..07d7916822 --- /dev/null +++ b/chef-server-api/config/init.rb @@ -0,0 +1,60 @@ +# +# ==== Standalone Chefserver configuration +# +# This configuration/environment file is only loaded by bin/slice, which can be +# used during development of the slice. It has no effect on this slice being +# loaded in a host application. To run your slice in standalone mode, just +# run 'slice' from its directory. The 'slice' command is very similar to +# the 'merb' command, and takes all the same options, including -i to drop +# into an irb session for example. +# +# The usual Merb configuration directives and init.rb setup methods apply, +# including use_orm and before_app_loads/after_app_loads. +# +# If you need need different configurations for different environments you can +# even create the specific environment file in config/environments/ just like +# in a regular Merb application. +# +# In fact, a slice is no different from a normal # Merb application - it only +# differs by the fact that seamlessly integrates into a so called 'host' +# application, which in turn can override or finetune the slice implementation +# code and views. +# + +$: << File.join(File.dirname(__FILE__), "..", "..", "chef", "lib") +require 'chef' + +merb_gems_version = " > 1.0" +dependency "merb-haml", merb_gems_version +dependency "merb-assets", merb_gems_version +dependency "merb-helpers", merb_gems_version +dependency "chef", :immediate=>true unless defined?(Chef) + +require 'rubygems' + +use_template_engine :haml + +Merb::Config.use do |c| + # BUGBUG [cb] For some reason, this next line + # causes a merb slice to vomit around openid + # c[:fork_for_class_load] = false + + c[:couchdb_uri] = 'localhost:5984' + c[:guidservice_host] = 'localhost' + c[:guidservice_port] = 8000 + c[:certificateservice_uri] = 'http://localhost:4000/certificates' + c[:couchdb_database] = 'opscode_account' + c[:authorizationservice_uri] = 'http://localhost:5959' + c[:service_private_key] = OpenSSL::PKey::RSA.new(File.read('/etc/opscode/azs.pem')) + + c[:session_id_key] = '_chef_server_session_id' + c[:session_secret_key] = Chef::Config.manage_secret_key + c[:session_store] = 'cookie' + c[:exception_details] = true + c[:reload_classes] = true + c[:log_level] = Chef::Config[:log_level] + if Chef::Config[:log_location].kind_of?(String) + c[:log_file] = Chef::Config[:log_location] + end +end + diff --git a/chef-server-slice/config/router.rb b/chef-server-api/config/router.rb index 3142a1d1a9..3142a1d1a9 100644 --- a/chef-server-slice/config/router.rb +++ b/chef-server-api/config/router.rb diff --git a/chef-server-api/lib/chef-server-api.rb b/chef-server-api/lib/chef-server-api.rb new file mode 100644 index 0000000000..ab05d11ad7 --- /dev/null +++ b/chef-server-api/lib/chef-server-api.rb @@ -0,0 +1,153 @@ +if defined?(Merb::Plugins) + $:.unshift File.dirname(__FILE__) + $:.unshift File.join(File.dirname(__FILE__), "..", "..", "chef-solr", "lib") + $:.unshift File.join(File.dirname(__FILE__), "..", "..", "chef", "lib") + + dependency 'merb-slices', :immediate => true + dependency 'chef', :immediate=>true unless defined?(Chef) + dependency 'nanite', :immediate=>true + + require 'chef/role' + require 'chef/data_bag' + require 'chef/data_bag_item' + require 'chef/nanite' + + require 'mixlib/auth' + + require 'chef/data_bag' + require 'chef/data_bag_item' + require 'ohai' + require 'chef/nanite' + + require 'syntax/convertors/html' + + Merb::Plugins.add_rakefiles "chef-server-api/merbtasks", "chef-server-api/slicetasks", "chef-server-api/spectasks" + + # Register the Slice for the current host application + Merb::Slices::register(__FILE__) + + Merb.disable :json + + # Slice configuration - set this in a before_app_loads callback. + # By default a Slice uses its own layout, so you can switch to + # the main application layout or no layout at all if needed. + # + # Configuration options: + # :layout - the layout to use; defaults to :chefserverslice + # :mirror - which path component types to use on copy operations; defaults to all + Merb::Slices::config[:chef_server_api][:layout] ||= :chef_server_api + + # All Slice code is expected to be namespaced inside a module + module ChefServerApi + # Slice metadata + self.description = "ChefServerApi.. serving up some piping hot infrastructure!" + self.version = Chef::VERSION + self.author = "Opscode" + + # Stub classes loaded hook - runs before LoadClasses BootLoader + # right after a slice's classes have been loaded internally. + def self.loaded + Chef::Log.info("Compiling routes... (totally normal to see 'Cannot find resource model')") + end + + # Initialization hook - runs before AfterAppLoads BootLoader + def self.init + end + + # Activation hook - runs after AfterAppLoads BootLoader + def self.activate + Nanite::Log.logger = Mixlib::Auth::Log.logger = Ohai::Log.logger = Chef::Log.logger + Merb.logger.set_log(STDOUT, Chef::Config[:log_level]) + Thread.new do + until EM.reactor_running? + sleep 1 + end + Chef::Nanite.in_event { Chef::Log.info("Nanite is ready") } + + # create the couch design docs for nodes, roles, and databags + Chef::CouchDB.new.create_id_map + Chef::Node.create_design_document + Chef::Role.create_design_document + Chef::DataBag.create_design_document + + Chef::Log.info('Loading roles') + Chef::Role.sync_from_disk_to_couchdb + end + end + + # Deactivation hook - triggered by Merb::Slices.deactivate(Chefserver) + def self.deactivate + end + + # Setup routes inside the host application + # + # @param scope<Merb::Router::Behaviour> + # Routes will be added within this scope (namespace). In fact, any + # router behaviour is a valid namespace, so you can attach + # routes at any level of your router setup. + # + # @note prefix your named routes with :chefserverslice_ + # to avoid potential conflicts with global named routes. + def self.setup_router(scope) + # Nodes + scope.match('/nodes/:id/cookbooks', :method => 'get').to(:controller => "nodes", :action => "cookbooks") + scope.resources :nodes + + # Roles + scope.resources :roles + + # Status + scope.match("/status").to(:controller => "status", :action => "index").name(:status) + + + # Search + scope.resources :search + + # Cookbooks + scope.match('/nodes/:id/cookbooks', :method => 'get').to(:controller => "nodes", :action => "cookbooks") + + scope.match("/cookbooks", :method => 'get').to(:controller => "cookbooks", :action => "index") + scope.match("/cookbooks", :method => 'post').to(:controller => "cookbooks", :action => "create") + scope.match("/cookbooks/:cookbook_id", :method => 'get', :cookbook_id => /[\w\.]+/).to(:controller => "cookbooks", :action => "show").name(:cookbook) + scope.match("/cookbooks/:cookbook_id", :method => 'delete', :cookbook_id => /[\w\.]+/).to(:controller => "cookbooks", :action => "destroy") + scope.match("/cookbooks/:cookbook_id/_content", :method => 'get', :cookbook_id => /[\w\.]+/).to(:controller => "cookbooks", :action => "get_tarball") + scope.match("/cookbooks/:cookbook_id/_content", :method => 'put', :cookbook_id => /[\w\.]+/).to(:controller => "cookbooks", :action => "update") + scope.match("/cookbooks/:cookbook_id/:segment", :cookbook_id => /[\w\.]+/).to(:controller => "cookbooks", :action => "show_segment").name(:cookbook_segment) + + # Data + scope.match("/data/:data_bag_id/:id", :method => 'get').to(:controller => "data_item", :action => "show").name("data_bag_item") + scope.match("/data/:data_bag_id/:id", :method => 'put').to(:controller => "data_item", :action => "create").name("create_data_bag_item") + scope.match("/data/:data_bag_id/:id", :method => 'delete').to(:controller => "data_item", :action => "destroy").name("destroy_data_bag_item") + scope.resources :data + + scope.match('/').to(:controller => 'main', :action =>'index').name(:top) + end + end + + # TODO: make this read from an environment-specific file + Merb::Config.use do |c| + c[:couchdb_uri] = Chef::Config[:couchdb_url] + c[:couchdb_database] = Chef::Config[:couchdb_database] + end + + COUCHDB = CouchRest.new(Merb::Config[:couchdb_uri]) + COUCHDB.database!(Merb::Config[:couchdb_database]) + COUCHDB.default_database = Merb::Config[:couchdb_database] + + Mixlib::Auth::AuthJoin.use_database(COUCHDB.default_database) + Mixlib::Auth::PRIVKEY = Chef::Config[:validation_key] + + # Setup the slice layout for ChefServerApi + # + # Use ChefServerApi.push_path and ChefServerApi.push_app_path + # to set paths to chefserver-level and app-level paths. Example: + # + # ChefServerApi.push_path(:application, ChefServerApi.root) + # ChefServerApi.push_app_path(:application, Merb.root / 'slices' / 'chefserverslice') + # ... + # + # Any component path that hasn't been set will default to ChefServerApi.root + # + # Or just call setup_default_structure! to setup a basic Merb MVC structure. + ChefServerApi.setup_default_structure! +end diff --git a/chef-server-api/lib/chef-server-api/merbtasks.rb b/chef-server-api/lib/chef-server-api/merbtasks.rb new file mode 100644 index 0000000000..65cb589b82 --- /dev/null +++ b/chef-server-api/lib/chef-server-api/merbtasks.rb @@ -0,0 +1,103 @@ +namespace :slices do + namespace :chefserverslice do + + desc "Install Chefserver" + task :install => [:preflight, :setup_directories, :copy_assets, :migrate] + + desc "Test for any dependencies" + task :preflight do # see slicetasks.rb + end + + desc "Setup directories" + task :setup_directories do + puts "Creating directories for host application" + ChefServerApi.mirrored_components.each do |type| + if File.directory?(ChefServerApi.dir_for(type)) + if !File.directory?(dst_path = ChefServerApi.app_dir_for(type)) + relative_path = dst_path.relative_path_from(Merb.root) + puts "- creating directory :#{type} #{File.basename(Merb.root) / relative_path}" + mkdir_p(dst_path) + end + end + end + end + + # desc "Copy stub files to host application" + # task :stubs do + # puts "Copying stubs for ChefServerApi - resolves any collisions" + # copied, preserved = ChefServerApi.mirror_stubs! + # puts "- no files to copy" if copied.empty? && preserved.empty? + # copied.each { |f| puts "- copied #{f}" } + # preserved.each { |f| puts "! preserved override as #{f}" } + # end + + # desc "Copy stub files and views to host application" + # task :patch => [ "stubs", "freeze:views" ] + + desc "Copy public assets to host application" + task :copy_assets do + puts "Copying assets for ChefServerApi - resolves any collisions" + copied, preserved = ChefServerApi.mirror_public! + puts "- no files to copy" if copied.empty? && preserved.empty? + copied.each { |f| puts "- copied #{f}" } + preserved.each { |f| puts "! preserved override as #{f}" } + end + + desc "Migrate the database" + task :migrate do # see slicetasks.rb + end + + desc "Freeze ChefServerApi into your app (only chefserverslice/app)" + task :freeze => [ "freeze:app" ] + + namespace :freeze do + + # desc "Freezes ChefServerApi by installing the gem into application/gems" + # task :gem do + # ENV["GEM"] ||= "chefserverslice" + # Rake::Task['slices:install_as_gem'].invoke + # end + + desc "Freezes ChefServerApi by copying all files from chefserverslice/app to your application" + task :app do + puts "Copying all chefserverslice/app files to your application - resolves any collisions" + copied, preserved = ChefServerApi.mirror_app! + puts "- no files to copy" if copied.empty? && preserved.empty? + copied.each { |f| puts "- copied #{f}" } + preserved.each { |f| puts "! preserved override as #{f}" } + end + + desc "Freeze all views into your application for easy modification" + task :views do + puts "Copying all view templates to your application - resolves any collisions" + copied, preserved = ChefServerApi.mirror_files_for :view + puts "- no files to copy" if copied.empty? && preserved.empty? + copied.each { |f| puts "- copied #{f}" } + preserved.each { |f| puts "! preserved override as #{f}" } + end + + desc "Freeze all models into your application for easy modification" + task :models do + puts "Copying all models to your application - resolves any collisions" + copied, preserved = ChefServerApi.mirror_files_for :model + puts "- no files to copy" if copied.empty? && preserved.empty? + copied.each { |f| puts "- copied #{f}" } + preserved.each { |f| puts "! preserved override as #{f}" } + end + + desc "Freezes ChefServerApi as a gem and copies over chefserver/app" + task :app_with_gem => [:gem, :app] + + desc "Freezes ChefServerApi by unpacking all files into your application" + task :unpack do + puts "Unpacking ChefServerApi files to your application - resolves any collisions" + copied, preserved = ChefServerApi.unpack_slice! + puts "- no files to copy" if copied.empty? && preserved.empty? + copied.each { |f| puts "- copied #{f}" } + preserved.each { |f| puts "! preserved override as #{f}" } + end + + end + + end +end diff --git a/chef-server-slice/lib/chef-server-slice/slicetasks.rb b/chef-server-api/lib/chef-server-api/slicetasks.rb index 187323a299..187323a299 100644 --- a/chef-server-slice/lib/chef-server-slice/slicetasks.rb +++ b/chef-server-api/lib/chef-server-api/slicetasks.rb diff --git a/chef-server-api/lib/chef-server-api/spectasks.rb b/chef-server-api/lib/chef-server-api/spectasks.rb new file mode 100644 index 0000000000..e00f1b9490 --- /dev/null +++ b/chef-server-api/lib/chef-server-api/spectasks.rb @@ -0,0 +1,53 @@ +namespace :slices do + namespace :chefserverslice do + + desc "Run slice specs within the host application context" + task :spec => [ "spec:explain", "spec:default" ] + + namespace :spec do + + slice_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')) + + task :explain do + puts "\nNote: By running ChefServerApi specs inside the application context any\n" + + "overrides could break existing specs. This isn't always a problem,\n" + + "especially in the case of views. Use these spec tasks to check how\n" + + "well your application conforms to the original slice implementation." + end + + Spec::Rake::SpecTask.new('default') do |t| + t.spec_opts = ["--format", "specdoc", "--colour"] + t.spec_files = Dir["#{slice_root}/spec/**/*_spec.rb"].sort + end + + desc "Run all model specs, run a spec for a specific Model with MODEL=MyModel" + Spec::Rake::SpecTask.new('model') do |t| + t.spec_opts = ["--format", "specdoc", "--colour"] + if(ENV['MODEL']) + t.spec_files = Dir["#{slice_root}/spec/models/**/#{ENV['MODEL']}_spec.rb"].sort + else + t.spec_files = Dir["#{slice_root}/spec/models/**/*_spec.rb"].sort + end + end + + desc "Run all request specs, run a spec for a specific request with REQUEST=MyRequest" + Spec::Rake::SpecTask.new('request') do |t| + t.spec_opts = ["--format", "specdoc", "--colour"] + if(ENV['REQUEST']) + t.spec_files = Dir["#{slice_root}/spec/requests/**/#{ENV['REQUEST']}_spec.rb"].sort + else + t.spec_files = Dir["#{slice_root}/spec/requests/**/*_spec.rb"].sort + end + end + + desc "Run all specs and output the result in html" + Spec::Rake::SpecTask.new('html') do |t| + t.spec_opts = ["--format", "html"] + t.libs = ['lib', 'server/lib' ] + t.spec_files = Dir["#{slice_root}/spec/**/*_spec.rb"].sort + end + + end + + end +end diff --git a/chef-server-slice/public/images/avatar.png b/chef-server-api/public/images/avatar.png Binary files differindex 66488481ae..66488481ae 100644 --- a/chef-server-slice/public/images/avatar.png +++ b/chef-server-api/public/images/avatar.png diff --git a/chef-server-slice/public/images/indicator.gif b/chef-server-api/public/images/indicator.gif Binary files differindex 085ccaecaf..085ccaecaf 100644 --- a/chef-server-slice/public/images/indicator.gif +++ b/chef-server-api/public/images/indicator.gif diff --git a/chef-server-slice/public/images/merb.jpg b/chef-server-api/public/images/merb.jpg Binary files differindex a19dcf4048..a19dcf4048 100644 --- a/chef-server-slice/public/images/merb.jpg +++ b/chef-server-api/public/images/merb.jpg diff --git a/chef-server-slice/public/stylesheets/base.css b/chef-server-api/public/stylesheets/base.css index 0da62086ed..0da62086ed 100644 --- a/chef-server-slice/public/stylesheets/base.css +++ b/chef-server-api/public/stylesheets/base.css diff --git a/chef-server-slice/public/stylesheets/chef.css b/chef-server-api/public/stylesheets/chef.css index 52a06d056c..52a06d056c 100644 --- a/chef-server-slice/public/stylesheets/chef.css +++ b/chef-server-api/public/stylesheets/chef.css diff --git a/chef-server-slice/public/stylesheets/themes/bec-green/style.css b/chef-server-api/public/stylesheets/themes/bec-green/style.css index 225b6d4e35..225b6d4e35 100644 --- a/chef-server-slice/public/stylesheets/themes/bec-green/style.css +++ b/chef-server-api/public/stylesheets/themes/bec-green/style.css diff --git a/chef-server-slice/public/stylesheets/themes/bec/style.css b/chef-server-api/public/stylesheets/themes/bec/style.css index c94474866a..c94474866a 100644 --- a/chef-server-slice/public/stylesheets/themes/bec/style.css +++ b/chef-server-api/public/stylesheets/themes/bec/style.css diff --git a/chef-server-slice/public/stylesheets/themes/blue/style.css b/chef-server-api/public/stylesheets/themes/blue/style.css index cce8f4bdf0..cce8f4bdf0 100644 --- a/chef-server-slice/public/stylesheets/themes/blue/style.css +++ b/chef-server-api/public/stylesheets/themes/blue/style.css diff --git a/chef-server-slice/public/stylesheets/themes/default/style.css b/chef-server-api/public/stylesheets/themes/default/style.css index e7a5ee1271..e7a5ee1271 100644 --- a/chef-server-slice/public/stylesheets/themes/default/style.css +++ b/chef-server-api/public/stylesheets/themes/default/style.css diff --git a/chef-server-slice/public/stylesheets/themes/djime-cerulean/style.css b/chef-server-api/public/stylesheets/themes/djime-cerulean/style.css index 9b050c6785..9b050c6785 100644 --- a/chef-server-slice/public/stylesheets/themes/djime-cerulean/style.css +++ b/chef-server-api/public/stylesheets/themes/djime-cerulean/style.css diff --git a/chef-server-slice/public/stylesheets/themes/kathleene/style.css b/chef-server-api/public/stylesheets/themes/kathleene/style.css index e68a545431..e68a545431 100644 --- a/chef-server-slice/public/stylesheets/themes/kathleene/style.css +++ b/chef-server-api/public/stylesheets/themes/kathleene/style.css diff --git a/chef-server-slice/public/stylesheets/themes/orange/style.css b/chef-server-api/public/stylesheets/themes/orange/style.css index 90e7d8de58..90e7d8de58 100644 --- a/chef-server-slice/public/stylesheets/themes/orange/style.css +++ b/chef-server-api/public/stylesheets/themes/orange/style.css diff --git a/chef-server-slice/public/stylesheets/themes/reidb-greenish/style.css b/chef-server-api/public/stylesheets/themes/reidb-greenish/style.css index 23e5245eb4..23e5245eb4 100644 --- a/chef-server-slice/public/stylesheets/themes/reidb-greenish/style.css +++ b/chef-server-api/public/stylesheets/themes/reidb-greenish/style.css diff --git a/chef-server-api/stubs/app/controllers/application.rb b/chef-server-api/stubs/app/controllers/application.rb new file mode 100644 index 0000000000..c082fba50d --- /dev/null +++ b/chef-server-api/stubs/app/controllers/application.rb @@ -0,0 +1,2 @@ +class ChefServerApi::Application < Merb::Controller +end diff --git a/chef-server-api/stubs/app/controllers/main.rb b/chef-server-api/stubs/app/controllers/main.rb new file mode 100644 index 0000000000..bbb75747f6 --- /dev/null +++ b/chef-server-api/stubs/app/controllers/main.rb @@ -0,0 +1,2 @@ +class ChefServerApi::Main < ChefServerApi::Application +end diff --git a/chef-server-slice/app/controllers/cookbook_attributes.rb b/chef-server-slice/app/controllers/cookbook_attributes.rb deleted file mode 100644 index fd888448b0..0000000000 --- a/chef-server-slice/app/controllers/cookbook_attributes.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'mixin' / 'checksum' - -class ChefServerSlice::CookbookAttributes < ChefServerSlice::Application - - provides :html, :json - - before :login_required - - include Chef::Mixin::Checksum - - def load_cookbook_attributes() - @attribute_files = load_cookbook_segment(params[:cookbook_id], :attributes) - end - - def index - if params[:id] - show - else - load_cookbook_attributes() - display @attribute_files - end - end - - def show - only_provides :json - load_cookbook_attributes - raise NotFound, "Cannot find a suitable attribute file!" unless @attribute_files.has_key?(params[:id]) - to_send = @attribute_files[params[:id]][:file] - current_checksum = checksum(to_send) - Chef::Log.debug("old sum: #{params[:checksum]}, new sum: #{current_checksum}") - if current_checksum == params[:checksum] - display "File #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end - end - -end - - diff --git a/chef-server-slice/app/controllers/cookbook_definitions.rb b/chef-server-slice/app/controllers/cookbook_definitions.rb deleted file mode 100644 index da5654fbcc..0000000000 --- a/chef-server-slice/app/controllers/cookbook_definitions.rb +++ /dev/null @@ -1,60 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'mixin' / 'checksum' - -class ChefServerSlice::CookbookDefinitions < ChefServerSlice::Application - - provides :html, :json - - before :login_required - - include Chef::Mixin::Checksum - - def load_cookbook_definitions() - @definition_files = load_cookbook_segment(params[:cookbook_id], :definitions) - end - - def index - if params[:id] - show - else - load_cookbook_definitions() - display @definition_files - end - end - - def show - only_provides :json - load_cookbook_definitions - raise NotFound, "Cannot find a suitable definition file!" unless @definition_files.has_key?(params[:id]) - - to_send = @definition_files[params[:id]][:file] - current_checksum = checksum(to_send) - Chef::Log.debug("Old sum: #{params[:checksum]}, New sum: #{current_checksum}") - if current_checksum == params[:checksum] - display "File #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end - end - -end - - diff --git a/chef-server-slice/app/controllers/cookbook_files.rb b/chef-server-slice/app/controllers/cookbook_files.rb deleted file mode 100644 index b739366d83..0000000000 --- a/chef-server-slice/app/controllers/cookbook_files.rb +++ /dev/null @@ -1,94 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'mixin' / 'checksum' -require 'chef' / 'cookbook_loader' -require 'chef' / 'mixin' / 'find_preferred_file' - -class ChefServerSlice::CookbookFiles < ChefServerSlice::Application - - provides :html, :json - before :login_required - - include Chef::Mixin::Checksum - include Chef::Mixin::FindPreferredFile - - layout nil - - def index - if params[:id] - if params[:recursive] == "true" - show_directory - else - show - end - else - @remote_files = load_cookbook_files(params[:cookbook_id], :remote_file) - display @remote_files - end - end - - def show - only_provides :json - begin - to_send = find_preferred_file( - params[:cookbook_id], - :remote_file, - params[:id], - params[:fqdn], - params[:platform], - params[:version] - ) - rescue Chef::Exceptions::FileNotFound - raise NotFound, "Cannot find a suitable file!" - end - - current_checksum = checksum(to_send) - Chef::Log.debug("old sum: #{params[:checksum]}, new sum: #{current_checksum}") - if current_checksum == params[:checksum] - render "File #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end - end - - def show_directory - dir_to_send = find_preferred_file( - params[:cookbook_id], - :remote_file, - params[:id], - params[:fqdn], - params[:platform], - params[:version] - ) - unless (dir_to_send && File.directory?(dir_to_send)) - raise NotFound, "Cannot find a suitable directory" - end - - @directory_listing = Array.new - Dir[::File.join(dir_to_send, '**', '*')].sort { |a,b| b <=> a }.each do |file_to_send| - next if File.directory?(file_to_send) - file_to_send =~ /^#{dir_to_send}\/(.+)$/ - @directory_listing << $1 - end - - display @directory_listing - end - -end diff --git a/chef-server-slice/app/controllers/cookbook_libraries.rb b/chef-server-slice/app/controllers/cookbook_libraries.rb deleted file mode 100644 index 72b296f351..0000000000 --- a/chef-server-slice/app/controllers/cookbook_libraries.rb +++ /dev/null @@ -1,60 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'mixin' / 'checksum' -require 'chef' / 'cookbook_loader' - -class ChefServerSlice::CookbookLibraries < ChefServerSlice::Application - - provides :html, :json - before :login_required - - include Chef::Mixin::Checksum - - def load_cookbook_libs() - @lib_files = load_cookbook_segment(params[:cookbook_id], :libraries) - end - - def index - if params[:id] - show - else - load_cookbook_libs() - display @lib_files - end - end - - def show - only_provides :json - load_cookbook_libs - raise NotFound, "Cannot find a suitable library file!" unless @lib_files.has_key?(params[:id]) - - to_send = @lib_files[params[:id]][:file] - current_checksum = checksum(to_send) - Chef::Log.debug("Old sum: #{params[:checksum]}, New sum: #{current_checksum}") - if current_checksum == params[:checksum] - display "File #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end - end - -end - - diff --git a/chef-server-slice/app/controllers/cookbook_recipes.rb b/chef-server-slice/app/controllers/cookbook_recipes.rb deleted file mode 100644 index fbcb427eb3..0000000000 --- a/chef-server-slice/app/controllers/cookbook_recipes.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'mixin' / 'checksum' - -class ChefServerSlice::CookbookRecipes < ChefServerSlice::Application - - provides :html, :json - before :login_required - - include Chef::Mixin::Checksum - - def load_cookbook_recipes() - @recipe_files = load_cookbook_segment(params[:cookbook_id], :recipes) - end - - def index - if params[:id] - show - else - load_cookbook_recipes() - display @recipe_files - end - end - - def show - only_provides :json - load_cookbook_recipes - raise NotFound, "Cannot find a suitable recipe file!" unless @recipe_files.has_key?(params[:id]) - - to_send = @recipe_files[params[:id]][:file] - current_checksum = checksum(to_send) - Chef::Log.debug("old sum: #{params[:checksum]}, new sum: #{current_checksum}") - if current_checksum == params[:checksum] - display "File #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end - end - -end - - diff --git a/chef-server-slice/app/controllers/main.rb b/chef-server-slice/app/controllers/main.rb deleted file mode 100644 index 871328943e..0000000000 --- a/chef-server-slice/app/controllers/main.rb +++ /dev/null @@ -1,7 +0,0 @@ -class ChefServerSlice::Main < ChefServerSlice::Application - - def index - render - end - -end diff --git a/chef-server-slice/app/controllers/nodes.rb b/chef-server-slice/app/controllers/nodes.rb deleted file mode 100644 index 4faa3c727e..0000000000 --- a/chef-server-slice/app/controllers/nodes.rb +++ /dev/null @@ -1,144 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef' / 'node' - -class ChefServerSlice::Nodes < ChefServerSlice::Application - - provides :html, :json - - before :fix_up_node_id - before :login_required - before :authorized_node, :only => [ :update, :destroy ] - - def index - @node_list = Chef::Node.list - display(@node_list.collect { |n| absolute_slice_url(:node, escape_node_id(n)) }) - end - - def show - begin - @node = Chef::Node.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load node #{params[:id]}" - end - # TODO - might as well expand the run list here, too, rather than take multiple round trips. - recipes, defaults, overrides = @node.run_list.expand("couchdb") - @node.default = defaults - @node.override = overrides - display @node - end - - def new - @node = Chef::Node.new - @available_recipes = get_available_recipes - @available_roles = Chef::Role.list.sort - @run_list = @node.run_list - render - end - - def edit - begin - @node = Chef::Node.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load node #{params[:id]}" - end - @available_recipes = get_available_recipes - @available_roles = Chef::Role.list.sort - @run_list = @node.run_list - render - end - - def create - if params.has_key?("inflated_object") - @node = params["inflated_object"] - exists = true - begin - Chef::Node.load(@node.name) - rescue Net::HTTPServerException - exists = false - end - raise Forbidden, "Node already exists" if exists - self.status = 201 - @node.save - display({ :uri => absolute_slice_url(:node, escape_node_id(@node.name)) }) - else - begin - @node = Chef::Node.new - @node.name params[:name] - @node.attribute = JSON.parse(params[:attributes]) - @node.run_list params[:for_node] - @node.save - redirect(slice_url(:nodes), :message => { :notice => "Created Node #{@node.name}" }) - rescue - @node.attribute = JSON.parse(params[:attributes]) - @available_recipes = get_available_recipes - @available_roles = Chef::Role.list.sort - @run_list = params[:for_node] - @_message = { :error => $! } - render :new - end - end - end - - def update - begin - @node = Chef::Node.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load node #{params[:id]}" - end - - if params.has_key?("inflated_object") - updated = params['inflated_object'] - @node.run_list.reset(updated.run_list) - @node.attribute = updated.attribute - @node.save - display(@node) - else - begin - @node.run_list.reset(params[:for_node] ? params[:for_node] : []) - @node.attribute = JSON.parse(params[:attributes]) - @node.save - @_message = { :notice => "Updated Node" } - render :show - rescue - @available_recipes = get_available_recipes - @available_roles = Chef::Role.list.sort - @run_list = Chef::RunList.new - @run_list.reset(params[:for_node]) - render :edit - end - end - end - - def destroy - begin - @node = Chef::Node.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load node #{params[:id]}" - end - @node.destroy - if request.accept == 'application/json' - display @node - else - redirect(absolute_slice_url(:nodes), {:message => { :notice => "Node #{params[:id]} deleted successfully" }, :permanent => true}) - end - end - -end diff --git a/chef-server-slice/app/controllers/openid_register.rb b/chef-server-slice/app/controllers/openid_register.rb deleted file mode 100644 index 1a15762447..0000000000 --- a/chef-server-slice/app/controllers/openid_register.rb +++ /dev/null @@ -1,113 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'openid' -require 'chef' / 'openid_registration' - -class ChefServerSlice::OpenidRegister < ChefServerSlice::Application - - provides :html, :json - - before :fix_up_node_id - before :login_required, :only => [ :index, :update, :destroy, :validate, :admin ] - before :authorized_node, :only => [ :update, :destroy, :validate, :admin ] - - def index - @headers['X-XRDS-Location'] = Chef::Config[:openid_url] + "/openid/server/server/xrds" - @registered_nodes = Chef::OpenIDRegistration.list(true) - Chef::Log.debug(@registered_nodes.inspect) - display @registered_nodes - end - - def show - begin - @registered_node = Chef::OpenIDRegistration.load(params[:id]) - rescue Net::HTTPServerException => e - if e.message =~ /^404/ - raise NotFound, "Cannot load node registration for #{params[:id]}" - else - raise e - end - end - Chef::Log.debug(@registered_node.inspect) - display @registered_node - end - - def create - params.has_key?(:id) or raise BadRequest, "You must provide an id to register" - params.has_key?(:password) or raise BadRequest, "You must provide a password to register" - if Chef::OpenIDRegistration.has_key?(params[:id]) - raise BadRequest, "You cannot re-register #{params[:id]}!" - end - @registered_node = Chef::OpenIDRegistration.new - @registered_node.name = params[:id] - @registered_node.set_password(params[:password]) - if Chef::Config[:validation_token] - if params[:validation_token] == Chef::Config[:validation_token] - @registered_node.validated = true - else - @registered_node.validated = false - end - else - @registered_node.validated = false - end - @registered_node.save - display @registered_node - end - - def update - raise BadRequest, "You cannot update your registration -- delete #{params[:id]} and re-register" - end - - def destroy - begin - r = Chef::OpenIDRegistration.load(params[:id]) - rescue Exception => e - raise BadRequest, "Cannot find the registration for #{params[:id]}" - end - r.destroy - if content_type == :html - redirect slice_url(:registrations) - else - display({ :message => "Deleted registration for #{params[:id]}"}) - end - end - - def validate - begin - r = Chef::OpenIDRegistration.load(params[:id]) - rescue Exception => e - raise BadRequest, "Cannot find the registration for #{params[:id]}" - end - r.validated = r.validated ? false : true - r.save - redirect slice_url(:registrations) - end - - def admin - begin - r = Chef::OpenIDRegistration.load(params[:id]) - rescue Exception => e - raise BadRequest, "Cannot find the registration for #{params[:id]}" - end - r.admin = r.admin ? false : true - r.save - redirect slice_url(:registrations) - end -end diff --git a/chef-server-slice/app/controllers/openid_server.rb b/chef-server-slice/app/controllers/openid_server.rb deleted file mode 100644 index 17dd564e09..0000000000 --- a/chef-server-slice/app/controllers/openid_server.rb +++ /dev/null @@ -1,252 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Christopher Brown (<cb@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'pathname' - -# load the openid library, first trying rubygems -#begin -# require "rubygems" -# require_gem "ruby-openid", ">= 1.0" -#rescue LoadError -require "openid" -require "openid/consumer/discovery" -require 'json' -require 'chef' / 'openid_registration' -#end - -class ChefServerSlice::OpenidServer < ChefServerSlice::Application - - provides :html, :json - - include Merb::ChefServerSlice::OpenidServerHelper - include OpenID::Server - - layout nil - - before :fix_up_node_id - after :dump_cookies_and_session - - def index - - oidreq = server.decode_request(params.reject{|k,v| k == "controller" || k == "action"}) - - # no openid.mode was given - unless oidreq - return "This is the Chef OpenID server endpoint." - end - - oidresp = nil - - if oidreq.kind_of?(CheckIDRequest) - identity = oidreq.identity - - if oidresp - nil - elsif self.is_authorized(identity, oidreq.trust_root) - oidresp = oidreq.answer(true, nil, identity) - elsif oidreq.immediate - server_url = slice_url :openid_server - oidresp = oidreq.answer(false, server_url) - else - if content_type == :json - session[:last_oidreq] = oidreq - response = { :action => slice_url(:openid_server_decision) } - return response.to_json - else - return show_decision_page(oidreq) - end - end - else - oidresp = server.handle_request(oidreq) - end - - self.render_response(oidresp) - end - - def show_decision_page(oidreq, message="Do you trust this site with your identity?") - session[:last_oidreq] = oidreq - @oidreq = oidreq - - if message - session[:notice] = message - end - - render :template => 'openid_server/decide' - end - - def node_page - unless Chef::OpenIDRegistration.has_key?(params[:id]) - raise NotFound, "Cannot find registration for #{params[:id]}" - end - - # Yadis content-negotiation: we want to return the xrds if asked for. - accept = request.env['HTTP_ACCEPT'] - - # This is not technically correct, and should eventually be updated - # to do real Accept header parsing and logic. Though I expect it will work - # 99% of the time. - if accept and accept.include?('application/xrds+xml') - return node_xrds - end - - # content negotiation failed, so just render the user page - xrds_url = absolute_slice_url(:openid_node_xrds, :id => params[:id]) - identity_page = <<EOS -<html><head> -<meta http-equiv="X-XRDS-Location" content="#{xrds_url}" /> -<link rel="openid.server" href="#{absolute_slice_url(:openid_node, :id => params[:id])}" /> -</head><body><p>OpenID identity page for registration #{params[:id]}</p> -</body></html> -EOS - - # Also add the Yadis location header, so that they don't have - # to parse the html unless absolutely necessary. - @headers['X-XRDS-Location'] = xrds_url - render identity_page - end - - def node_xrds - types = [ - OpenID::OPENID_2_0_TYPE, - OpenID::OPENID_1_0_TYPE - ] - - render_xrds(types) - end - - def idp_xrds - types = [ - OpenID::OPENID_IDP_2_0_TYPE, - ] - - render_xrds(types) - end - - def decision - oidreq = session[:last_oidreq] - session[:last_oidreq] = nil - - if params.has_key?(:cancel) - Chef::Log.info("Cancelling OpenID Authentication") - return(redirect(oidreq.cancel_url)) - else - identity = oidreq.identity - identity =~ /node\/(.+)$/ - openid_node = Chef::OpenIDRegistration.load($1) - unless openid_node.validated - raise Unauthorized, "This nodes registration has not been validated" - end - if openid_node.password == encrypt_password(openid_node.salt, params[:password]) - if session[:approvals] and !session[:approvals].include?(oidreq.trust_root) - session[:approvals] << oidreq.trust_root - else - session[:approvals] = [oidreq.trust_root] - end - oidresp = oidreq.answer(true, nil, identity) - return self.render_response(oidresp) - else - raise Unauthorized, "Invalid credentials" - end - end - end - - protected - - def encrypt_password(salt, password) - Digest::SHA1.hexdigest("--#{salt}--#{password}--") - end - - def server - if @server.nil? - server_url = absolute_slice_url(:openid_server) - if Chef::Config[:openid_store_couchdb] - require 'openid-store-couchdb' - store = OpenID::Store::CouchDB.new(Chef::Config[:couchdb_url]) - else - require 'openid/store/filesystem' - dir = Chef::Config[:openid_store_path] - store = OpenID::Store::Filesystem.new(dir) - end - @server = Server.new(store, server_url) - end - return @server - end - - def approved(trust_root) - return false if session[:approvals].nil? - return session[:approvals].member?(trust_root) - end - - def is_authorized(identity_url, trust_root) - return (session[:username] and (identity_url == url_for_user) and self.approved(trust_root)) - end - - def render_xrds(types) - type_str = "" - - types.each { |uri| - type_str += "<Type>#{uri}</Type>\n " - } - - yadis = <<EOS -<?xml version="1.0" encoding="UTF-8"?> -<xrds:XRDS - xmlns:xrds="xri://$xrds" - xmlns="xri://$xrd*($v*2.0)"> - <XRD> - <Service priority="0"> - #{type_str} - <URI>#{absolute_slice_url(:openid_server)}</URI> - </Service> - </XRD> -</xrds:XRDS> -EOS - - @headers['content-type'] = 'application/xrds+xml' - render yadis - end - - def render_response(oidresp) - if oidresp.needs_signing - signed_response = server.signatory.sign(oidresp) - end - web_response = server.encode_response(oidresp) - - case web_response.code - when HTTP_OK - @status = 200 - render web_response.body - when HTTP_REDIRECT - redirect web_response.headers['location'] - else - @status = 400 - render web_response.body - end - end - - def dump_cookies_and_session - unless session.empty? or request.cookies.empty? - cookie_size = request.cookies.inject(0) {|sum,c| sum + c[1].length } - c, s = request.cookies.inspect, session.inspect - Chef::Log.debug("cookie dump (size: #{cookie_size}): #{c}") - Chef::Log.debug("session dump #{s}") - end - end - -end diff --git a/chef-server-slice/app/controllers/roles.rb b/chef-server-slice/app/controllers/roles.rb deleted file mode 100644 index 0ca70ea01b..0000000000 --- a/chef-server-slice/app/controllers/roles.rb +++ /dev/null @@ -1,138 +0,0 @@ -require 'chef/role' - -class ChefServerSlice::Roles < ChefServerSlice::Application - - provides :html, :json - before :login_required - - # GET /roles - def index - @role_list = Chef::Role.list(true) - display(@role_list.collect { |r| absolute_slice_url(:role, r.name) }) - end - - # GET /roles/:id - def show - begin - @role = Chef::Role.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load role #{params[:id]}" - end - display @role - end - - # GET /roles/new - def new - @available_recipes = get_available_recipes - @role = Chef::Role.new - @current_recipes = @role.recipes - render - end - - # GET /roles/:id/edit - def edit - begin - @role = Chef::Role.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load role #{params[:id]}" - end - @available_recipes = get_available_recipes - @current_recipes = @role.recipes - render - end - - # GET /roles/:id/delete - def delete - - end - - # POST /roles - def create - if params.has_key?("inflated_object") - @role = params["inflated_object"] - exists = true - begin - Chef::Role.load(@role.name) - rescue Net::HTTPServerException - exists = false - end - raise Forbidden, "Role already exists" if exists - - @role.save - self.status = 201 - display({ :uri => absolute_slice_url(:role, @role.name) }) - else - begin - @role = Chef::Role.new - @role.name(params[:name]) - @role.recipes(params[:for_role] ? params[:for_role] : []) - @role.description(params[:description]) if params[:description] != '' - @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' - @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' - @role.save - redirect(slice_url(:roles), :message => { :notice => "Created Role #{@role.name}" }) - rescue ArgumentError - @available_recipes = get_available_recipes - @role = Chef::Role.new - @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' - @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' - @current_recipes = params[:for_role] ? params[:for_role] : [] - @_message = { :error => $! } - render :new - end - end - end - - # PUT /roles/:id - def update - begin - @role = Chef::Role.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load role #{params[:id]}" - end - - if params.has_key?("inflated_object") - @role.description(params["inflated_object"].description) - @role.recipes(params["inflated_object"].recipes) - @role.default_attributes(params["inflated_object"].default_attributes) - @role.override_attributes(params["inflated_object"].override_attributes) - @role.save - self.status = 200 - display(@role) - else - begin - @role.recipes(params[:for_role]) - @role.description(params[:description]) if params[:description] != '' - @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' - @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' - @role.save - @_message = { :notice => "Updated Role" } - render :show - rescue ArgumentError - @available_recipes = get_available_recipes - @current_recipes = params[:for_role] ? params[:for_role] : [] - @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' - @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' - render :edit - end - - end - end - - # DELETE /roles/:id - def destroy - begin - @role = Chef::Role.load(params[:id]) - rescue Net::HTTPServerException => e - raise NotFound, "Cannot load role #{params[:id]}" - end - @role.destroy - - if request.accept == "application/json" - display @role - else - redirect(absolute_slice_url(:roles), :message => { :notice => "Role #{@role.name} deleted successfully." }, :permanent => true) - end - end - -end diff --git a/chef-server-webui/LICENSE b/chef-server-webui/LICENSE new file mode 100644 index 0000000000..11069edd79 --- /dev/null +++ b/chef-server-webui/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/chef-server-slice/NOTICE b/chef-server-webui/NOTICE index 02f03a496d..02f03a496d 100644 --- a/chef-server-slice/NOTICE +++ b/chef-server-webui/NOTICE diff --git a/chef-server-slice/README.rdoc b/chef-server-webui/README.rdoc index 2c2eaa706b..2c2eaa706b 100644 --- a/chef-server-slice/README.rdoc +++ b/chef-server-webui/README.rdoc diff --git a/chef-server-webui/Rakefile b/chef-server-webui/Rakefile new file mode 100644 index 0000000000..600458efe2 --- /dev/null +++ b/chef-server-webui/Rakefile @@ -0,0 +1,65 @@ +require 'rubygems' +require 'rake/gempackagetask' + +require 'merb-core' +require 'merb-core/tasks/merb' + +GEM_NAME = "chef-server-webui" +CHEF_SERVER_VERSION="0.8.0" +AUTHOR = "Opscode" +EMAIL = "chef@opscode.com" +HOMEPAGE = "http://wiki.opscode.com/display/chef" +SUMMARY = "A systems integration framework, built to bring the benefits of configuration management to your entire infrastructure." + +spec = Gem::Specification.new do |s| + s.rubyforge_project = 'chef' + s.name = GEM_NAME + s.version = CHEF_SERVER_VERSION + s.platform = Gem::Platform::RUBY + s.has_rdoc = true + s.extra_rdoc_files = ["README.rdoc", "LICENSE" ] + s.summary = SUMMARY + s.description = s.summary + s.author = AUTHOR + s.email = EMAIL + s.homepage = HOMEPAGE + + ["merb-slices", + "stomp", + "stompserver", + "ferret", + "merb-core", + "merb-haml", + "merb-assets", + "merb-helpers", + "mongrel", + "haml", + "ruby-openid", + "json", + "syntax",].each { |g| s.add_dependency g} + + s.require_path = 'lib' + s.files = %w(LICENSE README.rdoc Rakefile) + Dir.glob("{config,lib,spec,app,public,stubs}/**/*") +end + +Rake::GemPackageTask.new(spec) do |pkg| + pkg.gem_spec = spec +end + +desc "Install the gem" +task :install => :package do + sh %{sudo gem install pkg/#{GEM_NAME}-#{CHEF_SERVER_VERSION} --no-rdoc --no-ri} +end + +desc "Uninstall the gem" +task :uninstall do + sh %{sudo gem uninstall #{GEM_NAME} -x -v #{CHEF_SERVER_VERSION} } +end + +desc "Create a gemspec file" +task :gemspec do + File.open("#{GEM_NAME}.gemspec", "w") do |file| + file.puts spec.to_ruby + end +end + diff --git a/chef-server-slice/app/controllers/application.rb b/chef-server-webui/app/controllers/application.rb index 0aeda27c59..60fd6ddec3 100644 --- a/chef-server-slice/app/controllers/application.rb +++ b/chef-server-webui/app/controllers/application.rb @@ -20,7 +20,7 @@ require "chef" / "mixin" / "checksum" require "chef" / "cookbook_loader" -class ChefServerSlice::Application < Merb::Controller +class ChefServerWebui::Application < Merb::Controller include Chef::Mixin::Checksum @@ -61,7 +61,7 @@ class ChefServerSlice::Application < Merb::Controller end arg.gsub(/\./, '_') end - + def login_required if session[:openid] return session[:openid] diff --git a/chef-server-webui/app/controllers/cookbook_attributes.rb b/chef-server-webui/app/controllers/cookbook_attributes.rb new file mode 100644 index 0000000000..f0fa51a9dc --- /dev/null +++ b/chef-server-webui/app/controllers/cookbook_attributes.rb @@ -0,0 +1,41 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'mixin' / 'checksum' + +class ChefServerWebui::CookbookAttributes < ChefServerWebui::Application + + provides :html + + before :login_required + + include Chef::Mixin::Checksum + + def load_cookbook_attributes() + @attribute_files = load_cookbook_segment(params[:cookbook_id], :attributes) + end + + def index + load_cookbook_attributes() + render + end + +end + + diff --git a/chef-server-webui/app/controllers/cookbook_definitions.rb b/chef-server-webui/app/controllers/cookbook_definitions.rb new file mode 100644 index 0000000000..5437d41141 --- /dev/null +++ b/chef-server-webui/app/controllers/cookbook_definitions.rb @@ -0,0 +1,41 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'mixin' / 'checksum' + +class ChefServerWebui::CookbookDefinitions < ChefServerWebui::Application + + provides :html + + before :login_required + + include Chef::Mixin::Checksum + + def load_cookbook_definitions() + @definition_files = load_cookbook_segment(params[:cookbook_id], :definitions) + end + + def index + load_cookbook_definitions() + render + end + +end + + diff --git a/chef-server-webui/app/controllers/cookbook_files.rb b/chef-server-webui/app/controllers/cookbook_files.rb new file mode 100644 index 0000000000..a16662c384 --- /dev/null +++ b/chef-server-webui/app/controllers/cookbook_files.rb @@ -0,0 +1,39 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'mixin' / 'checksum' +require 'chef' / 'cookbook_loader' +require 'chef' / 'mixin' / 'find_preferred_file' + +class ChefServerWebui::CookbookFiles < ChefServerWebui::Application + + provides :html + before :login_required + + include Chef::Mixin::Checksum + include Chef::Mixin::FindPreferredFile + + layout nil + + def index + @remote_files = load_cookbook_files(params[:cookbook_id], :remote_file) + render + end + +end diff --git a/chef-server-webui/app/controllers/cookbook_libraries.rb b/chef-server-webui/app/controllers/cookbook_libraries.rb new file mode 100644 index 0000000000..65dd23221f --- /dev/null +++ b/chef-server-webui/app/controllers/cookbook_libraries.rb @@ -0,0 +1,41 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'mixin' / 'checksum' +require 'chef' / 'cookbook_loader' + +class ChefServerWebui::CookbookLibraries < ChefServerWebui::Application + + provides :html + before :login_required + + include Chef::Mixin::Checksum + + def load_cookbook_libs() + @lib_files = load_cookbook_segment(params[:cookbook_id], :libraries) + end + + def index + load_cookbook_libs() + render + end + +end + + diff --git a/chef-server-webui/app/controllers/cookbook_recipes.rb b/chef-server-webui/app/controllers/cookbook_recipes.rb new file mode 100644 index 0000000000..9a43abcb09 --- /dev/null +++ b/chef-server-webui/app/controllers/cookbook_recipes.rb @@ -0,0 +1,40 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'mixin' / 'checksum' + +class ChefServerWebui::CookbookRecipes < ChefServerWebui::Application + + provides :html + before :login_required + + include Chef::Mixin::Checksum + + def load_cookbook_recipes() + @recipe_files = load_cookbook_segment(params[:cookbook_id], :recipes) + end + + def index + load_cookbook_recipes() + render + end + +end + + diff --git a/chef-server-slice/app/controllers/cookbook_templates.rb b/chef-server-webui/app/controllers/cookbook_templates.rb index bce79bd573..1ab991b63e 100644 --- a/chef-server-slice/app/controllers/cookbook_templates.rb +++ b/chef-server-webui/app/controllers/cookbook_templates.rb @@ -21,9 +21,9 @@ require 'chef' / 'mixin' / 'checksum' require 'chef' / 'cookbook_loader' require 'chef' / 'mixin' / 'find_preferred_file' -class ChefServerSlice::CookbookTemplates < ChefServerSlice::Application +class ChefServerWebui::CookbookTemplates < ChefServerWebui::Application - provides :html, :json + provides :html before :login_required include Chef::Mixin::Checksum @@ -50,31 +50,8 @@ class ChefServerSlice::CookbookTemplates < ChefServerSlice::Application # end def index - if params[:id] - show - else - @templates = load_cookbook_files(params[:cookbook_id], :template) - display @templates - end - end - - def show - to_send = find_preferred_file( - params[:cookbook_id], - :template, - params[:id], - params[:fqdn], - params[:platform], - params[:version] - ) - raise NotFound, "Cannot find a suitable template!" unless to_send - current_checksum = checksum(to_send) - Chef::Log.debug("old sum: #{params[:checksum]}, new sum: #{current_checksum}") - if current_checksum == params[:checksum] - render "Template #{to_send} has not changed", :status => 304 - else - send_file(to_send) - end + @templates = load_cookbook_files(params[:cookbook_id], :template) + display @templates end end diff --git a/chef-server-slice/app/controllers/cookbooks.rb b/chef-server-webui/app/controllers/cookbooks.rb index 24c8be5f17..4460ac6cde 100644 --- a/chef-server-slice/app/controllers/cookbooks.rb +++ b/chef-server-webui/app/controllers/cookbooks.rb @@ -19,7 +19,7 @@ require 'chef' / 'cookbook_loader' -class ChefServerSlice::Cookbooks < ChefServerSlice::Application +class ChefServerWebui::Cookbooks < ChefServerWebui::Application provides :html, :json before :login_required diff --git a/chef-server-webui/app/controllers/exceptions.rb b/chef-server-webui/app/controllers/exceptions.rb new file mode 100644 index 0000000000..c1344b8fae --- /dev/null +++ b/chef-server-webui/app/controllers/exceptions.rb @@ -0,0 +1,19 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + diff --git a/chef-server-webui/app/controllers/main.rb b/chef-server-webui/app/controllers/main.rb new file mode 100644 index 0000000000..7ee2c60182 --- /dev/null +++ b/chef-server-webui/app/controllers/main.rb @@ -0,0 +1,7 @@ +class ChefServerWebui::Main < ChefServerWebui::Application + + def index + render + end + +end diff --git a/chef-server-webui/app/controllers/nodes.rb b/chef-server-webui/app/controllers/nodes.rb new file mode 100644 index 0000000000..d95e7fd262 --- /dev/null +++ b/chef-server-webui/app/controllers/nodes.rb @@ -0,0 +1,114 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' / 'node' + +class ChefServerWebui::Nodes < ChefServerWebui::Application + + provides :html + + before :fix_up_node_id + before :login_required + before :authorized_node, :only => [ :update, :destroy ] + + def index + @node_list = Chef::Node.list + render + end + + def show + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + render + end + + def new + @node = Chef::Node.new + @available_recipes = get_available_recipes + @available_roles = Chef::Role.list.sort + @run_list = @node.run_list + render + end + + def edit + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + @available_recipes = get_available_recipes + @available_roles = Chef::Role.list.sort + @run_list = @node.run_list + render + end + + def create + begin + @node = Chef::Node.new + @node.name params[:name] + @node.attribute = JSON.parse(params[:attributes]) + @node.run_list params[:for_node] + @node.save + redirect(slice_url(:nodes), :message => { :notice => "Created Node #{@node.name}" }) + rescue + @node.attribute = JSON.parse(params[:attributes]) + @available_recipes = get_available_recipes + @available_roles = Chef::Role.list.sort + @run_list = params[:for_node] + @_message = { :error => $! } + render :new + end + end + + def update + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + + begin + @node.run_list.reset(params[:for_node] ? params[:for_node] : []) + @node.attribute = JSON.parse(params[:attributes]) + @node.save + @_message = { :notice => "Updated Node" } + render :show + rescue + @available_recipes = get_available_recipes + @available_roles = Chef::Role.list.sort + @run_list = Chef::RunList.new + @run_list.reset(params[:for_node]) + render :edit + end + end + + def destroy + begin + @node = Chef::Node.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load node #{params[:id]}" + end + @node.destroy + redirect(absolute_slice_url(:nodes), {:message => { :notice => "Node #{params[:id]} deleted successfully" }, :permanent => true}) + end + +end diff --git a/chef-server-slice/app/controllers/openid_consumer.rb b/chef-server-webui/app/controllers/openid_consumer.rb index e42ef2bb74..e2bab98606 100644 --- a/chef-server-slice/app/controllers/openid_consumer.rb +++ b/chef-server-webui/app/controllers/openid_consumer.rb @@ -21,9 +21,9 @@ require 'pathname' require 'openid' require (Chef::Config[:openid_cstore_couchdb] ? 'openid-store-couchdb' : 'openid/store/filesystem') -class ChefServerSlice::OpenidConsumer < ChefServerSlice::Application +class ChefServerWebui::OpenidConsumer < ChefServerWebui::Application - provides :html, :json + provides :html def index if request.xhr? @@ -107,12 +107,12 @@ class ChefServerSlice::OpenidConsumer < ChefServerSlice::Application true end end - + def is_authorized_openid_identifier?(openid, authorized_identifiers) Chef::Log.debug("checking for valid openid identifier: openid: #{openid}, authorized openids: #{authorized_identifiers}") if authorized_identifiers and openid - if authorized_identifiers.length > 0 - authorized_identifiers.detect { |p| Chef::Log.debug("openid: #{openid} (#{openid.class}), p: #{p} (#{p.class})"); openid == p } + if authorized_identifiers.length > 0 + authorized_identifiers.detect { |p| Chef::Log.debug("openid: #{openid} (#{openid.class}), p: #{p} (#{p.class})"); openid == p } else true end diff --git a/chef-server-webui/app/controllers/roles.rb b/chef-server-webui/app/controllers/roles.rb new file mode 100644 index 0000000000..10ad8b84db --- /dev/null +++ b/chef-server-webui/app/controllers/roles.rb @@ -0,0 +1,103 @@ +require 'chef/role' + +class ChefServerWebui::Roles < ChefServerWebui::Application + + provides :html + before :login_required + + # GET /roles + def index + @role_list = Chef::Role.list(true) + render + end + + # GET /roles/:id + def show + begin + @role = Chef::Role.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load role #{params[:id]}" + end + render + end + + # GET /roles/new + def new + @available_recipes = get_available_recipes + @role = Chef::Role.new + @current_recipes = @role.recipes + render + end + + # GET /roles/:id/edit + def edit + begin + @role = Chef::Role.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load role #{params[:id]}" + end + @available_recipes = get_available_recipes + @current_recipes = @role.recipes + render + end + + # POST /roles + def create + begin + @role = Chef::Role.new + @role.name(params[:name]) + @role.recipes(params[:for_role] ? params[:for_role] : []) + @role.description(params[:description]) if params[:description] != '' + @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' + @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' + @role.save + redirect(slice_url(:roles), :message => { :notice => "Created Role #{@role.name}" }) + rescue ArgumentError + @available_recipes = get_available_recipes + @role = Chef::Role.new + @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' + @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' + @current_recipes = params[:for_role] ? params[:for_role] : [] + @_message = { :error => $! } + render :new + end + end + + # PUT /roles/:id + def update + begin + @role = Chef::Role.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load role #{params[:id]}" + end + + begin + @role.recipes(params[:for_role]) + @role.description(params[:description]) if params[:description] != '' + @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' + @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' + @role.save + @_message = { :notice => "Updated Role" } + render :show + rescue ArgumentError + @available_recipes = get_available_recipes + @current_recipes = params[:for_role] ? params[:for_role] : [] + @role.default_attributes(JSON.parse(params[:default_attributes])) if params[:default_attributes] != '' + @role.override_attributes(JSON.parse(params[:override_attributes])) if params[:override_attributes] != '' + render :edit + end + end + + # DELETE /roles/:id + def destroy + begin + @role = Chef::Role.load(params[:id]) + rescue Net::HTTPServerException => e + raise NotFound, "Cannot load role #{params[:id]}" + end + @role.destroy + + redirect(absolute_slice_url(:roles), :message => { :notice => "Role #{@role.name} deleted successfully." }, :permanent => true) + end + +end diff --git a/chef-server-slice/app/controllers/search.rb b/chef-server-webui/app/controllers/search.rb index 0f56106492..71eaf86faa 100644 --- a/chef-server-slice/app/controllers/search.rb +++ b/chef-server-webui/app/controllers/search.rb @@ -20,15 +20,15 @@ require 'chef' / 'search' require 'chef' / 'queue' -class ChefServerSlice::Search < ChefServerSlice::Application +class ChefServerWebui::Search < ChefServerWebui::Application - provides :html, :json + provides :html before :login_required def index @s = Chef::Search.new @search_indexes = @s.list_indexes - display @search_indexes + render end def show @@ -37,8 +37,8 @@ class ChefServerSlice::Search < ChefServerSlice::Application query = params[:q].nil? ? "*" : (params[:q].empty? ? "*" : params[:q]) attributes = params[:a].nil? ? [] : params[:a].split(",").collect { |a| a.to_sym } @results = @s.search(params[:id], query, attributes) - - display @results + + render end def destroy @@ -48,11 +48,7 @@ class ChefServerSlice::Search < ChefServerSlice::Application Chef::Queue.send_msg(:queue, :remove, entry) end @status = 202 - if content_type == :html - redirect url(:search) - else - display @entries - end + redirect url(:search) end end diff --git a/chef-server-slice/app/controllers/search_entries.rb b/chef-server-webui/app/controllers/search_entries.rb index dfb90e0b27..ab5d034300 100644 --- a/chef-server-slice/app/controllers/search_entries.rb +++ b/chef-server-webui/app/controllers/search_entries.rb @@ -20,21 +20,21 @@ require 'chef' / 'search' require 'chef' / 'queue' -class ChefServerSlice::SearchEntries < ChefServerSlice::Application +class ChefServerWebui::SearchEntries < ChefServerWebui::Application - provides :html, :json + provides :html before :login_required def index @s = Chef::Search.new @entries = @s.search(params[:search_id]) - display @entries + render end def show @s = Chef::Search.new @entry = @s.search(params[:search_id], "id:'#{params[:search_id]}_#{params[:id]}'").first - display @entry + render end def create @@ -44,12 +44,7 @@ class ChefServerSlice::SearchEntries < ChefServerSlice::Application @to_index["id"] = "#{params[:search_id]}_#{params[:id]}" @to_index.delete(:search_id) Chef::Queue.send_msg(:queue, :index, @to_index) - if content_type == :html - redirect url(:search) - else - @status = 202 - display @to_index - end + redirect url(:search) end def update @@ -63,11 +58,7 @@ class ChefServerSlice::SearchEntries < ChefServerSlice::Application Chef::Queue.send_msg(:queue, :remove, entry) end @status = 202 - if content_type == :html - redirect url(:search) - else - display @entries - end + redirect url(:search) end end diff --git a/chef-server-slice/app/controllers/status.rb b/chef-server-webui/app/controllers/status.rb index 86cef9d377..fd1cf14695 100644 --- a/chef-server-slice/app/controllers/status.rb +++ b/chef-server-webui/app/controllers/status.rb @@ -18,9 +18,9 @@ require 'chef' / 'node' -class ChefServerSlice::Status < ChefServerSlice::Application +class ChefServerWebui::Status < ChefServerWebui::Application - provides :html, :json + provides :html before :login_required def index @@ -28,7 +28,7 @@ class ChefServerSlice::Status < ChefServerSlice::Application result << item["value"] result end - display @status + render end end diff --git a/chef-server-slice/app/helpers/application_helper.rb b/chef-server-webui/app/helpers/application_helper.rb index ca1da4b7b2..1b1d467829 100644 --- a/chef-server-slice/app/helpers/application_helper.rb +++ b/chef-server-webui/app/helpers/application_helper.rb @@ -1,7 +1,7 @@ require 'chef/mixin/deep_merge' module Merb - module ChefServerSlice + module ChefServerWebui module ApplicationHelper # @param *segments<Array[#to_s]> Path segments to append. @@ -36,7 +36,7 @@ module Merb # @return <String> # A path relative to the public directory, with added segments. def public_path_for(type, *segments) - ::ChefServerSlice.public_path_for(type, *segments) + ::ChefServerWebui.public_path_for(type, *segments) end # Construct an app-level path. @@ -47,7 +47,7 @@ module Merb # @return <String> # A path within the host application, with added segments. def app_path_for(type, *segments) - ::ChefServerSlice.app_path_for(type, *segments) + ::ChefServerWebui.app_path_for(type, *segments) end # Construct a slice-level path. @@ -58,7 +58,7 @@ module Merb # @return <String> # A path within the slice source (Gem), with added segments. def slice_path_for(type, *segments) - ::ChefServerSlice.slice_path_for(type, *segments) + ::ChefServerWebui.slice_path_for(type, *segments) end def build_tree(name, node, default={}, override={}) diff --git a/chef-server-slice/app/helpers/cookbook_attributes_helper.rb b/chef-server-webui/app/helpers/cookbook_attributes_helper.rb index 3246d45239..641c0bbfd4 100644 --- a/chef-server-slice/app/helpers/cookbook_attributes_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_attributes_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookAttributesHelper end diff --git a/chef-server-slice/app/helpers/cookbook_definitions_helper.rb b/chef-server-webui/app/helpers/cookbook_definitions_helper.rb index a7d7792261..905db82418 100644 --- a/chef-server-slice/app/helpers/cookbook_definitions_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_definitions_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookDefinitionsHelper end diff --git a/chef-server-slice/app/helpers/cookbook_files_helper.rb b/chef-server-webui/app/helpers/cookbook_files_helper.rb index 9a7b890722..0425d1664e 100644 --- a/chef-server-slice/app/helpers/cookbook_files_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_files_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookFilesHelper end diff --git a/chef-server-slice/app/helpers/cookbook_libraries_helper.rb b/chef-server-webui/app/helpers/cookbook_libraries_helper.rb index f0aaa8578f..bc6eded61b 100644 --- a/chef-server-slice/app/helpers/cookbook_libraries_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_libraries_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookLibrariesHelper end diff --git a/chef-server-slice/app/helpers/cookbook_recipes_helper.rb b/chef-server-webui/app/helpers/cookbook_recipes_helper.rb index 8038ec052c..98aa8a16f8 100644 --- a/chef-server-slice/app/helpers/cookbook_recipes_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_recipes_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookRecipesHelper end diff --git a/chef-server-slice/app/helpers/cookbook_templates_helper.rb b/chef-server-webui/app/helpers/cookbook_templates_helper.rb index d003b794af..ba62387f52 100644 --- a/chef-server-slice/app/helpers/cookbook_templates_helper.rb +++ b/chef-server-webui/app/helpers/cookbook_templates_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module CookbookTemplatesHelper end diff --git a/chef-server-slice/app/helpers/cookbooks_helper.rb b/chef-server-webui/app/helpers/cookbooks_helper.rb index 1d52d9ee54..e3e06165d8 100644 --- a/chef-server-slice/app/helpers/cookbooks_helper.rb +++ b/chef-server-webui/app/helpers/cookbooks_helper.rb @@ -14,17 +14,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +# module Merb - module ChefServerSlice + module ChefServerWebui module CookbooksHelper def syntax_highlight(code) + converter = Syntax::Convertors::HTML.for_syntax "ruby" if File.exists?(code) - tokens = CodeRay.scan_file(code, :ruby) + converter.convert(File.read(code), false) else - tokens = CodeRay.scan(code, :ruby) + converter.convert(code, false) end - return CodeRay.encode_tokens(tokens, :span) end end end diff --git a/chef-server-webui/app/helpers/exceptions_helper.rb b/chef-server-webui/app/helpers/exceptions_helper.rb new file mode 100644 index 0000000000..e27ca180af --- /dev/null +++ b/chef-server-webui/app/helpers/exceptions_helper.rb @@ -0,0 +1,6 @@ +module Merb + module ChefServerWebui + module ExceptionsHelper + end + end +end # Merb diff --git a/chef-server-slice/app/helpers/global_helpers.rb b/chef-server-webui/app/helpers/global_helpers.rb index 645343a6a7..17e26abd0e 100644 --- a/chef-server-slice/app/helpers/global_helpers.rb +++ b/chef-server-webui/app/helpers/global_helpers.rb @@ -17,7 +17,7 @@ # module Merb - module ChefServerSlice + module ChefServerWebui module GlobalHelpers # helpers defined here available to all views. diff --git a/chef-server-slice/app/helpers/nodes_helper.rb b/chef-server-webui/app/helpers/nodes_helper.rb index c108442415..c6ef880ab1 100644 --- a/chef-server-slice/app/helpers/nodes_helper.rb +++ b/chef-server-webui/app/helpers/nodes_helper.rb @@ -18,7 +18,7 @@ # module Merb - module ChefServerSlice + module ChefServerWebui module NodesHelper def recipe_list(node) response = "" @@ -28,6 +28,14 @@ module Merb response end + def attribute_list(node) + response = "" + node.each_attribute do |k,v| + response << "<li><b>#{k}</b>: #{v}</li>\n" + end + response + end + end end end diff --git a/chef-server-slice/app/helpers/openid_consumer_helper.rb b/chef-server-webui/app/helpers/openid_consumer_helper.rb index 5564b9a73c..33bfe193ff 100644 --- a/chef-server-slice/app/helpers/openid_consumer_helper.rb +++ b/chef-server-webui/app/helpers/openid_consumer_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module OpenidConsumerHelper end diff --git a/chef-server-slice/app/helpers/openid_register_helper.rb b/chef-server-webui/app/helpers/openid_register_helper.rb index 4c31be158f..1eadb4dfeb 100644 --- a/chef-server-slice/app/helpers/openid_register_helper.rb +++ b/chef-server-webui/app/helpers/openid_register_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module OpenidRegisterHelper end diff --git a/chef-server-slice/app/helpers/openid_server_helper.rb b/chef-server-webui/app/helpers/openid_server_helper.rb index 2bb6faf912..366e36a091 100644 --- a/chef-server-slice/app/helpers/openid_server_helper.rb +++ b/chef-server-webui/app/helpers/openid_server_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module OpenidServerHelper end end diff --git a/chef-server-slice/app/helpers/openid_server_helpers.rb b/chef-server-webui/app/helpers/openid_server_helpers.rb index facf5d22b9..0e248a6c40 100644 --- a/chef-server-slice/app/helpers/openid_server_helpers.rb +++ b/chef-server-webui/app/helpers/openid_server_helpers.rb @@ -17,7 +17,7 @@ # module Merb - module ChefServerSlice + module ChefServerWebui module OpenidServerHelper def url_for_user diff --git a/chef-server-webui/app/helpers/roles_helper.rb b/chef-server-webui/app/helpers/roles_helper.rb new file mode 100644 index 0000000000..b6d46cdcd1 --- /dev/null +++ b/chef-server-webui/app/helpers/roles_helper.rb @@ -0,0 +1,5 @@ +module Merb + module RolesHelper + + end +end # Merb
\ No newline at end of file diff --git a/chef-server-slice/app/helpers/search_entries_helper.rb b/chef-server-webui/app/helpers/search_entries_helper.rb index 750ed3749b..08ced2366b 100644 --- a/chef-server-slice/app/helpers/search_entries_helper.rb +++ b/chef-server-webui/app/helpers/search_entries_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module SearchEntriesHelper end diff --git a/chef-server-slice/app/helpers/search_helper.rb b/chef-server-webui/app/helpers/search_helper.rb index d7de6e6d70..c6cde5c9f3 100644 --- a/chef-server-slice/app/helpers/search_helper.rb +++ b/chef-server-webui/app/helpers/search_helper.rb @@ -1,5 +1,5 @@ module Merb - module ChefServerSlice + module ChefServerWebui module SearchHelper def output_path(attributes) res = Hash.new diff --git a/chef-server-slice/app/helpers/status_helper.rb b/chef-server-webui/app/helpers/status_helper.rb index 9f5694ae70..2ce9fa6148 100644 --- a/chef-server-slice/app/helpers/status_helper.rb +++ b/chef-server-webui/app/helpers/status_helper.rb @@ -18,7 +18,7 @@ require 'chef' / 'node' module Merb - module ChefServerSlice + module ChefServerWebui module StatusHelper end diff --git a/chef-server-slice/app/views/cookbook_templates/index.html.haml b/chef-server-webui/app/views/cookbook_templates/index.html.haml index 0e52aa9212..0e52aa9212 100644 --- a/chef-server-slice/app/views/cookbook_templates/index.html.haml +++ b/chef-server-webui/app/views/cookbook_templates/index.html.haml diff --git a/chef-server-slice/app/views/cookbooks/index.html.haml b/chef-server-webui/app/views/cookbooks/index.html.haml index d8e9eabda6..d8e9eabda6 100644 --- a/chef-server-slice/app/views/cookbooks/index.html.haml +++ b/chef-server-webui/app/views/cookbooks/index.html.haml diff --git a/chef-server-slice/app/views/cookbooks/show.html.haml b/chef-server-webui/app/views/cookbooks/show.html.haml index dedd5fefa0..dedd5fefa0 100644 --- a/chef-server-slice/app/views/cookbooks/show.html.haml +++ b/chef-server-webui/app/views/cookbooks/show.html.haml diff --git a/chef-server-webui/app/views/exceptions/bad_request.json.erb b/chef-server-webui/app/views/exceptions/bad_request.json.erb new file mode 100644 index 0000000000..f266cf99b9 --- /dev/null +++ b/chef-server-webui/app/views/exceptions/bad_request.json.erb @@ -0,0 +1 @@ +<%= { :error => params[:exception], :code => 400 }.to_json %>
\ No newline at end of file diff --git a/chef-server-webui/app/views/exceptions/internal_server_error.html.erb b/chef-server-webui/app/views/exceptions/internal_server_error.html.erb new file mode 100644 index 0000000000..aadbfad350 --- /dev/null +++ b/chef-server-webui/app/views/exceptions/internal_server_error.html.erb @@ -0,0 +1,216 @@ +<html> +<head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <title><%= @exception_name %></title> + <style type="text/css" media="screen"> + body { + font-family:arial; + font-size:11px; + } + h1 { + font-size:48px; + letter-spacing:-4px; + margin:0; + line-height:36px; + color:#333; + } + h1 sup { + font-size: 0.5em; + } + h1 sup.error_500, h1 sup.error_400 { + color:#990E05; + } + h1 sup.error_100, h1 sup.error_200 { + color:#00BF10; + } + h1 sup.error_300 { + /* pretty sure you cant 'see' status 300 + errors but if you could I think they + would be blue */ + color:#1B2099; + } + h2 { + font-size:36px; + letter-spacing:-3px; + margin:0; + line-height:28px; + color:#444; + } + a, a:visited { + color:#00BF10; + } + .internalError { + width:800px; + margin:50px auto; + } + .header { + border-bottom:10px solid #333; + margin-bottom:1px; + background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7"); + padding:20px; + } + table.trace { + width:100%; + font-family:courier, monospace; + letter-spacing:-1px; + border-collapse: collapse; + border-spacing:0; + } + table.trace tr td{ + padding:0; + height:26px; + font-size:13px; + vertical-align:middle; + } + table.trace tr.file{ + border-top:2px solid #fff; + background-color:#F3F3F3; + } + table.trace tr.source { + background-color:#F8F8F8; + display:none; + } + table.trace .open tr.source { + display:table-row; + } + table.trace tr.file td.expand { + width:23px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAXCAIAAABvSEP3AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdVJREFUeNqMVL+TwUAYxaRIOlEhlZHGDAUzzOQ61+AqXMV1lJSU7q/QRqm8KFUcJTNn5qJkaPyoKKVz7y4mF8na5Kt29tt9+/Z97/u81+vVQ4r9frdarS6Xi7ETDIZisRjxMGPfmk4niNPpZE+xLAugbPaZ53nzvtfMBe/3+/3dbuehBrAKhZdUKkVAWa9Xsiybv0CPZDJZLr/qa5/BwgwRjYqOKIvFYjQa/aNommZh0Ww2K5UqzwfoQOPxaLPZ3FAmk0+7lplMpt1u53J5OpBOR0eZEE9wHJfP5zud93g88QhluwWbjW+5VOmKBgKBer3eaDTDYeGBQF8+x7rqIYoiPgixWJazpA6HA+MSxRArkUgMh0M409g8Ho8+9wYxxCqVSq1W26EDHGM2m4HOHQrEc38f/Yn7cLmlIRhBENzcx8cVRZnPZ/YUep2BWkjTIfA+PKVpZAXR5QxsjiqCKvGEqqp443w+0dvy17swqD0HB3S73V5PpkNg1qBqt8kwGCjmPkinM0QJbIoEa7U6UG6ToVgs4V9G2g0ESoP5Aoi7KYX5oCgf8IKbkvn9/mr1LRQKESamzgJy0g0tSZIuB3nuGqRU9Vv9C4sKkUhEkp4soxvxI8AAhWrrtXa3X8EAAAAASUVORK5CYII=); + background-position:top left; + background-repeat:no-repeat; + } + table.trace .open tr.file td.expand { + width:19px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXZJREFUeNrslK1ywkAUhcMOBomEOiSdqLxEBJX0NaijOsjyHGGmCGyQQYaiiiw4gktkcOmZbpsuuzQ/M5XnqJ2d3S/n3nM3rTzPLUP7/Tt0+pLcGQwG3W53OLyHzPMtjYL7q9UqSRLrD4E1Gj1orCvKYuFHUWTVkOM44/HjDcp8/lL4r6NerzeZPMm1KFw0QkDn83m5fP2lHA4fNQvRtNvtjsfDd0WzmSfb2e/fdTqdOvdh/HLJZLOn0+d2HJ+KRGzbdl23EpFlmed5cp2maRzHQq1lvQ5KMi6EUZBGfup6E1pTfd+vrGW7jbQ2C9hTt9BpqNyIWaAwAy6xg2eBz5iRC/NomiZhGN5sqmnkauo0BUGgVQoBjQ80oCACgNQdZHfTYBkF2mxCtWWAqunWpahxIDUt3QYUxIFQpJHyIWpXjinabKbbwItMHT+NyjchrP8QKaSQQgoppJBCCimkkEIKKaSQQgoppJBCCimkkEIKKaSo+hRgAEFD17X08O2NAAAAAElFTkSuQmCC); + background-position:top left; + background-repeat:no-repeat; + } + table.trace tr.source td.collapse { + width:19px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAVxJREFUeNrs0zFygkAUBmBlUkgJHdABlQwVkVJKKUxBYWbkALTxMJwhltyDFkss03IF8pudIcwaDaDl/6pd2P327b7d+eHwMXs4lNkzggoVKlSoUKFChQoVKlSoUKFChQoVKlSoUKFChQqVEYqm6ft9+qiSJEkYho7jTlcw2fd9NOI4nq4gEdFwXXe1Cqco63VkWVbXRTqLhTpOwQRpF7quR1E0TgGhqvLKUFCyoQqG/rks3O6kZKW/eRFpevOCoGTXVTcMQ5EyxyDEkML1c5RzuZOICIyXqn7JBVez6282MWrx731HOv2qB8Hri2lamNk0DfpVVdV1Peodappmmua8bdvzuc7zfNprzrLMth1FnGh/X8MjCAIQv/cFz/+65PcDh7rbvYv2ZUfdj+PxsyzLgVl0hKwgTqeqKApx2LeOc7t98zyv/1FWOgvx9RPii23bmL9cetJ8Ed8CDAC6aFW8bCzFhwAAAABJRU5ErkJggg==); + background-position:bottom left; + background-repeat:no-repeat; + background-color:#6F706F; + } + table.trace tr td.path { + padding-left:10px; + } + table.trace tr td.code { + padding-left:35px; + white-space: pre; + line-height:9px; + padding-bottom:10px; + } + table.trace tr td.code em { + font-weight:bold; + color:#00BF10; + } + table.trace tr td.code a { + width: 20px; + float: left; + } + table.trace tr td.code .more { + color:#666; + } + table.trace tr td.line { + width:30px; + text-align:right; + padding-right:4px; + } + .footer { + margin-top:5px; + font-size:11px; + color:#444; + text-align:right; + } + </style> +</head> +<body> + <div class="internalError"> + + <div class="header"> + <h1><%= @exception_name %> <sup class="error_<%= @exception.class::STATUS %>"><%= @exception.class::STATUS %></sup></h1> + <% if show_details = ::Merb::Config[:exception_details] -%> + <h2><%= @exception.message %></h2> + <% else -%> + <h2>Sorry about that...</h2> + <% end -%> + <h3>Parameters</h3> + <ul> + <% params[:original_params].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_params].empty? %> + </ul> + + <h3>Session</h3> + <ul> + <% params[:original_session].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_session].empty? %> + </ul> + + <h3>Cookies</h3> + <ul> + <% params[:original_cookies].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_cookies].empty? %> + </ul> + </div> + + <% if show_details %> + <table class="trace"> + <% @exception.backtrace.each_with_index do |line, index| %> + <tbody class="close"> + <tr class="file"> + <td class="expand"> + </td> + <td class="path"> + <%= (line.match(/^([^:]+)/)[1] rescue 'unknown').sub(/\/((opt|usr)\/local\/lib\/(ruby\/)?(gems\/)?(1.8\/)?(gems\/)?|.+\/app\/)/, '') %> + <% unless line.match(/\.erb:/) %> + in "<strong><%= line.match(/:in `(.+)'$/)[1] rescue '?' %></strong>" + <% else %> + (<strong>ERB Template</strong>) + <% end %> + </td> + <td class="line"> + <a href="txmt://open?url=file://<%=file = (line.match(/^([^:]+)/)[1] rescue 'unknown')%>&line=<%= lineno = line.match(/:([0-9]+):/)[1] rescue '?' %>"><%=lineno%></a> + </td> + </tr> + <tr class="source"> + <td class="collapse"> + </td> + <td class="code" colspan="2"><% (__caller_lines__(file, lineno, 5) rescue []).each do |llineno, lcode, lcurrent| %> +<a href="txmt://open?url=file://<%=file%>&line=<%=llineno%>"><%= llineno %></a><%='<em>' if llineno==lineno.to_i %><%= lcode.size > 90 ? CGI.escapeHTML(lcode[0..90])+'<span class="more">......</span>' : CGI.escapeHTML(lcode) %><%='</em>' if llineno==lineno.to_i %> +<% end %> + +</td> + </tr> + </tbody> + <% end %> + </table> + <script type="text/javascript" charset="utf-8"> + // swop the open & closed classes + els = document.getElementsByTagName('td'); + for(i=0; i<els.length; i++){ + if(els[i].className=='expand' || els[i].className=='collapse'){ + els[i].onclick = function(e){ + tbody = this.parentNode.parentNode; + if(tbody.className=='open'){ + tbody.className='closed'; + }else{ + tbody.className='open'; + } + } + } + } + </script> + <% end %> + <div class="footer"> + lots of love, from <a href="#">merb</a> + </div> + </div> +</body> +</html>
\ No newline at end of file diff --git a/chef-server-slice/app/views/exceptions/not_acceptable.html.erb b/chef-server-webui/app/views/exceptions/not_acceptable.html.erb index f632712bb2..f632712bb2 100644 --- a/chef-server-slice/app/views/exceptions/not_acceptable.html.erb +++ b/chef-server-webui/app/views/exceptions/not_acceptable.html.erb diff --git a/chef-server-webui/app/views/exceptions/not_found.html.erb b/chef-server-webui/app/views/exceptions/not_found.html.erb new file mode 100644 index 0000000000..388c72c31d --- /dev/null +++ b/chef-server-webui/app/views/exceptions/not_found.html.erb @@ -0,0 +1,47 @@ +<div id="container"> + <div id="header-container"> + <img src="/images/merb.jpg" /> + <!-- <h1>Mongrel + Erb</h1> --> + <h2>pocket rocket web framework</h2> + <hr /> + </div> + + <div id="left-container"> + <h3>Exception:</h3> + <p><%= params[:exception] %></p> + </div> + + <div id="main-container"> + <h3>Welcome to Merb!</h3> + <p>Merb is a light-weight MVC framework written in Ruby. We hope you enjoy it.</p> + + <h3>Where can I find help?</h3> + <p>If you have any questions or if you can't figure something out, please take a + look at our <a href="http://merbivore.com/"> project page</a>, + feel free to come chat at irc.freenode.net, channel #merb, + or post to <a href="http://groups.google.com/group/merb">merb mailing list</a> + on Google Groups.</p> + + <h3>What if I've found a bug?</h3> + <p>If you want to file a bug or make your own contribution to Merb, + feel free to register and create a ticket at our + <a href="http://merb.lighthouseapp.com/">project development page</a> + on Lighthouse.</p> + + <h3>How do I edit this page?</h3> + <p>You're seeing this page because you need to edit the following files: + <ul> + <li>config/router.rb <strong><em>(recommended)</em></strong></li> + <li>app/views/exceptions/not_found.html.erb <strong><em>(recommended)</em></strong></li> + <li>app/views/layout/application.html.erb <strong><em>(change this layout)</em></strong></li> + </ul> + </p> + </div> + + <div id="footer-container"> + <hr /> + <div class="left"></div> + <div class="right">© 2007 the merb dev team</div> + <p> </p> + </div> +</div> diff --git a/chef-server-webui/app/views/exceptions/standard_error.html.erb b/chef-server-webui/app/views/exceptions/standard_error.html.erb new file mode 100644 index 0000000000..edb45ddf90 --- /dev/null +++ b/chef-server-webui/app/views/exceptions/standard_error.html.erb @@ -0,0 +1,217 @@ +<html> +<head> + <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> + <title><%= @exception_name %></title> + <style type="text/css" media="screen"> + body { + font-family:arial; + font-size:11px; + } + h1 { + font-size:48px; + letter-spacing:-4px; + margin:0; + line-height:36px; + color:#333; + } + h1 sup { + font-size: 0.5em; + } + h1 sup.error_500, h1 sup.error_400 { + color:#990E05; + } + h1 sup.error_100, h1 sup.error_200 { + color:#00BF10; + } + h1 sup.error_300 { + /* pretty sure you cant 'see' status 300 + errors but if you could I think they + would be blue */ + color:#1B2099; + } + h2 { + font-size:36px; + letter-spacing:-3px; + margin:0; + line-height:28px; + color:#444; + } + a, a:visited { + color:#00BF10; + } + .internalError { + width:800px; + margin:50px auto; + } + .header { + border-bottom:10px solid #333; + margin-bottom:1px; + background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7"); + padding:20px; + } + table.trace { + width:100%; + font-family:courier, monospace; + letter-spacing:-1px; + border-collapse: collapse; + border-spacing:0; + } + table.trace tr td{ + padding:0; + height:26px; + font-size:13px; + vertical-align:middle; + } + table.trace tr.file{ + border-top:2px solid #fff; + background-color:#F3F3F3; + } + table.trace tr.source { + background-color:#F8F8F8; + display:none; + } + table.trace .open tr.source { + display:table-row; + } + table.trace tr.file td.expand { + width:23px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAXCAIAAABvSEP3AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdVJREFUeNqMVL+TwUAYxaRIOlEhlZHGDAUzzOQ61+AqXMV1lJSU7q/QRqm8KFUcJTNn5qJkaPyoKKVz7y4mF8na5Kt29tt9+/Z97/u81+vVQ4r9frdarS6Xi7ETDIZisRjxMGPfmk4niNPpZE+xLAugbPaZ53nzvtfMBe/3+/3dbuehBrAKhZdUKkVAWa9Xsiybv0CPZDJZLr/qa5/BwgwRjYqOKIvFYjQa/aNommZh0Ww2K5UqzwfoQOPxaLPZ3FAmk0+7lplMpt1u53J5OpBOR0eZEE9wHJfP5zud93g88QhluwWbjW+5VOmKBgKBer3eaDTDYeGBQF8+x7rqIYoiPgixWJazpA6HA+MSxRArkUgMh0M409g8Ho8+9wYxxCqVSq1W26EDHGM2m4HOHQrEc38f/Yn7cLmlIRhBENzcx8cVRZnPZ/YUep2BWkjTIfA+PKVpZAXR5QxsjiqCKvGEqqp443w+0dvy17swqD0HB3S73V5PpkNg1qBqt8kwGCjmPkinM0QJbIoEa7U6UG6ToVgs4V9G2g0ESoP5Aoi7KYX5oCgf8IKbkvn9/mr1LRQKESamzgJy0g0tSZIuB3nuGqRU9Vv9C4sKkUhEkp4soxvxI8AAhWrrtXa3X8EAAAAASUVORK5CYII=); + background-position:top left; + background-repeat:no-repeat; + } + table.trace .open tr.file td.expand { + width:19px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAXZJREFUeNrslK1ywkAUhcMOBomEOiSdqLxEBJX0NaijOsjyHGGmCGyQQYaiiiw4gktkcOmZbpsuuzQ/M5XnqJ2d3S/n3nM3rTzPLUP7/Tt0+pLcGQwG3W53OLyHzPMtjYL7q9UqSRLrD4E1Gj1orCvKYuFHUWTVkOM44/HjDcp8/lL4r6NerzeZPMm1KFw0QkDn83m5fP2lHA4fNQvRtNvtjsfDd0WzmSfb2e/fdTqdOvdh/HLJZLOn0+d2HJ+KRGzbdl23EpFlmed5cp2maRzHQq1lvQ5KMi6EUZBGfup6E1pTfd+vrGW7jbQ2C9hTt9BpqNyIWaAwAy6xg2eBz5iRC/NomiZhGN5sqmnkauo0BUGgVQoBjQ80oCACgNQdZHfTYBkF2mxCtWWAqunWpahxIDUt3QYUxIFQpJHyIWpXjinabKbbwItMHT+NyjchrP8QKaSQQgoppJBCCimkkEIKKaSQQgoppJBCCimkkEIKKaSo+hRgAEFD17X08O2NAAAAAElFTkSuQmCC); + background-position:top left; + background-repeat:no-repeat; + } + table.trace tr.source td.collapse { + width:19px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAB1CAIAAAAqdO2mAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAVxJREFUeNrs0zFygkAUBmBlUkgJHdABlQwVkVJKKUxBYWbkALTxMJwhltyDFkss03IF8pudIcwaDaDl/6pd2P327b7d+eHwMXs4lNkzggoVKlSoUKFChQoVKlSoUKFChQoVKlSoUKFChQqVEYqm6ft9+qiSJEkYho7jTlcw2fd9NOI4nq4gEdFwXXe1Cqco63VkWVbXRTqLhTpOwQRpF7quR1E0TgGhqvLKUFCyoQqG/rks3O6kZKW/eRFpevOCoGTXVTcMQ5EyxyDEkML1c5RzuZOICIyXqn7JBVez6282MWrx731HOv2qB8Hri2lamNk0DfpVVdV1Peodappmmua8bdvzuc7zfNprzrLMth1FnGh/X8MjCAIQv/cFz/+65PcDh7rbvYv2ZUfdj+PxsyzLgVl0hKwgTqeqKApx2LeOc7t98zyv/1FWOgvx9RPii23bmL9cetJ8Ed8CDAC6aFW8bCzFhwAAAABJRU5ErkJggg==); + background-position:bottom left; + background-repeat:no-repeat; + background-color:#6F706F; + } + table.trace tr td.path { + padding-left:10px; + } + table.trace tr td.code { + padding-left:35px; + white-space: pre; + line-height:9px; + padding-bottom:10px; + } + table.trace tr td.code em { + font-weight:bold; + color:#00BF10; + } + table.trace tr td.code a { + width: 20px; + float: left; + } + table.trace tr td.code .more { + color:#666; + } + table.trace tr td.line { + width:30px; + text-align:right; + padding-right:4px; + } + .footer { + margin-top:5px; + font-size:11px; + color:#444; + text-align:right; + } + </style> +</head> +<body> + <div class="internalError"> + + <div class="header"> + <h1><%= @exception_name %> <sup class="error_<%= @exception.class::STATUS %>"><%= @exception.class::STATUS %></sup></h1> + <% if show_details = ::Merb::Config[:exception_details] -%> + <h2><%= @exception.message %></h2> + <% else -%> + <h2>Sorry about that...</h2> + <% end -%> + <h3>Parameters</h3> + <ul> + <% params[:original_params].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_params].empty? %> + </ul> + + <h3>Session</h3> + <ul> + <% params[:original_session].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_session].empty? %> + </ul> + + <h3>Cookies</h3> + <ul> + <% params[:original_cookies].each do |param, value| %> + <li><strong><%= param %>:</strong> <%= value.inspect %></li> + <% end %> + <%= "<li>None</li>" if params[:original_cookies].empty? %> + </ul> + </div> + + <% if show_details %> + <table class="trace"> + <% @exception.backtrace.each_with_index do |line, index| %> + <tbody class="close"> + <tr class="file"> + <td class="expand"> + </td> + <td class="path"> + <%= (line.match(/^([^:]+)/)[1] rescue 'unknown').sub(/\/((opt|usr)\/local\/lib\/(ruby\/)?(gems\/)?(1.8\/)?(gems\/)?|.+\/app\/)/, '') %> + <% unless line.match(/\.erb:/) %> + in "<strong><%= line.match(/:in `(.+)'$/)[1] rescue '?' %></strong>" + <% else %> + (<strong>ERB Template</strong>) + <% end %> + </td> + <td class="line"> + <a href="txmt://open?url=file://<%=file = (line.match(/^([^:]+)/)[1] rescue 'unknown')%>&line=<%= lineno = line.match(/:([0-9]+):/)[1] rescue '?' %>"><%=lineno%></a> + </td> + </tr> + <tr class="source"> + <td class="collapse"> + </td> + <td class="code" colspan="2"><% (__caller_lines__(file, lineno, 5) rescue []).each do |llineno, lcode, lcurrent| %> +<a href="txmt://open?url=file://<%=file%>&line=<%=llineno%>"><%= llineno %></a><%='<em>' if llineno==lineno.to_i %><%= lcode.size > 90 ? CGI.escapeHTML(lcode[0..90])+'<span class="more">......</span>' : CGI.escapeHTML(lcode) %><%='</em>' if llineno==lineno.to_i %> +<% end %> + +</td> + </tr> + </tbody> + <% end %> + </table> + <script type="text/javascript" charset="utf-8"> + // swop the open & closed classes + els = document.getElementsByTagName('td'); + for(i=0; i<els.length; i++){ + if(els[i].className=='expand' || els[i].className=='collapse'){ + els[i].onclick = function(e){ + tbody = this.parentNode.parentNode; + if(tbody.className=='open'){ + tbody.className='closed'; + }else{ + tbody.className='open'; + } + } + } + } + </script> + <% end %> + <div class="footer"> + lots of love, from <a href="#">merb</a> + </div> + </div> +</body> +</html> + diff --git a/chef-server-slice/app/views/layout/chef_server_slice.html.haml b/chef-server-webui/app/views/layout/chef_server_webui.html.haml index e480ad71e1..e480ad71e1 100644 --- a/chef-server-slice/app/views/layout/chef_server_slice.html.haml +++ b/chef-server-webui/app/views/layout/chef_server_webui.html.haml diff --git a/chef-server-slice/app/views/layout/login.html.haml b/chef-server-webui/app/views/layout/login.html.haml index 8ac6c4d6f4..8ac6c4d6f4 100644 --- a/chef-server-slice/app/views/layout/login.html.haml +++ b/chef-server-webui/app/views/layout/login.html.haml diff --git a/chef-server-slice/app/views/main/index.html.erb b/chef-server-webui/app/views/main/index.html.erb index 755c73de7f..755c73de7f 100644 --- a/chef-server-slice/app/views/main/index.html.erb +++ b/chef-server-webui/app/views/main/index.html.erb diff --git a/chef-server-slice/app/views/nodes/_action.html.haml b/chef-server-webui/app/views/nodes/_action.html.haml index 8adb39338d..8adb39338d 100644 --- a/chef-server-slice/app/views/nodes/_action.html.haml +++ b/chef-server-webui/app/views/nodes/_action.html.haml diff --git a/chef-server-slice/app/views/nodes/_form.html.haml b/chef-server-webui/app/views/nodes/_form.html.haml index d150eb59a7..d150eb59a7 100644 --- a/chef-server-slice/app/views/nodes/_form.html.haml +++ b/chef-server-webui/app/views/nodes/_form.html.haml diff --git a/chef-server-slice/app/views/nodes/_navigation.html.haml b/chef-server-webui/app/views/nodes/_navigation.html.haml index 1cb47f918a..1cb47f918a 100644 --- a/chef-server-slice/app/views/nodes/_navigation.html.haml +++ b/chef-server-webui/app/views/nodes/_navigation.html.haml diff --git a/chef-server-slice/app/views/nodes/_resource.html.haml b/chef-server-webui/app/views/nodes/_resource.html.haml index 7b9776b816..7b9776b816 100644 --- a/chef-server-slice/app/views/nodes/_resource.html.haml +++ b/chef-server-webui/app/views/nodes/_resource.html.haml diff --git a/chef-server-slice/app/views/nodes/edit.html.haml b/chef-server-webui/app/views/nodes/edit.html.haml index dff693ac7d..dff693ac7d 100644 --- a/chef-server-slice/app/views/nodes/edit.html.haml +++ b/chef-server-webui/app/views/nodes/edit.html.haml diff --git a/chef-server-slice/app/views/nodes/index.html.haml b/chef-server-webui/app/views/nodes/index.html.haml index 744d0b7375..744d0b7375 100644 --- a/chef-server-slice/app/views/nodes/index.html.haml +++ b/chef-server-webui/app/views/nodes/index.html.haml diff --git a/chef-server-slice/app/views/nodes/new.html.haml b/chef-server-webui/app/views/nodes/new.html.haml index d2d032599a..d2d032599a 100644 --- a/chef-server-slice/app/views/nodes/new.html.haml +++ b/chef-server-webui/app/views/nodes/new.html.haml diff --git a/chef-server-slice/app/views/nodes/show.html.haml b/chef-server-webui/app/views/nodes/show.html.haml index 121cbcedf4..121cbcedf4 100644 --- a/chef-server-slice/app/views/nodes/show.html.haml +++ b/chef-server-webui/app/views/nodes/show.html.haml diff --git a/chef-server-slice/app/views/openid_consumer/index.html.haml b/chef-server-webui/app/views/openid_consumer/index.html.haml index 1d1572d5c1..1d1572d5c1 100644 --- a/chef-server-slice/app/views/openid_consumer/index.html.haml +++ b/chef-server-webui/app/views/openid_consumer/index.html.haml diff --git a/chef-server-slice/app/views/openid_consumer/start.html.haml b/chef-server-webui/app/views/openid_consumer/start.html.haml index 75ed9a9257..75ed9a9257 100644 --- a/chef-server-slice/app/views/openid_consumer/start.html.haml +++ b/chef-server-webui/app/views/openid_consumer/start.html.haml diff --git a/chef-server-slice/app/views/openid_login/index.html.haml b/chef-server-webui/app/views/openid_login/index.html.haml index e5cf180b9b..e5cf180b9b 100644 --- a/chef-server-slice/app/views/openid_login/index.html.haml +++ b/chef-server-webui/app/views/openid_login/index.html.haml diff --git a/chef-server-slice/app/views/openid_register/index.html.haml b/chef-server-webui/app/views/openid_register/index.html.haml index ba316d6062..ba316d6062 100644 --- a/chef-server-slice/app/views/openid_register/index.html.haml +++ b/chef-server-webui/app/views/openid_register/index.html.haml diff --git a/chef-server-slice/app/views/openid_register/show.html.haml b/chef-server-webui/app/views/openid_register/show.html.haml index 63d84d8364..63d84d8364 100644 --- a/chef-server-slice/app/views/openid_register/show.html.haml +++ b/chef-server-webui/app/views/openid_register/show.html.haml diff --git a/chef-server-slice/app/views/openid_server/decide.html.haml b/chef-server-webui/app/views/openid_server/decide.html.haml index d08b50cd4b..d08b50cd4b 100644 --- a/chef-server-slice/app/views/openid_server/decide.html.haml +++ b/chef-server-webui/app/views/openid_server/decide.html.haml diff --git a/chef-server-slice/app/views/roles/_form.html.haml b/chef-server-webui/app/views/roles/_form.html.haml index ac1591a86b..ac1591a86b 100644 --- a/chef-server-slice/app/views/roles/_form.html.haml +++ b/chef-server-webui/app/views/roles/_form.html.haml diff --git a/chef-server-slice/app/views/roles/_navigation.html.haml b/chef-server-webui/app/views/roles/_navigation.html.haml index 1eb2a51cdf..1eb2a51cdf 100644 --- a/chef-server-slice/app/views/roles/_navigation.html.haml +++ b/chef-server-webui/app/views/roles/_navigation.html.haml diff --git a/chef-server-slice/app/views/roles/edit.html.haml b/chef-server-webui/app/views/roles/edit.html.haml index 7714ed764c..7714ed764c 100644 --- a/chef-server-slice/app/views/roles/edit.html.haml +++ b/chef-server-webui/app/views/roles/edit.html.haml diff --git a/chef-server-slice/app/views/roles/index.html.haml b/chef-server-webui/app/views/roles/index.html.haml index 6ab526b5d1..6ab526b5d1 100644 --- a/chef-server-slice/app/views/roles/index.html.haml +++ b/chef-server-webui/app/views/roles/index.html.haml diff --git a/chef-server-slice/app/views/roles/new.html.haml b/chef-server-webui/app/views/roles/new.html.haml index 3af7544053..3af7544053 100644 --- a/chef-server-slice/app/views/roles/new.html.haml +++ b/chef-server-webui/app/views/roles/new.html.haml diff --git a/chef-server-slice/app/views/roles/show.html.haml b/chef-server-webui/app/views/roles/show.html.haml index 4e3b8e22b0..4e3b8e22b0 100644 --- a/chef-server-slice/app/views/roles/show.html.haml +++ b/chef-server-webui/app/views/roles/show.html.haml diff --git a/chef-server-slice/app/views/search/_search_form.html.haml b/chef-server-webui/app/views/search/_search_form.html.haml index 5396e65123..5396e65123 100644 --- a/chef-server-slice/app/views/search/_search_form.html.haml +++ b/chef-server-webui/app/views/search/_search_form.html.haml diff --git a/chef-server-slice/app/views/search/index.html.haml b/chef-server-webui/app/views/search/index.html.haml index 8f3b199592..8f3b199592 100644 --- a/chef-server-slice/app/views/search/index.html.haml +++ b/chef-server-webui/app/views/search/index.html.haml diff --git a/chef-server-slice/app/views/search/show.html.haml b/chef-server-webui/app/views/search/show.html.haml index f20f9fa2ea..f20f9fa2ea 100644 --- a/chef-server-slice/app/views/search/show.html.haml +++ b/chef-server-webui/app/views/search/show.html.haml diff --git a/chef-server-slice/app/views/search_entries/index.html.haml b/chef-server-webui/app/views/search_entries/index.html.haml index c6fab2b649..c6fab2b649 100644 --- a/chef-server-slice/app/views/search_entries/index.html.haml +++ b/chef-server-webui/app/views/search_entries/index.html.haml diff --git a/chef-server-slice/app/views/search_entries/show.html.haml b/chef-server-webui/app/views/search_entries/show.html.haml index f8a8c3813a..f8a8c3813a 100644 --- a/chef-server-slice/app/views/search_entries/show.html.haml +++ b/chef-server-webui/app/views/search_entries/show.html.haml diff --git a/chef-server-slice/app/views/status/index.html.haml b/chef-server-webui/app/views/status/index.html.haml index c271242c67..c271242c67 100644 --- a/chef-server-slice/app/views/status/index.html.haml +++ b/chef-server-webui/app/views/status/index.html.haml diff --git a/chef-server-slice/config/init.rb b/chef-server-webui/config/init.rb index f418f93a4e..6ee1a8f50e 100644 --- a/chef-server-slice/config/init.rb +++ b/chef-server-webui/config/init.rb @@ -43,6 +43,8 @@ Merb::Config.use do |c| c[:exception_details] = true c[:reload_classes] = true c[:log_level] = Chef::Config[:log_level] - c[:log_file] = Chef::Config[:log_location] + if Chef::Config[:log_location].kind_of?(String) + c[:log_file] = Chef::Config[:log_location] + end end diff --git a/chef-server-webui/config/router.rb b/chef-server-webui/config/router.rb new file mode 100644 index 0000000000..3142a1d1a9 --- /dev/null +++ b/chef-server-webui/config/router.rb @@ -0,0 +1,6 @@ +# This file is here so slice can be testing as a stand alone application. + +#Merb::Router.prepare do +# resources :roles +# ... +#end diff --git a/chef-server-slice/lib/chef-server-slice.rb b/chef-server-webui/lib/chef-server-webui.rb index 46e5b97fdb..43871b2e57 100644 --- a/chef-server-slice/lib/chef-server-slice.rb +++ b/chef-server-webui/lib/chef-server-webui.rb @@ -5,9 +5,9 @@ if defined?(Merb::Plugins) dependency 'chef', :immediate=>true unless defined?(Chef) require 'chef/role' - require 'coderay' + require 'syntax/convertors/html' - Merb::Plugins.add_rakefiles "chef-server-slice/merbtasks", "chef-server-slice/slicetasks", "chef-server-slice/spectasks" + Merb::Plugins.add_rakefiles "chef-server-webui/merbtasks", "chef-server-webui/slicetasks", "chef-server-webui/spectasks" # Register the Slice for the current host application Merb::Slices::register(__FILE__) @@ -21,19 +21,19 @@ if defined?(Merb::Plugins) # Configuration options: # :layout - the layout to use; defaults to :chefserverslice # :mirror - which path component types to use on copy operations; defaults to all - Merb::Slices::config[:chef_server_slice][:layout] ||= :chef_server_slice + Merb::Slices::config[:chef_server_webui][:layout] ||= :chef_server_webui # All Slice code is expected to be namespaced inside a module - module ChefServerSlice + module ChefServerWebui # Slice metadata - self.description = "ChefServerSlice.. serving up some piping hot infrastructure!" + self.description = "ChefServerWebui.. serving up some piping hot infrastructure!" self.version = Chef::VERSION self.author = "Opscode" # Stub classes loaded hook - runs before LoadClasses BootLoader # right after a slice's classes have been loaded internally. def self.loaded - Chef::Queue.connect + # Chef::Queue.connect # create the couch databases for openid association and nonce storage, if configured if Chef::Config[:openid_store_couchdb] || Chef::Config[:openid_cstore_couchdb] @@ -48,14 +48,13 @@ if defined?(Merb::Plugins) end # create the couch design docs for nodes and openid registrations - Chef::Node.create_design_document - Chef::Role.create_design_document - Chef::OpenIDRegistration.create_design_document - - Chef::Log.logger = Merb.logger - Chef::Log.info("Compiling routes... (totally normal to see 'Cannot find resource model')") - Chef::Log.info("Loading roles") - Chef::Role.sync_from_disk_to_couchdb + # Chef::Node.create_design_document + # Chef::Role.create_design_document + # Chef::OpenIDRegistration.create_design_document + + # Chef::Log.info("Compiling routes... (totally normal to see 'Cannot find resource model')") + # Chef::Log.info("Loading roles") + # Chef::Role.sync_from_disk_to_couchdb end # Initialization hook - runs before AfterAppLoads BootLoader @@ -134,17 +133,17 @@ if defined?(Merb::Plugins) end - # Setup the slice layout for ChefServerSlice + # Setup the slice layout for ChefServerWebui # - # Use ChefServerSlice.push_path and ChefServerSlice.push_app_path + # Use ChefServerWebui.push_path and ChefServerWebui.push_app_path # to set paths to chefserver-level and app-level paths. Example: # - # ChefServerSlice.push_path(:application, ChefServerSlice.root) - # ChefServerSlice.push_app_path(:application, Merb.root / 'slices' / 'chefserverslice') + # ChefServerWebui.push_path(:application, ChefServerWebui.root) + # ChefServerWebui.push_app_path(:application, Merb.root / 'slices' / 'chefserverslice') # ... # - # Any component path that hasn't been set will default to ChefServerSlice.root + # Any component path that hasn't been set will default to ChefServerWebui.root # # Or just call setup_default_structure! to setup a basic Merb MVC structure. - ChefServerSlice.setup_default_structure! + ChefServerWebui.setup_default_structure! end diff --git a/chef-server-slice/lib/chef-server-slice/merbtasks.rb b/chef-server-webui/lib/chef-server-webui/merbtasks.rb index 554799bf65..9da80eca15 100644 --- a/chef-server-slice/lib/chef-server-slice/merbtasks.rb +++ b/chef-server-webui/lib/chef-server-webui/merbtasks.rb @@ -11,9 +11,9 @@ namespace :slices do desc "Setup directories" task :setup_directories do puts "Creating directories for host application" - ChefServerSlice.mirrored_components.each do |type| - if File.directory?(ChefServerSlice.dir_for(type)) - if !File.directory?(dst_path = ChefServerSlice.app_dir_for(type)) + ChefServerWebui.mirrored_components.each do |type| + if File.directory?(ChefServerWebui.dir_for(type)) + if !File.directory?(dst_path = ChefServerWebui.app_dir_for(type)) relative_path = dst_path.relative_path_from(Merb.root) puts "- creating directory :#{type} #{File.basename(Merb.root) / relative_path}" mkdir_p(dst_path) @@ -24,8 +24,8 @@ namespace :slices do # desc "Copy stub files to host application" # task :stubs do - # puts "Copying stubs for ChefServerSlice - resolves any collisions" - # copied, preserved = ChefServerSlice.mirror_stubs! + # puts "Copying stubs for ChefServerWebui - resolves any collisions" + # copied, preserved = ChefServerWebui.mirror_stubs! # puts "- no files to copy" if copied.empty? && preserved.empty? # copied.each { |f| puts "- copied #{f}" } # preserved.each { |f| puts "! preserved override as #{f}" } @@ -36,8 +36,8 @@ namespace :slices do desc "Copy public assets to host application" task :copy_assets do - puts "Copying assets for ChefServerSlice - resolves any collisions" - copied, preserved = ChefServerSlice.mirror_public! + puts "Copying assets for ChefServerWebui - resolves any collisions" + copied, preserved = ChefServerWebui.mirror_public! puts "- no files to copy" if copied.empty? && preserved.empty? copied.each { |f| puts "- copied #{f}" } preserved.each { |f| puts "! preserved override as #{f}" } @@ -47,21 +47,21 @@ namespace :slices do task :migrate do # see slicetasks.rb end - desc "Freeze ChefServerSlice into your app (only chefserverslice/app)" + desc "Freeze ChefServerWebui into your app (only chefserverslice/app)" task :freeze => [ "freeze:app" ] namespace :freeze do - # desc "Freezes ChefServerSlice by installing the gem into application/gems" + # desc "Freezes ChefServerWebui by installing the gem into application/gems" # task :gem do # ENV["GEM"] ||= "chefserverslice" # Rake::Task['slices:install_as_gem'].invoke # end - desc "Freezes ChefServerSlice by copying all files from chefserverslice/app to your application" + desc "Freezes ChefServerWebui by copying all files from chefserverslice/app to your application" task :app do puts "Copying all chefserverslice/app files to your application - resolves any collisions" - copied, preserved = ChefServerSlice.mirror_app! + copied, preserved = ChefServerWebui.mirror_app! puts "- no files to copy" if copied.empty? && preserved.empty? copied.each { |f| puts "- copied #{f}" } preserved.each { |f| puts "! preserved override as #{f}" } @@ -70,7 +70,7 @@ namespace :slices do desc "Freeze all views into your application for easy modification" task :views do puts "Copying all view templates to your application - resolves any collisions" - copied, preserved = ChefServerSlice.mirror_files_for :view + copied, preserved = ChefServerWebui.mirror_files_for :view puts "- no files to copy" if copied.empty? && preserved.empty? copied.each { |f| puts "- copied #{f}" } preserved.each { |f| puts "! preserved override as #{f}" } @@ -79,19 +79,19 @@ namespace :slices do desc "Freeze all models into your application for easy modification" task :models do puts "Copying all models to your application - resolves any collisions" - copied, preserved = ChefServerSlice.mirror_files_for :model + copied, preserved = ChefServerWebui.mirror_files_for :model puts "- no files to copy" if copied.empty? && preserved.empty? copied.each { |f| puts "- copied #{f}" } preserved.each { |f| puts "! preserved override as #{f}" } end - desc "Freezes ChefServerSlice as a gem and copies over chefserver/app" + desc "Freezes ChefServerWebui as a gem and copies over chefserver/app" task :app_with_gem => [:gem, :app] - desc "Freezes ChefServerSlice by unpacking all files into your application" + desc "Freezes ChefServerWebui by unpacking all files into your application" task :unpack do - puts "Unpacking ChefServerSlice files to your application - resolves any collisions" - copied, preserved = ChefServerSlice.unpack_slice! + puts "Unpacking ChefServerWebui files to your application - resolves any collisions" + copied, preserved = ChefServerWebui.unpack_slice! puts "- no files to copy" if copied.empty? && preserved.empty? copied.each { |f| puts "- copied #{f}" } preserved.each { |f| puts "! preserved override as #{f}" } diff --git a/chef-server-webui/lib/chef-server-webui/slicetasks.rb b/chef-server-webui/lib/chef-server-webui/slicetasks.rb new file mode 100644 index 0000000000..187323a299 --- /dev/null +++ b/chef-server-webui/lib/chef-server-webui/slicetasks.rb @@ -0,0 +1,20 @@ +namespace :slices do + namespace :chefserverslice do + + # add your own chefserver tasks here + + # # Uncomment the following lines and edit the pre defined tasks + # + # # implement this to test for structural/code dependencies + # # like certain directories or availability of other files + # desc "Test for any dependencies" + # task :preflight do + # end + # + # # implement this to perform any database related setup steps + # desc "Migrate the database" + # task :migrate do + # end + + end +end diff --git a/chef-server-slice/lib/chef-server-slice/spectasks.rb b/chef-server-webui/lib/chef-server-webui/spectasks.rb index 328b517cb0..fb61dcd6a3 100644 --- a/chef-server-slice/lib/chef-server-slice/spectasks.rb +++ b/chef-server-webui/lib/chef-server-webui/spectasks.rb @@ -9,7 +9,7 @@ namespace :slices do slice_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')) task :explain do - puts "\nNote: By running ChefServerSlice specs inside the application context any\n" + + puts "\nNote: By running ChefServerWebui specs inside the application context any\n" + "overrides could break existing specs. This isn't always a problem,\n" + "especially in the case of views. Use these spec tasks to check how\n" + "well your application conforms to the original slice implementation." diff --git a/chef-server-slice/public/facebox/README.txt b/chef-server-webui/public/facebox/README.txt index d4fc2d5e81..d4fc2d5e81 100644 --- a/chef-server-slice/public/facebox/README.txt +++ b/chef-server-webui/public/facebox/README.txt diff --git a/chef-server-slice/public/facebox/b.png b/chef-server-webui/public/facebox/b.png Binary files differindex f184e6269b..f184e6269b 100644 --- a/chef-server-slice/public/facebox/b.png +++ b/chef-server-webui/public/facebox/b.png diff --git a/chef-server-slice/public/facebox/bl.png b/chef-server-webui/public/facebox/bl.png Binary files differindex f6271859d5..f6271859d5 100644 --- a/chef-server-slice/public/facebox/bl.png +++ b/chef-server-webui/public/facebox/bl.png diff --git a/chef-server-slice/public/facebox/br.png b/chef-server-webui/public/facebox/br.png Binary files differindex 31f204fc45..31f204fc45 100644 --- a/chef-server-slice/public/facebox/br.png +++ b/chef-server-webui/public/facebox/br.png diff --git a/chef-server-slice/public/facebox/closelabel.gif b/chef-server-webui/public/facebox/closelabel.gif Binary files differindex 87b4f8bd69..87b4f8bd69 100755 --- a/chef-server-slice/public/facebox/closelabel.gif +++ b/chef-server-webui/public/facebox/closelabel.gif diff --git a/chef-server-slice/public/facebox/facebox.css b/chef-server-webui/public/facebox/facebox.css index 97ebe3cab3..97ebe3cab3 100644 --- a/chef-server-slice/public/facebox/facebox.css +++ b/chef-server-webui/public/facebox/facebox.css diff --git a/chef-server-slice/public/facebox/facebox.js b/chef-server-webui/public/facebox/facebox.js index cbbb450b10..cbbb450b10 100644 --- a/chef-server-slice/public/facebox/facebox.js +++ b/chef-server-webui/public/facebox/facebox.js diff --git a/chef-server-slice/public/facebox/loading.gif b/chef-server-webui/public/facebox/loading.gif Binary files differindex f864d5fd38..f864d5fd38 100755 --- a/chef-server-slice/public/facebox/loading.gif +++ b/chef-server-webui/public/facebox/loading.gif diff --git a/chef-server-slice/public/facebox/tl.png b/chef-server-webui/public/facebox/tl.png Binary files differindex d99c8f6c6e..d99c8f6c6e 100644 --- a/chef-server-slice/public/facebox/tl.png +++ b/chef-server-webui/public/facebox/tl.png diff --git a/chef-server-slice/public/facebox/tr.png b/chef-server-webui/public/facebox/tr.png Binary files differindex e99b6ec831..e99b6ec831 100644 --- a/chef-server-slice/public/facebox/tr.png +++ b/chef-server-webui/public/facebox/tr.png diff --git a/chef-server-webui/public/images/avatar.png b/chef-server-webui/public/images/avatar.png Binary files differnew file mode 100644 index 0000000000..66488481ae --- /dev/null +++ b/chef-server-webui/public/images/avatar.png diff --git a/chef-server-slice/public/images/black_big.png b/chef-server-webui/public/images/black_big.png Binary files differindex 9cfe72609c..9cfe72609c 100644 --- a/chef-server-slice/public/images/black_big.png +++ b/chef-server-webui/public/images/black_big.png diff --git a/chef-server-webui/public/images/indicator.gif b/chef-server-webui/public/images/indicator.gif Binary files differnew file mode 100644 index 0000000000..085ccaecaf --- /dev/null +++ b/chef-server-webui/public/images/indicator.gif diff --git a/chef-server-webui/public/images/merb.jpg b/chef-server-webui/public/images/merb.jpg Binary files differnew file mode 100644 index 0000000000..a19dcf4048 --- /dev/null +++ b/chef-server-webui/public/images/merb.jpg diff --git a/chef-server-slice/public/images/toggle-collapse-dark.png b/chef-server-webui/public/images/toggle-collapse-dark.png Binary files differindex 76577a57a2..76577a57a2 100644 --- a/chef-server-slice/public/images/toggle-collapse-dark.png +++ b/chef-server-webui/public/images/toggle-collapse-dark.png diff --git a/chef-server-slice/public/images/toggle-collapse-light.png b/chef-server-webui/public/images/toggle-collapse-light.png Binary files differindex ed1612fd9b..ed1612fd9b 100644 --- a/chef-server-slice/public/images/toggle-collapse-light.png +++ b/chef-server-webui/public/images/toggle-collapse-light.png diff --git a/chef-server-slice/public/images/toggle-collapse.gif b/chef-server-webui/public/images/toggle-collapse.gif Binary files differindex f0979304ac..f0979304ac 100644 --- a/chef-server-slice/public/images/toggle-collapse.gif +++ b/chef-server-webui/public/images/toggle-collapse.gif diff --git a/chef-server-slice/public/images/toggle-expand-dark.png b/chef-server-webui/public/images/toggle-expand-dark.png Binary files differindex cfb42a4512..cfb42a4512 100644 --- a/chef-server-slice/public/images/toggle-expand-dark.png +++ b/chef-server-webui/public/images/toggle-expand-dark.png diff --git a/chef-server-slice/public/images/toggle-expand-light.png b/chef-server-webui/public/images/toggle-expand-light.png Binary files differindex 27b52344dc..27b52344dc 100644 --- a/chef-server-slice/public/images/toggle-expand-light.png +++ b/chef-server-webui/public/images/toggle-expand-light.png diff --git a/chef-server-slice/public/images/toggle-expand.gif b/chef-server-webui/public/images/toggle-expand.gif Binary files differindex 03fa8360dd..03fa8360dd 100644 --- a/chef-server-slice/public/images/toggle-expand.gif +++ b/chef-server-webui/public/images/toggle-expand.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/Thumbs.db b/chef-server-webui/public/images/treeBuilderImages/Thumbs.db Binary files differindex 934ed17cb5..934ed17cb5 100755 --- a/chef-server-slice/public/images/treeBuilderImages/Thumbs.db +++ b/chef-server-webui/public/images/treeBuilderImages/Thumbs.db diff --git a/chef-server-slice/public/images/treeBuilderImages/doc.gif b/chef-server-webui/public/images/treeBuilderImages/doc.gif Binary files differindex a0f4dd5dfb..a0f4dd5dfb 100755 --- a/chef-server-slice/public/images/treeBuilderImages/doc.gif +++ b/chef-server-webui/public/images/treeBuilderImages/doc.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/docNode.gif b/chef-server-webui/public/images/treeBuilderImages/docNode.gif Binary files differindex 40167db1a5..40167db1a5 100755 --- a/chef-server-slice/public/images/treeBuilderImages/docNode.gif +++ b/chef-server-webui/public/images/treeBuilderImages/docNode.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/docNodeLast.gif b/chef-server-webui/public/images/treeBuilderImages/docNodeLast.gif Binary files differindex b7b3e55cd0..b7b3e55cd0 100755 --- a/chef-server-slice/public/images/treeBuilderImages/docNodeLast.gif +++ b/chef-server-webui/public/images/treeBuilderImages/docNodeLast.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/docNodeLastFirst.gif b/chef-server-webui/public/images/treeBuilderImages/docNodeLastFirst.gif Binary files differindex 2cbb765d7c..2cbb765d7c 100755 --- a/chef-server-slice/public/images/treeBuilderImages/docNodeLastFirst.gif +++ b/chef-server-webui/public/images/treeBuilderImages/docNodeLastFirst.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folder.gif b/chef-server-webui/public/images/treeBuilderImages/folder.gif Binary files differindex 3952b3d234..3952b3d234 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folder.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folder.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNode.gif b/chef-server-webui/public/images/treeBuilderImages/folderNode.gif Binary files differindex 5b680136ee..5b680136ee 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNode.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNode.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeFirst.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeFirst.gif Binary files differindex 9039327677..9039327677 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeFirst.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeFirst.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeLast.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeLast.gif Binary files differindex b87f003154..b87f003154 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeLast.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeLast.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeLastFirst.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeLastFirst.gif Binary files differindex 64c4d7ae07..64c4d7ae07 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeLastFirst.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeLastFirst.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpen.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpen.gif Binary files differindex b43ce87bf7..b43ce87bf7 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpen.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpen.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenFirst.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenFirst.gif Binary files differindex d8ee1fc1b1..d8ee1fc1b1 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenFirst.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenFirst.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenLast.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenLast.gif Binary files differindex 11ae43a5ae..11ae43a5ae 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenLast.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenLast.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenLastFirst.gif b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenLastFirst.gif Binary files differindex ba5c0d168d..ba5c0d168d 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderNodeOpenLastFirst.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderNodeOpenLastFirst.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/folderOpen.gif b/chef-server-webui/public/images/treeBuilderImages/folderOpen.gif Binary files differindex 7df8d32261..7df8d32261 100755 --- a/chef-server-slice/public/images/treeBuilderImages/folderOpen.gif +++ b/chef-server-webui/public/images/treeBuilderImages/folderOpen.gif diff --git a/chef-server-slice/public/images/treeBuilderImages/vertLine.gif b/chef-server-webui/public/images/treeBuilderImages/vertLine.gif Binary files differindex 63ee93a0f3..63ee93a0f3 100755 --- a/chef-server-slice/public/images/treeBuilderImages/vertLine.gif +++ b/chef-server-webui/public/images/treeBuilderImages/vertLine.gif diff --git a/chef-server-slice/public/javascripts/JSONeditor.js b/chef-server-webui/public/javascripts/JSONeditor.js index b6dac054ba..b6dac054ba 100755 --- a/chef-server-slice/public/javascripts/JSONeditor.js +++ b/chef-server-webui/public/javascripts/JSONeditor.js diff --git a/chef-server-slice/public/javascripts/chef.js b/chef-server-webui/public/javascripts/chef.js index dfb8b5a1e4..dfb8b5a1e4 100644 --- a/chef-server-slice/public/javascripts/chef.js +++ b/chef-server-webui/public/javascripts/chef.js diff --git a/chef-server-slice/public/javascripts/jquery-1.3.2.min.js b/chef-server-webui/public/javascripts/jquery-1.3.2.min.js index b1ae21d8b2..b1ae21d8b2 100755 --- a/chef-server-slice/public/javascripts/jquery-1.3.2.min.js +++ b/chef-server-webui/public/javascripts/jquery-1.3.2.min.js diff --git a/chef-server-slice/public/javascripts/jquery-ui-1.7.1.custom.min.js b/chef-server-webui/public/javascripts/jquery-ui-1.7.1.custom.min.js index 2ea9f4cd52..2ea9f4cd52 100755 --- a/chef-server-slice/public/javascripts/jquery-ui-1.7.1.custom.min.js +++ b/chef-server-webui/public/javascripts/jquery-ui-1.7.1.custom.min.js diff --git a/chef-server-slice/public/javascripts/jquery.editinline.js b/chef-server-webui/public/javascripts/jquery.editinline.js index e97abe1d15..e97abe1d15 100644 --- a/chef-server-slice/public/javascripts/jquery.editinline.js +++ b/chef-server-webui/public/javascripts/jquery.editinline.js diff --git a/chef-server-slice/public/javascripts/jquery.jeditable.mini.js b/chef-server-webui/public/javascripts/jquery.jeditable.mini.js index fdc46ff456..fdc46ff456 100644 --- a/chef-server-slice/public/javascripts/jquery.jeditable.mini.js +++ b/chef-server-webui/public/javascripts/jquery.jeditable.mini.js diff --git a/chef-server-slice/public/javascripts/jquery.livequery.js b/chef-server-webui/public/javascripts/jquery.livequery.js index dde8ad8e32..dde8ad8e32 100644 --- a/chef-server-slice/public/javascripts/jquery.livequery.js +++ b/chef-server-webui/public/javascripts/jquery.livequery.js diff --git a/chef-server-slice/public/javascripts/jquery.localscroll.js b/chef-server-webui/public/javascripts/jquery.localscroll.js index 596e1ba200..596e1ba200 100644 --- a/chef-server-slice/public/javascripts/jquery.localscroll.js +++ b/chef-server-webui/public/javascripts/jquery.localscroll.js diff --git a/chef-server-slice/public/javascripts/jquery.scrollTo.js b/chef-server-webui/public/javascripts/jquery.scrollTo.js index 688d58e55d..688d58e55d 100644 --- a/chef-server-slice/public/javascripts/jquery.scrollTo.js +++ b/chef-server-webui/public/javascripts/jquery.scrollTo.js diff --git a/chef-server-slice/public/javascripts/jquery.tools.min.js b/chef-server-webui/public/javascripts/jquery.tools.min.js index 4133a43498..4133a43498 100644 --- a/chef-server-slice/public/javascripts/jquery.tools.min.js +++ b/chef-server-webui/public/javascripts/jquery.tools.min.js diff --git a/chef-server-slice/public/javascripts/jquery.treeTable.min.js b/chef-server-webui/public/javascripts/jquery.treeTable.min.js index be94663a80..be94663a80 100644 --- a/chef-server-slice/public/javascripts/jquery.treeTable.min.js +++ b/chef-server-webui/public/javascripts/jquery.treeTable.min.js diff --git a/chef-server-webui/public/stylesheets/base.css b/chef-server-webui/public/stylesheets/base.css new file mode 100644 index 0000000000..0da62086ed --- /dev/null +++ b/chef-server-webui/public/stylesheets/base.css @@ -0,0 +1,336 @@ +* {margin:0;padding:0} +.clear { clear: both; height: 0; } + +h1 { margin: 15px 0; font-size: 22px; font-weight: normal; } +h2 { font-size: 22px; margin: 15px 0; font-weight: normal;} +h3 { font-size: 18px; margin: 10px 0; font-weight: normal;} +h4 { font-size: 16px; margin: 10px 0; font-weight: normal;} +hr {height: 1px; border: 0; } +p { margin: 15px 0;} +a img { border: none; } + +body { + font-size: 12px; + font-family: sans-serif; +} + +#container { + min-width: 960px; +} + +#header, #wrapper { + padding: 0 20px; +} + +#header { + position: relative; + padding-top: 1px; +} + +#header h1 { + margin: 0; + padding: 10px 0; + font-size: 30px; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + text-decoration: none; +} + +#main { + width: 80%; + float: left; +} + +.actions-bar { + padding: 10px 1px; +} + +.actions-bar .actions { + float: left; +} + + +.actions-bar .pagination { + float: right; + padding: 1px 0; +} + +#sidebar { + width: 15%; + float: right; +} + +#sidebar h3 { + padding: 10px 15px; + margin: 0; + font-size: 13px; +} + +#sidebar .block { + margin-bottom: 20px; + padding-bottom: 10px; +} + +#sidebar .block .content { + padding: 0 15px; +} + +#sidebar ul.navigation li a:link, #sidebar ul.navigation li a:visited { + display: block; + padding: 10px 15px; +} + +#sidebar .block .sidebar-block, #sidebar .notice { + padding:10px; +} + +#wrapper { + padding-top: 20px; +} + +#main .block { + margin-bottom: 20px; + padding-top: 1px; +} + +#main .block .content .inner { + padding: 0 15px 15px; +} + +#main .main p.first { + margin-top: 0; +} + +#user-navigation { + position: absolute; + top: 0px; + right: 20px; +} + +#main-navigation { + width: 100%; +} + +#user-navigation ul, #main-navigation ul, .secondary-navigation ul, #sidebar ul.navigation { + margin: 0; + padding: 0; + list-style-type: none; +} + +#user-navigation ul li, #main-navigation ul li, .secondary-navigation ul li { + float: left; +} + +#main-navigation ul li { + margin-right: 5px; +} + +#user-navigation ul li { + padding: 5px 10px; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; +} + +#main-navigation ul li a { + font-size: 15px; + display: block; + padding: 8px 15px; +} + +.secondary-navigation { + font-size: 13px; + border-bottom-width: 10px; + border-bottom-style: solid; +} + +.secondary-navigation ul li a { + display: block; + padding: 10px 15px; +} + +#footer { + padding-bottom: 20px; +} + +/* pagination */ + +.pagination a, .pagination span { + padding: 2px 5px; + margin-right: 5px; + display: block; + float: left; + border-style: solid; + border-width: 1px; +} + +.pagination span.current { + font-weight: bold; +} + +.pagination a { + text-decoration: none; +} + +/* tables */ +.table { + width: 100%; + border-collapse: collapse; + margin-bottom: 15px; +} + +.table th { + padding: 10px; + font-weight: bold; + text-align: left; +} + +.table th.first { + width: 30px; +} + +.table th.last { + width: 200px; +} + +.table .checkbox { + margin-left: 10px; +} + +.table td { + padding: 10px; +} + +.table td.last { + text-align: right; +} + +/* forms */ + +input.checkbox { + margin: 0; + padding: 0; +} + +.form .group { + margin-bottom: 15px; +} + +.form div.left { + width: 20%; + float: left; +} + +.form div.right { + width: 75%; + float: right; +} + +.form .columns .column { + width: 48%; +} + +.form .columns .left { + float: left; +} + +.form .columns .right { + float: right; +} + +.form label.label, .form input.text_field, .form textarea.text_area { + font-size: 1.2em; + padding: 1px 0; + margin: 0; +} + +.form label.right { + text-align: right; +} + +.form input.checkbox, .form input.radio { + margin-right: 5px; +} + +.form label.checkbox, .form label.radio { + line-height: 1.5em; +} + +.form label.label { + display: block; + padding-bottom: 2px; + font-weight: bold; +} + +.form div.fieldWithErrors label.label { + display: inline; +} + +.form .fieldWithErrors .error { + color: red; +} + +.form input.text_field, .form textarea.text_area { + width: 100%; + border-width: 1px; + border-style: solid; +} + +/* lists */ + +ul.list { + margin: 0; + padding: 0; + list-style-type: none; +} + +ul.list li { + clear: left; + padding-bottom: 5px; +} + +ul.list li .left { + float: left; +} + +ul.list li .left .avatar { + width: 50px; + height: 50px; +} + +ul.list li .item { + margin-left: 80px; +} + +ul.list li .item .avatar { + float: left; + margin: 0 5px 5px 0; + width: 30px; + height: 30px; +} + +/* box */ + +#box { + width: 500px; + margin: 50px auto; +} + +#box .block { + margin-bottom: 20px; +} + +#box .block h2 { + padding: 10px 15px; + margin: 0; +} + +#box .block .content { + padding: 10px 20px; +} + + diff --git a/chef-server-webui/public/stylesheets/chef.css b/chef-server-webui/public/stylesheets/chef.css new file mode 100644 index 0000000000..52a06d056c --- /dev/null +++ b/chef-server-webui/public/stylesheets/chef.css @@ -0,0 +1,157 @@ +.ruby .normal {} +.ruby .comment { color: #005; font-style: italic; } +.ruby .keyword { color: #A00; font-weight: bold; } +.ruby .method { color: #077; } +.ruby .class { color: #074; } +.ruby .module { color: #050; } +.ruby .punct { color: #447; font-weight: bold; } +.ruby .symbol { color: #099; } +.ruby .string { color: #944; background: #FFE; } +.ruby .char { color: #F07; } +.ruby .ident { color: #004; } +.ruby .constant { color: #07F; } +.ruby .regex { color: #B66; background: #FEF; } +.ruby .number { color: #F99; } +.ruby .attribute { color: #7BB; } +.ruby .global { color: #7FB; } +.ruby .expr { color: #227; } +.ruby .escape { color: #277; } + +.files { + padding-left: 20px; +} + +.code { + overflow: auto; + font-size: 0.8em; +} + +.search td { + /*overflow: auto;*/ + font-size: 0.8em; +} + +dl dt { font-weight: bold; } +.content td dl { margin: 0; padding: 0; } +.content td dt { + background: transparent url(/images/toggle-collapse.gif) 0 3px no-repeat; + clear: left; color: #333; cursor: pointer; line-height: 1em; + margin-left: -12px; padding-left: 14px; +} +.content td dd { margin: 0; + padding: 0 0 0 1em; +} +.content td dt.collapsed { + background-image: url(/images/toggle-expand.gif); +} +.content td dt.inline { background-image: none; cursor: default; + float: left; margin-left: 0; padding-left: 2px; padding-right: .5em; + padding-top: 2px; +} + +div.sortable { + height: 200px; + width: 225px; + margin-right: 10px; + border: 1px solid black; + overflow: scroll; + background: #777777; +} + +div.sortable.run-list { + height: 430px; + width: 225px; + margin-right: 10px; + border: 1px solid black; + overflow: scroll; + background: #777777; +} + +div.clear { + clear: left; +} + +table.form th { + /* vertical-align: top; */ + font-weight: bold; + text-align: right; + padding-right: 10px; +} + +div.help { + padding: 10px; + text-align: left; + vertical-align: top; +} + +table.form td { + text-align: left; + /* vertical-align: top; */ +} + +table.sortable td { + vertical-align: top; +} + +table.sortable ul { + background: #ff00ff; + margin: 5px; + padding: 5px; +} + +td.table-key { + font-weight: bold; +} + +div.json-attr { + overflow: auto; + max-width: 400px; +} + +td.position { + font-weight: bold; +} + +div#tree { + float: left; +} + +div#jform { + float: left; + margin-left: 50px; +} + +div.editor { + border: 1px solid black; +} + +#node_available_roles, #node_available_recipes, #for_node, #for_role, #available_recipes { list-style-type: none; margin: 0; padding: 0; float: left; margin-right: 10px; background: #eee; padding: 5px; width: 200px;} + +#node_available_roles li, #node_available_recipes li, #for_node li, #for_role li, #available_recipes li { margin: 0 5px 5px 5px; padding: 5px; width: 175px; } + +#sidebar_block { + display: none; +} + +#sidebar_block_notice { + display: none; +} + +div.tooltip { + background-color: #000; + outline: 1px solid #669; + border: 2px solid #fff; + padding: 10px 15px; + /* width: 200px; */ + display: none; + color: #fff; + text-align: left; + font-size: 12px; + outline-radius: 4px; + -moz-outline-radius: 4px; + -webkit-outline-radius: 4px; +} + +table.tooltip { + width: 190px; +} diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_diagonals-small_0_aaaaaa_40x40.png b/chef-server-webui/public/stylesheets/images/ui-bg_diagonals-small_0_aaaaaa_40x40.png Binary files differindex d5359734ad..d5359734ad 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_diagonals-small_0_aaaaaa_40x40.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_diagonals-small_0_aaaaaa_40x40.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_diagonals-thick_15_444444_40x40.png b/chef-server-webui/public/stylesheets/images/ui-bg_diagonals-thick_15_444444_40x40.png Binary files differindex 88e46a6d36..88e46a6d36 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_diagonals-thick_15_444444_40x40.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_diagonals-thick_15_444444_40x40.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_glass_100_f0f0f0_1x400.png b/chef-server-webui/public/stylesheets/images/ui-bg_glass_100_f0f0f0_1x400.png Binary files differindex 33896e710c..33896e710c 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_glass_100_f0f0f0_1x400.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_glass_100_f0f0f0_1x400.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_glass_50_99c2ff_1x400.png b/chef-server-webui/public/stylesheets/images/ui-bg_glass_50_99c2ff_1x400.png Binary files differindex 8249158b01..8249158b01 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_glass_50_99c2ff_1x400.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_glass_50_99c2ff_1x400.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_glass_55_fbf5d0_1x400.png b/chef-server-webui/public/stylesheets/images/ui-bg_glass_55_fbf5d0_1x400.png Binary files differindex f25dd91065..f25dd91065 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_glass_55_fbf5d0_1x400.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_glass_55_fbf5d0_1x400.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_glass_80_e6e6e6_1x400.png b/chef-server-webui/public/stylesheets/images/ui-bg_glass_80_e6e6e6_1x400.png Binary files differindex abaa23f001..abaa23f001 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_glass_80_e6e6e6_1x400.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_glass_80_e6e6e6_1x400.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png b/chef-server-webui/public/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png Binary files differindex 4443fdc1a1..4443fdc1a1 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_highlight-hard_100_f9f9f9_1x100.png b/chef-server-webui/public/stylesheets/images/ui-bg_highlight-hard_100_f9f9f9_1x100.png Binary files differindex 2cb80364bb..2cb80364bb 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_highlight-hard_100_f9f9f9_1x100.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_highlight-hard_100_f9f9f9_1x100.png diff --git a/chef-server-slice/public/stylesheets/images/ui-bg_highlight-soft_100_e7eef3_1x100.png b/chef-server-webui/public/stylesheets/images/ui-bg_highlight-soft_100_e7eef3_1x100.png Binary files differindex d39c182270..d39c182270 100755 --- a/chef-server-slice/public/stylesheets/images/ui-bg_highlight-soft_100_e7eef3_1x100.png +++ b/chef-server-webui/public/stylesheets/images/ui-bg_highlight-soft_100_e7eef3_1x100.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_222222_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_222222_256x240.png Binary files differindex 67560da9be..67560da9be 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_222222_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_222222_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_2694e8_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_2694e8_256x240.png Binary files differindex dbd78b68ad..dbd78b68ad 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_2694e8_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_2694e8_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_2e83ff_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_2e83ff_256x240.png Binary files differindex b425c446d2..b425c446d2 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_2e83ff_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_2e83ff_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_72a7cf_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_72a7cf_256x240.png Binary files differindex 58aed4074e..58aed4074e 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_72a7cf_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_72a7cf_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_888888_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_888888_256x240.png Binary files differindex 2e5180e473..2e5180e473 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_888888_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_888888_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_cd0a0a_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_cd0a0a_256x240.png Binary files differindex 2db88b796a..2db88b796a 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_cd0a0a_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_cd0a0a_256x240.png diff --git a/chef-server-slice/public/stylesheets/images/ui-icons_ffffff_256x240.png b/chef-server-webui/public/stylesheets/images/ui-icons_ffffff_256x240.png Binary files differindex 746e6fa257..746e6fa257 100755 --- a/chef-server-slice/public/stylesheets/images/ui-icons_ffffff_256x240.png +++ b/chef-server-webui/public/stylesheets/images/ui-icons_ffffff_256x240.png diff --git a/chef-server-slice/public/stylesheets/jquery-ui-1.7.1.custom.css b/chef-server-webui/public/stylesheets/jquery-ui-1.7.1.custom.css index 0e93ed5dd1..0e93ed5dd1 100755 --- a/chef-server-slice/public/stylesheets/jquery-ui-1.7.1.custom.css +++ b/chef-server-webui/public/stylesheets/jquery-ui-1.7.1.custom.css diff --git a/chef-server-slice/public/stylesheets/jquery.treeTable.css b/chef-server-webui/public/stylesheets/jquery.treeTable.css index 1f37f1ca15..1f37f1ca15 100644 --- a/chef-server-slice/public/stylesheets/jquery.treeTable.css +++ b/chef-server-webui/public/stylesheets/jquery.treeTable.css diff --git a/chef-server-webui/public/stylesheets/themes/bec-green/style.css b/chef-server-webui/public/stylesheets/themes/bec-green/style.css new file mode 100644 index 0000000000..225b6d4e35 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/bec-green/style.css @@ -0,0 +1,290 @@ +a:link, a:visited, a:hover, a:active { color: #33f; } +h1, h2, h3 {color:#444} + +body { + color: #222; + background: #e5e5e5; + font-family: "Bitstream Vera Sans", verdana, sans-serif; +} + +hr { + background: #f0f0ee; +} + +p { + font-size: 14px; + line-height: 20px; +} + +input.checkbox { + vertical-align:middle; +} + +#header h1 { + font-size: 28px; + padding: 5px 0; + margin: 5px 0; +} + +.hightlight { + background-color: #ffc; +} +.small { + font-size: 11px; +} +.gray { + color: #999; +} +#header { + background: #48625B; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#main { + background: #e5e5e5; + width: 69%; +} + +#main .block { + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + padding: 0; + margin-bottom:20px; + padding-bottom: 20px; + background: #fff; +} + +#main .block h2.title { + margin: 0 0 20px 0; + background-color: #E9FAE6; + padding: 5px 5px 5px 15px; + font-size:18px; +} + +.main_container { + padding:10px; +} + +/* #sidebar .block { background: #FFF; padding-bottom:0px; } */ + +#sidebar .notice { + background-color: #ffc; + padding: 0 10px; + border-bottom:1px solid #ddd; + border-right:1px solid #ddd; + border-top:1px solid #fff; + border-left:1px solid #fff; +} +#sidebar .notice h2 { + font-size:16px; + margin: 5px 0; + border-bottom:1px solid #aaa; +} +#sidebar .notice p { + font-size:12px; +} + +#sidebar .block { + padding-bottom: 0; +} + +#sidebar .block .content { + padding: 0 10px; +} + + +#sidebar h3 { + background: #c7d8d8; + border-bottom:1px solid #999; + padding: 5px 10px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + font-size:14px; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #E9FAE6; + color: #444; + font-size:14px; + text-decoration:underline; +} +#sidebar ul.navigation li.last a { + border-bottom: none; +} + +#sidebar ul.navigation li a:link,#sidebar ul.navigation li a:visited { + padding: 5px 10px; + color:#444; + text-decoration: none; +} +#sidebar ul.navigation li a:hover { + text-decoration:underline; +} +#sidebar .block .sidebar-block h4 { + border-bottom: 1px solid #bbb; +} +#main-navigation ul li { + background: #30423E; +} + +#main-navigation ul li:hover { + background: #23302D; +} + +#main-navigation ul li.active { + background: #e5e5e5; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +#main-navigation ul li a { + font-size: 14px; + padding: 4px 10px; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #333; +} +#user-navigation ul li a:hover { + text-decoration: underline; +} +.secondary-navigation { + background: #48625B; + border-bottom-color: #30423e; + border-bottom: 5px solid #30423e; +} + +.secondary-navigation ul li.active { + background-color: #30423e; +} + +.secondary-navigation ul li:hover { + background-color: #23302d; +} + +/* pagination */ + +.pagination span.current { + background: #30423e; + color: #FFF; + border: 1px solid #30423e; + -moz-border-radius:5px; +} + +.pagination a { + color: #364B69; + border: 1px solid #ddd; + -moz-border-radius:5px; +} + +.pagination a:hover { + color: #444; + background: #E9FAE6; +} + +/* tables */ + +.table th { + background: #48625B; + color: #FFF; + font-weight:normal; + padding:3px; +} + +.table th a.toggle { + display: block; + width: 12px; + height: 12px; + background: transparent url('images/tick.png') center no-repeat; + text-indent: -9999px; + -moz-outline: none; +} + +.table th.first { + width: 30px; + text-align: center; +} + +.table td { + border-bottom: 1px solid #F0F0EE; +} + +/* forms */ + +.form input.text, .form textarea.textarea { + border: 1px solid #ddd; + padding: 5px; + width: 95%; +} + +.form .navform { + padding:10px; + background-color: #E9FAE6; + font-size:14px; + border-bottom:1px solid #ddd; + border-right:1px solid #ddd; + border-top:1px solid #eee; + border-left:1px solid #eee; +} +.form .navform input { + font-size:14px; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin:0 auto 5px; + width:80%; +} +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #ddf; + background-color: #eef; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + margin: 3px 10px 0 0; +} + +ul.list li .left { + padding: 5px 5px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #48625B; + color: #FFF; +} diff --git a/chef-server-webui/public/stylesheets/themes/bec/style.css b/chef-server-webui/public/stylesheets/themes/bec/style.css new file mode 100644 index 0000000000..c94474866a --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/bec/style.css @@ -0,0 +1,301 @@ +a:link, a:visited, a:hover, a:active { color: #33f; } +h1, h2, h3 {color:#444} + +body { + color: #333; + background: #e5e5e5; + font-family: Verdana, Arial, "Bitstream Vera Sans", sans-serif; +} + +hr { + background: #f0f0ee; +} + +p { + font-size: 12px; + line-height: 20px; +} + +input.checkbox { + vertical-align:middle; +} + +#header h1 { + font-size: 28px; + padding: 5px 0; + margin: 5px 0; +} + +.hightlight { + background-color: #ffc; +} +.small { + font-size: 11px; +} +.gray { + color: #999; +} +#header { + background: #006666; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#main { + background: #e5e5e5; + width: 73%; +} + +#main .block { + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + padding: 0; + margin-bottom:20px; + padding-bottom: 20px; + background: #fff; +} + +#main .block h2.title { + margin: 0 0 20px 0; + background-color: #E6FAFA; + padding: 5px 5px 5px 15px; + font-size:18px; +} + +.main_container { + padding:10px; +} + +#footer .block p { + color:#aaa; + font-size:11px; +} + +/* #sidebar .block { background: #FFF; padding-bottom:0px; } */ + +#sidebar .notice { + background-color: #ffc; + padding: 0 10px; + border-bottom:1px solid #ddd; + border-right:1px solid #ddd; + border-top:1px solid #fff; + border-left:1px solid #fff; +} +#sidebar .notice h2 { + font-size:16px; + margin: 5px 0; + border-bottom:1px solid #aaa; +} +#sidebar .notice p { + font-size:12px; +} + +#sidebar .block { + padding-bottom: 0; +} + +#sidebar .block .content { + padding: 0 10px; +} + + +#sidebar h3 { + background: #c7d8d8; + border-bottom:1px solid #999; + padding: 5px 10px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + font-size:12px; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #e1efef; + color: #444; + font-size:12px; + text-decoration:underline; +} +#sidebar ul.navigation li.last a { + border-bottom: none; +} + +#sidebar ul.navigation li a:link,#sidebar ul.navigation li a:visited { + padding: 5px 10px; + color:#444; + text-decoration: none; +} +#sidebar ul.navigation li a:hover { + text-decoration:underline; +} +#sidebar .block .sidebar-block h4 { + border-bottom: 1px solid #bbb; +} +#main-navigation ul li { + background: #008c8c; +} + +#main-navigation ul li:hover { + background: #00b2b2; +} + +#main-navigation ul li.active { + background: #f0f0ee; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +#main-navigation ul li a { + font-size: 12px; + padding: 4px 10px; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #364b69; +} +#user-navigation ul li a:hover { + text-decoration: underline; +} +.secondary-navigation { + background: #006666; + border-bottom-color: #008c8c; + border-bottom: 5px solid #008c8c; +} + +.secondary-navigation ul li.active { + background-color: #008c8c; +} + +.secondary-navigation ul li:hover { + background-color: #00b2b2; +} + +/* pagination */ + +.pagination span.current { + background: #008c8c; + color: #FFF; + border: 1px solid #008c8c; + -moz-border-radius:5px; +} + +.pagination a { + color: #364B69; + border: 1px solid #ddd; + -moz-border-radius:5px; + font-size:11px; +} + +.pagination a:hover { + color: #444; + background: #E6FAFA; +} + +/* tables */ + +.table th { + background: #006666; + color: #FFF; + font-weight:normal; + padding:3px; +} + +.table th a.toggle { + display: block; + width: 12px; + height: 12px; + background: transparent url('images/tick.png') center no-repeat; + text-indent: -9999px; + -moz-outline: none; +} + +.table th.first { + width: 30px; + text-align: center; +} + +.table td { + border-bottom: 1px solid #F0F0EE; +} + +/* forms */ + +.form input.text, .form textarea.textarea { + border: 1px solid #ddd; + padding: 5px; + width:99%; +} + +.form .navform { + padding:10px; + background-color: #f1f8f8; + font-size:14px; + border-bottom:1px solid #ddd; + border-right:1px solid #ddd; + border-top:1px solid #eee; + border-left:1px solid #eee; +} +.form .navform input { + font-size:14px; +} + +.description { + color:#aaa; + font-family:Georgia, serif; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin:0 auto 5px; + width:80%; +} +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #ddf; + background-color: #eef; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + margin: 3px 10px 0 0; +} + +ul.list li .left { + padding: 5px 5px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #006666; + color: #FFF; +} diff --git a/chef-server-webui/public/stylesheets/themes/blue/style.css b/chef-server-webui/public/stylesheets/themes/blue/style.css new file mode 100644 index 0000000000..cce8f4bdf0 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/blue/style.css @@ -0,0 +1,280 @@ +.small { font-size:12px; } +.gray { color:#999999; } +.hightlight { background-color:#FFFFCC; } + +a:link, a:visited, a:hover, a:active, h1, h2, h3 { color: #2F427A; } +a { -moz-outline: none; } + +body { + color: #222; + background: #f0f0ee; + font-family: helvetica, arial, sans-serif; + font-size: 14px; +} + +hr { + background: #f0f0ee; + color: #f0f0ee; +} + +#header { + background: #2F427A; +} + +#header h1 { + padding: 20px 0; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#user-navigation { + top: auto; + bottom: 5px; + right: 25px; +} + +#main .block .content { + background: #FFF; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + +#sidebar .block { + background: #FFF; +} + +#sidebar .notice { + background: #FFFFCC; +} + +#sidebar h3 { + background: #2F427A; + color: #FFF; + border-bottom: 10px solid #262626; + font-size: 15px; +} + +#main-navigation ul li { + padding-left: 15px; +} + +#main-navigation ul li a { + padding: 8px 0; +} + +#main-navigation ul li.active { + padding: 0; + margin-left: 15px; +} + +#main-navigation ul li.active { + margin-left: 15px; +} + +#main-navigation ul li.active a { + padding: 8px 15px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + background: #FFF; + border-bottom: 1px solid #F0F0EE; + text-decoration: none; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #316291; + color: #FFF; +} + +#main-navigation { + background: #262626; +} + +#main-navigation ul li { + background: #262626; + margin-right: 0; +} + +#main-navigation ul li.active { + background: #f0f0ee; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +.secondary-navigation li a:hover { + background: #5C637A; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #262626; +} + +.secondary-navigation { + background: #2F427A; + border-bottom-color: #262626; + font-size: 15px; +} + +.secondary-navigation ul li.active, .secondary-navigation ul li.active a:hover { + background-color: #262626; +} + +#footer .block { + color: #FFF; + background: #262626; +} + +#footer .block p { + margin: 0; + padding: 10px; +} + +/* pagination */ + +.pagination span, .pagination a { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + padding-top: 4px; +} + +.pagination span.current { + background: #262626; + color: #FFF; + border-color: #262626; +} + +.pagination a { + color: #262626; + border-color: #262626; +} + +.pagination a:hover { + color: #FFF; + background: #262626; +} + +/* tables */ + +.table th { + background: #262626; + color: #FFF; +} + +.table td { + border-bottom:1px solid #F0F0EE; +} + +/* forms */ + +.form input.text_field, .form textarea.text_area { + width: 100%; + border: 1px solid #262626; +} + +.form input.button { + background: #EEE; + color: #262626; + padding: 2px 5px; + border: 1px solid #262626; + cursor: pointer; +} + +.form .description { + font-style: italic; + color: #8C8C8C; + font-size: .9em; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin: 0 auto 15px; + +} + +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #1FDF00; + background-color: #BBFFB6; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #2F427A; + color: #FFF; +} + + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#footer .block, .form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, .secondary-navigation ul li.first, .table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, #sidebar ul.navigation, ul.list li, #footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +}
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/themes/default/style.css b/chef-server-webui/public/stylesheets/themes/default/style.css new file mode 100644 index 0000000000..e7a5ee1271 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/default/style.css @@ -0,0 +1,267 @@ +.small { font-size:11px; } +.gray { color:#999999; } +.hightlight { background-color:#FFFFCC; } + +a:link, a:visited, a:hover, a:active, h1, h2, h3 { color: #7A1818; } +a { -moz-outline: none; } + +body { + color: #222; + background: #f0f0ee; + font-family: helvetica, arial, sans-serif; +} + +hr { + background: #f0f0ee; + color: #f0f0ee; +} + +#header { + background: #7A1818; +} + +#header h1 { + padding: 20px 0; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#user-navigation { + top: auto; + bottom: 5px; + right: 25px; +} + +#main .block .content { + background: #FFF; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + +#sidebar .block { + background: #FFF; +} + +#sidebar h3 { + background: #7A1818; + color: #FFF; + border-bottom: 10px solid #262626; +} + +#main-navigation ul li { + padding-left: 15px; +} + +#main-navigation ul li a { + padding: 8px 0; +} + +#main-navigation ul li.active { + padding: 0; + margin-left: 15px; +} + +#main-navigation ul li.active { + margin-left: 15px; +} + +#main-navigation ul li.active a { + padding: 8px 15px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + background: #FFF; + border-bottom: 1px solid #F0F0EE; + text-decoration: none; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #470E0E; + color: #FFF; +} + +#main-navigation { + background: #262626; +} + +#main-navigation ul li { + background: #262626; + margin-right: 0; +} + +#main-navigation ul li.active { + background: #f0f0ee; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +.secondary-navigation li a:hover { + background: #470E0E; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #262626; +} + +.secondary-navigation { + background: #7A1818; + border-bottom-color: #262626; +} + +.secondary-navigation ul li.active, .secondary-navigation ul li.active a:hover { + background-color: #262626; +} + +#footer .block { + color: #FFF; + background: #262626; +} + +#footer .block p { + margin: 0; + padding: 10px; +} + +/* pagination */ + +.pagination span.current { + background: #262626; + color: #FFF; + border-color: #262626; +} + +.pagination a { + color: #262626; + border-color: #262626; +} + +.pagination a:hover { + color: #FFF; + background: #262626; +} + +/* tables */ + +.table th { + background: #262626; + color: #FFF; +} + +.table td { + border-bottom:1px solid #F0F0EE; +} + +/* forms */ + +.form input.text_field, .form textarea.text_area { + width: 100%; + border: 1px solid #262626; +} + +.form input.button { + background: #EEE; + color: #262626; + padding: 2px 5px; + border: 1px solid #262626; + cursor: pointer; +} + +.form .description { + font-style: italic; + color: #8C8C8C; + font-size: .9em; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin: 0 auto 15px; + +} + +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #1FDF00; + background-color: #BBFFB6; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #7A1818; + color: #FFF; +} + + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#footer .block, .form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, .secondary-navigation ul li.first, .table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, #sidebar ul.navigation, ul.list li, #footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +}
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css b/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css new file mode 100644 index 0000000000..9b050c6785 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/djime-cerulean/style.css @@ -0,0 +1,298 @@ +/** + * Cerulean web-app-theme made for Djime: http://github.com/mikl/djime/ + * + * Please note that we're using CSSEdit's @group comment syntax. + * + * Colour sheme: + * Cerulean: #007BA7 + * Bright blue: #01B8DE + * Near-white: #F7F7F8 + * Silver grey: #C2C8D1 + * Dark blue: #001C26 + * + * http://www.colourlovers.com/palette/646252/Cerulean_touch + */ + +/* @group General styles */ + +.small { font-size:11px; } +.gray { color:#999; } +.hightlight { background-color:#ffc; } + +a:link, a:visited, a:hover, a:active, h1, h2, h3 { color: #007BA7; } + +body { + color: #222; + background: #C2C8D1; + font-family: "Helvetica Neue",Helvetica,Arial,"Bitstream Vera Sans",sans-serif; +} + +hr { + background: #EEF0F0; + color: #EEF0F0; +} + +/* @end */ + +/* @group Header */ + +#header { + background: #007BA7; +} + +#header h1 { + padding: 20px 0; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #F7F7F8; +} + +/* @end */ + +#user-navigation { + top: auto; + bottom: 5px; + right: 25px; +} + +#main .block .content { + background: #F7F7F8; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + + + +/* @group Main navigation */ + +#main-navigation ul li { + padding-left: 0; +} + +#main-navigation ul li a { + padding: 8px 0; +} + +#main-navigation ul li a { + padding: 8px 15px; +} + +#main-navigation { + background-color: #005573; +} + +#main-navigation ul li a:hover { + background-color: #001C26; +} + +#main-navigation ul li.active a { + background-color: #C2C8D1; + background: -webkit-gradient(linear, left top, left bottom, from(#C2C8D1), to(#C2C8D1), color-stop(0.5, #F7F7F8), color-stop(0.5, #F7F7F8)); + +} + +/* @end */ + +/* @group Secondary navigation */ + +.secondary-navigation li a:hover { + background: #005573; +} + +.secondary-navigation { + background: #007BA7; + border-bottom-width: 7px; + border-bottom-color: #005573; +} + +.secondary-navigation ul li.active, .secondary-navigation ul li.active a:hover { + background-color: #005573; +} + +/* @end */ + +/* @group Sidebar */ + +#sidebar .block { + background: #F7F7F8; +} + +#sidebar h3 { + background: #007BA7; + color: #F7F7F8; + border-bottom: 7px solid #005573; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + background: #F7F7F8; + border-bottom: 1px solid #EEF0F0; + text-decoration: none; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #005573; + color: #F7F7F8; +} + +/* @end */ + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #F7F7F8; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #001C26; +} + +#footer .block { + color: #F7F7F8; + background: #005573; +} + +#footer .block p { + margin: 0; + padding: 10px; +} + +/* pagination */ + +.pagination span.current { + background: #005573; + color: #F7F7F8; + border-color: #005573; +} + +.pagination a, +.pagination span { + color: #001C26; + border-color: #005573; +} + +.pagination a:hover { + color: #F7F7F8; + background: #005573; +} + +/* tables */ + +.table th { + background: #C2C8D1; + color: #001C26; +} + +.table td { + border-bottom:1px solid #EEF0F0; +} + +/* forms */ + +.form input.text_field, .form textarea.text_area { + width: 100%; + border: 1px solid #001C26; +} + +.form input.button { + background: #EEE; + color: #001C26; + padding: 2px 5px; + border: 1px solid #001C26; + cursor: pointer; +} + +.form .description { + color: #8C8C8C; + font-size: .9em; +} + +/* @group Flash messages */ + +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin: 0 auto 15px; +} + +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #e0d300; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #8ec4df; + background-color: #dffaff; +} + +/* @end */ + +/* lists */ + +ul.list li { + border-bottom-color: #EEF0F0; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #EEF0F0; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #F7F7F8; +} + +#box .block h2 { + background: #005573; + color: #F7F7F8; +} + + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, #main-navigation li a, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#footer .block, .form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, .secondary-navigation ul li.first, .table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, #sidebar ul.navigation, ul.list li, #footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +}
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/themes/kathleene/style.css b/chef-server-webui/public/stylesheets/themes/kathleene/style.css new file mode 100644 index 0000000000..e68a545431 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/kathleene/style.css @@ -0,0 +1,272 @@ +.small { font-size:11px; } +.gray { color:#999999; } +.hightlight { background-color:#FFFFCC; } + +a:link, a:visited, a:hover, a:active, h1, h2, h3 { color: #AF0000; } +a { -moz-outline: none; } + +body { + color: #222; + background: #f0f0ee; + font-family: helvetica, arial, sans-serif; +} + +hr { + background: #f0f0ee; + color: #f0f0ee; +} + +#header { + background: #AF0000; +} + +#header h1 { + padding: 20px 0; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#user-navigation { + top: auto; + bottom: 5px; + right: 25px; +} + +#main .block .content { + background: #FFF; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + +#sidebar .block { + background: #FFF; +} + +#sidebar h3 { + background: #AF0000; + color: #FFF; + border-bottom: 5px solid #2a0000; +} + +#main-navigation ul li { + padding-left: 15px; +} + +#main-navigation ul li a { + padding: 8px 0; +} + +#main-navigation ul li.active { + padding: 0; + margin-left: 15px; +} + +#main-navigation ul li.active { + margin-left: 15px; +} + +#main-navigation ul li.active a { + padding: 8px 15px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + background: #FFF; + border-bottom: 1px solid #F0F0EE; + text-decoration: none; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #470E0E; + color: #FFF; +} + +#main-navigation { + background: #2a0000; +} + +#main-navigation ul li { + background: #2a0000; + margin-right: 0; +} + +#main-navigation ul li.active { + background: #f0f0ee; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +.secondary-navigation li a:hover { + background: #470E0E; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #2a0000; +} + +.secondary-navigation { + background: #AF0000; + border-bottom-color: #2a0000; +} + +.secondary-navigation ul li.active, .secondary-navigation ul li.active a:hover { + background-color: #2a0000; +} + +#footer .block { + color: #FFF; + background: #2a0000; +} + +#footer .block p { + margin: 0; + padding: 10px; +} + +/* pagination */ + +.pagination span.current { + background: #2a0000; + color: #FFF; + border-color: #2a0000; +} + +.pagination a { + color: #2a0000; + border-color: #2a0000; +} + +.pagination a:hover { + color: #FFF; + background: #2a0000; +} + +/* tables */ + +.table th { + background: #100000; + border-bottom: 3px solid #700000; + color: #FFF; +} + +.table td { + border-bottom:1px solid #F0F0EE; +} + +/* forms */ + +.form input.text_field, .form textarea.text_area { + width: 100%; + border: 1px solid #2a0000; +} + +.form input.button { + background: #EEE; + color: #2a0000; + padding: 2px 5px; + border: 1px solid #2a0000; + cursor: pointer; +} + +.form .description { + font-style: italic; + color: #8C8C8C; + font-size: .9em; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin: 0 auto 15px; + +} + +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #1FDF00; + background-color: #BBFFB6; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #AF0000; + color: #FFF; +} + + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#footer .block, .form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, .secondary-navigation ul li.first, .table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, #sidebar ul.navigation, ul.list li, #footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +} + +.secondary-navigation { + border-bottom-width: 5px; +} diff --git a/chef-server-webui/public/stylesheets/themes/orange/style.css b/chef-server-webui/public/stylesheets/themes/orange/style.css new file mode 100644 index 0000000000..90e7d8de58 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/orange/style.css @@ -0,0 +1,263 @@ +.small { font-size:11px; } +.gray { color:#999999; } +.hightlight { background-color:#FFFFCC; } + +a:link, a:visited, a:hover, a:active, h1, h2, h3 { color: #ff7900; } +a { -moz-outline: none; } + +body { + color: #222; + background: #f0f0ee; + font-family: helvetica, arial, sans-serif; +} + +hr { + background: #f0f0ee; + color: #f0f0ee; +} + +#header { + background: #ff7900; +} + +#header h1 a:link, #header h1 a:active, #header h1 a:hover, #header h1 a:visited { + color: #FFF; +} + +#user-navigation { + top: auto; + bottom: 5px; + right: 25px; +} + +#main .block .content { + background: #FFF; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + +#sidebar .block { + background: #FFF; +} + +#sidebar h3 { + background: #ff7900; + color: #FFF; + border-bottom: 10px solid #262626; +} + +#main-navigation ul li { + padding-left: 15px; +} + +#main-navigation ul li a { + padding: 8px 0; +} + +#main-navigation ul li.active { + padding: 0; + margin-left: 15px; +} + +#main-navigation ul li.active { + margin-left: 15px; +} + +#main-navigation ul li.active a { + padding: 8px 15px; +} + +#sidebar ul li a:link, #sidebar ul li a:visited { + background: #FFF; + border-bottom: 1px solid #F0F0EE; + text-decoration: none; +} + +#sidebar ul li a:hover, #sidebar ul li a:active { + background: #863800; + color: #FFF; +} + +#main-navigation { + background: #262626; +} + +#main-navigation ul li { + background: #262626; + margin-right: 0; +} + +#main-navigation ul li.active { + background: #f0f0ee; +} + +#main-navigation ul li a:link, #main-navigation ul li a:visited, #main-navigation ul li a:hover, #main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, .secondary-navigation ul li a:hover, .secondary-navigation ul li a:active, +#user-navigation ul li a:link, #user-navigation ul li a:visited, #user-navigation ul li a:hover, #user-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +.secondary-navigation li a:hover { + background: #863800; +} + +#main-navigation ul li.active a:link, #main-navigation ul li.active a:visited, #main-navigation ul li.active a:hover, #main-navigation ul li.active a:active { + color: #262626; +} + +.secondary-navigation { + background: #ff7900; + border-bottom-color: #262626; +} + +.secondary-navigation ul li.active, .secondary-navigation ul li.active a:hover { + background-color: #262626; +} + +#footer .block { + color: #FFF; + background: #262626; + width: 70%; +} + +#footer .block p { + margin: 0; + padding: 10px; +} + +/* pagination */ + +.pagination span.current { + background: #262626; + color: #FFF; + border-color: #262626; +} + +.pagination a { + color: #262626; + border-color: #262626; +} + +.pagination a:hover { + color: #FFF; + background: #262626; +} + +/* tables */ + +.table th { + background: #262626; + color: #FFF; +} + +.table td { + border-bottom:1px solid #F0F0EE; +} + +/* forms */ + +.form input.text, .form textarea { + width: 100%; + border: 1px solid #262626; +} + +.form input.button { + background: #EEE; + color: #262626; + padding: 2px 5px; + border: 1px solid #262626; + cursor: pointer; +} + +.form .description { + font-style: italic; + color: #8C8C8C; + font-size: .9em; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin:0 auto 5px; + +} +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #1FDF00; + background-color: #BBFFB6; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + background: #ff7900; + color: #FFF; +} + + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, .secondary-navigation, #main .block, #sidebar .block, #sidebar h3, ul.list li, +#footer .block, .form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, .secondary-navigation ul li.first, .table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, #sidebar ul.navigation, ul.list li, #footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +}
\ No newline at end of file diff --git a/chef-server-webui/public/stylesheets/themes/reidb-greenish/style.css b/chef-server-webui/public/stylesheets/themes/reidb-greenish/style.css new file mode 100644 index 0000000000..23e5245eb4 --- /dev/null +++ b/chef-server-webui/public/stylesheets/themes/reidb-greenish/style.css @@ -0,0 +1,301 @@ +.small { font-size:11px; } +.gray { color:#a2b0b6; } +.hightlight { background-color:#d6e7c7; } + +a:link, a:visited, a:hover, a:active { color: #81B953; } +h1, h2, h3 { color: #3B5526; } +a { -moz-outline: none; } + +body { + color: #222; + background: #e4ebe4; + font-family: helvetica, arial, sans-serif; +} + +hr { + background: #f0f0ee; + color: #f0f0ee; +} + +#header { + background: #c9deb7; + +} + +#header h1{ + padding: 20px 0; + font-weight: bold; + +} + +#header h1 a:link, #header h1 a:active, +#header h1 a:hover, #header h1 a:visited { + color: #3B5526; +} + +#main .block .content { + background: #FFF; + padding-top: 1px; +} + +#main .block .content h2 { + margin-left: 15px; +} + +#main .content { border: 1px solid #81B953;} + +#sidebar .block { + background: #FFF; + border: none; +} + +#sidebar h3 { + padding: 8px 12px; + background: #3B5526; + color: #FFF; + font-weight: bold; + border-bottom: 5px solid #81B953; +} + + +#sidebar ul li a:link, +#sidebar ul li a:visited { + background: #FFF; + border-bottom: 1px solid #F0F0EE; + text-decoration: none; +} + +#sidebar ul li a:hover, +#sidebar ul li a:active { + background: #D3E8C1; + color: #FFF; +} + +#main-navigation { + background: #44721e; + margin-right: 20px; + padding: 7px 7px 0 7px; +} + +#main-navigation ul li { + background: #91B96F; + margin-right: 7px; +} + +#main-navigation ul li a { + padding: 10px 10px 5px 10px; +} + +#main-navigation ul li.active { + background: #e4ebe4; + border: 1px solid #91B96F; + border-bottom: none; + font-weight: bold; +} + +#main-navigation ul li a:hover, +#main-navigation ul li a:link, +#main-navigation ul li a:visited, +#main-navigation ul li a:active, +.secondary-navigation ul li a:link, .secondary-navigation ul li a:visited, +.secondary-navigation ul li a:hover, .secondary-navigation ul li a:active { + text-decoration: none; + color: #FFF; +} + +#user-navigation ul li a:link, +#user-navigation ul li a:visited, +#user-navigation ul li a:active { + color: #3B5526; +} + +#user-navigation ul li a:hover { color: #fff; } + +#main-navigation ul li.active a:link, +#main-navigation ul li.active a:visited, +#main-navigation ul li.active a:hover, +#main-navigation ul li.active a:active { + color: #262626; +} + +.secondary-navigation { + background: #3B5526; + border-bottom: 5px solid #81b953; +} + +.secondary-navigation ul li a { +display:block; +padding: 8px 12px; +} + +.secondary-navigation ul li.active {background: #81b953; font-weight: bold;} +.secondary-navigation ul li.active a:hover { + background-color: #81B953; +} + +.secondary-navigation li a:hover { + background: #81B953; +} + +#footer .block { + color: #FFF; + background: #3B5526; +} + +#footer .block p { + margin: 0; + padding: 5px; +} + +/* pagination */ + +.pagination span.current { + background: #262626; + color: #FFF; + border-color: #262626; +} + +.pagination a { + color: #262626; + border-color: #262626; +} + +.pagination a:hover { + color: #FFF; + background: #262626; +} + +/* tables */ + + +.table th { + background: #253618; + color: #FFF; +} + +.table tr th { padding: 5px; } + +.table td { + border-bottom:1px solid #F0F0EE; +} +.table tr.odd {background: #ebfadf;} +.table tr.even {background: #d3e8c1;} + + +/* forms */ + +.form input.text_field, .form textarea.text_area { + width: 100%; + border: 1px solid #262626; +} + +.form input.button { + background: #EEE; + color: #262626; + padding: 2px 5px; + border: 1px solid #262626; + cursor: pointer; +} + +.form .description { + font-style: italic; + color: #8C8C8C; + font-size: .9em; +} + +/* flash-messages */ +.flash .message { + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-align:center; + margin: 0 auto 15px; + +} + +.flash .message p { + margin:8px; +} +.flash .error { + border: 1px solid #fbb; + background-color: #fdd; +} +.flash .warning { + border: 1px solid #fffaaa; + background-color: #ffffcc; +} +.flash .notice { + border: 1px solid #1FDF00; + background-color: #BBFFB6; +} + +/* lists */ + +ul.list li { + border-bottom-color: #F0F0EE; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +ul.list li .item .avatar { + border-color: #F0F0EE; + border-width: 1px; + border-style: solid; + padding: 2px; +} + +/* box */ + +#box .block { + background: #FFF; +} + +#box .block h2 { + color: #fff; + background: #3B5526; + border-bottom: 5px solid #81b953; +} + +#box .block .content { border: 1px solid #81b953; border} + +/* login */ + +#block-login { } +#block-login h2 { background: #3B5526;border-bottom: 5px solid #81b953;} + +/* rounded borders */ + +#main, #main-navigation, #main-navigation li, +.secondary-navigation, #main .block, #sidebar .block, +#sidebar h3, ul.list li, #footer .block, +.form input.button, #box .block, #box .block h2 { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation li.first a, +.secondary-navigation ul li.first, +.table th.first, .table th.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +.table th.last { + -moz-border-radius-topright: 4px; + -webkit-border-top-right-radius: 4px; +} + +.secondary-navigation ul li.first { + -moz-border-radius-topleft: 4px; + -webkit-border-top-left-radius: 4px; +} + +#sidebar, #sidebar .block, #main .block, +#sidebar ul.navigation, ul.list li, +#footer .block, .form input.button, #box .block { + -moz-border-radius-bottomleft: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -webkit-border-bottom-right-radius: 4px; +}
\ No newline at end of file diff --git a/chef-server-webui/stubs/app/controllers/application.rb b/chef-server-webui/stubs/app/controllers/application.rb new file mode 100644 index 0000000000..0b2f8726fd --- /dev/null +++ b/chef-server-webui/stubs/app/controllers/application.rb @@ -0,0 +1,2 @@ +class ChefServerWebui::Application < Merb::Controller +end diff --git a/chef-server-webui/stubs/app/controllers/main.rb b/chef-server-webui/stubs/app/controllers/main.rb new file mode 100644 index 0000000000..fdb090e693 --- /dev/null +++ b/chef-server-webui/stubs/app/controllers/main.rb @@ -0,0 +1,2 @@ +class ChefServerWebui::Main < ChefServerWebui::Application +end diff --git a/chef-server/Rakefile b/chef-server/Rakefile index d60352e421..c686268118 100644 --- a/chef-server/Rakefile +++ b/chef-server/Rakefile @@ -18,7 +18,7 @@ require 'chef' unless defined?(Chef) include FileUtils GEM = "chef-server" -CHEF_SERVER_VERSION = "0.7.9" +CHEF_SERVER_VERSION = "0.8.0" AUTHOR = "Opscode" EMAIL = "chef@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" diff --git a/chef-server/bin/chef-server b/chef-server/bin/chef-server index 70595c07ad..cd4be70a33 100755 --- a/chef-server/bin/chef-server +++ b/chef-server/bin/chef-server @@ -26,7 +26,8 @@ require "rubygems" require "merb-core" -[ 'chef', 'chef-server-slice' ].each do |lib| +[ 'chef', 'chef-server-api' ].each do |lib| + $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib"))) library = File.join(File.dirname(__FILE__), "..", "..", lib, "lib", "#{lib}.rb") require library if File.exists?(library) end @@ -50,7 +51,7 @@ if ARGV[0] && ARGV[0] =~ /^[^-]/ ARGV.push "-H" end unless %w[-a --adapter -i --irb-console -r --script-runner].any? { |o| ARGV.index(o) } - ARGV.push *%w[-a mongrel] + ARGV.push *%w[-a thin] end ARGV.push *[ "-I", File.join(__DIR__, "config", "init.rb") ] ARGV.push *[ "-m", __DIR__] diff --git a/chef-server/config/dependencies.rb b/chef-server/config/dependencies.rb index bb15302631..37c02f1e54 100644 --- a/chef-server/config/dependencies.rb +++ b/chef-server/config/dependencies.rb @@ -1,13 +1,24 @@ # dependencies are generated using a strict version, don't forget to edit the dependency versions when upgrading. merb_gems_version = "> 1.0" +%w{chef chef-server-api chef-solr}.each do |dep| + $: << File.join(File.dirname(__FILE__), "..", "..", dep, "lib") +end + +begin + require 'chef' + require 'chef-server-api' +rescue +end + # For more information about each component, please read http://wiki.merbivore.com/faqs/merb_components dependency "merb-core", merb_gems_version dependency "merb-assets", merb_gems_version dependency "merb-helpers", merb_gems_version dependency "merb-slices", merb_gems_version if defined?(CHEF_SERVER_VERSION) - dependency "chef-server-slice", CHEF_SERVER_VERSION unless defined?(ChefServerSlice) + dependency "chef-server-api", CHEF_SERVER_VERSION unless defined?(ChefServerApi) else - dependency "chef-server-slice" unless defined?(ChefServerSlice) + dependency "chef-server-api" unless defined?(ChefServerSlice) end + diff --git a/chef-server/config/init.rb b/chef-server/config/init.rb index 4af56589d0..b3ba2bf418 100644 --- a/chef-server/config/init.rb +++ b/chef-server/config/init.rb @@ -1,5 +1,5 @@ # Go to http://wiki.merbivore.com/pages/init-rb - + require 'config/dependencies.rb' unless defined?(Chef) gem "chef", "=" + CHEF_SERVER_VERSION if CHEF_SERVER_VERSION @@ -17,7 +17,9 @@ Merb::Config.use do |c| c[:exception_details] = true c[:reload_classes] = false c[:log_level] = Chef::Config[:log_level] - c[:log_stream] = Chef::Config[:log_location] + if Chef::Config[:log_location].kind_of?(String) + c[:log_file] = Chef::Config[:log_location] + end end Merb::BootLoader.before_app_loads do @@ -25,6 +27,6 @@ Merb::BootLoader.before_app_loads do end Merb::BootLoader.after_app_loads do - # This will get executed after your app's classes have been loaded. - OpenID::Util.logger = Merb.logger + # This will get executed after your app's classes have been loaded. OpenID::Util.logger = Merb.logger end + diff --git a/chef-server/config/rack.rb b/chef-server/config/rack.rb index e6bde4d226..4ed0f70b55 100644 --- a/chef-server/config/rack.rb +++ b/chef-server/config/rack.rb @@ -1,3 +1,5 @@ +$: << File.join(File.dirname(__FILE__)) + # use PathPrefix Middleware if :path_prefix is set in Merb::Config if prefix = ::Merb::Config[:path_prefix] use Merb::Rack::PathPrefix, prefix diff --git a/chef-server/config/router.rb b/chef-server/config/router.rb index 9123858cfa..cefef0c79a 100644 --- a/chef-server/config/router.rb +++ b/chef-server/config/router.rb @@ -29,16 +29,11 @@ Merb.logger.info("Compiling routes...") Merb::Router.prepare do # RESTful routes # resources :posts - + # Adds the required routes for merb-auth using the password slice # slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "") - slice(:chef_server_slice) - # This is the default route for /:controller/:action/:id - # This is fine for most cases. If you're heavily using resource-based - # routes, you may want to comment/remove this line to prevent - # clients from calling your create or destroy actions with a GET - default_routes - - # Change this for your home page to be available at / - # match('/').to(:controller => 'whatever', :action =>'index') + + slice(:chef_server_api) + # slice(:chef_server_webui, :path_prefix => "html") + end diff --git a/chef-solr/.document b/chef-solr/.document new file mode 100644 index 0000000000..ecf3673194 --- /dev/null +++ b/chef-solr/.document @@ -0,0 +1,5 @@ +README.rdoc +lib/**/*.rb +bin/* +features/**/*.feature +LICENSE diff --git a/chef-solr/.gitignore b/chef-solr/.gitignore new file mode 100644 index 0000000000..0ae00dc051 --- /dev/null +++ b/chef-solr/.gitignore @@ -0,0 +1,9 @@ +*.sw? +.DS_Store +coverage +rdoc +pkg +solr/work +solr/solr/data/* +solr/logs +solr/clustering diff --git a/chef-solr/README.rdoc b/chef-solr/README.rdoc new file mode 100644 index 0000000000..722ffd1c00 --- /dev/null +++ b/chef-solr/README.rdoc @@ -0,0 +1,7 @@ += chef-solr + +Description goes here. + +== Copyright + +Copyright (c) 2009 Adam Jacob. See LICENSE for details. diff --git a/chef-solr/Rakefile b/chef-solr/Rakefile new file mode 100644 index 0000000000..18d78d2bc6 --- /dev/null +++ b/chef-solr/Rakefile @@ -0,0 +1,57 @@ +require 'rubygems' +require 'rake' + +begin + require 'jeweler' + Jeweler::Tasks.new do |gem| + gem.name = "chef-solr" + gem.summary = %Q{Search indexing for Chef} + gem.email = "adam@opscode.com" + gem.homepage = "http://github.com/adamhjk/chef-solr" + gem.authors = ["Adam Jacob"] + # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings + end + +rescue LoadError + puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" +end + +require 'spec/rake/spectask' +Spec::Rake::SpecTask.new(:spec) do |spec| + spec.libs << 'lib' << 'spec' + spec.spec_files = FileList['spec/**/*_spec.rb'] + spec.spec_opts = %w{-fs --color} +end + +Spec::Rake::SpecTask.new(:rcov) do |spec| + spec.libs << 'lib' << 'spec' + spec.pattern = 'spec/**/*_spec.rb' + spec.rcov = true +end + +begin + require 'cucumber/rake/task' + Cucumber::Rake::Task.new(:features) +rescue LoadError + task :features do + abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber" + end +end + +task :default => :spec + +require 'rake/rdoctask' +Rake::RDocTask.new do |rdoc| + if File.exist?('VERSION.yml') + config = YAML.load(File.read('VERSION.yml')) + version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" + else + version = "" + end + + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "chef-solr #{version}" + rdoc.rdoc_files.include('README*') + rdoc.rdoc_files.include('lib/**/*.rb') +end + diff --git a/chef-solr/VERSION b/chef-solr/VERSION new file mode 100644 index 0000000000..a3df0a6959 --- /dev/null +++ b/chef-solr/VERSION @@ -0,0 +1 @@ +0.8.0 diff --git a/chef-solr/bin/chef-solr b/chef-solr/bin/chef-solr new file mode 100755 index 0000000000..b46d351747 --- /dev/null +++ b/chef-solr/bin/chef-solr @@ -0,0 +1,27 @@ +#!/usr/bin/ruby +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "chef", "lib"))) + +require 'rubygems' +require 'chef/solr/application/solr' + +Chef::Solr::Application::Solr.new.run + diff --git a/chef-solr/bin/chef-solr-indexer b/chef-solr/bin/chef-solr-indexer new file mode 100755 index 0000000000..6d32b2cbdf --- /dev/null +++ b/chef-solr/bin/chef-solr-indexer @@ -0,0 +1,27 @@ +#!/usr/bin/ruby +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "chef", "lib"))) + +require 'rubygems' +require 'chef/solr/application/indexer' + +Chef::Solr::Application::Indexer.new.run + diff --git a/chef-solr/bin/chef-solr-rebuild b/chef-solr/bin/chef-solr-rebuild new file mode 100755 index 0000000000..78f6eedaca --- /dev/null +++ b/chef-solr/bin/chef-solr-rebuild @@ -0,0 +1,27 @@ +#!/usr/bin/ruby +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) +$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "chef", "lib"))) + +require 'rubygems' +require 'chef/solr/application/rebuild' + +Chef::Solr::Application::Rebuild.new.run + diff --git a/chef-solr/chef-solr.gemspec b/chef-solr/chef-solr.gemspec new file mode 100644 index 0000000000..a6d307fdd7 --- /dev/null +++ b/chef-solr/chef-solr.gemspec @@ -0,0 +1,37 @@ +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |s| + s.name = %q{chef-solr} + s.version = "0.8.0" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.authors = ["Adam Jacob"] + s.date = %q{2009-07-19} + s.email = %q{adam@opscode.com} + s.executables = ["chef-solr", "chef-solr-indexer", "chef-solr-rebuild"] + s.extra_rdoc_files = [ + "README.rdoc" + ] + s.has_rdoc = true + s.homepage = %q{http://github.com/adamhjk/chef-solr} + s.rdoc_options = ["--charset=UTF-8"] + s.require_paths = ["lib"] + s.rubygems_version = %q{1.3.1} + s.summary = %q{Search indexing for Chef} + s.test_files = [ + "spec/chef/solr/index_spec.rb", + "spec/chef/solr/query_spec.rb", + "spec/chef/solr_spec.rb", + "spec/spec_helper.rb" + ] + + if s.respond_to? :specification_version then + current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION + s.specification_version = 2 + + if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then + else + end + else + end +end diff --git a/chef-solr/lib/chef/solr.rb b/chef-solr/lib/chef/solr.rb new file mode 100644 index 0000000000..abc2106231 --- /dev/null +++ b/chef-solr/lib/chef/solr.rb @@ -0,0 +1,181 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rubygems' +require 'chef/log' +require 'chef/config' +require 'chef/couchdb' +require 'chef/role' +require 'chef/node' +require 'chef/data_bag' +require 'chef/data_bag_item' +require 'net/http' +require 'libxml' +require 'uri' + +class Chef + class Solr + + attr_accessor :solr_url, :http + + def initialize(solr_url=Chef::Config[:solr_url]) + @solr_url = solr_url + uri = URI.parse(@solr_url) + @http = Net::HTTP.new(uri.host, uri.port) + end + + def solr_select(database, type, options={}) + options[:wt] = :ruby + options[:indent] = "off" + if type.kind_of?(Array) + options[:fq] = "+X_CHEF_database_CHEF_X:#{database} +X_CHEF_type_CHEF_X:#{type[0]} +data_bag:#{type[1]}" + else + options[:fq] = "+X_CHEF_database_CHEF_X:#{database} +X_CHEF_type_CHEF_X:#{type}" + end + select_url = "/solr/select?#{to_params(options)}" + Chef::Log.debug("Sending #{select_url} to Solr") + req = Net::HTTP::Get.new(select_url) + res = @http.request(req) + unless res.kind_of?(Net::HTTPSuccess) + res.error! + end + eval(res.body) + end + + def post_to_solr(doc) + req = Net::HTTP::Post.new("/solr/update", "Content-Type" => "text/xml") + req.body = doc.to_s + res = @http.request(req) + unless res.kind_of?(Net::HTTPSuccess) + res.error! + end + res + end + + def solr_add(data) + data = [data] unless data.is_a?(Array) + + xml_document = LibXML::XML::Document.new + xml_add = LibXML::XML::Node.new("add") + data.each do |doc| + xml_doc = LibXML::XML::Node.new("doc") + doc.each do |field, values| + values = [values] unless values.kind_of?(Array) + values.each do |v| + xml_field = LibXML::XML::Node.new("field") + xml_field["name"] = field + xml_field.content = v.to_s + xml_doc << xml_field + end + end + xml_add << xml_doc + end + xml_document.root = xml_add + post_to_solr(xml_document.to_s(:indent => false)) + end + + def solr_commit(opts={}) + post_to_solr(generate_single_element("commit", opts)) + end + + def solr_optimize(opts={}) + post_to_solr(generate_single_element("optimize", opts)) + end + + def solr_rollback + post_to_solr(generate_single_element("rollback")) + end + + def solr_delete_by_id(ids) + post_to_solr(generate_delete_document("id", ids)) + end + + def solr_delete_by_query(queries) + post_to_solr(generate_delete_document("query", queries)) + end + + def rebuild_index(url=Chef::Config[:couchdb_url], db=Chef::Config[:couchdb_database]) + solr_delete_by_query("X_CHEF_database_CHEF_X:#{db}") + couchdb = Chef::CouchDB.new(url, db) + Chef::Node.list(true, couchdb).each { |i| i.save } + Chef::Role.list(true, couchdb).each { |i| i.save } + Chef::DataBag.list(true, couchdb).each { |i| i.save; i.list(true).each { |x| x.save } } + true + end + + private + + def generate_single_element(elem, opts={}) + xml_document = LibXML::XML::Document.new + xml_elem = LibXML::XML::Node.new(elem) + opts.each { |k,v| xml_elem[k.to_s] = v.to_s } + xml_document.root = xml_elem + xml_document.to_s(:indent => false) + end + + def generate_delete_document(type, list) + list = [list] unless list.is_a?(Array) + xml_document = LibXML::XML::Document.new + xml_delete = LibXML::XML::Node.new("delete") + xml_document.root = xml_delete + list.each do |id| + xml_id = LibXML::XML::Node.new(type) + xml_id.content = id.to_s + xml_delete << xml_id + end + xml_document.to_s(:indent => false) + end + + # Thanks to Merb! + def to_params(params_hash) + params = '' + stack = [] + + params_hash.each do |k, v| + if v.is_a?(Hash) + stack << [k,v] + else + params << "#{k}=#{escape(v)}&" + end + end + + stack.each do |parent, hash| + hash.each do |k, v| + if v.is_a?(Hash) + stack << ["#{parent}[#{k}]", escape(v)] + else + params << "#{parent}[#{k}]=#{escape(v)}&" + end + end + end + + params.chop! # trailing & + params + end + + # escapes a query key/value for http + # Thanks to RSolr! + def escape(s) + s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { + '%'+$1.unpack('H2'*$1.size).join('%').upcase + }.tr(' ', '+') + end + + end +end + diff --git a/chef-solr/lib/chef/solr/application/indexer.rb b/chef-solr/lib/chef/solr/application/indexer.rb new file mode 100644 index 0000000000..e41d9daa22 --- /dev/null +++ b/chef-solr/lib/chef/solr/application/indexer.rb @@ -0,0 +1,140 @@ +# +# Author:: AJ Christensen (<aj@opscode.com) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'chef' +require 'chef/log' +require 'chef/config' +require 'chef/application' +require 'chef/solr' +require 'chef/solr/index' +require 'chef/solr/index_actor' +require 'chef/daemon' +require 'chef/nanite' +require 'nanite' +require 'eventmachine' + +class Chef + class Solr + class Application + class Indexer < Chef::Application + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => "/etc/chef/solr.rb", + :description => "The configuration file to use" + + option :log_level, + :short => "-l LEVEL", + :long => "--log_level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :proc => lambda { |l| l.to_sym } + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing", + :proc => nil + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + option :user, + :short => "-u USER", + :long => "--user USER", + :description => "User to set privilege to", + :proc => nil + + option :group, + :short => "-g GROUP", + :long => "--group GROUP", + :description => "Group to set privilege to", + :proc => nil + + option :daemonize, + :short => "-d", + :long => "--daemonize", + :description => "Daemonize the process", + :proc => lambda { |p| true } + + option :nanite_identity, + :long => "--nanite-identity ID", + :description => "The nanite identity" + + option :nanite_host, + :long => "--nanite-host HOST", + :description => "The nanite host" + + option :nanite_port, + :long => "--nanite-port PORT", + :description => "The nanite port" + + option :nanite_user, + :long => "--nanite-user USER", + :description => "The nanite user" + + option :nanite_pass, + :long => "--nanite-pass PASS", + :description => "The nanite password" + + option :nanite_vhost, + :long => "--nanite-vhost VHOST", + :description => "The nanite vhost" + + def initialize + super + + @index = Chef::Solr::Index.new + ::Nanite::Log.logger = Chef::Log.logger + end + + def setup_application + Chef::Daemon.change_privilege + identity = Chef::Config[:nanite_identity] ? Chef::Config[:nanite_identity] : Chef::Nanite.get_identity("solr-indexer") + @nanite_config = { + :host => Chef::Config[:nanite_host], + :port => 5672, + :user => Chef::Config[:nanite_user], + :pass => Chef::Config[:nanite_pass], + :vhost => Chef::Config[:nanite_vhost], + :identity => identity, + :format => :json + } + Chef::Log.level(Chef::Config[:log_level]) + end + + def run_application + if Chef::Config[:daemonize] + Chef::Daemon.daemonize("chef-solr-indexer") + end + + EM.run do + agent = ::Nanite::Agent.start(@nanite_config) + agent.register(Chef::Solr::IndexActor.new, 'index') + agent.send :advertise_services + end + end + end + end + end +end diff --git a/chef-solr/lib/chef/solr/application/rebuild.rb b/chef-solr/lib/chef/solr/application/rebuild.rb new file mode 100644 index 0000000000..0c33dc3a92 --- /dev/null +++ b/chef-solr/lib/chef/solr/application/rebuild.rb @@ -0,0 +1,120 @@ +# +# Author:: AJ Christensen (<aj@opscode.com) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'chef' +require 'chef/log' +require 'chef/config' +require 'chef/application' +require 'chef/solr' +require 'chef/solr/index' +require 'nanite' +require 'eventmachine' + +class Chef + class Solr + class Application + class Rebuild < Chef::Application + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => "/etc/chef/solr.rb", + :description => "The configuration file to use" + + option :log_level, + :short => "-l LEVEL", + :long => "--log_level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :proc => lambda { |l| l.to_sym } + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing", + :proc => nil + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + option :nanite_identity, + :long => "--nanite-identity ID", + :description => "The nanite identity" + + option :nanite_host, + :long => "--nanite-host HOST", + :description => "The nanite host" + + option :nanite_port, + :long => "--nanite-port PORT", + :description => "The nanite port" + + option :nanite_user, + :long => "--nanite-user USER", + :description => "The nanite user" + + option :nanite_pass, + :long => "--nanite-pass PASS", + :description => "The nanite password" + + option :nanite_vhost, + :long => "--nanite-vhost VHOST", + :description => "The nanite vhost" + + option :couchdb_database, + :short => "-d DB", + :long => "--couchdb-database DB", + :description => "The CouchDB Database to re-index" + + option :couchdb_url, + :short => "-u URL", + :long => "--couchdb-url URL", + :description => "The CouchDB URL" + + def initialize + super + + @index = Chef::Solr::Index.new + ::Nanite::Log.logger = Chef::Log.logger + end + + def setup_application + Chef::Log.level(Chef::Config[:log_level]) + Chef::Log.warn("This operation is destructive!") + Chef::Log.warn("I'm going to count to 10, and then delete your Solr index and rebuild it.") + Chef::Log.warn("CTRL-C will, of course, stop this disaster.") + 0.upto(10) do |num| + Chef::Log.warn("... #{num}") + sleep 1 + end + Chef::Log.warn("... Bombs away!") + end + + def run_application + s = Chef::Solr.new(Chef::Config[:solr_url]) + Chef::Log.info("Destroying the index") + s.rebuild_index + end + end + end + end +end diff --git a/chef-solr/lib/chef/solr/application/solr.rb b/chef-solr/lib/chef/solr/application/solr.rb new file mode 100644 index 0000000000..44bc80c0a8 --- /dev/null +++ b/chef-solr/lib/chef/solr/application/solr.rb @@ -0,0 +1,165 @@ +# +# Author:: AJ Christensen (<aj@opscode.com) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'chef' +require 'chef/log' +require 'chef/config' +require 'chef/application' +require 'chef/daemon' +require 'chef/client' + +class Chef + class Solr + class Application + class Solr < Chef::Application + + option :config_file, + :short => "-c CONFIG", + :long => "--config CONFIG", + :default => "/etc/chef/solr.rb", + :description => "The configuration file to use" + + option :log_level, + :short => "-l LEVEL", + :long => "--log_level LEVEL", + :description => "Set the log level (debug, info, warn, error, fatal)", + :proc => lambda { |l| l.to_sym } + + option :log_location, + :short => "-L LOGLOCATION", + :long => "--logfile LOGLOCATION", + :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing", + :proc => nil + + option :help, + :short => "-h", + :long => "--help", + :description => "Show this message", + :on => :tail, + :boolean => true, + :show_options => true, + :exit => 0 + + option :user, + :short => "-u USER", + :long => "--user USER", + :description => "User to set privilege to", + :proc => nil + + option :group, + :short => "-g GROUP", + :long => "--group GROUP", + :description => "Group to set privilege to", + :proc => nil + + option :daemonize, + :short => "-d", + :long => "--daemonize", + :description => "Daemonize the process", + :proc => lambda { |p| true } + + option :solr_jetty_path, + :short => "-W PATH", + :long => "--solr-jetty-dir PATH", + :description => "Where to place the Solr Jetty instance" + + option :solr_data_path, + :short => "-D PATH", + :long => "--solr-data-dir PATH", + :description => "Where the Solr data lives" + + option :solr_home_path, + :short => "-H PATH", + :long => "--solr-home-dir PATH", + :description => "Solr home directory" + + option :solr_heap_size, + :short => "-x SIZE", + :long => "--solor-heap-size SIZE", + :description => "Set the size of the Java Heap" + + option :solr_java_opts, + :short => "-j OPTS", + :long => "--java-opts OPTS", + :description => "Raw options passed to Java" + + def initialize + super + Chef::Log.level(Chef::Config[:log_level]) + end + + def setup_application + Chef::Daemon.change_privilege + + # Build up a client + c = Chef::Client.new + c.build_node(nil, true) + + solr_base = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "solr")) + + # Create the Jetty container + unless File.directory?(Chef::Config[:solr_jetty_path]) + Chef::Log.warn("Initializing the Jetty container") + solr_jetty_dir = Chef::Resource::Directory.new(Chef::Config[:solr_jetty_path], nil, c.node) + solr_jetty_dir.recursive(true) + solr_jetty_dir.run_action(:create) + solr_jetty_untar = Chef::Resource::Execute.new("untar_jetty", nil, c.node) + solr_jetty_untar.command("tar zxvf #{File.join(solr_base, 'solr-jetty.tar.gz')}") + solr_jetty_untar.cwd(Chef::Config[:solr_jetty_path]) + solr_jetty_untar.run_action(:run) + end + + # Create the solr home + unless File.directory?(Chef::Config[:solr_home_path]) + Chef::Log.warn("Initializing Solr home directory") + solr_home_dir = Chef::Resource::Directory.new(Chef::Config[:solr_home_path], nil, c.node) + solr_home_dir.recursive(true) + solr_home_dir.run_action(:create) + solr_jetty_untar = Chef::Resource::Execute.new("untar_solr_home", nil, c.node) + solr_jetty_untar.command("tar zxvf #{File.join(solr_base, 'solr-home.tar.gz')}") + solr_jetty_untar.cwd(Chef::Config[:solr_home_path]) + solr_jetty_untar.run_action(:run) + end + + # Create the solr data path + unless File.directory?(Chef::Config[:solr_data_path]) + Chef::Log.warn("Initializing Solr data directory") + solr_data_dir = Chef::Resource::Directory.new(Chef::Config[:solr_data_path], nil, c.node) + solr_data_dir.recursive(true) + solr_data_dir.run_action(:create) + end + end + + def run_application + if Chef::Config[:daemonize] + Chef::Daemon.daemonize("chef-solr") + end + Dir.chdir(Chef::Config[:solr_jetty_path]) do + command = "java -Xmx#{Chef::Config[:solr_heap_size]} -Xms#{Chef::Config[:solr_heap_size]}" + command << " -Dsolr.solr.data=#{Chef::Config[:solr_data_path]}" + command << " -Dsolr.solr.home=#{Chef::Config[:solr_home_path]}" + command << " #{Chef::Config[:solr_java_opts]}" if Chef::Config[:solr_java_opts] + command << " -jar #{File.join(Chef::Config[:solr_jetty_path], 'start.jar')}" + Chef::Log.info("Starting Solr with #{command}") + Kernel.exec(command) + + end + end + end + end + end +end diff --git a/chef-solr/lib/chef/solr/index.rb b/chef-solr/lib/chef/solr/index.rb new file mode 100644 index 0000000000..0ebd553356 --- /dev/null +++ b/chef-solr/lib/chef/solr/index.rb @@ -0,0 +1,153 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/config' +require 'chef/solr' +require 'libxml' +require 'net/http' + +class Chef + class Solr + class Index < Solr + + def add(id, database, type, item) + raise ArgumentError, "Object must respond to keys!" unless item.respond_to?(:keys) + to_index = flatten_and_expand(item) + to_index["X_CHEF_id_CHEF_X"] = id + to_index["X_CHEF_database_CHEF_X"] = database + to_index["X_CHEF_type_CHEF_X"] = type + solr_add(to_index) + to_index + end + + def delete(id) + solr_delete_by_id(id) + end + + def delete_by_query(query) + solr_delete_by_query(query) + end + + def flatten_and_expand(item, fields=Hash.new, parent=nil) + item.keys.each do |key| + # If we have a parent, we want to add the current key as a value + if parent + # foo_bar = bar + set_field_value(fields, parent, key) + # foo_X = bar, etc. + make_expando_fields(parent).each do |ex_key| + set_field_value(fields, ex_key, key) + end + end + case item[key] + when Hash + parent_key = parent ? "#{parent}_#{key}" : key + flatten_and_expand(item[key], fields, parent_key) + else + parent_key = parent ? "#{parent}_#{key}" : key + set_field_value(fields, key, item[key]) + set_field_value(fields, parent_key, item[key]) if parent + make_expando_fields(parent_key).each do |ex_key| + set_field_value(fields, ex_key, item[key]) + end + end + end + fields + end + + def make_expando_fields(key) + key = key.to_s + fields = Array.new + parts = key.split("_") + length = parts.length + parts.each_index do |i| + beginning = nil + remainder = nil + if i == 0 + beginning = "X" + else + beginning = parts[0..i-1].join("_") + end + + if i == length-1 + remainder = "X" + else + remainder = parts[i+1..-1].join("_") + end + + if beginning == "X" || remainder == "X" + unless beginning == "X" && remainder == "X" + fields << "#{beginning}_#{remainder}" + end + else + fields << "#{beginning}_X_#{remainder}" + end + end + fields + end + + def set_field_value(fields, key, value) + key = key.to_s + if fields.has_key?(key) + convert_field_to_array(fields, key, value) unless fields[key].kind_of?(Array) + add_value_to_field_array(fields, key, value) + else + check_value(value) + if value.kind_of?(Array) + fields[key] = Array.new + value.each do |v| + fields[key] << v.to_s + end + else + fields[key] = value.to_s + end + end + fields + end + + def add_value_to_field_array(fields, key, value) + check_value(value) + if value.kind_of?(Array) + value.each do |v| + check_value(v) + fields[key] << v.to_s unless fields[key].include?(v.to_s) + end + else + fields[key] << value.to_s unless fields[key].include?(value.to_s) + end + fields + end + + def convert_field_to_array(fields, key, value) + if fields[key] != value + safe = fields[key] + fields[key] = [ safe ] + end + fields + end + + def check_value(value) + raise ArgumentError, "Value must not be a type of hash!" if value.kind_of?(Hash) + value + end + + end + end +end + diff --git a/chef-solr/lib/chef/solr/index_actor.rb b/chef-solr/lib/chef/solr/index_actor.rb new file mode 100644 index 0000000000..164785889f --- /dev/null +++ b/chef-solr/lib/chef/solr/index_actor.rb @@ -0,0 +1,90 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rubygems' +require 'chef/log' +require 'chef/config' +require 'chef/solr' +require 'chef/solr/index' +require 'chef/node' +require 'chef/role' +require 'chef/rest' +require 'chef/data_bag' +require 'chef/data_bag_item' +require 'chef/couchdb' +require 'nanite/actor' + +class Chef + class Solr + class IndexActor + include ::Nanite::Actor + + expose :add, :delete, :commit, :optimize + + def add(payload) + index = Chef::Solr::Index.new + + pitem = nil + if payload["item"].respond_to?(:keys) + pitem = payload["item"] + elsif payload["item"].respond_to?(:to_hash) + pitem = payload["item"].to_hash + else + return generate_response() { raise ArgumentError, "Payload item does not respond to :keys or :to_hash, cannot index!" } + end + response = generate_response { index.add(payload["id"], payload["database"], payload["type"], pitem) } + Chef::Log.info("Indexing #{payload["type"]} #{payload["id"]} from #{payload["database"]} status #{response[:status]}#{response[:status] == :error ? ' ' + response[:error] : ''}") + response + end + + def delete(payload) + index = Chef::Solr::Index.new + generate_response { index.delete(payload["id"]) } + Chef::Log.info("Removed #{payload["id"]} from the index") + end + + def commit(payload) + index = Chef::Solr::Index.new + generate_response { index.solr_commit } + Chef::Log.info("Committed the index") + end + + def optimize(payload) + index = Chef::Solr::Index.new + generate_response { index.solr_optimize } + Chef::Log.info("Optimized the index") + end + + private + def generate_response(&block) + response = {} + begin + block.call + rescue + response[:status] = :error + response[:error] = $! + else + response[:status] = :ok + end + response + end + + end + end +end + diff --git a/chef-solr/lib/chef/solr/query.rb b/chef-solr/lib/chef/solr/query.rb new file mode 100644 index 0000000000..ec788677bf --- /dev/null +++ b/chef-solr/lib/chef/solr/query.rb @@ -0,0 +1,87 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/couchdb' +require 'chef/node' +require 'chef/role' +require 'chef/data_bag' +require 'chef/data_bag_item' +require 'chef/solr' +require 'chef/log' +require 'chef/config' + +class Chef + class Solr + class Query < Chef::Solr + + # Create a new Query object - takes the solr_url and optional + # couchdb_database to inflate objects into. + def initialize(solr_url=Chef::Config[:solr_url], database=Chef::Config[:couchdb_database]) + super(solr_url) + @database = database + @couchdb = Chef::CouchDB.new(nil, database) + end + + # A raw query against CouchDB - takes the type of object to find, and raw + # Solr options. + # + # You'll wind up having to page things yourself. + def raw(type, options={}) + case type + when "role",:role,"node",:node + qtype = type + else + qtype = [ "data_bag_item", type ] + end + Chef::Log.debug("Searching #{@database} #{qtype.inspect} for #{options.inspect}") + results = solr_select(@database, qtype, options) + if results["response"]["docs"].length > 0 + objects = @couchdb.bulk_get( + results["response"]["docs"].collect { |d| d["X_CHEF_id_CHEF_X"] } + ) + else + objects = [] + end + [ objects, results["response"]["start"], results["response"]["numFound"], results["responseHeader"] ] + end + + # Search Solr for objects of a given type, for a given query. If you give + # it a block, it will handle the paging for you dynamically. + def search(type, query="*:*", sort=nil, start=0, rows=20, &block) + options = { + :q => query, + :start => start, + :rows => rows + } + options[:sort] = sort if sort && ! sort.empty? + objects, start, total, response_header = raw(type, options) + if block + objects.each { |o| block.call(o) } + unless (start + objects.length) >= total + nstart = start + rows + search(type, query, sort, nstart, rows, &block) + end + true + else + [ objects, start, total ] + end + end + end + end +end + diff --git a/chef-solr/solr/solr-home.tar.gz b/chef-solr/solr/solr-home.tar.gz Binary files differnew file mode 100644 index 0000000000..e1c0ab2db7 --- /dev/null +++ b/chef-solr/solr/solr-home.tar.gz diff --git a/chef-solr/solr/solr-jetty.tar.gz b/chef-solr/solr/solr-jetty.tar.gz Binary files differnew file mode 100644 index 0000000000..ca010de6cf --- /dev/null +++ b/chef-solr/solr/solr-jetty.tar.gz diff --git a/chef-solr/spec/chef/solr/index_spec.rb b/chef-solr/spec/chef/solr/index_spec.rb new file mode 100644 index 0000000000..90a33ecc8f --- /dev/null +++ b/chef-solr/spec/chef/solr/index_spec.rb @@ -0,0 +1,168 @@ +require File.expand_path(File.join("#{File.dirname(__FILE__)}", '..', '..', 'spec_helper')) + +describe Chef::Solr::Index do + before(:each) do + @index = Chef::Solr::Index.new + end + + describe "initialize" do + it "should return a Chef::Solr::Index" do + @index.should be_a_kind_of(Chef::Solr::Index) + end + end + + describe "add" do + before(:each) do + @index.stub!(:solr_add).and_return(true) + @index.stub!(:solr_commit).and_return(true) + end + + it "should take an object that responds to .keys as it's argument" do + lambda { @index.add(1, "chef_opscode", "node", { :one => :two }) }.should_not raise_error(ArgumentError) + lambda { @index.add(1, "chef_opscode", "node", "SOUP") }.should raise_error(ArgumentError) + lambda { @index.add(2, "chef_opscode", "node", mock("Foo", :keys => true)) }.should_not raise_error(ArgumentError) + end + + it "should index the object as a single flat hash, with only strings or arrays as values" do + validate = { + "X_CHEF_id_CHEF_X" => 1, + "X_CHEF_database_CHEF_X" => "monkey", + "X_CHEF_type_CHEF_X" => "snakes", + "foo" => "bar", + "battles" => [ "often", "but", "for" ], + "battles_often" => "sings like smurfs", + "often" => "sings like smurfs", + "battles_but" => "still has good records", + "but" => "still has good records", + "battles_for" => [ "all", "of", "that" ], + "for" => [ "all", "of", "that" ], + "snoopy" => "sits_in_a_barn", + "battles_X" => [ "sings like smurfs", "still has good records", "all", "of", "that" ], + "X_often" => "sings like smurfs", + "X_but" => "still has good records", + "X_for" => [ "all", "of", "that" ] + } + to_index = @index.add(1, "monkey", "snakes", { + "foo" => :bar, + "battles" => { + "often" => "sings like smurfs", + "but" => "still has good records", + "for" => [ "all", "of", "that" ] + }, + "snoopy" => "sits_in_a_barn" + }) + validate.each do |k, v| + if v.kind_of?(Array) + # Every entry in to_index[k] should be in v + r = to_index[k] & v + r.length.should == to_index[k].length + else + to_index[k].should == v + end + end + end + + it "should send the document to solr" do + @index.should_receive(:solr_add) + @index.add(1, "monkey", "snakes", { "foo" => "bar" }) + end + end + + describe "delete" do + it "should delete by id" do + @index.should_receive(:solr_delete_by_id).with(1) + @index.delete(1) + end + end + + describe "delete_by_query" do + it "should delete by query" do + @index.should_receive(:solr_delete_by_query).with("foo:bar") + @index.delete_by_query("foo:bar") + end + end + + describe "flatten_and_expand" do + before(:each) do + @fields = Hash.new + end + + it "should set a value for the parent as key, with the key as the value" do + @index.flatten_and_expand({ "one" => "woot" }, @fields, "omerta") + @fields["omerta"].should == "one" + end + + it "should call itself recursively for values that are hashes" do + @index.flatten_and_expand({ "one" => { "two" => "three", "four" => { "five" => "six" } }}, @fields) + { + "one" => [ "two", "four" ], + "one_two" => "three", + "X_two" => "three", + "two" => "three", + "one_four" => "five", + "X_four" => "five", + "one_X" => [ "three", "five" ], + "one_four_five" => "six", + "X_four_five" => "six", + "one_X_five" => "six", + "one_four_X" => "six", + "five" => "six" + }.each do |k, v| + @fields[k].should == v + end + end + + end + + describe "set_field_value" do + before(:each) do + @fields = Hash.new + end + + it "should set a value in the fields hash" do + @index.set_field_value(@fields, "one", "two") + @fields["one"].should eql("two") + end + + it "should create an array of all values, if a field is set twice" do + @index.set_field_value(@fields, "one", "two") + @index.set_field_value(@fields, "one", "three") + @fields["one"].should eql([ "two", "three" ]) + end + + it "should not add duplicate values to a field when there is one string entry" do + @index.set_field_value(@fields, "one", "two") + @index.set_field_value(@fields, "one", "two") + @fields["one"].should eql("two") + end + + it "should not add duplicate values to a field when it is an array" do + @index.set_field_value(@fields, "one", "two") + @index.set_field_value(@fields, "one", "three") + @index.set_field_value(@fields, "one", "two") + @fields["one"].should eql([ "two", "three" ]) + end + + it "should accept arrays as values" do + @index.set_field_value(@fields, "one", [ "two", "three" ]) + @fields["one"].should eql([ "two", "three" ]) + end + + it "should not duplicate values when a field has been set with multiple arrays" do + @index.set_field_value(@fields, "one", [ "two", "three" ]) + @index.set_field_value(@fields, "one", [ "two", "four" ]) + @fields["one"].should eql([ "two", "three", "four" ]) + end + + + it "should allow you to set a value in the fields hash to an array" do + @index.set_field_value(@fields, "one", [ "foo", "bar", "baz" ]) + end + + it "should not allow you to set a value in the fields hash to a hash" do + lambda { + @index.set_field_value(@fields, "one", { "two" => "three" }) + }.should raise_error(ArgumentError) + end + end +end diff --git a/chef-solr/spec/chef/solr/query_spec.rb b/chef-solr/spec/chef/solr/query_spec.rb new file mode 100644 index 0000000000..ef8afe3127 --- /dev/null +++ b/chef-solr/spec/chef/solr/query_spec.rb @@ -0,0 +1,14 @@ +require File.expand_path(File.join("#{File.dirname(__FILE__)}", '..', '..', 'spec_helper')) + +describe Chef::Solr::Query do + before(:each) do + @query = Chef::Solr::Query.new + end + + describe "initialize" do + it "should return a Chef::Solr::Query" do + @query.should be_a_kind_of(Chef::Solr::Query) + end + end +end + diff --git a/chef-solr/spec/chef/solr_spec.rb b/chef-solr/spec/chef/solr_spec.rb new file mode 100644 index 0000000000..ef5174756b --- /dev/null +++ b/chef-solr/spec/chef/solr_spec.rb @@ -0,0 +1,167 @@ +require File.expand_path(File.join("#{File.dirname(__FILE__)}", '..', 'spec_helper')) + +describe Chef::Solr do + before(:each) do + @solr = Chef::Solr.new + end + + describe "initialize" do + it "should create a new Chef::Solr object" do + @solr.should be_a_kind_of(Chef::Solr) + end + + it "should accept an alternate solr url" do + solr = Chef::Solr.new("http://example.com") + solr.solr_url.should eql("http://example.com") + end + end + + describe "solr_select" do + before(:each) do + @http_response = mock( + "Net::HTTP::Response", + :kind_of? => Net::HTTPSuccess, + :body => "{ :some => :hash }" + ) + @http = mock("Net::HTTP", :request => @http_response) + @solr.http = @http + end + + it "should call get to /solr/select with the escaped query" do + Net::HTTP::Get.should_receive(:new).with(%r(q=hostname%3Alatte)) + @solr.solr_select("chef_opscode", "node", :q => "hostname:latte") + end + + it "should call get to /solr/select with wt=ruby" do + Net::HTTP::Get.should_receive(:new).with(%r(wt=ruby)) + @solr.solr_select("chef_opscode", "node", :q => "hostname:latte") + end + + it "should call get to /solr/select with indent=off" do + Net::HTTP::Get.should_receive(:new).with(%r(indent=off)) + @solr.solr_select("chef_opscode", "node", :q => "hostname:latte") + end + + it "should call get to /solr/select with filter query" do + Net::HTTP::Get.should_receive(:new).with(/fq=%2BX_CHEF_database_CHEF_X%3Achef_opscode\+%2BX_CHEF_type_CHEF_X%3Anode/) + @solr.solr_select("chef_opscode", "node", :q => "hostname:latte") + end + + it "should return the evaluated response body" do + res = @solr.solr_select("chef_opscode", "node", :q => "hostname:latte") + res.should == { :some => :hash } + end + end + + + describe "post_to_solr" do + before(:each) do + @http_response = mock( + "Net::HTTP::Response", + :kind_of? => Net::HTTPSuccess, + :body => "{ :some => :hash }" + ) + @http_request = mock( + "Net::HTTP::Request", + :body= => true + ) + @http = mock("Net::HTTP", :request => @http_response) + @solr.http = @http + Net::HTTP::Post.stub!(:new).and_return(@http_request) + @doc = { "foo" => "bar" } + end + + it "should post to /solr/update" do + Net::HTTP::Post.should_receive(:new).with("/solr/update", "Content-Type" => "text/xml").and_return(@http_request) + @solr.post_to_solr(@doc) + end + + it "should set the body of the request to the stringified doc" do + @http_request.should_receive(:body=).with("foo") + @solr.post_to_solr(:foo) + end + + it "should send the request to solr" do + @http.should_receive(:request).with(@http_request).and_return(@http_response) + @solr.post_to_solr(:foo) + end + end + + describe "solr_add" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + @data = { "foo" => "bar" } + end + + it "should send valid XML to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<add><doc><field name=\"foo\">bar</field></doc></add>\n") + @solr.solr_add(@data) + end + end + + describe "solr_commit" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + end + + it "should send valid commit xml to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<commit/>\n") + @solr.solr_commit + end + end + + describe "solr_optimize" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + end + + it "should send valid commit xml to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<optimize/>\n") + @solr.solr_optimize + end + end + + describe "solr_rollback" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + end + + it "should send valid commit xml to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rollback/>\n") + @solr.solr_rollback + end + end + + describe "solr_delete_by_id" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + end + + it "should send valid delete id xml to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<delete><id>1</id></delete>\n") + @solr.solr_delete_by_id(1) + end + + it "should accept multiple ids" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<delete><id>1</id><id>2</id></delete>\n") + @solr.solr_delete_by_id([ 1, 2 ]) + end + end + + describe "solr_delete_by_query" do + before(:each) do + @solr.stub!(:post_to_solr).and_return(true) + end + + it "should send valid delete id xml to solr" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<delete><query>foo:bar</query></delete>\n") + @solr.solr_delete_by_query("foo:bar") + end + + it "should accept multiple ids" do + @solr.should_receive(:post_to_solr).with("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<delete><query>foo:bar</query><query>baz:bum</query></delete>\n") + @solr.solr_delete_by_query([ "foo:bar", "baz:bum" ]) + end + end + +end diff --git a/chef-solr/spec/spec_helper.rb b/chef-solr/spec/spec_helper.rb new file mode 100644 index 0000000000..0a6ee8b08a --- /dev/null +++ b/chef-solr/spec/spec_helper.rb @@ -0,0 +1,13 @@ +require 'rubygems' +require 'spec' + +$LOAD_PATH.unshift(File.dirname(__FILE__)) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +require 'chef' +require 'chef/solr' +require 'chef/solr/index' +require 'chef/solr/query' + +Spec::Runner.configure do |config| + +end diff --git a/chef/Rakefile b/chef/Rakefile index 45a0da15d0..30eb3f7d8a 100644 --- a/chef/Rakefile +++ b/chef/Rakefile @@ -4,7 +4,7 @@ require 'rake/rdoctask' require './tasks/rspec.rb' GEM = "chef" -CHEF_VERSION = "0.7.9" +CHEF_VERSION = "0.8.0" AUTHOR = "Adam Jacob" EMAIL = "adam@opscode.com" HOMEPAGE = "http://wiki.opscode.com/display/chef" diff --git a/chef/lib/chef.rb b/chef/lib/chef.rb index 9cec0cb7e3..9551cedc4a 100644 --- a/chef/lib/chef.rb +++ b/chef/lib/chef.rb @@ -27,7 +27,7 @@ require 'chef/config' Dir[File.join(File.dirname(__FILE__), 'chef/mixin/**/*.rb')].sort.each { |lib| require lib } class Chef - VERSION = '0.7.9' + VERSION = '0.8.0' end # Adds a Dir.glob to Ruby 1.8.5, for compat diff --git a/chef/lib/chef/application/indexer.rb b/chef/lib/chef/application/indexer.rb deleted file mode 100644 index 92bb9f08a7..0000000000 --- a/chef/lib/chef/application/indexer.rb +++ /dev/null @@ -1,141 +0,0 @@ -# -# Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require 'chef/application' -require 'chef/queue' -require 'chef/search' -require 'chef/search_index' -require 'chef/config' -require 'chef/daemon' -require 'chef/log' - - -class Chef::Application::Indexer < Chef::Application - - option :config_file, - :short => "-c CONFIG", - :long => "--config CONFIG", - :default => "/etc/chef/server.rb", - :description => "The configuration file to use" - - option :log_level, - :short => "-l LEVEL", - :long => "--log_level LEVEL", - :description => "Set the log level (debug, info, warn, error, fatal)", - :proc => lambda { |l| l.to_sym } - - option :log_location, - :short => "-L LOGLOCATION", - :long => "--logfile LOGLOCATION", - :description => "Set the log file location, defaults to STDOUT - recommended for daemonizing", - :proc => nil - - option :help, - :short => "-h", - :long => "--help", - :description => "Show this message", - :on => :tail, - :boolean => true, - :show_options => true, - :exit => 0 - - option :user, - :short => "-u USER", - :long => "--user USER", - :description => "User to set privilege to", - :proc => nil - - option :group, - :short => "-g GROUP", - :long => "--group GROUP", - :description => "Group to set privilege to", - :proc => nil - - option :daemonize, - :short => "-d", - :long => "--daemonize", - :description => "Daemonize the process", - :proc => lambda { |p| true } - - option :version, - :short => "-v", - :long => "--version", - :description => "Show chef version", - :boolean => true, - :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, - :exit => 0 - - def initialize - super - - @chef_search_indexer = nil - end - - # Create a new search indexer and connect to the stomp queues - def setup_application - Chef::Daemon.change_privilege - - @chef_search_indexer = Chef::SearchIndex.new - Chef::Queue.connect - Chef::Queue.subscribe(:queue, "index") - Chef::Queue.subscribe(:queue, "remove") - end - - # Run the indexer, optionally daemonizing. - def run_application - if Chef::Config[:daemonize] - Chef::Daemon.daemonize("chef-indexer") - end - - if Chef::Config[:queue_prefix] - queue_prefix = Chef::Config[:queue_prefix] - queue_partial_url = "/queue/#{queue_prefix}/chef" - else - queue_partial_url = "/queue/chef" - end - - loop do - object, headers = Chef::Queue.receive_msg - Chef::Log.info("Headers #{headers.inspect}") - if headers["destination"] == "#{queue_partial_url}/index" - start_timer = Time.new - @chef_search_indexer.add(object) - @chef_search_indexer.commit - final_timer = Time.new - Chef::Log.info("Indexed object from #{headers['destination']} in #{final_timer - start_timer} seconds") - elsif headers["destination"] == "#{queue_partial_url}/remove" - start_timer = Time.new - @chef_search_indexer.delete(object) - @chef_search_indexer.commit - final_timer = Time.new - Chef::Log.info("Removed object from #{headers['destination']} in #{final_timer - start_timer} seconds") - end - end - rescue SystemExit => e - raise - rescue Exception => e - if Chef::Config[:interval] - Chef::Log.error("#{e.class}") - Chef::Log.fatal("#{e}\n#{e.backtrace.join("\n")}") - Chef::Log.fatal("Sleeping for #{Chef::Config[:delay]} seconds before trying again") - sleep Chef::Config[:delay] - retry - else - raise - end - end -end diff --git a/chef/lib/chef/client.rb b/chef/lib/chef/client.rb index c561fedb32..7f67c6bef0 100644 --- a/chef/lib/chef/client.rb +++ b/chef/lib/chef/client.rb @@ -46,9 +46,14 @@ class Chef @json_attribs = nil @node_name = nil @node_exists = true - Ohai::Log.logger = Chef::Log.logger + Mixlib::Auth::Log.logger = Ohai::Log.logger = Chef::Log.logger @ohai = Ohai::System.new - @rest = Chef::REST.new(Chef::Config[:registration_url]) + @ohai_has_run = false + if File.exists?(Chef::Config[:client_key]) + @rest = Chef::REST.new(Chef::Config[:chef_server_url]) + else + @rest = Chef::REST.new(Chef::Config[:chef_server_url], nil, nil) + end end # Do a full run for this Chef::Client. Calls: @@ -71,13 +76,9 @@ class Chef determine_node_name register - authenticate build_node(@node_name) save_node - sync_library_files - sync_attribute_files - sync_definitions - sync_recipes + sync_cookbooks save_node converge save_node @@ -116,9 +117,14 @@ class Chef end def determine_node_name - run_ohai unless @safe_name && @node_name - @node_name ||= @ohai[:fqdn] ? @ohai[:fqdn] : @ohai[:hostname] + run_ohai + if Chef::Config[:node_name] + @node_name = Chef::Config[:node_name] + else + @node_name ||= @ohai[:fqdn] ? @ohai[:fqdn] : @ohai[:hostname] + Chef::Config[:node_name] = @node_name + end @safe_name = @node_name.gsub(/\./, '_') end @node_name @@ -177,68 +183,22 @@ class Chef @node[:tags] = Array.new unless @node.attribute?(:tags) @node end - - # If this node has been registered before, this method will fetch the current registration - # data. - # - # If it has not, we register it by calling create_registration. - # + + # # === Returns # true:: Always returns true def register - determine_node_name unless @node_name - Chef::Log.debug("Registering #{@safe_name} for an openid") - - begin - if @rest.get_rest("registrations/#{@safe_name}") - @secret = Chef::FileCache.load(File.join("registration", @safe_name)) - end - rescue Net::HTTPServerException => e - case e.message - when /^404/ - create_registration - else - raise - end - rescue Chef::Exceptions::FileNotFound - Chef::Application.fatal! "A remote registration already exists for #{@safe_name}, however the local shared secret does not exist." + - " To remedy this, you could delete the registration via webUI/REST, change the node_name option in config.rb" + - " (or use the -N/--node-name option to the CLI) or" + - " copy the old shared secret to #{File.join(Chef::Config[:file_cache_path], 'registration', @safe_name)}", 3 + if File.exists?(Chef::Config[:validation_key]) + @vr = Chef::REST.new(Chef::Config[:client_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]) + @vr.register(@node_name, Chef::Config[:client_key]) + else + Chef::Log.debug("Validation key #{Chef::Config[:validation_key]} is not present - skipping registration") end - + # We now have the client key, and should use it from now on. + @rest = Chef::REST.new(Chef::Config[:chef_server_url]) true end - # Generates a random secret, stores it in the Chef::Filestore with the "registration" key, - # and posts our nodes registration information to the server. - # - # === Returns - # true:: Always returns true - def create_registration - @secret = random_password(500) - Chef::FileCache.store(File.join("registration", @safe_name), @secret) - @rest.post_rest("registrations", { :id => @safe_name, :password => @secret, :validation_token => @validation_token }) - true - end - - # Authenticates the node via OpenID. - # - # === Returns - # true:: Always returns true - def authenticate - determine_node_name unless @node_name - Chef::Log.debug("Authenticating #{@safe_name} via openid") - response = @rest.post_rest('openid/consumer/start', { - "openid_identifier" => "#{Chef::Config[:openid_url]}/openid/server/node/#{@safe_name}", - "submit" => "Verify" - }) - @rest.post_rest( - "#{Chef::Config[:openid_url]}#{response["action"]}", - { "password" => @secret } - ) - end - # Update the file caches for a given cache segment. Takes a segment name # and a hash that matches one of the cookbooks/_attribute_files style # remote file listings. @@ -246,103 +206,81 @@ class Chef # === Parameters # segment<String>:: The cache segment to update # remote_list<Hash>:: A cookbooks/_attribute_files style remote file listing - def update_file_cache(segment, remote_list) - # We need the list of known good attribute files, so we can delete any that are - # just laying about. + def update_file_cache(cookbook_name, parts) + Chef::Log.debug("Synchronizing cookbook #{cookbook_name}") + file_canonical = Hash.new - - remote_list.each do |rf| - cache_file = File.join("cookbooks", rf['cookbook'], segment, rf['name']) - file_canonical[cache_file] = true - # For back-compat between older clients and new chef servers - rf['checksum'] ||= nil - - current_checksum = nil - if Chef::FileCache.has_key?(cache_file) - current_checksum = checksum(Chef::FileCache.load(cache_file, false)) - end + parts.each do |segment, remote_list| + # segement = cookbook segment + # remote_list = list of file hashes + # + # We need the list of known good attribute files, so we can delete any that are + # just laying about. + + + remote_list.each do |rf| + cache_file = File.join("cookbooks", cookbook_name, segment, rf['name']) + file_canonical[cache_file] = true - rf_url = generate_cookbook_url( - rf['name'], - rf['cookbook'], - segment, - @node, - current_checksum ? { 'checksum' => current_checksum } : nil - ) - Chef::Log.debug(rf_url) + # For back-compat between older clients and new chef servers + rf['checksum'] ||= nil + + current_checksum = nil + if Chef::FileCache.has_key?(cache_file) + current_checksum = checksum(Chef::FileCache.load(cache_file, false)) + end - if current_checksum != rf['checksum'] - changed = true - begin - raw_file = @rest.get_rest(rf_url, true) - rescue Net::HTTPRetriableError => e - if e.response.kind_of?(Net::HTTPNotModified) - changed = false - Chef::Log.debug("Cache file #{cache_file} is unchanged") - else - raise e + rf_url = generate_cookbook_url( + rf['name'], + cookbook_name, + segment, + @node, + current_checksum ? { 'checksum' => current_checksum } : nil + ) + if current_checksum != rf['checksum'] + changed = true + begin + raw_file = @rest.get_rest(rf_url, true) + rescue Net::HTTPRetriableError => e + if e.response.kind_of?(Net::HTTPNotModified) + changed = false + Chef::Log.debug("Cache file #{cache_file} is unchanged") + else + raise e + end end - end - if changed - Chef::Log.info("Storing updated #{cache_file} in the cache.") - Chef::FileCache.move_to(raw_file.path, cache_file) + if changed + Chef::Log.info("Storing updated #{cache_file} in the cache.") + Chef::FileCache.move_to(raw_file.path, cache_file) + end end end - end - - Chef::FileCache.list.each do |cache_file| - if cache_file.match("cookbooks/.+?/#{segment}") - unless file_canonical[cache_file] - Chef::Log.info("Removing #{cache_file} from the cache; it is no longer on the server.") - Chef::FileCache.delete(cache_file) + + Chef::FileCache.list.each do |cache_file| + if cache_file =~ /^cookbooks\/(recipes|attributes|definitions|libraries)\// + unless file_canonical[cache_file] + Chef::Log.info("Removing #{cache_file} from the cache; it is no longer on the server.") + Chef::FileCache.delete(cache_file) + end end end + end end - - # Gets all the attribute files included in all the cookbooks available on the server, - # and executes them. - # - # === Returns - # true:: Always returns true - def sync_attribute_files - Chef::Log.debug("Synchronizing attributes") - update_file_cache("attributes", @rest.get_rest("cookbooks/_attribute_files?node=#{@node.name}")) - true - end - - # Gets all the library files included in all the cookbooks available on the server, - # and loads them. - # - # === Returns - # true:: Always returns true - def sync_library_files - Chef::Log.debug("Synchronizing libraries") - update_file_cache("libraries", @rest.get_rest("cookbooks/_library_files?node=#{@node.name}")) - true - end - - # Gets all the definition files included in all the cookbooks available on the server, - # and loads them. - # - # === Returns - # true:: Always returns true - def sync_definitions - Chef::Log.debug("Synchronizing definitions") - update_file_cache("definitions", @rest.get_rest("cookbooks/_definition_files?node=#{@node.name}")) - end - - # Gets all the recipe files included in all the cookbooks available on the server, - # and loads them. + + # Synchronizes all the cookbooks from the chef-server. # # === Returns # true:: Always returns true - def sync_recipes - Chef::Log.debug("Synchronizing recipes") - update_file_cache("recipes", @rest.get_rest("cookbooks/_recipe_files?node=#{@node.name}")) + def sync_cookbooks + Chef::Log.debug("Synchronizing cookbooks") + cookbook_hash = @rest.get_rest("nodes/#{@safe_name}/cookbooks") + cookbook_hash.each do |cookbook_name, parts| + update_file_cache(cookbook_name, parts) + end end # Updates the current node configuration on the server. @@ -382,15 +320,6 @@ class Chef cr.converge true end - - protected - # Generates a random password of "len" length. - def random_password(len) - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - newpass = "" - 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] } - newpass - end end end diff --git a/chef/lib/chef/config.rb b/chef/lib/chef/config.rb index 977786a37d..11a4e4203d 100644 --- a/chef/lib/chef/config.rb +++ b/chef/lib/chef/config.rb @@ -17,6 +17,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/log' require 'mixlib/config' class Chef @@ -54,11 +55,13 @@ class Chef :template_url, :remotefile_url, :search_url, + :chef_server_url, :role_url ].each do |u| c[u] = url end end end + # Override the config dispatch to set the value of log_location configuration option # # === Parameters @@ -79,7 +82,7 @@ class Chef authorized_openid_identifiers nil authorized_openid_providers nil - chef_server_url nil + chef_server_url "http://localhost:4000" cookbook_path [ "/var/chef/site-cookbooks", "/var/chef/cookbooks" ] couchdb_database "chef" couchdb_url "http://localhost:5984" @@ -105,14 +108,9 @@ class Chef openid_store_path "/var/chef/openid/db" openid_url "http://localhost:4001" pid_file nil - queue_host "localhost" - queue_password "" - queue_port 61613 - queue_retry_count 5 - queue_retry_delay 5 - queue_user "" - queue_prefix nil registration_url "http://localhost:4000" + certificate_url "http://localhost:4000" + client_url "http://localhost:4042" remotefile_url "http://localhost:4000" rest_timeout 60 run_command_stderr_timeout 120 @@ -130,5 +128,22 @@ class Chef role_path "/var/chef/roles" role_url "http://localhost:4000" recipe_url nil + solr_url "http://localhost:8983" + solr_jetty_path "/var/chef/solr-jetty" + solr_data_path "/var/chef/solr/data" + solr_home_path "/var/chef/solr" + solr_heap_size "256M" + solr_java_opts nil + nanite_host '0.0.0.0' + nanite_port '5672' + nanite_user 'nanite' + nanite_pass 'testing' + nanite_vhost '/nanite' + nanite_identity nil + + client_key "/etc/chef/client.pem" + validation_key "/etc/chef/validation.pem" + validation_client_name "chef-validator" + end end diff --git a/chef/lib/chef/cookbook_helper.rb b/chef/lib/chef/cookbook_helper.rb new file mode 100644 index 0000000000..520fa38fec --- /dev/null +++ b/chef/lib/chef/cookbook_helper.rb @@ -0,0 +1,90 @@ +module CookbookHelper + + require 'aws/s3' + + def put_in_couchdb_and_s3(cookbook_name, file, revision) + # TODO: set inital state of cookbook to something like 'pre-upload' + cookbook = Cookbook.on(database_from_orgname(params[:organization_id])).new(:display_name => cookbook_name, :revision => revision) + save cookbook + + id = cookbook['_id'] + Merb.logger.debug "Creating cookbook with id = #{id}" + + stream_to_s3(params[:file][:tempfile], id) + + # TODO: if upload successful, set cookbook state to something like 'active' + end + + def validate_tarball(filepath, cookbook_name) + raise "(try creating with 'tar czf cookbook.tgz cookbook/')" unless system("tar", "tzf", filepath) + + # TODO: modify/implement tests and uncomment the next lines + +# required_entry_roots = [cookbook_name] +# allowed_entry_roots = [cookbook_name, "ignore"] + +# entry_roots = `tar tzf #{filepath}`.split("\n").map{|e|e.split('/').first}.uniq + +# illegal_roots = entry_roots - allowed_entry_roots +# raise "tarball root may only contain #{allowed_entry_roots.join(', ')}" unless illegal_roots.empty? + +# missing_required_roots = required_entry_roots - entry_roots +# raise "tarball root must contain #{required_entry_roots.join(', ')}" unless missing_required_roots.empty? + end + + def get_all_cookbook_entries(cookbook_name) + rows = Cookbook.on(database_from_orgname(params[:organization_id])).by_display_name(:key => cookbook_name, :include_docs => true) + Merb.logger.debug "Cookbook has the following entries: #{rows.inspect}" + rows + end + + def cookbook_id(cookbook_name) + rows = get_all_cookbook_entries(cookbook_name) + return nil if rows.empty? + most_recent_record = rows.sort_by{|row| row['revision'].to_i}.last + Merb.logger.debug "Selected #{most_recent_record.inspect}" + [most_recent_record['_id'], most_recent_record['revision']] + end + + # TODO: should we do this once at start-up and test the connection before establishing it? + def establish_connection + AWS::S3::Base.establish_connection!( + :access_key_id => Merb::Config[:aws_secret_access_key_id], + :secret_access_key => Merb::Config[:aws_secret_access_key] + ) + end + + def stream_to_s3(path, object_id) + establish_connection + AWS::S3::S3Object.store("#{object_id}.tgz", open(path), Merb::Config[:aws_cookbook_tarball_s3_bucket]) + end + + def stream_from_s3(cookbook_name, id) + establish_connection + # TODO: if the cookbook is large and the user has a slow connection, will this cause the process's memory to bloat or will it just read from S3 slowly? + stream_file do |response| + AWS::S3::S3Object.stream("#{id}.tgz", Merb::Config[:aws_cookbook_tarball_s3_bucket]) do |chunk| + response.write chunk + end + end + end + + # TODO: the following methods were heisted from opscode-account. if this is how we want to do it, then do some hoisting + + def save(object) + if object.valid? + object.save + else + raise BadRequest, object.errors.full_messages + end + end + + def orgname_to_dbname(orgname) + "chef_#{orgname}" + end + + def database_from_orgname(orgname) + CouchRest::Database.new(CouchRest::Server.new,orgname_to_dbname(orgname)) + end + +end diff --git a/chef/lib/chef/couchdb.rb b/chef/lib/chef/couchdb.rb index 10f43b72c8..9f63ad6aaa 100644 --- a/chef/lib/chef/couchdb.rb +++ b/chef/lib/chef/couchdb.rb @@ -21,40 +21,77 @@ require 'chef/rest' require 'chef/log' require 'digest/sha2' require 'json' +require 'uuidtools' +require 'chef/nanite' class Chef class CouchDB include Chef::Mixin::ParamsValidate - def initialize(url=nil) + def initialize(url=nil, db=Chef::Config[:couchdb_database]) url ||= Chef::Config[:couchdb_url] - @rest = Chef::REST.new(url) + @db = db + @rest = Chef::REST.new(url, nil, nil) + end + + def couchdb_database(args=nil) + if args + @db = args + else + @db + end + end + + def create_id_map + create_design_document( + "id_map", + { + "version" => 1, + "language" => "javascript", + "views" => { + "name_to_id" => { + "map" => <<-EOJS + function(doc) { + emit([ doc.chef_type, doc.name], doc._id); + } + EOJS + }, + "id_to_name" => { + "map" => <<-EOJS + function(doc) { + emit(doc._id, [ doc.chef_type, doc.name ]); + } + EOJS + } + } + } + ) end def create_db @database_list = @rest.get_rest("_all_dbs") - unless @database_list.detect { |db| db == Chef::Config[:couchdb_database] } - response = @rest.put_rest(Chef::Config[:couchdb_database], Hash.new) + unless @database_list.detect { |db| db == couchdb_database } + response = @rest.put_rest(couchdb_database, Hash.new) end - Chef::Config[:couchdb_database] + couchdb_database end def create_design_document(name, data) create_db to_update = true begin - old_doc = @rest.get_rest("#{Chef::Config[:couchdb_database]}/_design%2F#{name}") + old_doc = @rest.get_rest("#{couchdb_database}/_design/#{name}") if data["version"] != old_doc["version"] data["_rev"] = old_doc["_rev"] Chef::Log.debug("Updating #{name} views") else to_update = false end - rescue - Chef::Log.debug("Creating #{name} views for the first time") + rescue + Chef::Log.debug("Creating #{name} views for the first time because: #{$!}") end if to_update - @rest.put_rest("#{Chef::Config[:couchdb_database]}/_design%2F#{name}", data) + @rest.put_rest("#{couchdb_database}/_design%2F#{name}", data) end true end @@ -70,7 +107,29 @@ class Chef :object => { :respond_to => :to_json }, } ) - @rest.put_rest("#{Chef::Config[:couchdb_database]}/#{obj_type}_#{safe_name(name)}", object) + r = get_view("id_map", "name_to_id", :key => [ obj_type, name ]) + uuid = nil + if r["rows"].length == 1 + uuid = r["rows"][0]["id"] + else + uuid = UUIDTools::UUID.random_create.to_s + end + + r = @rest.put_rest("#{couchdb_database}/#{uuid}", object) + Chef::Log.info("Sending #{uuid} to Nanite for indexing..") + n = Chef::Nanite.request( + "/index/add", + { + :id => uuid, + :database => couchdb_database, + :type => obj_type, + :item => object + }, + :persistent => true + ) do |response_full| + Chef::Log.debug("Finished indexing #{obj_type} #{uuid} in #{couchdb_database}"); + end + r end def load(obj_type, name) @@ -84,7 +143,9 @@ class Chef :name => { :kind_of => String }, } ) - @rest.get_rest("#{Chef::Config[:couchdb_database]}/#{obj_type}_#{safe_name(name)}") + doc = find_by_name(obj_type, name) + doc.couchdb = self if doc.respond_to?(:couchdb) + doc end def delete(obj_type, name, rev=nil) @@ -98,15 +159,30 @@ class Chef :name => { :kind_of => String }, } ) + del_id = nil + last_obj, obj_id = find_by_name(obj_type, name, true) unless rev - last_obj = @rest.get_rest("#{Chef::Config[:couchdb_database]}/#{obj_type}_#{safe_name(name)}") if last_obj.respond_to?(:couchdb_rev) rev = last_obj.couchdb_rev else rev = last_obj['_rev'] end end - @rest.delete_rest("#{Chef::Config[:couchdb_database]}/#{obj_type}_#{safe_name(name)}?rev=#{rev}") + r = @rest.delete_rest("#{couchdb_database}/#{obj_id}?rev=#{rev}") + r.couchdb = self if r.respond_to?(:couchdb) + Chef::Log.info("Sending #{obj_id} to Nanite for deletion..") + n = Chef::Nanite.request( + "/index/delete", + { + :id => obj_id, + :database => couchdb_database, + :type => obj_type + }, + :persistent => true + ) do |response_full| + Chef::Log.debug("Finished Deleting #{obj_type} #{obj_id} in #{couchdb_database}"); + end + r end def list(view, inflate=false) @@ -119,10 +195,13 @@ class Chef } ) if inflate - @rest.get_rest(view_uri(view, "all")) + r = @rest.get_rest(view_uri(view, "all")) + r["rows"].each { |i| i["value"].couchdb = self if i["value"].respond_to?(:couchdb=) } + r else - @rest.get_rest(view_uri(view, "all_id")) + r = @rest.get_rest(view_uri(view, "all_id")) end + r end def has_key?(obj_type, name) @@ -137,13 +216,25 @@ class Chef } ) begin - @rest.get_rest("#{Chef::Config[:couchdb_database]}/#{obj_type}_#{safe_name(name)}") + find_by_name(obj_type, name) true rescue false end end + def find_by_name(obj_type, name, with_id=false) + r = get_view("id_map", "name_to_id", :key => [ obj_type, name ], :include_docs => true) + if r["rows"].length == 0 + raise Chef::Exceptions::CouchDBNotFound, "Cannot find #{obj_type} #{name} in CouchDB!" + end + if with_id + [ r["rows"][0]["doc"], r["rows"][0]["id"] ] + else + r["rows"][0]["doc"] + end + end + def get_view(design, view, options={}) view_string = view_uri(design, view) view_string << "?" if options.length != 0 @@ -151,14 +242,21 @@ class Chef options.each { |k,v| view_string << "#{first ? '' : '&'}#{k}=#{URI.escape(v.to_json)}"; first = false } @rest.get_rest(view_string) end + + def bulk_get(*to_fetch) + response = @rest.post_rest("#{couchdb_database}/_all_docs?include_docs=true", { "keys" => to_fetch.flatten }) + response["rows"].collect { |r| r["doc"].couchdb = self; r["doc"] } + end def view_uri(design, view) - Chef::Config[:couchdb_version] ||= @rest.run_request(:GET, URI.parse(@rest.url + "/"), false, 10, false)["version"].gsub(/-.+/,"").to_f + Chef::Config[:couchdb_version] ||= @rest.run_request(:GET, URI.parse(@rest.url + "/"), {}, false, 10, false)["version"].gsub(/-.+/,"").to_f case Chef::Config[:couchdb_version] when 0.9 - "#{Chef::Config[:couchdb_database]}/_design/#{design}/_view/#{view}" + "#{couchdb_database}/_design/#{design}/_view/#{view}" when 0.8 - "#{Chef::Config[:couchdb_database]}/_view/#{design}/#{view}" + "#{couchdb_database}/_view/#{design}/#{view}" + else + "#{couchdb_database}/_design/#{design}/_view/#{view}" end end @@ -167,6 +265,6 @@ class Chef def safe_name(name) name.gsub(/\./, "_") end - + end end diff --git a/chef/lib/chef/data_bag.rb b/chef/lib/chef/data_bag.rb new file mode 100644 index 0000000000..e23ce04c83 --- /dev/null +++ b/chef/lib/chef/data_bag.rb @@ -0,0 +1,167 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/couchdb' +require 'chef/data_bag_item' +require 'extlib' +require 'json' + +class Chef + class DataBag + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + + DESIGN_DOCUMENT = { + "version" => 2, + "language" => "javascript", + "views" => { + "all" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "data_bag") { + emit(doc.name, doc); + } + } + EOJS + }, + "all_id" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "data_bag") { + emit(doc.name, doc.name); + } + } + EOJS + }, + "entries" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "data_bag_item") { + emit(doc.data_bag, doc.raw_data.id); + } + } + EOJS + } + } + } + + attr_accessor :couchdb_rev + + # Create a new Chef::DataBag + def initialize + @name = '' + @couchdb_rev = nil + @couchdb = Chef::CouchDB.new + end + + def name(arg=nil) + set_or_return( + :name, + arg, + :regex => /^[\-[:alnum:]_]+$/ + ) + end + + def to_hash + result = { + "name" => @name, + 'json_class' => self.class.name, + "chef_type" => "data_bag", + } + result["_rev"] = @couchdb_rev if @couchdb_rev + result + end + + # Serialize this object as a hash + def to_json(*a) + to_hash.to_json(*a) + end + + # Create a Chef::Role from JSON + def self.json_create(o) + bag = new + bag.name(o["name"]) + bag.couchdb_rev = o["_rev"] if o.has_key?("_rev") + bag + end + + # List all the Chef::DataBag objects in the CouchDB. If inflate is set to true, you will get + # the full list of all Roles, fully inflated. + def self.list(inflate=false) + couchdb = Chef::CouchDB.new + rs = couchdb.list("data_bags", inflate) + if inflate + rs["rows"].collect { |r| r["value"] } + else + rs["rows"].collect { |r| r["key"] } + end + end + + # Load a Data Bag by name from CouchDB + def self.load(name) + couchdb = Chef::CouchDB.new + couchdb.load("data_bag", name) + end + + # Remove this Data Bag from CouchDB + def destroy + removed = @couchdb.delete("data_bag", @name, @couchdb_rev) + rs = @couchdb.get_view("data_bags", "entries", :include_docs => true, :startkey => @name, :endkey => @name) + rs["rows"].each do |row| + row["doc"].couchdb = @couchdb + row["doc"].destroy + end + removed + end + + # Save this Data Bag to the CouchDB + def save + results = @couchdb.store("data_bag", @name, self) + @couchdb_rev = results["rev"] + end + + # List all the items in this Bag + def list(inflate=false) + rs = nil + if inflate + rs = @couchdb.get_view("data_bags", "entries", :include_docs => true, :startkey => @name, :endkey => @name) + rs["rows"].collect { |r| r["doc"].couchdb = @couchdb; r["doc"] } + else + rs = @couchdb.get_view("data_bags", "entries", :startkey => @name, :endkey => @name) + rs["rows"].collect { |r| r["value"] } + end + end + + # Set up our CouchDB design document + def self.create_design_document + couchdb = Chef::CouchDB.new + couchdb.create_design_document("data_bags", DESIGN_DOCUMENT) + end + + # As a string + def to_s + "data_bag[#{@name}]" + end + + end +end + diff --git a/chef/lib/chef/data_bag_item.rb b/chef/lib/chef/data_bag_item.rb new file mode 100644 index 0000000000..b59f4314da --- /dev/null +++ b/chef/lib/chef/data_bag_item.rb @@ -0,0 +1,188 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/couchdb' +require 'extlib' +require 'json' + +class Chef + class DataBagItem + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + + DESIGN_DOCUMENT = { + "version" => 1, + "language" => "javascript", + "views" => { + "all" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "data_bag_item") { + emit(doc.name, doc); + } + } + EOJS + }, + "all_id" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "data_bag_item") { + emit(doc.name, doc.name); + } + } + EOJS + } + } + } + + attr_accessor :couchdb_rev, :raw_data + + # Create a new Chef::DataBagItem + def initialize(couchdb=nil) + @couchdb_rev = nil + @data_bag = nil + @raw_data = Hash.new + @couchdb = Chef::CouchDB.new + end + + def raw_data + @raw_data + end + + def raw_data=(new_data) + unless new_data.kind_of?(Hash) || new_data.kind_of?(Mash) + raise ArgumentError, "Data Bag Items must contain a Hash or Mash!" + end + unless new_data.has_key?("id") + raise ArgumentError, "Data Bag Items must have an id key in the hash! #{new_data.inspect}" + end + unless new_data["id"] =~ /^[\-[:alnum:]_]+$/ + raise ArgumentError, "Data Bag Item id does not match alphanumeric/-/_!" + end + @raw_data = new_data + end + + def data_bag(arg=nil) + set_or_return( + :data_bag, + arg, + :regex => /^[\-[:alnum:]_]+$/ + ) + end + + def name + object_name + end + + def object_name + if raw_data.has_key?('id') + id = raw_data['id'] + else + raise ArgumentError, "You must have an 'id' or :id key in the raw data" + end + + data_bag_name = self.data_bag + unless data_bag_name + raise ArgumentError, "You must have declared what bag this item belongs to!" + end + "data_bag_item_#{data_bag_name}_#{id}" + end + + def self.object_name(data_bag_name, id) + "data_bag_item_#{data_bag_name}_#{id}" + end + + def to_hash + result = self.raw_data + result["chef_type"] = "data_bag_item" + result["data_bag"] = self.data_bag + result["_rev"] = @couchdb_rev if @couchdb_rev + result + end + + # Serialize this object as a hash + def to_json(*a) + result = { + "name" => self.object_name, + "json_class" => self.class.name, + "chef_type" => "data_bag_item", + "data_bag" => self.data_bag, + "raw_data" => self.raw_data + } + result["_rev"] = @couchdb_rev if @couchdb_rev + result.to_json(*a) + end + + # Create a Chef::DataBagItem from JSON + def self.json_create(o) + bag_item = new + bag_item.data_bag(o["data_bag"]) + o.delete("data_bag") + o.delete("chef_type") + o.delete("json_class") + o.delete("name") + if o.has_key?("_rev") + bag_item.couchdb_rev = o["_rev"] + o.delete("_rev") + end + bag_item.raw_data = o["raw_data"] + bag_item + end + + # The Data Bag Item behaves like a hash - we pass all that stuff along to @raw_data. + def method_missing(method_symbol, *args, &block) + self.raw_data.send(method_symbol, *args, &block) + end + + # Load a Data Bag Item by name from CouchDB + def self.load(data_bag, name) + couchdb = Chef::CouchDB.new + couchdb.load("data_bag_item", object_name(data_bag, name)) + end + + # Remove this Data Bag Item from CouchDB + def destroy + removed = @couchdb.delete("data_bag_item", object_name, @couchdb_rev) + removed + end + + # Save this Data Bag Item to CouchDB + def save + results = @couchdb.store("data_bag_item", object_name, self) + @couchdb_rev = results["rev"] + end + + # Set up our CouchDB design document + def self.create_design_document + couchdb = Chef::CouchDB.new + couchdb.create_design_document("data_bag_items", DESIGN_DOCUMENT) + end + + # As a string + def to_s + "data_bag_item[#{@name}]" + end + + end +end + + diff --git a/chef/lib/chef/exceptions.rb b/chef/lib/chef/exceptions.rb index 7ab7fa5664..13b93502cc 100644 --- a/chef/lib/chef/exceptions.rb +++ b/chef/lib/chef/exceptions.rb @@ -32,5 +32,8 @@ class Chef class Group < RuntimeError; end class Link < RuntimeError; end class Mount < RuntimeError; end + class CouchDBNotFound < RuntimeError; end + class PrivateKeyMissing < RuntimeError; end + class CannotWritePrivateKey < RuntimeError; end end end diff --git a/chef/lib/chef/mixin/generate_url.rb b/chef/lib/chef/mixin/generate_url.rb index 0ea7cc7a31..6a3bfbb73e 100644 --- a/chef/lib/chef/mixin/generate_url.rb +++ b/chef/lib/chef/mixin/generate_url.rb @@ -29,19 +29,28 @@ class Chef else new_url = "cookbooks/#{cookbook}/#{type}?" new_url += "id=#{url}" - platform, version = Chef::Platform.find_platform_and_version(node) - if type == "files" || type == "templates" - new_url += "&platform=#{platform}&version=#{version}&fqdn=#{node[:fqdn]}&node_name=#{node.name}" - end - if args - args.each do |key, value| - new_url += "&#{key}=#{value}" - end - end + new_url = generate_cookbook_url_from_uri(new_url, node, args) end return new_url end + + def generate_cookbook_url_from_uri(uri, node, args=nil) + platform, version = Chef::Platform.find_platform_and_version(node) + uri =~ /cookbooks\/(.+?)\/(.+)\?/ + cookbook = $1 + type = $2 + if type == "files" || type == "templates" + uri += "&platform=#{platform}&version=#{version}&fqdn=#{node[:fqdn]}&node_name=#{node.name}" + end + if args + args.each do |key, value| + uri += "&#{key}=#{value}" + end + end + + uri + end end end diff --git a/chef/lib/chef/mixin/language.rb b/chef/lib/chef/mixin/language.rb index cf049926de..704dbf0c05 100644 --- a/chef/lib/chef/mixin/language.rb +++ b/chef/lib/chef/mixin/language.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/search/query' + class Chef module Mixin module Language @@ -73,6 +75,10 @@ class Chef has_platform end + + def search(*args, &block) + Chef::Search::Query.new.search(*args, &block) + end end end diff --git a/chef/lib/chef/nanite.rb b/chef/lib/chef/nanite.rb new file mode 100644 index 0000000000..a0f4827a68 --- /dev/null +++ b/chef/lib/chef/nanite.rb @@ -0,0 +1,84 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef' +require 'chef/config' +require 'chef/mixin/params_validate' +require 'nanite' +require 'json' + +class Chef + class Nanite + + class << self + + def start_mapper(config={}) + Chef::Log.info("Running the Nanite Mapper") + ::Nanite::Log.logger = Chef::Log.logger + identity = Chef::Config[:nanite_identity] ? Chef::Config[:nanite_identity] : get_identity + ::Nanite.start_mapper( + :host => Chef::Config[:nanite_host], + :user => Chef::Config[:nanite_user], + :pass => Chef::Config[:nanite_pass], + :vhost => Chef::Config[:nanite_vhost], + :identity => identity, + :format => :json, + :log_level => Chef::Config[:log_level] + ) + end + + def get_identity(type="mapper") + id = nil + if Chef::FileCache.has_key?("nanite-#{type}-identity") + id = Chef::FileCache.load("nanite-#{type}-identity") + else + id = ::Nanite::Identity.generate + Chef::FileCache.store("nanite-#{type}-identity", id) + end + id + end + + def request(*args) + in_event do + ::Nanite.request(*args) + end + end + + def in_event(&block) + if EM.reactor_running? + begin + ::Nanite.ensure_mapper + rescue ::Nanite::MapperNotRunning + start_mapper + end + block.call + else + Chef::Log.warn("Starting Event Machine Loop") + Thread.new do + EM.run do + start_mapper + block.call + end + end + end + end + + end + + end +end diff --git a/chef/lib/chef/node.rb b/chef/lib/chef/node.rb index 824db99c2b..11ee5d471f 100644 --- a/chef/lib/chef/node.rb +++ b/chef/lib/chef/node.rb @@ -21,7 +21,6 @@ require 'chef/mixin/check_helper' require 'chef/mixin/params_validate' require 'chef/mixin/from_file' require 'chef/couchdb' -require 'chef/queue' require 'chef/run_list' require 'chef/node/attribute' require 'extlib' @@ -30,14 +29,14 @@ require 'json' class Chef class Node - attr_accessor :attribute, :recipe_list, :couchdb_rev, :run_state, :run_list, :override, :default + attr_accessor :attribute, :recipe_list, :couchdb_rev, :couchdb_id, :run_state, :run_list, :override, :default include Chef::Mixin::CheckHelper include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate DESIGN_DOCUMENT = { - "version" => 8, + "version" => 9, "language" => "javascript", "views" => { "all" => { @@ -120,7 +119,7 @@ class Chef } # Create a new Chef::Node object. - def initialize() + def initialize @name = nil @attribute = Mash.new @@ -129,6 +128,7 @@ class Chef @run_list = Chef::RunList.new @couchdb_rev = nil + @couchdb_id = nil @couchdb = Chef::CouchDB.new @run_state = { :template_cache => Hash.new, @@ -279,53 +279,32 @@ class Chef @run_list.detect { |r| r == item } ? true : false end - # Turns the node into an object that we can index. I apologize up front for the - # super confusion that is the recursive index_flatten hash, which comes up next. - # Faith, young one, faith. - # - # === Returns - # index_hash<Hash>:: A flattened hash of all the nodes attributes, suitable for indexing. - def to_index - index_hash = { - "index_name" => "node", - "id" => "node_#{@name}", - "name" => @name, - } - @attribute.each do |key, value| - if value.kind_of?(Hash) || value.kind_of?(Mash) - index_flatten_hash(key, value).each do |to_index| - to_index.each do |nk, nv| - index_hash[nk] = nv - end - end + # Set an attribute based on the missing method. If you pass an argument, we'll use that + # to set the attribute values. Otherwise, we'll wind up just returning the attributes + # value. + def method_missing(symbol, *args) + if args.length != 0 + @attribute[symbol] = args.length == 1 ? args[0] : args + else + if @attribute.has_key?(symbol) + @attribute[symbol] else - index_hash[key] = value + raise ArgumentError, "Attribute #{symbol.to_s} is not defined!" end end - index_hash["recipe"] = @run_list.recipes if @run_list.recipes.length > 0 + end + + # Transform the node to a Hash + def to_hash + index_hash = @attribute + index_hash["chef_type"] = "node" + index_hash["name"] = @name + index_hash["recipes"] = @run_list.recipes if @run_list.recipes.length > 0 index_hash["roles"] = @run_list.roles if @run_list.roles.length > 0 index_hash["run_list"] = @run_list.run_list if @run_list.run_list.length > 0 index_hash end - # Ah, song of my heart, index_flatten_hash. This method flattens a hash in preparation - # for indexing, by appending the name of it's parent to a current key with an _. Hence, - # node[:bar][:baz] = 'monkey' becomes bar_baz:monkey. - # - # === Returns - # results<Array>:: An array of hashes with one element. - def index_flatten_hash(parent_name, hash) - results = Array.new - hash.each do |k, v| - if v.kind_of?(Hash) || v.kind_of?(Mash) - results << index_flatten_hash("#{parent_name}_#{k}", v) - else - results << { "#{parent_name}_#{k}", v } - end - end - results.flatten - end - # Serialize this object as a hash def to_json(*a) result = { @@ -358,13 +337,15 @@ class Chef o["recipes"].each { |r| node.recipes << r } end node.couchdb_rev = o["_rev"] if o.has_key?("_rev") + node.couchdb_id = o["_id"] if o.has_key?("_id") node end # List all the Chef::Node objects in the CouchDB. If inflate is set to true, you will get # the full list of all Nodes, fully inflated. def self.list(inflate=false) - rs = Chef::CouchDB.new.list("nodes", inflate) + couchdb = Chef::CouchDB.new + rs = couchdb.list("nodes", inflate) if inflate rs["rows"].collect { |r| r["value"] } else @@ -374,25 +355,25 @@ class Chef # Load a node by name from CouchDB def self.load(name) - Chef::CouchDB.new.load("node", name) + couchdb = Chef::CouchDB.new + couchdb.load("node", name) end # Remove this node from the CouchDB def destroy - Chef::Queue.send_msg(:queue, :remove, self) @couchdb.delete("node", @name, @couchdb_rev) end # Save this node to the CouchDB def save - Chef::Queue.send_msg(:queue, :index, self) results = @couchdb.store("node", @name, self) @couchdb_rev = results["rev"] end - + # Set up our CouchDB design document def self.create_design_document - Chef::CouchDB.new.create_design_document("nodes", DESIGN_DOCUMENT) + couchdb = Chef::CouchDB.new + couchdb.create_design_document("nodes", DESIGN_DOCUMENT) end # As a string diff --git a/chef/lib/chef/openid_registration.rb b/chef/lib/chef/openid_registration.rb index 0939746494..dbe1b1d45c 100644 --- a/chef/lib/chef/openid_registration.rb +++ b/chef/lib/chef/openid_registration.rb @@ -178,4 +178,4 @@ class Chef end end -end
\ No newline at end of file +end diff --git a/chef/lib/chef/platform.rb b/chef/lib/chef/platform.rb index e5b0be0d1d..fa24d6a5c8 100644 --- a/chef/lib/chef/platform.rb +++ b/chef/lib/chef/platform.rb @@ -142,7 +142,6 @@ class Chef if pmap.has_key?(rtkey) pmap[rtkey] else - Chef::Log.error("#{rtkey.inspect} #{pmap.inspect}") raise( ArgumentError, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" diff --git a/chef/lib/chef/provider/http_request.rb b/chef/lib/chef/provider/http_request.rb index a93c1c062a..447df10b0a 100644 --- a/chef/lib/chef/provider/http_request.rb +++ b/chef/lib/chef/provider/http_request.rb @@ -25,7 +25,7 @@ class Chef attr_accessor :rest def load_current_resource - @rest = Chef::REST.new(@new_resource.url) + @rest = Chef::REST.new(@new_resource.url, nil, nil) end # Send a GET request to @new_resource.url, with ?message=@new_resource.message @@ -99,4 +99,4 @@ class Chef end end -end
\ No newline at end of file +end diff --git a/chef/lib/chef/provider/remote_file.rb b/chef/lib/chef/provider/remote_file.rb index 3aff5650ff..9b5c9db04a 100644 --- a/chef/lib/chef/provider/remote_file.rb +++ b/chef/lib/chef/provider/remote_file.rb @@ -95,7 +95,7 @@ class Chef begin uri = URI.parse(source) if uri.absolute - r = Chef::REST.new(source) + r = Chef::REST.new(source, nil, nil) Chef::Log.debug("Downloading from absolute URI: #{source}") r.get_rest(source, true).open end diff --git a/chef/lib/chef/provider/template.rb b/chef/lib/chef/provider/template.rb index e3cbbb47ed..7f5b87346d 100644 --- a/chef/lib/chef/provider/template.rb +++ b/chef/lib/chef/provider/template.rb @@ -34,7 +34,6 @@ class Chef include Chef::Mixin::FindPreferredFile def action_create - Chef::Log.debug(@node.run_state.inspect) raw_template_file = nil cookbook_name = @new_resource.cookbook || @new_resource.cookbook_name @@ -137,4 +136,4 @@ class Chef end end -end
\ No newline at end of file +end diff --git a/chef/lib/chef/queue.rb b/chef/lib/chef/queue.rb deleted file mode 100644 index 72883b4bb2..0000000000 --- a/chef/lib/chef/queue.rb +++ /dev/null @@ -1,145 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/mixin/params_validate' -require 'json' -require 'stomp' - -class Chef - class Queue - - @client = nil - @queue_retry_delay = Chef::Config[:queue_retry_delay] - @queue_retry_count = Chef::Config[:queue_retry_count] - - class << self - include Chef::Mixin::ParamsValidate - - def connect - queue_user = Chef::Config[:queue_user] - queue_password = Chef::Config[:queue_password] - queue_host = Chef::Config[:queue_host] - queue_port = Chef::Config[:queue_port] - queue_retries = 1 unless queue_retries - - # Connection.open(login = "", passcode = "", host='localhost', port=61613, reliable=FALSE, reconnectDelay=5) - @client = Stomp::Connection.open(queue_user, queue_password, queue_host, queue_port, false) - - rescue Errno::ECONNREFUSED - Chef::Log.error("Connection refused connecting to stomp queue at #{queue_host}:#{queue_port}, retry #{queue_retries}/#{@queue_retry_count}") - sleep(@queue_retry_delay) - retry if (queue_retries += 1) < @queue_retry_count - raise Errno::ECONNREFUSED, "Connection refused connecting to stomp queue at #{queue_host}:#{queue_port}, giving up" - rescue Timeout::Error - Chef::Log.error("Timeout connecting to stomp queue at #{queue_host}:#{queue_port}, retry #{queue_retries}/#{@queue_retry_count}") - sleep(@queue_retry_delay) - retry if (queue_retries += 1) < @queue_retry_count - raise Timeout::Error, "Timeout connecting to stomp queue at #{queue_host}:#{queue_port}, giving up" - else - queue_retries = 1 # reset the number of retries on success - end - - def make_url(type, name) - validate( - { - :queue_type => type.to_sym, - :queue_name => name.to_sym, - }, - { - :queue_type => { - :equal_to => [ :topic, :queue ], - }, - :queue_name => { - :kind_of => [ String, Symbol ], - } - } - ) - if Chef::Config[:queue_prefix] - queue_prefix = Chef::Config[:queue_prefix] - queue_url = "/#{type}/#{queue_prefix}/chef/#{name}" - else - queue_url = "/#{type}/chef/#{name}" - end - queue_url - end - - def subscribe(type, name) - queue_url = make_url(type, name) - Chef::Log.debug("Subscribing to #{queue_url}") - connect if @client == nil - @client.subscribe(queue_url) - end - - def send_msg(type, name, msg) - queue_retries = 1 unless queue_retries - validate( - { - :message => msg, - }, - { - :message => { - :respond_to => :to_json - } - } - ) - queue_url = make_url(type, name) - json = msg.to_json - connect if @client == nil - Chef::Log.debug("Sending to #{queue_url}: #{json}") - begin - @client.send(queue_url, json) - rescue Errno::EPIPE - Chef::Log.debug("Lost connection to stomp queue, reconnecting") - connect - retry if (queue_retries += 1) < @queue_retry_count - raise Errno::EPIPE, "Lost connection to stomp queue, giving up" - else - queue_retries = 1 # reset the number of retries on success - end - end - - def receive_msg - connect if @client == nil - begin - raw_msg = @client.receive() - Chef::Log.debug("Received Message from #{raw_msg.headers["destination"]} containing: #{raw_msg.body}") - rescue - Chef::Log.debug("Received nil message from stomp, retrying") - retry - end - msg = JSON.parse(raw_msg.body) - return msg, raw_msg.headers - end - - def poll_msg - connect if @client == nil - raw_msg = @client.poll() - if raw_msg - msg = JSON.parse(raw_msg.body) - else - nil - end - end - - def disconnect - raise ArgumentError, "You must call connect before you can disconnect!" unless @client - @client.disconnect - end - end - end -end diff --git a/chef/lib/chef/recipe.rb b/chef/lib/chef/recipe.rb index 5421c08b57..d3e64d0737 100644 --- a/chef/lib/chef/recipe.rb +++ b/chef/lib/chef/recipe.rb @@ -23,7 +23,6 @@ require 'chef/mixin/language' require 'chef/resource_collection' require 'chef/cookbook_loader' require 'chef/rest' -require 'chef/search/result' class Chef class Recipe @@ -89,21 +88,6 @@ class Chef @collection.resources(*args) end - def search(type, query, attributes=[], &block) - Chef::Log.debug("Searching #{type} index with #{query}") - r = Chef::REST.new(Chef::Config[:search_url]) - - results = r.get_rest("search/#{type}?q=#{query}&a=#{attributes.join(',')}") - Chef::Log.debug("Searching #{type} index with #{query} returned #{results.length} entries") - if block - results.each do |sr| - block.call(sr) - end - else - results - end - end - # Sets a tag, or list of tags, for this node. Syntactic sugar for # @node[:tags]. # diff --git a/chef/lib/chef/rest.rb b/chef/lib/chef/rest.rb index 229fac2cdc..0e40068061 100644 --- a/chef/lib/chef/rest.rb +++ b/chef/lib/chef/rest.rb @@ -1,7 +1,8 @@ # # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Thom May (<thom@clearairturbulence.org>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +25,9 @@ require 'uri' require 'json' require 'tempfile' require 'singleton' +require 'mixlib/auth/signedheaderauth' + +include Mixlib::Auth::SignedHeaderAuth class Chef class REST @@ -32,47 +36,57 @@ class Chef include Singleton end - attr_accessor :url, :cookies + attr_accessor :url, :cookies, :signing_key - def initialize(url) + def initialize(url, client_name=Chef::Config[:node_name], signing_key=Chef::Config[:client_key]) @url = url @cookies = CookieJar.instance + @client_name = client_name + if signing_key + @signing_key = load_signing_key(signing_key) + else + @signing_key = nil + end + end + + def load_signing_key(key) + if File.exists?(key) && File.readable?(key) + IO.read(key) + else + raise Chef::Exceptions::PrivateKeyMissing, "I cannot find #{key}, which you told me to use to sign requests!" + end end - # Register for an OpenID - def register(user, pass, validation_token=nil) - Chef::Log.debug("Registering #{user} for an openid") - registration = nil + # Register the client + def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key]) + + if File.exists?(destination) + raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?" unless File.writable?(destination) + end + + # First, try and create a new registration begin - registration = get_rest("registrations/#{user}") - rescue Net::HTTPServerException => e - unless e.message =~ /^404/ - raise e - end + Chef::Log.info("Registering API Client #{name}") + response = post_rest("clients", {:clientname => name}) + rescue Net::HTTPServerException + # If that fails, go ahead and try and update it + response = put_rest("clients/#{name}", { :clientname => name, :private_key => true }) end - unless registration - post_rest( - "registrations", - { - :id => user, - :password => pass, - :validation_token => validation_token - } - ) + + Chef::Log.debug("Registration response: #{response.inspect}") + + raise Chef::Exceptions::CannotWritePrivateKey, "The response from the server did not include a private key!" unless response.has_key?("private_key") + + begin + # Write out the private key + file = File.open(destination, "w") + file.print(response["private_key"]) + file.close + rescue + raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination}" end - end - - # Authenticate - def authenticate(user, pass) - Chef::Log.debug("Authenticating #{user} via openid") - response = post_rest('openid/consumer/start', { - "openid_identifier" => "#{Chef::Config[:openid_url]}/openid/server/node/#{user}", - "submit" => "Verify" - }) - post_rest( - "#{Chef::Config[:openid_url]}#{response["action"]}", - { "password" => pass } - ) + + true end # Send an HTTP GET request to the path @@ -81,23 +95,23 @@ class Chef # path:: The path to GET # raw:: Whether you want the raw body returned, or JSON inflated. Defaults # to JSON inflated. - def get_rest(path, raw=false) - run_request(:GET, create_url(path), false, 10, raw) + def get_rest(path, raw=false, headers={}) + run_request(:GET, create_url(path), headers, false, 10, raw) end # Send an HTTP DELETE request to the path - def delete_rest(path) - run_request(:DELETE, create_url(path)) + def delete_rest(path, headers={}) + run_request(:DELETE, create_url(path), headers) end # Send an HTTP POST request to the path - def post_rest(path, json) - run_request(:POST, create_url(path), json) + def post_rest(path, json, headers={}) + run_request(:POST, create_url(path), headers, json) end # Send an HTTP PUT request to the path - def put_rest(path, json) - run_request(:PUT, create_url(path), json) + def put_rest(path, json, headers={}) + run_request(:PUT, create_url(path), headers, json) end def create_url(path) @@ -108,6 +122,18 @@ class Chef end end + def sign_request(http_method, private_key, user_id, body = "", host="localhost") + #body = "" if body == false + timestamp = Time.now.utc.iso8601 + sign_obj = Mixlib::Auth::SignedHeaderAuth.signing_object( + :http_method=>http_method, + :body=>body, + :user_id=>user_id, + :timestamp=>timestamp) + signed = sign_obj.sign(private_key).merge({:host => host}) + signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo} + end + # Actually run an HTTP request. First argument is the HTTP method, # which should be one of :GET, :PUT, :POST or :DELETE. Next is the # URL, then an object to include in the body (which will be converted with @@ -117,7 +143,8 @@ class Chef # the helper methods (get_rest, post_rest, etc.) # # Will return the body of the response on success. - def run_request(method, url, data=false, limit=10, raw=false) + def run_request(method, url, headers={}, data=false, limit=10, raw=false) + http_retry_delay = Chef::Config[:http_retry_delay] http_retry_count = Chef::Config[:http_retry_count] @@ -129,21 +156,35 @@ class Chef if Chef::Config[:ssl_verify_mode] == :verify_none http.verify_mode = OpenSSL::SSL::VERIFY_NONE end - if File.exists?(Chef::Config[:ssl_client_cert]) + if Chef::Config[:ssl_client_cert] && File.exists?(Chef::Config[:ssl_client_cert]) http.cert = OpenSSL::X509::Certificate.new(File.read(Chef::Config[:ssl_client_cert])) http.key = OpenSSL::PKey::RSA.new(File.read(Chef::Config[:ssl_client_key])) end end + http.read_timeout = Chef::Config[:rest_timeout] - headers = Hash.new + unless raw - headers = { + headers = headers.merge({ 'Accept' => "application/json", - } + }) end + if @cookies.has_key?("#{url.host}:#{url.port}") headers['Cookie'] = @cookies["#{url.host}:#{url.port}"] end + + json_body = data ? data.to_json : nil + + if @signing_key + Chef::Log.debug("Signing the request as #{@client_name}") + if json_body + headers.merge!(sign_request(method, OpenSSL::PKey::RSA.new(@signing_key), @client_name, json_body, "#{url.host}:#{url.port}")) + else + headers.merge!(sign_request(method, OpenSSL::PKey::RSA.new(@signing_key), @client_name, "", "#{url.host}:#{url.port}")) + end + end + req = nil case method when :GET @@ -152,12 +193,16 @@ class Chef req = Net::HTTP::Get.new(req_path, headers) when :POST headers["Content-Type"] = 'application/json' if data - req = Net::HTTP::Post.new(url.path, headers) - req.body = data.to_json if data + req_path = "#{url.path}" + req_path << "?#{url.query}" if url.query + req = Net::HTTP::Post.new(req_path, headers) + req.body = json_body if json_body when :PUT headers["Content-Type"] = 'application/json' if data - req = Net::HTTP::Put.new(url.path, headers) - req.body = data.to_json if data + req_path = "#{url.path}" + req_path << "?#{url.query}" if url.query + req = Net::HTTP::Put.new(req_path, headers) + req.body = json_body if json_body when :DELETE req_path = "#{url.path}" req_path << "?#{url.query}" if url.query @@ -165,6 +210,8 @@ class Chef else raise ArgumentError, "You must provide :GET, :PUT, :POST or :DELETE as the method" end + + Chef::Log.debug("Sending HTTP Request via #{req.method} to #{url.host}:#{url.port}/#{req.path}") # Optionally handle HTTP Basic Authentication req.basic_auth(url.user, url.password) if url.user @@ -199,7 +246,8 @@ class Chef end response end - rescue Errno::ECONNREFUSED + + rescue Errno::ECONNREFUSED => e Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{req.path} #{http_retries}/#{http_retry_count}") sleep(http_retry_delay) retry if (http_retries += 1) < http_retry_count @@ -216,7 +264,8 @@ class Chef @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] end if res['content-type'] =~ /json/ - JSON.parse(res.body) + response_body = res.body.chomp + JSON.parse(response_body) else if raw tf @@ -228,11 +277,11 @@ class Chef if res['set-cookie'] @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] end - run_request(:GET, create_url(res['location']), false, limit - 1, raw) + run_request(:GET, create_url(res['location']), {}, false, limit - 1, raw) else res.error! end end - + end end diff --git a/chef/lib/chef/role.rb b/chef/lib/chef/role.rb index 506a197416..461db69eaa 100644 --- a/chef/lib/chef/role.rb +++ b/chef/lib/chef/role.rb @@ -30,7 +30,7 @@ class Chef include Chef::Mixin::ParamsValidate DESIGN_DOCUMENT = { - "version" => 3, + "version" => 6, "language" => "javascript", "views" => { "all" => { @@ -50,21 +50,22 @@ class Chef } } EOJS - }, - }, + } + } } - attr_accessor :couchdb_rev + attr_accessor :couchdb_rev, :couchdb_id # Create a new Chef::Role object. - def initialize() + def initialize @name = '' @description = '' @default_attributes = Mash.new @override_attributes = Mash.new @recipes = Array.new @couchdb_rev = nil - @couchdb = Chef::CouchDB.new + @couchdb_id = nil + @couchdb = Chef::CouchDB.new end def name(arg=nil) @@ -139,13 +140,15 @@ class Chef role.override_attributes(o["override_attributes"]) role.recipes(o["recipes"]) role.couchdb_rev = o["_rev"] if o.has_key?("_rev") + role.couchdb_id = o["_id"] if o.has_key?("_id") role end # List all the Chef::Role objects in the CouchDB. If inflate is set to true, you will get # the full list of all Roles, fully inflated. def self.list(inflate=false) - rs = Chef::CouchDB.new.list("roles", inflate) + couchdb = Chef::CouchDB.new + rs = couchdb.list("roles", inflate) if inflate rs["rows"].collect { |r| r["value"] } else @@ -155,7 +158,8 @@ class Chef # Load a role by name from CouchDB def self.load(name) - Chef::CouchDB.new.load("role", name) + couchdb = Chef::CouchDB.new + couchdb.load("role", name) end # Remove this role from the CouchDB @@ -186,7 +190,8 @@ class Chef # Set up our CouchDB design document def self.create_design_document - Chef::CouchDB.new.create_design_document("roles", DESIGN_DOCUMENT) + couchdb = Chef::CouchDB.new + couchdb.create_design_document("roles", DESIGN_DOCUMENT) end # As a string @@ -212,6 +217,7 @@ class Chef # Sync all the json roles with couchdb from disk def self.sync_from_disk_to_couchdb + couchdb = Chef::CouchDB.new Dir[File.join(Chef::Config[:role_path], "*.json")].each do |role_file| short_name = File.basename(role_file, ".json") Chef::Log.warn("Loading #{short_name}") @@ -220,7 +226,7 @@ class Chef couch_role = Chef::Role.load(short_name) r.couchdb_rev = couch_role.couchdb_rev Chef::Log.debug("Replacing role #{short_name} with data from #{role_file}") - rescue Net::HTTPServerException + rescue Chef::Exceptions::CouchDBNotFound Chef::Log.debug("Creating role #{short_name} with data from #{role_file}") end r.save diff --git a/chef/lib/chef/run_list.rb b/chef/lib/chef/run_list.rb index 6eb0157580..f565185633 100644 --- a/chef/lib/chef/run_list.rb +++ b/chef/lib/chef/run_list.rb @@ -111,7 +111,8 @@ class Chef self end - def expand(from='server') + def expand(from='server', couchdb=nil) + couchdb = couchdb ? couchdb : Chef::CouchDB.new recipes = Array.new default_attrs = Mash.new override_attrs = Mash.new @@ -132,7 +133,7 @@ class Chef role = r.get_rest("roles/#{name}") elsif from == 'couchdb' # Load the role from couchdb - role = Chef::Role.load(name) + role = Chef::Role.load(name, couchdb) end role.recipes.each { |r| recipes << r unless recipes.include?(r) } default_attrs = Chef::Mixin::DeepMerge.merge(default_attrs, role.default_attributes) diff --git a/chef/lib/chef/search.rb b/chef/lib/chef/search.rb deleted file mode 100644 index 0d0c176921..0000000000 --- a/chef/lib/chef/search.rb +++ /dev/null @@ -1,88 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/search/result' -require 'ferret' - -class Chef - class Search - - attr_reader :index - - def initialize - @index = Ferret::Index::Index.new(:path => Chef::Config[:search_index_path]) - end - - def search(type, query="*", attributes=[], &block) - search_query = build_search_query(type, query) - start_time = Time.now - results = [] - block ||= lambda { |b| b } - - @index.search_each(search_query, :limit => :all) do |id, score| - results << block.call(build_hash(@index.doc(id))) - end - - Chef::Log.debug("Search #{search_query} complete in #{Time.now - start_time} seconds") - - attributes.empty? ? results : filter_by_attributes(results,attributes) - end - - def filter_by_attributes(results, attributes) - results.collect do |r| - nr = Hash.new - nr[:index_name] = r[:index_name] - nr[:id] = r[:id] - attributes.each do |attrib| - if r.has_key?(attrib) - nr[attrib] = r[attrib] - end - end - nr - end - end - - private :filter_by_attributes - - def list_indexes - indexes = Hash.new - @index.search_each("index_name:*", :limit => :all) do |id, score| - indexes[@index.doc(id)["index_name"]] = true - end - indexes.keys - end - - def has_index?(index) - list_indexes.detect { |i| i == index } - end - - private - def build_search_query(type, query) - query = "id:*" if query == '*' - "index_name:#{type} AND (#{query})" - end - - def build_hash(doc) - result = Chef::Search::Result.new - doc.fields.each do |f| - result[f] = doc[f] - end - result - end - end -end diff --git a/chef/lib/chef/search/query.rb b/chef/lib/chef/search/query.rb new file mode 100644 index 0000000000..be5e15269a --- /dev/null +++ b/chef/lib/chef/search/query.rb @@ -0,0 +1,63 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/config' +require 'chef/node' +require 'chef/role' +require 'chef/data_bag' +require 'chef/data_bag_item' + +class Chef + class Search + class Query + def initialize(url=nil) + url ||= Chef::Config[:search_url] + @rest = Chef::REST.new(url) + end + + # Search Solr for objects of a given type, for a given query. If you give + # it a block, it will handle the paging for you dynamically. + def search(type, query="*:*", sort=nil, start=0, rows=20, &block) + unless type.kind_of?(String) || type.kind_of?(Symbol) + raise ArgumentError, "Type must be a string or a symbol!" + end + + response = @rest.get_rest("search/#{type}?q=#{escape(query)}&sort=#{escape(sort)}&start=#{escape(start)}&rows=#{escape(rows)}") + if block + response["rows"].each { |o| block.call(o) } + unless (response["start"] + response["rows"].length) >= response["total"] + nstart = response["start"] + rows + search(type, query, sort, nstart, rows, &block) + end + true + else + [ response["rows"], response["start"], response["total"] ] + end + end + + private + # escapes a query key/value for http + # Thanks to RSolr! + def escape(s) + s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { + '%'+$1.unpack('H2'*$1.size).join('%').upcase + }.tr(' ', '+') + end + end + end +end diff --git a/chef/lib/chef/search/result.rb b/chef/lib/chef/search/result.rb deleted file mode 100644 index 97d35132a8..0000000000 --- a/chef/lib/chef/search/result.rb +++ /dev/null @@ -1,64 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'extlib' - -class Chef - class Search - class Result - - def initialize - proc = lambda do |h,k| - newhash = Mash.new(&proc) - h.each do |pk, pv| - rx = /^#{k.to_s}_/ - if pk =~ rx - newhash[ pk.gsub(rx,'') ] = pv - end - end - newhash - end - @internal = Mash.new(&proc) - end - - def method_missing(symbol, *args, &block) - @internal.send(symbol, *args, &block) - end - - # Serialize this object as a hash - def to_json(*a) - result = { - 'json_class' => self.class.name, - 'results' => @internal - } - result.to_json(*a) - end - - # Create a Chef::Search::Result from JSON - def self.json_create(o) - result = self.new - o['results'].each do |k,v| - result[k] = v - end - result - end - end - end -end - - diff --git a/chef/lib/chef/search_index.rb b/chef/lib/chef/search_index.rb deleted file mode 100644 index 6a76595aa1..0000000000 --- a/chef/lib/chef/search_index.rb +++ /dev/null @@ -1,77 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'ferret' - -class Chef - class SearchIndex - - attr_reader :index - - def initialize - @index = Ferret::Index::Index.new( - :path => Chef::Config[:search_index_path], - :key => [ :id ] - ) - end - - def add(new_object) - index_hash = create_index_object(new_object) - Chef::Log.debug("Indexing #{index_hash[:index_name]} with #{index_hash.inspect}") - @index.add_document(index_hash) - end - - def create_index_object(new_object) - index_hash = nil - - if new_object.respond_to?(:to_index) - index_hash = new_object.to_index - elsif new_object.kind_of?(Hash) - index_hash = new_object - else - raise Chef::Exceptions::SearchIndex, "Cannot transform argument to a Hash!" - end - - unless index_hash.has_key?(:index_name) || index_hash.has_key?("index_name") - raise Chef::Exceptions::SearchIndex, "Cannot index without an index_name key: #{index_hash.inspect}" - end - - unless index_hash.has_key?(:id) || index_hash.has_key?("id") - raise Chef::Exceptions::SearchIndex, "Cannot index without an id key: #{index_hash.inspect}" - end - - sanitized_hash = Hash.new - index_hash.each do |k,v| - sanitized_hash[k.to_sym] = v - end - - sanitized_hash - end - - def delete(index_obj) - to_delete = create_index_object(index_obj) - Chef::Log.debug("Removing #{to_delete.inspect} from the #{to_delete[:index_name]} index") - @index.delete(to_delete[:id]) - end - - def commit - @index.commit - end - - end -end diff --git a/chef/lib/chef/streaming_cookbook_uploader.rb b/chef/lib/chef/streaming_cookbook_uploader.rb new file mode 100644 index 0000000000..73c9034b42 --- /dev/null +++ b/chef/lib/chef/streaming_cookbook_uploader.rb @@ -0,0 +1,178 @@ +require 'net/http' +require 'mixlib/auth/signedheaderauth' +require 'openssl' + +# inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html +# TODO: confirm that code is public domain +class Chef + class StreamingCookbookUploader + + DefaultHeaders = { 'accept' => 'application/json' } + + class << self + + def post(to_url, user_id, secret_key_filename, params = {}, headers = {}) + make_request(:post, to_url, user_id, secret_key_filename, params, headers) + end + + def put(to_url, user_id, secret_key_filename, params = {}, headers = {}) + make_request(:put, to_url, user_id, secret_key_filename, params, headers) + end + + def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {}) + boundary = '----RubyMultipartClient' + rand(1000000).to_s + 'ZZZZZ' + parts = [] + content_file = nil + content_body = nil + + timestamp = Time.now.utc.iso8601 + secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename)) + + unless params.nil? || params.empty? + params.each do |key, value| + if value.kind_of?(File) + content_file = value + filepath = value.path + filename = File.basename(filepath) + parts << StringPart.new( "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" + + "Content-Type: application/octet-stream\r\n\r\n") + parts << StreamPart.new(value, File.size(filepath)) + parts << StringPart.new("\r\n") + else + content_body = value.to_s + parts << StringPart.new( "--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n") + parts << StringPart.new(content_body + "\r\n") + end + end + parts << StringPart.new("--" + boundary + "--\r\n") + end + + body_stream = MultipartStream.new(parts) + + timestamp = Time.now.utc.iso8601 + + Chef::Log.logger.debug("Signing: method: #{http_verb}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") + + signing_options = { + :http_method=>http_verb, + :user_id=>user_id, + :timestamp=>timestamp} + (content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || "")) + + headers.merge!(Mixlib::Auth::SignedHeaderAuth.signing_object(signing_options).sign(secret_key)) + + content_file.rewind if content_file + + # net/http doesn't like symbols for header keys, so we'll to_s each one just in case + headers = DefaultHeaders.merge(Hash[*headers.map{ |k,v| [k.to_s, v] }.flatten]) + + url = URI.parse(to_url) + req = case http_verb + when :put + Net::HTTP::Put.new(url.path, headers) + when :post + Net::HTTP::Post.new(url.path, headers) + end + req.content_length = body_stream.size + req.content_type = 'multipart/form-data; boundary=' + boundary unless parts.empty? + req.body_stream = body_stream + res = Net::HTTP.new(url.host, url.port).start {|http| http.request(req) } + + # alias status to code and to_s to body for test purposes + # TODO: stop the following madness! + class << res + alias :to_s :body + + # BUGBUG this makes the response compatible with what respsonse_steps expects to test headers (response.headers[] -> response[]) + def headers + self + end + + def status + code.to_i + end + end + res + end + + end + + class StreamPart + def initialize(stream, size) + @stream, @size = stream, size + end + + def size + @size + end + + # read the specified amount from the stream + def read(offset, how_much) + @stream.read(how_much) + end + end + + class StringPart + def initialize(str) + @str = str + end + + def size + @str.length + end + + # read the specified amount from the string startiung at the offset + def read(offset, how_much) + @str[offset, how_much] + end + end + + class MultipartStream + def initialize(parts) + @parts = parts + @part_no = 0 + @part_offset = 0 + end + + def size + @parts.inject(0) {|size, part| size + part.size} + end + + def read(how_much) + return nil if @part_no >= @parts.size + + how_much_current_part = @parts[@part_no].size - @part_offset + + how_much_current_part = if how_much_current_part > how_much + how_much + else + how_much_current_part + end + + how_much_next_part = how_much - how_much_current_part + + current_part = @parts[@part_no].read(@part_offset, how_much_current_part) + + # recurse into the next part if the current one was not large enough + if how_much_next_part > 0 + @part_no += 1 + @part_offset = 0 + next_part = read(how_much_next_part) + current_part + if next_part + next_part + else + '' + end + else + @part_offset += how_much_current_part + current_part + end + end + end + + end + + +end diff --git a/chef/spec/unit/application/indexer_spec.rb b/chef/spec/unit/application/indexer_spec.rb deleted file mode 100644 index c15ddc71c0..0000000000 --- a/chef/spec/unit/application/indexer_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# -# Author:: AJ Christensen (<aj@junglist.gen.nz>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) - -describe Chef::Application::Indexer, "initialize" do - before do - @app = Chef::Application::Indexer.new - end - - it "should create an instance of Chef::Application::Indexer" do - @app.should be_kind_of(Chef::Application::Indexer) - end -end - -describe Chef::Application::Indexer, "setup_application" do - before do - Chef::Daemon.stub!(:change_privilege).and_return(true) - @chef_searchindex = mock("Chef::SearchIndex", :null_object => true) - Chef::SearchIndex.stub!(:new).and_return(@chef_searchindex) - Chef::Queue.stub!(:connect).and_return(true) - Chef::Queue.stub!(:subscribe).and_return(true) - @app = Chef::Application::Indexer.new - end - - it "should change privileges" do - Chef::Daemon.should_receive(:change_privilege).and_return(true) - @app.setup_application - end - - it "should instantiate a chef::client object" do - Chef::SearchIndex.should_receive(:new).and_return(@chef_searchindex) - @app.setup_application - end - - it "should connect to the queue" do - Chef::Queue.should_receive(:connect).and_return(true) - @app.setup_application - end - - it "should subscribe to index" do - Chef::Queue.should_receive(:subscribe).with(:queue, "index").and_return(true) - @app.setup_application - end - - it "should subscribe to remove" do - Chef::Queue.should_receive(:subscribe).with(:queue, "remove").and_return(true) - @app.setup_application - end -end diff --git a/chef/spec/unit/client_spec.rb b/chef/spec/unit/client_spec.rb index 303306a659..eb53803cf9 100644 --- a/chef/spec/unit/client_spec.rb +++ b/chef/spec/unit/client_spec.rb @@ -30,13 +30,9 @@ describe Chef::Client, "run" do to_stub = [ :build_node, :register, - :authenticate, - :sync_library_files, - :sync_attribute_files, - :sync_definitions, - :sync_recipes, + :sync_cookbooks, :save_node, - :save_node + :converge ] to_stub.each do |method| @client.stub!(method).and_return(true) @@ -58,37 +54,17 @@ describe Chef::Client, "run" do @client.run end - it "should register for an openid" do + it "should register for a client" do @client.should_receive(:register).and_return(true) @client.run end - it "should authenticate with the server" do - @client.should_receive(:authenticate).and_return(true) + it "should synchronize the cookbooks from the server" do + @client.should_receive(:sync_cookbooks).and_return(true) @client.run end - it "should synchronize definitions from the server" do - @client.should_receive(:sync_definitions).and_return(true) - @client.run - end - - it "should synchronize recipes from the server" do - @client.should_receive(:sync_recipes).and_return(true) - @client.run - end - - it "should synchronize and load library files from the server" do - @client.should_receive(:sync_library_files).and_return(true) - @client.run - end - - it "should synchronize and load attribute files from the server" do - @client.should_receive(:sync_attribute_files).and_return(true) - @client.run - end - - it "should save the nodes state on the server (twice!)" do + it "should save the nodes state on the server (thrice!)" do @client.should_receive(:save_node).exactly(3).times.and_return(true) @client.run end @@ -98,11 +74,6 @@ describe Chef::Client, "run" do @client.run end - it "should set the cookbook_path" do - Chef::Config.should_receive('[]').with(:file_cache_path). - and_return('/var/chef/cache/cookbooks') - @client.run - end end describe Chef::Client, "run_solo" do @@ -149,6 +120,7 @@ describe Chef::Client, "build_node" do Chef::REST.stub!(:new).and_return(@mock_rest) @client = Chef::Client.new Chef::Platform.stub!(:find_platform_and_version).and_return(["FooOS", "1.3.3.7"]) + Chef::Config[:node_name] = nil end it "should set the name equal to the FQDN" do @@ -208,35 +180,34 @@ describe Chef::Client, "register" do before do @mock_rest = mock("Chef::REST", :null_object => true) @mock_rest.stub!(:get_rest).and_return(true) + @mock_rest.stub!(:register).and_return(true) Chef::REST.stub!(:new).and_return(@mock_rest) @chef_client = Chef::Client.new @chef_client.safe_name = "testnode" + @chef_client.node_name = "testnode" @chef_client.stub!(:determine_node_name).and_return(true) - @chef_client.stub!(:create_registration).and_return(true) - Chef::Application.stub!(:fatal!).and_return(true) - Chef::FileCache.stub!(:create_cache_path).and_return("/tmp") - Chef::FileCache.stub!(:load).and_return("/tmp/testnode") - end - - it "should log an appropriate debug message regarding registering an openid" do - Chef::Log.should_receive(:debug).with("Registering testnode for an openid").and_return(true) - @chef_client.register + File.stub!(:exists?).and_return(false) end + + describe "when the validation key is present" do + before(:each) do + File.stub!(:exists?).with(Chef::Config[:validation_key]).and_return(true) + end - it "should fetch the registration based on safe_name from the chef server" do - @mock_rest.should_receive(:get_rest).with("registrations/testnode").and_return(true) - @chef_client.register - end + it "should sign requests with the validation key" do + Chef::REST.should_receive(:new).with(Chef::Config[:client_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]).and_return(@mock_rest) + @chef_client.register + end - it "should load the secret from disk" do - Chef::FileCache.should_receive(:load).with(File.join("registration", "testnode")).and_return("/tmp/testnode") - @chef_client.register + it "should register for a new key-pair" do + @mock_rest.should_receive(:register).with("testnode", Chef::Config[:client_key]) + @chef_client.register + end end - it "should cause chef to die fatally if the filecache cannot find the registration" do - Chef::FileCache.stub!(:load).with(File.join("registration", "testnode")).and_raise(Chef::Exceptions::FileNotFound) - Chef::Application.should_receive(:fatal!).with(/^.*$/, 3).and_return(true) - @chef_client.register + it "should setup the rest client to use the client key-pair" do + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(@mock_rest) + @chef_client.register end end @@ -256,3 +227,4 @@ describe Chef::Client, "run_ohai" do @chef_client.run_ohai end end + diff --git a/chef/spec/unit/couchdb_spec.rb b/chef/spec/unit/couchdb_spec.rb index ffcc4d1a2a..5f84384a71 100644 --- a/chef/spec/unit/couchdb_spec.rb +++ b/chef/spec/unit/couchdb_spec.rb @@ -18,236 +18,245 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) -describe Chef::CouchDB, "new" do - before do +describe Chef::CouchDB do + before(:each) do @mock_rest = mock("Chef::REST", :null_object => true) @mock_rest.stub!(:run_request).and_return({"couchdb" => "Welcome", "version" =>"0.9.0"}) @mock_rest.stub!(:url).and_return("http://localhost:5984") Chef::REST.stub!(:new).and_return(@mock_rest) - end - - it "should create a new Chef::REST object from the default url" do - Chef::Config[:couchdb_url] = "http://monkey" - Chef::REST.should_receive(:new).with("http://monkey") - Chef::CouchDB.new + @couchdb = Chef::CouchDB.new + Chef::Nanite.stub!(:request).and_return(true) + Chef::Nanite.stub!(:in_event).and_return(true) + Chef::Nanite.stub!(:start_mapper).and_return(true) end - it "should create a new Chef::REST object from a provided url" do - Chef::REST.should_receive(:new).with("http://monkeypants") - Chef::CouchDB.new("http://monkeypants") - end -end + describe "new" do + it "should create a new Chef::REST object from the default url" do + old_url = Chef::Config[:couchdb_url] + Chef::Config[:couchdb_url] = "http://monkey" + Chef::REST.should_receive(:new).with("http://monkey", nil, nil) + Chef::CouchDB.new + Chef::Config[:couchdb_url] = old_url + end -describe Chef::CouchDB, "create_db" do - before(:each) do - @mock_rest = mock("Chef::REST", :null_object => true) - @mock_rest.stub!(:get_rest).and_return([ "chef" ]) - @mock_rest.stub!(:put_rest).and_return(true) - Chef::REST.stub!(:new).and_return(@mock_rest) - end - - def do_create_db - couch = Chef::CouchDB.new - couch.create_db - end - - it "should get a list of current databases" do - @mock_rest.should_receive(:get_rest).and_return(["chef"]) - do_create_db - end - - it "should create the chef database if it does not exist" do - @mock_rest.stub!(:get_rest).and_return([]) - @mock_rest.should_receive(:put_rest).with("chef", {}).and_return(true) - do_create_db + it "should create a new Chef::REST object from a provided url" do + Chef::REST.should_receive(:new).with("http://monkeypants", nil, nil) + Chef::CouchDB.new("http://monkeypants") + end end - - it "should not create the chef database if it does exist" do - @mock_rest.stub!(:get_rest).and_return(["chef"]) - @mock_rest.should_not_receive(:put_rest) - do_create_db - end - - it "should return 'chef'" do - do_create_db.should eql("chef") + + describe "create_db" do + before(:each) do + @couchdb.stub!(:create_design_document).and_return(true) + end + + it "should get a list of current databases" do + @mock_rest.should_receive(:get_rest).and_return(["chef"]) + @couchdb.create_db + end + + it "should create the chef database if it does not exist" do + @mock_rest.stub!(:get_rest).and_return([]) + @mock_rest.should_receive(:put_rest).with("chef", {}).and_return(true) + @couchdb.create_db + end + + it "should not create the chef database if it does exist" do + @mock_rest.stub!(:get_rest).and_return(["chef"]) + @mock_rest.should_not_receive(:put_rest) + @couchdb.create_db + end + + it "should return 'chef'" do + @couchdb.create_db.should eql("chef") + end end -end -describe Chef::CouchDB, "create_design_document" do - before(:each) do - @mock_rest = mock("Chef::REST", :null_object => true) - @mock_design = { - "version" => 1, - "_rev" => 1 - } - @mock_data = { - "version" => 1, - "language" => "javascript", - "views" => { - "all" => { - "map" => <<-EOJS - function(doc) { - if (doc.chef_type == "node") { - emit(doc.name, doc); + describe "create_design_document" do + before(:each) do + @mock_design = { + "version" => 1, + "_rev" => 1 + } + @mock_data = { + "version" => 1, + "language" => "javascript", + "views" => { + "all" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "node") { + emit(doc.name, doc); + } } - } - EOJS - }, + EOJS + }, + } } - } - @mock_rest.stub!(:get_rest).and_return(@mock_design) - @mock_rest.stub!(:put_rest).and_return(true) - Chef::REST.stub!(:new).and_return(@mock_rest) - @couchdb = Chef::CouchDB.new - @couchdb.stub!(:create_db).and_return(true) - end - - def do_create_design_document - @couchdb.create_design_document("bob", @mock_data) - end - - it "should create the database if it does not exist" do - @couchdb.should_receive(:create_db).and_return(true) - do_create_design_document - end - - it "should fetch the existing design document" do - @mock_rest.should_receive(:get_rest).with("chef/_design%2Fbob") - do_create_design_document - end - - it "should populate the _rev in the new design if the versions dont match" do - @mock_data["version"] = 2 - do_create_design_document - @mock_data["_rev"].should eql(1) - end - - it "should create the view if it requires updating" do - @mock_data["version"] = 2 - @mock_rest.should_receive(:put_rest).with("chef/_design%2Fbob", @mock_data) - do_create_design_document - end - - it "should not create the view if it does not require updating" do - @mock_data["version"] = 1 - @mock_rest.should_not_receive(:put_rest) - do_create_design_document + @mock_rest.stub!(:get_rest).and_return(@mock_design) + @mock_rest.stub!(:put_rest).and_return(true) + @couchdb.stub!(:create_db).and_return(true) + end + + def do_create_design_document + @couchdb.create_design_document("bob", @mock_data) + end + + it "should create the database if it does not exist" do + @couchdb.should_receive(:create_db).and_return(true) + do_create_design_document + end + + it "should fetch the existing design document" do + @mock_rest.should_receive(:get_rest).with("chef/_design/bob") + do_create_design_document + end + + it "should populate the _rev in the new design if the versions dont match" do + @mock_data["version"] = 2 + do_create_design_document + @mock_data["_rev"].should eql(1) + end + + it "should create the view if it requires updating" do + @mock_data["version"] = 2 + @mock_rest.should_receive(:put_rest).with("chef/_design%2Fbob", @mock_data) + do_create_design_document + end + + it "should not create the view if it does not require updating" do + @mock_data["version"] = 1 + @mock_rest.should_not_receive(:put_rest) + do_create_design_document + end end -end -describe Chef::CouchDB, "store" do - it "should put the object into couchdb" do - @mock_rest = mock("Chef::REST", :null_object => true) - @mock_rest.should_receive(:put_rest).with("chef/node_bob", {}).and_return(true) - Chef::REST.stub!(:new).and_return(@mock_rest) - Chef::CouchDB.new.store("node", "bob", {}) - end -end + describe "store" do + before(:each) do + @mock_results = { + "rows" => [ + "id" => 'a0934635-e111-45d9-8223-cb58e1c9434c' + ] + } + @couchdb.stub!(:get_view).with("id_map", "name_to_id", :key => [ "node", "bob" ]).and_return(@mock_results) + end -describe Chef::CouchDB, "load" do - it "should load the object from couchdb" do - @mock_rest = mock("Chef::REST", :null_object => true) - @mock_rest.should_receive(:get_rest).with("chef/node_bob").and_return(true) - Chef::REST.stub!(:new).and_return(@mock_rest) - Chef::CouchDB.new.load("node", "bob").should eql(true) - end -end + it "should put the object into couchdb with a pre-existing GUID" do + @mock_rest.should_receive(:put_rest).with("chef/#{@mock_results["rows"][0]["id"]}", {}).and_return(true) + @couchdb.store("node", "bob", {}) + end -describe Chef::CouchDB, "delete" do - before(:each) do - @mock_current = { - "version" => 1, - "_rev" => 1 - } - @mock_rest = mock("Chef::REST", :null_object => true) - @mock_rest.stub!(:get_rest).and_return(@mock_current) - @mock_rest.stub!(:delete_rest).and_return(true) - Chef::REST.stub!(:new).and_return(@mock_rest) - end - - def do_delete(rev=nil) - Chef::REST.stub!(:new).and_return(@mock_rest) - Chef::CouchDB.new.delete("node", "bob", rev) - end - - it "should remove the object from couchdb with a specific revision" do - @mock_rest.should_receive(:delete_rest).with("chef/node_bob?rev=1") - do_delete(1) - end - - it "should remove the object from couchdb based on the couchdb_rev of the current obj" do - mock_real = mock("Inflated Object") - mock_real.stub!(:respond_to?).and_return(true) - mock_real.stub!(:couchdb_rev).and_return(2) - @mock_rest.should_receive(:get_rest).with("chef/node_bob").and_return(mock_real) - @mock_rest.should_receive(:delete_rest).with("chef/node_bob?rev=2") - do_delete - end - - it "should remove the object from couchdb based on the current objects rev" do - @mock_rest.should_receive(:delete_rest).with("chef/node_bob?rev=1") - do_delete + it "should put the object into couchdb with a new GUID" do + @mock_results = { "rows" => [] } + @couchdb.stub!(:get_view).with("id_map", "name_to_id", :key => [ "node", "bob" ]).and_return(@mock_results) + UUIDTools::UUID.stub!(:random_create).and_return("aaaaaaaa-xxxx-xxxx-xxxx-xxxxxxxxxxx") + @mock_rest.should_receive(:put_rest).with("chef/aaaaaaaa-xxxx-xxxx-xxxx-xxxxxxxxxxx", {}).and_return(true) + @couchdb.store("node", "bob", {}) + end + + it "should send the object to nanite for indexing" do + Chef::Nanite.should_receive(:request) + @couchdb.store("node", "bob", {}) + end end -end -describe Chef::CouchDB, "list" do - before(:each) do - @mock_rest = mock("Chef::REST", :null_object => true, :url => "http://monkeypants") - Chef::REST.stub!(:new).and_return(@mock_rest) - @couch = Chef::CouchDB.new("http://monkeypants") - Chef::Config.stub!(:[]).with(:couchdb_database).and_return("chef") + describe "load" do + before(:each) do + @mock_node = Chef::Node.new() + @mock_node.name("bob") + @couchdb.stub!(:find_by_name).with("node", "bob").and_return(@mock_node) + end + + it "should load the object from couchdb" do + @couchdb.load("node", "bob").should eql(@mock_node) + end end - - describe "on couchdb 0.8" do - before do - Chef::Config.stub!(:[]).with(:couchdb_version).and_return(0.8) + + describe "delete" do + before(:each) do + @mock_current = { + "version" => 1, + "_rev" => 1 + } + @mock_rest.stub!(:get_rest).and_return(@mock_current) + @mock_rest.stub!(:delete_rest).and_return(true) + @mock_node = Chef::Node.new() + @mock_node.name("bob") + @mock_node.couchdb_rev = 15 + @couchdb.stub!(:find_by_name).with("node", "bob", true).and_return([ @mock_node, "ax" ]) end - it "should get the view for all objects if inflate is true" do - @mock_rest.should_receive(:get_rest).with("chef/_view/node/all").and_return(true) - @couch.list("node", true) + def do_delete(rev=nil) + @couchdb.delete("node", "bob", rev) end - - it "should get the view for just the object id's if inflate is false" do - @mock_rest.should_receive(:get_rest).with("chef/_view/node/all_id").and_return(true) - @couch.list("node", false) + + it "should remove the object from couchdb with a specific revision" do + @mock_rest.should_receive(:delete_rest).with("chef/ax?rev=1") + do_delete(1) + end + + it "should remove the object from couchdb based on the couchdb_rev of the current obj" do + @mock_rest.should_receive(:delete_rest).with("chef/ax?rev=15") + do_delete end end - describe "on couchdb 0.9" do - before do - Chef::Config.stub!(:[]).with(:couchdb_version).and_return(0.9) + describe "list" do + before(:each) do + Chef::Config.stub!(:[]).with(:couchdb_database).and_return("chef") + @mock_response = mock("Chef::CouchDB::Response", :null_object => true) end - it "should get the view for all objects if inflate is true" do - @mock_rest.should_receive(:get_rest).with("chef/_design/node/_view/all").and_return(true) - @couch.list("node", true) + describe "on couchdb 0.8" do + before(:each) do + Chef::Config.stub!(:[]).with(:couchdb_version).and_return(0.8) + end + + it "should get the view for all objects if inflate is true" do + @mock_rest.should_receive(:get_rest).with("chef/_view/node/all").and_return(@mock_response) + @couchdb.list("node", true) + end + + it "should get the view for just the object id's if inflate is false" do + @mock_rest.should_receive(:get_rest).with("chef/_view/node/all_id").and_return(@mock_response) + @couchdb.list("node", false) + end end - it "should get the view for just the object id's if inflate is false" do - @mock_rest.should_receive(:get_rest).with("chef/_design/node/_view/all_id").and_return(true) - @couch.list("node", false) + describe "on couchdb 0.9" do + before do + Chef::Config.stub!(:[]).with(:couchdb_version).and_return(0.9) + end + + it "should get the view for all objects if inflate is true" do + @mock_rest.should_receive(:get_rest).with("chef/_design/node/_view/all").and_return(@mock_response) + @couchdb.list("node", true) + end + + it "should get the view for just the object id's if inflate is false" do + @mock_rest.should_receive(:get_rest).with("chef/_design/node/_view/all_id").and_return(@mock_response) + @couchdb.list("node", false) + end end end -end -describe Chef::CouchDB, "has_key?" do - before(:each) do - @mock_rest = mock("Chef::REST", :null_object => true) - Chef::REST.stub!(:new).and_return(@mock_rest) - end - - it "should return true if the object exists" do - @mock_rest.should_receive(:get_rest).and_return(true) - Chef::CouchDB.new.has_key?("node", "bob").should eql(true) - end - - it "should return false if the object does not exist" do - @mock_rest.should_receive(:get_rest).and_raise(ArgumentError) - Chef::CouchDB.new.has_key?("node", "bob").should eql(false) + describe "has_key?" do + it "should return true if the object exists" do + @couchdb.stub!(:find_by_name).with("node", "bob").and_return(true) + @couchdb.has_key?("node", "bob").should eql(true) + end + + it "should return false if the object does not exist" do + @couchdb.stub!(:find_by_name).and_raise(Chef::Exceptions::CouchDBNotFound) + @couchdb.has_key?("node", "bob").should eql(false) + end end + end + + + describe Chef::CouchDB, "get_view" do before do @mock_rest = mock("Chef::REST", :null_object => true, :url => "http://monkeypants") @@ -287,6 +296,7 @@ describe Chef::CouchDB, "view_uri" do @mock_rest.should_receive(:run_request).with( :GET, URI.parse("http://monkeypants/"), + {}, false, 10, false diff --git a/chef/spec/unit/data_bag_item_spec.rb b/chef/spec/unit/data_bag_item_spec.rb new file mode 100644 index 0000000000..43eb8f7e3d --- /dev/null +++ b/chef/spec/unit/data_bag_item_spec.rb @@ -0,0 +1,164 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) +require 'chef/data_bag_item' + +describe Chef::DataBagItem do + before(:each) do + @data_bag_item = Chef::DataBagItem.new + end + + describe "initialize" do + it "should be a Chef::DataBagItem" do + @data_bag_item.should be_a_kind_of(Chef::DataBagItem) + end + end + + describe "data_bag" do + it "should let you set the data_bag to a string" do + @data_bag_item.data_bag("clowns").should == "clowns" + end + + it "should return the current data_bag type" do + @data_bag_item.data_bag "clowns" + @data_bag_item.data_bag.should == "clowns" + end + + it "should not accept spaces" do + lambda { @data_bag_item.data_bag "clown masters" }.should raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + lambda { @data_bag_item.data_bag Hash.new }.should raise_error(ArgumentError) + end + end + + describe "raw_data" do + it "should let you set the raw_data with a hash" do + lambda { @data_bag_item.raw_data = { "id" => "octahedron" } }.should_not raise_error + end + + it "should let you set the raw_data from a mash" do + lambda { @data_bag_item.raw_data = Mash.new({ "id" => "octahedron" }) }.should_not raise_error + end + + it "should raise an exception if you set the raw data without a key" do + lambda { @data_bag_item.raw_data = { "monkey" => "pants" } }.should raise_error(ArgumentError) + end + + it "should raise an exception if you set the raw data to something other than a hash" do + lambda { @data_bag_item.raw_data = "katie rules" }.should raise_error(ArgumentError) + end + + it "should accept alphanum/-/_ for the id" do + lambda { @data_bag_item.raw_data = { "id" => "h1-_" } }.should_not raise_error(ArgumentError) + end + + it "should raise an exception if the id contains anything but alphanum/-/_" do + lambda { @data_bag_item.raw_data = { "id" => "!@#" } }.should raise_error(ArgumentError) + end + + it "should return the raw data" do + @data_bag_item.raw_data = { "id" => "highway_of_emptiness" } + @data_bag_item.raw_data.should == { "id" => "highway_of_emptiness" } + end + end + + describe "object_name" do + before(:each) do + @data_bag_item.data_bag("dreams") + @data_bag_item.raw_data = { "id" => "the_beatdown" } + end + + it "should return an object name based on the bag name and the raw_data id" do + @data_bag_item.object_name.should == "data_bag_item_dreams_the_beatdown" + end + end + + describe "class method object_name" do + it "should return an object name based based on the bag name and an id" do + Chef::DataBagItem.object_name("zen", "master").should == "data_bag_item_zen_master" + end + end + + describe "hash behaviour" do + before(:each) do + @data_bag_item.raw_data = { "id" => "journey", "trials" => "been through" } + end + + it "should respond to keys" do + @data_bag_item.keys.should include("id") + @data_bag_item.keys.should include("trials") + end + + it "should allow lookups with []" do + @data_bag_item["id"].should == "journey" + end + end + + describe "to_hash" do + before(:each) do + @data_bag_item.data_bag("still_lost") + @data_bag_item.raw_data = { "id" => "whoa", "i_know" => "kung_fu" } + @to_hash = @data_bag_item.to_hash + end + + it "should return a hash" do + @to_hash.should be_a_kind_of(Hash) + end + + it "should have the raw_data keys as top level keys" do + @to_hash["id"].should == "whoa" + @to_hash["i_know"].should == "kung_fu" + end + + it "should have the chef_type of data_bag_item" do + @to_hash["chef_type"].should == "data_bag_item" + end + + it "should have the data_bag set" do + @to_hash["data_bag"].should == "still_lost" + end + end + + describe "deserialize" do + before(:each) do + @data_bag_item.data_bag('mars_volta') + @data_bag_item.raw_data = { "id" => "octahedron", "snooze" => { "finally" => :world_will }} + @deserial = JSON.parse(@data_bag_item.to_json) + end + + it "should deserialize to a Chef::DataBagItem object" do + @deserial.should be_a_kind_of(Chef::DataBagItem) + end + + it "should have a matching 'data_bag' value" do + @deserial.data_bag.should == @data_bag_item.data_bag + end + + it "should have a matching 'id' key" do + @deserial["id"].should == "octahedron" + end + + it "should have a matching 'snooze' key" do + @deserial["snooze"].should == { "finally" => "world_will" } + end + end +end + diff --git a/chef/spec/unit/data_bag_spec.rb b/chef/spec/unit/data_bag_spec.rb new file mode 100644 index 0000000000..1f140b5f88 --- /dev/null +++ b/chef/spec/unit/data_bag_spec.rb @@ -0,0 +1,72 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) +require 'chef/data_bag' + +describe Chef::DataBag do + before(:each) do + @data_bag = Chef::DataBag.new + end + + describe "initialize" do + it "should be a Chef::DataBag" do + @data_bag.should be_a_kind_of(Chef::DataBag) + end + end + + describe "name" do + it "should let you set the name to a string" do + @data_bag.name("clowns").should == "clowns" + end + + it "should return the current name" do + @data_bag.name "clowns" + @data_bag.name.should == "clowns" + end + + it "should not accept spaces" do + lambda { @data_bag.name "clown masters" }.should raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + lambda { @data_bag.name Hash.new }.should raise_error(ArgumentError) + end + end + + describe "deserialize" do + before(:each) do + @data_bag.name('mars_volta') + @deserial = JSON.parse(@data_bag.to_json) + end + + it "should deserialize to a Chef::DataBag object" do + @deserial.should be_a_kind_of(Chef::DataBag) + end + + %w{ + name + }.each do |t| + it "should match '#{t}'" do + @deserial.send(t.to_sym).should == @data_bag.send(t.to_sym) + end + end + + end +end + diff --git a/chef/spec/unit/node_spec.rb b/chef/spec/unit/node_spec.rb index 3edf03120a..dc10f8b37a 100644 --- a/chef/spec/unit/node_spec.rb +++ b/chef/spec/unit/node_spec.rb @@ -21,6 +21,7 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) describe Chef::Node do before(:each) do Chef::Config.node_path(File.join(File.dirname(__FILE__), "..", "data", "nodes")) + Nanite.stub!(:request).and_return(true) @node = Chef::Node.new() end @@ -252,17 +253,6 @@ describe Chef::Node do end end - describe "to_index" do - before(:each) do - @node.foo("bar") - end - - it "should return a hash with :index attributes" do - @node.name("airplane") - @node.to_index.should == { "foo" => "bar", "index_name" => "node", "id" => "node_airplane", "name" => "airplane" } - end - end - describe "to_s" do it "should turn into a string like node[name]" do @node.name("airplane") @@ -311,7 +301,6 @@ describe Chef::Node do node = Chef::Node.new node.name "bob" node.couchdb_rev = 1 - Chef::Queue.should_receive(:send_msg).with(:queue, :remove, node) node.destroy end end @@ -320,14 +309,12 @@ describe Chef::Node do before(:each) do @mock_couch.stub!(:store).and_return({ "rev" => 33 }) Chef::CouchDB.stub!(:new).and_return(@mock_couch) - Chef::Queue.stub!(:send_msg).and_return(true) @node = Chef::Node.new @node.name "bob" @node.couchdb_rev = 1 end it "should save the node to couchdb" do - Chef::Queue.should_receive(:send_msg).with(:queue, :index, @node) @mock_couch.should_receive(:store).with("node", "bob", @node).and_return({ "rev" => 33 }) @node.save end diff --git a/chef/spec/unit/provider/group/groupadd_spec.rb b/chef/spec/unit/provider/group/groupadd_spec.rb index 6e8b11a663..e0fb144b93 100644 --- a/chef/spec/unit/provider/group/groupadd_spec.rb +++ b/chef/spec/unit/provider/group/groupadd_spec.rb @@ -177,6 +177,7 @@ end describe Chef::Provider::Group::Groupadd, "load_current_resource" do before do @node = mock("Chef::Node", :null_object => true) + Chef::Node.stub!(:new).and_return(@node) @new_resource = mock("Chef::Resource::Group", :null_object => true, :group_name => "aj") @provider = Chef::Provider::Group::Groupadd.new(@node, @new_resource) File.stub!(:exists?).and_return(false) diff --git a/chef/spec/unit/provider/http_request_spec.rb b/chef/spec/unit/provider/http_request_spec.rb index d47db173cc..5357999589 100644 --- a/chef/spec/unit/provider/http_request_spec.rb +++ b/chef/spec/unit/provider/http_request_spec.rb @@ -47,8 +47,8 @@ describe Chef::Provider::HttpRequest, "load_current_resource" do @provider = Chef::Provider::HttpRequest.new(@node, @new_resource) end - it "should set up a Chef::REST client" do - Chef::REST.should_receive(:new).with(@new_resource.url) + it "should set up a Chef::REST client, with no authentication" do + Chef::REST.should_receive(:new).with(@new_resource.url, nil, nil) @provider.load_current_resource end end @@ -205,4 +205,4 @@ describe Chef::Provider::HttpRequest, "action_delete" do @new_resource.should_receive(:updated=).with(true) @provider.action_delete end -end
\ No newline at end of file +end diff --git a/chef/spec/unit/queue_spec.rb b/chef/spec/unit/queue_spec.rb deleted file mode 100644 index 8f0fdcbc73..0000000000 --- a/chef/spec/unit/queue_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) - -describe Chef::Queue do - - it "should connect to a stomp server on localhost and 61613" do - Stomp::Connection.should_receive(:open).with("", "", "localhost", 61613, false).once - Chef::Queue.connect - end - - it "should allow config options to override defaults on connect" do - Chef::Config[:queue_user] = "monkey" - Chef::Config[:queue_password] = "password" - Chef::Config[:queue_host] = "10.10.10.10" - Chef::Config[:queue_port] = 61614 - Stomp::Connection.should_receive(:open).with("monkey", "password", "10.10.10.10", 61614, false).once - Chef::Queue.connect - end - - it "should make a url based on type and name" do - Chef::Queue.make_url("topic", "goal").should eql("/topic/chef/goal") - Chef::Queue.make_url("queue", "pool").should eql("/queue/chef/pool") - end - - it "should allow you to subscribe to a queue" do - queue = mock("Queue", :null_object => true) - queue.should_receive(:subscribe).with(Chef::Queue.make_url(:topic, :node)).once - Stomp::Connection.stub!(:open).and_return(queue) - Chef::Queue.connect - Chef::Queue.subscribe(:topic, :node) - end - - it "should allow you to send a message" do - message = mock("Message", :null_object => true) - message.should_receive(:to_json).once.and_return("some json") - connection = mock("Connection", :null_object => true) - connection.should_receive(:send).with(Chef::Queue.make_url(:queue, :node), "some json").once.and_return(true) - Stomp::Connection.stub!(:open).and_return(connection) - Chef::Queue.connect - Chef::Queue.send_msg(:queue, :node, message) - end - - it "should receive a message with receive_msg" do - raw_msg = mock("Stomp Message", :null_object => true) - raw_msg.should_receive(:body).twice.and_return("the body") - connection = mock("Connection", :null_object => true) - connection.should_receive(:receive).once.and_return(raw_msg) - JSON.should_receive(:parse).with("the body").and_return("the body") - Stomp::Connection.stub!(:open).and_return(connection) - Chef::Queue.connect - Chef::Queue.receive_msg.should eql([ "the body", raw_msg ]) - end - - it "should poll for a message with poll_msg, returning a message if there is one" do - raw_msg = mock("Stomp Message", :null_object => true) - raw_msg.should_receive(:body).once.and_return("the body") - connection = mock("Connection", :null_object => true) - connection.should_receive(:poll).once.and_return(raw_msg) - JSON.should_receive(:parse).with("the body").and_return("the body") - Stomp::Connection.stub!(:open).and_return(connection) - Chef::Queue.connect - Chef::Queue.poll_msg.should eql("the body") - end - - it "should poll for a message with poll_msg, returning nil if there is not a message" do - connection = mock("Connection", :null_object => true) - connection.should_receive(:poll).once.and_return(nil) - JSON.should_not_receive(:parse).with(nil) - Stomp::Connection.stub!(:open).and_return(connection) - Chef::Queue.connect - Chef::Queue.poll_msg.should eql(nil) - end - - it "should raise an exception if you disconnect without a connection" do - Stomp::Connection.stub!(:open).and_return(nil) - Chef::Queue.connect - lambda { Chef::Queue.disconnect }.should raise_error(ArgumentError) - end - - it "should disconnect an active connection" do - connection = mock("Connection", :null_object => true) - connection.should_receive(:disconnect).once.and_return(true) - Stomp::Connection.stub!(:open).and_return(connection) - Chef::Queue.connect - Chef::Queue.disconnect - end - -end diff --git a/chef/spec/unit/rest_spec.rb b/chef/spec/unit/rest_spec.rb index 3f93e60504..c8bbd854c8 100644 --- a/chef/spec/unit/rest_spec.rb +++ b/chef/spec/unit/rest_spec.rb @@ -20,333 +20,365 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper")) require 'uri' require 'net/https' -describe Chef::REST, "initialize method" do - it "should create a new Chef::REST" do - Chef::REST.new("url").should be_kind_of(Chef::REST) +describe Chef::REST do + before(:each) do + Chef::REST::CookieJar.stub!(:instance).and_return({}) + @rest = Chef::REST.new("url", nil, nil) end -end -describe Chef::REST, "get_rest method" do - it "should create a url from the path and base url" do - URI.should_receive(:parse).with("url/monkey") - r = Chef::REST.new("url") - r.stub!(:run_request) - r.get_rest("monkey") - end - - it "should call run_request :GET with the composed url object" do - URI.stub!(:parse).and_return(true) - r = Chef::REST.new("url") - r.should_receive(:run_request).with(:GET, true, false, 10, false).and_return(true) - r.get_rest("monkey") + describe "initialize" do + it "should create a new Chef::REST" do + @rest.should be_kind_of(Chef::REST) + end end -end -describe Chef::REST, "delete_rest method" do - it "should create a url from the path and base url" do - URI.should_receive(:parse).with("url/monkey") - r = Chef::REST.new("url") - r.stub!(:run_request) - r.delete_rest("monkey") - end - - it "should call run_request :DELETE with the composed url object" do - URI.stub!(:parse).and_return(true) - r = Chef::REST.new("url") - r.should_receive(:run_request).with(:DELETE, true).and_return(true) - r.delete_rest("monkey") - end -end + describe "load_signing_key" do + before(:each) do + @private_key = <<EOH +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAx8xAfO2BO8kUughpjWwHPN2rgDcES15PbMEGe6OdJgjFARkt +FMdEusbGxmXKpk51Ggxi2P6ZYEoZfniZWt4qSt4i1vanDayRlJ1qoRCOaYj5cQS7 +gpHspHWqkY3HfGvx4svdutQ06o/gypx2QYfi68YrIUQexPiTUhsnP9FlgNt40Rl1 +YgBiIlJUk7d3q+1b/+POTNKPeyjGK9hoTloplbSx+cYdZgc4/YpU0eLBoHPuPv5l +QD+Y8VNS39bvY2NWbCqhV508gExAK26FxXTDNpi2mTZmbRZ8U0PKrCgF6gBSeod5 +EdQnNgoZHmA2fzfPHWfJd2OEuMcNM7DWpPDizQIDAQABAoIBAAGVDYGvw9E8Y2yh +umxDSb9ipgQK637JTWm4EZwTDKCLezvp/iBm/5VXE6XoknVEs8q0BGhhg8dubstA +mz5L+hvDrJT1ORdzoWeC46BI6EfPrOIHPpDnJO+cevBSJh1HIZBBOw1KtuyQnSAd +oxYbxGFHnXnS90dqDIie7G2l897UWoiQWNMLY+A+l5H4GLC+4Phq02pLd4OQwXA3 +Nd+3Nq69aOeccyfSDeeG7u35TKrjQPIxU210aR18d/0trR20BKsKbT30GPE1tQQd +jm4uReSPttTQ+NjwBQKKYmO2F9b9MPzmQ7c+KycBRmf+IOgZeZ54JN0GzUXsDTjJ ++ZSgdgUCgYEA41aetBJwsKkF973gL54QCB5vFhRw3TdUgYhQgz04B5JGouGTSALy +u1XtO6il65Zf6FwFSzXiggYYxTKyP/zwL88CQAVA7rleyhoZrw2bD6R2RZLivRba +50rstltUbjevd96TagFY7i9gVHL9E6DKJH4unZfIM0Bl2IZQraqCR8MCgYEA4PzC +FfUwiLa5drN6OVWZZfwxOeMbQUsYVeq7pHyeuvIe0euhcCLabBqfVt0pxqf1hem+ +l2+PnSKtvbI9siwt6WvJCtB3e/3aHOA3d6Y9TYxoyJAK007mRlQbbgqLzG83tZH2 +twO2tjo+h1+nv5yjE7aF9ItszegwTWsupvR+Ei8CgYAy0nt6MCEnLTIbV0RWANT+ +q6cT3Y/5tFPc/Vdab4YmEypdYWZmk9olzSjSzHoDN8PLEz9PuAUiIjDJbPLyYR5k +4bdUDpicha5OKhWRz83Zal/SX+r2cLSRPmu6vKIcXbCJcKWt7g0uekLjvi0bhTeL +fvX23yavZnceN7Czkkm7twKBgEFTgrNHdykrDRzXLhT5ssm2+UAani5ONKm1t3gi +KyCS7rn7FevuYsdiz4M0Qk4JNLQGU6262dNBX3smBt32D/qnrj8ymo7o/WzG+bQH +E+OxcjdSA6KpVRl0kGZaL49Td7SDxkQLkwDEVqWN87IiNAOkSq7f0N7UnTnNdkVJ +1lVHAoGBANYgMoEj7gIJdch7hMdQcFfq9+4ntAAbsl3JFW+T9ChATn0XHAylP9ha +ZaGlRrC7vxcF06vMe0HXyH1XVK3J9186zliTa4oDjkQ0D5X7Ga7KktLXAmQTysUH +V3jwIQbAF6LqLUnGOq6rJzQxrWKvFt0mVDyuJzIJGSbnN/Sl5J6P +-----END RSA PRIVATE KEY----- +EOH + IO.stub!(:read).and_return(@private_key) + end -describe Chef::REST, "post_rest method" do - it "should create a url from the path and base url" do - URI.should_receive(:parse).with("url/monkey") - r = Chef::REST.new("url") - r.stub!(:run_request) - r.post_rest("monkey", "data") - end - - it "should call run_request :POST with the composed url object and data" do - URI.stub!(:parse).and_return(true) - r = Chef::REST.new("url") - r.should_receive(:run_request).with(:POST, true, "data").and_return(true) - r.post_rest("monkey", "data") - end -end + it "should return the contents of the key file" do + File.stub!(:exists?).and_return(true) + File.stub!(:readable?).and_return(true) + @rest.load_signing_key("/tmp/keyfile.pem").should be(@private_key) + end -describe Chef::REST, "put_rest method" do - it "should create a url from the path and base url" do - URI.should_receive(:parse).with("url/monkey") - r = Chef::REST.new("url") - r.stub!(:run_request) - r.put_rest("monkey", "data") - end - - it "should call run_request :PUT with the composed url object and data" do - URI.stub!(:parse).and_return(true) - r = Chef::REST.new("url") - r.should_receive(:run_request).with(:PUT, true, "data").and_return(true) - r.put_rest("monkey", "data") - end -end + it "should raise a Chef::Exceptions::PrivateKeyMissing exception if the key cannot be found" do + File.stub!(:exists?).and_return(false) + File.stub!(:readable?).and_return(true) #42! + lambda { + @rest.load_signing_key("/tmp/keyfile.pem") + }.should raise_error(Chef::Exceptions::PrivateKeyMissing) + end -describe Chef::REST, "run_request method" do - before(:each) do - Chef::REST::CookieJar.stub!(:instance).and_return({}) - @r = Chef::REST.new("url") - @url_mock = mock("URI", :null_object => true) - @url_mock.stub!(:host).and_return("one") - @url_mock.stub!(:port).and_return("80") - @url_mock.stub!(:path).and_return("/") - @url_mock.stub!(:query).and_return("foo=bar") - @url_mock.stub!(:scheme).and_return("https") - @url_mock.stub!(:user).and_return(nil) - @url_mock.stub!(:password).and_return(nil) - @url_mock.stub!(:to_s).and_return("https://one:80/?foo=bar") - @http_response_mock = mock("Net::HTTPSuccess", :null_object => true) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(true) - @http_response_mock.stub!(:body).and_return("ninja") - @http_response_mock.stub!(:error!).and_return(true) - @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" }) - @http_mock = mock("Net::HTTP", :null_object => true) - @http_mock.stub!(:verify_mode=).and_return(true) - @http_mock.stub!(:read_timeout=).and_return(true) - @http_mock.stub!(:use_ssl=).with(true).and_return(true) - @data_mock = mock("Data", :null_object => true) - @data_mock.stub!(:to_json).and_return('{ "one": "two" }') - @request_mock = mock("Request", :null_object => true) - @request_mock.stub!(:body=).and_return(true) - @request_mock.stub!(:method).and_return(true) - @request_mock.stub!(:path).and_return(true) - @http_mock.stub!(:request).and_return(@http_response_mock) - @tf_mock = mock(Tempfile, { :print => true, :close => true, :write => true }) - Tempfile.stub!(:new).with("chef-rest").and_return(@tf_mock) - end - - def do_run_request(method=:GET, data=false, limit=10, raw=false) - Net::HTTP.stub!(:new).and_return(@http_mock) - @r.run_request(method, @url_mock, data, limit, raw) + it "should raise a Chef::Exceptions::PrivateKeyMissing exception if the key cannot be read" do + File.stub!(:exists?).and_return(true) + File.stub!(:readable?).and_return(false) + lambda { + @rest.load_signing_key("/tmp/keyfile.pem") + }.should raise_error(Chef::Exceptions::PrivateKeyMissing) + end end - - it "should raise an exception if the redirect limit is 0" do - lambda { @r.run_request(:GET, "/", false, 0)}.should raise_error(ArgumentError) + + describe "get_rest" do + it "should create a url from the path and base url" do + URI.should_receive(:parse).with("url/monkey") + @rest.stub!(:run_request) + @rest.get_rest("monkey") + end + + it "should call run_request :GET with the composed url object" do + URI.stub!(:parse).and_return(true) + @rest.should_receive(:run_request).with(:GET, true, {}, false, 10, false).and_return(true) + @rest.get_rest("monkey") + end end - it "should not fail if URI contains %d characters" do - @http_response_mock.stub!(:read_body).and_yield("ninja") - @url_mock.stub!(:path).and_return("/myfile?Expires=1247484042&Signature=hpK%2BbHchAmUCErdz1yCBPazzGRQ%3D") - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.should_receive(:read_body).and_return(true) - do_run_request(:GET, false, 10, true) + describe "delete_rest" do + it "should create a url from the path and base url" do + URI.should_receive(:parse).with("url/monkey") + @rest.stub!(:run_request) + @rest.delete_rest("monkey") + end + + it "should call run_request :DELETE with the composed url object" do + URI.stub!(:parse).and_return(true) + @rest.should_receive(:run_request).with(:DELETE, true, {}).and_return(true) + @rest.delete_rest("monkey") + end end - - it "should use SSL if the url starts with https" do - @url_mock.should_receive(:scheme).and_return("https") - @http_mock.should_receive(:use_ssl=).with(true).and_return(true) - do_run_request + + describe "post_rest" do + it "should create a url from the path and base url" do + URI.should_receive(:parse).with("url/monkey") + @rest.stub!(:run_request) + @rest.post_rest("monkey", "data") + end + + it "should call run_request :POST with the composed url object and data" do + URI.stub!(:parse).and_return(true) + @rest.should_receive(:run_request).with(:POST, true, {}, "data").and_return(true) + @rest.post_rest("monkey", "data") + end end - - it "should set the OpenSSL Verify Mode to verify_none if requested" do - @http_mock.should_receive(:verify_mode=).and_return(true) - do_run_request + + describe "put_rest" do + it "should create a url from the path and base url" do + URI.should_receive(:parse).with("url/monkey") + @rest.stub!(:run_request) + @rest.put_rest("monkey", "data") + end + + it "should call run_request :PUT with the composed url object and data" do + URI.stub!(:parse).and_return(true) + @rest.should_receive(:run_request).with(:PUT, true, {}, "data").and_return(true) + @rest.put_rest("monkey", "data") + end end - - describe "with a client SSL cert" do + + describe Chef::REST, "run_request method" do before(:each) do - Chef::Config[:ssl_client_cert] = "/etc/chef/client-cert.pem" - Chef::Config[:ssl_client_key] = "/etc/chef/client-cert.key" - File.stub!(:exists?).with("/etc/chef/client-cert.pem").and_return(true) - File.stub!(:exists?).with("/etc/chef/client-cert.key").and_return(true) - File.stub!(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") - File.stub!(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") - OpenSSL::X509::Certificate.stub!(:new).and_return("monkey magic client data") - OpenSSL::PKey::RSA.stub!(:new).and_return("monkey magic key data") + @url_mock = mock("URI", :null_object => true) + @url_mock.stub!(:host).and_return("one") + @url_mock.stub!(:port).and_return("80") + @url_mock.stub!(:path).and_return("/") + @url_mock.stub!(:query).and_return("foo=bar") + @url_mock.stub!(:scheme).and_return("https") + @url_mock.stub!(:to_s).and_return("https://one:80/?foo=bar") + @http_response_mock = mock("Net::HTTPSuccess", :null_object => true) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(true) + @http_response_mock.stub!(:body).and_return("ninja") + @http_response_mock.stub!(:error!).and_return(true) + @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" }) + @http_mock = mock("Net::HTTP", :null_object => true) + @http_mock.stub!(:verify_mode=).and_return(true) + @http_mock.stub!(:read_timeout=).and_return(true) + @http_mock.stub!(:use_ssl=).with(true).and_return(true) + @data_mock = mock("Data", :null_object => true) + @data_mock.stub!(:to_json).and_return('{ "one": "two" }') + @request_mock = mock("Request", :null_object => true) + @request_mock.stub!(:body=).and_return(true) + @request_mock.stub!(:method).and_return(true) + @request_mock.stub!(:path).and_return(true) + @http_mock.stub!(:request).and_return(@http_response_mock) + @tf_mock = mock(Tempfile, { :print => true, :close => true, :write => true }) + Tempfile.stub!(:new).with("chef-rest").and_return(@tf_mock) end - - it "should check that the client cert file exists" do - File.should_receive(:exists?).with("/etc/chef/client-cert.pem").and_return(true) + + def do_run_request(method=:GET, data=false, limit=10, raw=false) + Net::HTTP.stub!(:new).and_return(@http_mock) + @rest.run_request(method, @url_mock, {}, data, limit, raw) + end + + it "should raise an exception if the redirect limit is 0" do + lambda { @rest.run_request(:GET, "/", {}, false, 0)}.should raise_error(ArgumentError) + end + + it "should use SSL if the url starts with https" do + @url_mock.should_receive(:scheme).and_return("https") + @http_mock.should_receive(:use_ssl=).with(true).and_return(true) do_run_request end - - it "should read the cert file" do - File.should_receive(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") + + it "should set the OpenSSL Verify Mode to verify_none if requested" do + @http_mock.should_receive(:verify_mode=).and_return(true) do_run_request end + + describe "with a client SSL cert" do + before(:each) do + Chef::Config[:ssl_client_cert] = "/etc/chef/client-cert.pem" + Chef::Config[:ssl_client_key] = "/etc/chef/client-cert.key" + File.stub!(:exists?).with("/etc/chef/client-cert.pem").and_return(true) + File.stub!(:exists?).with("/etc/chef/client-cert.key").and_return(true) + File.stub!(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") + File.stub!(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") + OpenSSL::X509::Certificate.stub!(:new).and_return("monkey magic client data") + OpenSSL::PKey::RSA.stub!(:new).and_return("monkey magic key data") + end + + it "should check that the client cert file exists" do + File.should_receive(:exists?).with("/etc/chef/client-cert.pem").and_return(true) + do_run_request + end + + it "should read the cert file" do + File.should_receive(:read).with("/etc/chef/client-cert.pem").and_return("monkey magic client") + do_run_request + end + + it "should read the cert into OpenSSL" do + OpenSSL::X509::Certificate.should_receive(:new).and_return("monkey magic client data") + do_run_request + end + + it "should set the cert" do + @http_mock.should_receive(:cert=).and_return(true) + do_run_request + end + + it "should read the key file" do + File.should_receive(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") + do_run_request + end + + it "should read the key into OpenSSL" do + OpenSSL::PKey::RSA.should_receive(:new).and_return("monkey magic key data") + do_run_request + end + + it "should set the key" do + @http_mock.should_receive(:key=).and_return(true) + do_run_request + end - it "should read the cert into OpenSSL" do - OpenSSL::X509::Certificate.should_receive(:new).and_return("monkey magic client data") - do_run_request end - it "should set the cert" do - @http_mock.should_receive(:cert=).and_return(true) + it "should set a read timeout based on the rest_timeout config option" do + Chef::Config[:rest_timeout] = 10 + @http_mock.should_receive(:read_timeout=).with(10).and_return(true) do_run_request end - - it "should read the key file" do - File.should_receive(:read).with("/etc/chef/client-cert.key").and_return("monkey magic key") + + it "should set the cookie for this request if one exists for the given host:port" do + @rest.cookies = { "#{@url_mock.host}:#{@url_mock.port}" => "cookie monster" } + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", + { 'Accept' => 'application/json', 'Cookie' => 'cookie monster' } + ).and_return(@request_mock) do_run_request + @rest.cookies = Hash.new end - - it "should read the key into OpenSSL" do - OpenSSL::PKey::RSA.should_receive(:new).and_return("monkey magic key data") + + it "should build a new HTTP GET request" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", + { 'Accept' => 'application/json' } + ).and_return(@request_mock) do_run_request end - - it "should set the key" do - @http_mock.should_receive(:key=).and_return(true) + + it "should build a new HTTP POST request" do + Net::HTTP::Post.should_receive(:new).with("/?foo=bar", + { 'Accept' => 'application/json', "Content-Type" => 'application/json' } + ).and_return(@request_mock) + do_run_request(:POST, @data_mock) + end + + it "should build a new HTTP PUT request" do + Net::HTTP::Put.should_receive(:new).with("/?foo=bar", + { 'Accept' => 'application/json', "Content-Type" => 'application/json' } + ).and_return(@request_mock) + do_run_request(:PUT, @data_mock) + end + + it "should build a new HTTP DELETE request" do + Net::HTTP::Delete.should_receive(:new).with("/?foo=bar", + { 'Accept' => 'application/json' } + ).and_return(@request_mock) + do_run_request(:DELETE) + end + + it "should raise an error if the method is not GET/PUT/POST/DELETE" do + lambda { do_run_request(:MONKEY) }.should raise_error(ArgumentError) + end + + it "should run an http request" do + @http_mock.should_receive(:request).and_return(@http_response_mock) do_run_request end - - end - - it "should set a read timeout based on the rest_timeout config option" do - Chef::Config[:rest_timeout] = 10 - @http_mock.should_receive(:read_timeout=).with(10).and_return(true) - do_run_request - end - - it "should set the cookie for this request if one exists for the given host:port" do - @r.cookies = { "#{@url_mock.host}:#{@url_mock.port}" => "cookie monster" } - Net::HTTP::Get.should_receive(:new).with("/?foo=bar", - { 'Accept' => 'application/json', 'Cookie' => 'cookie monster' } - ).and_return(@request_mock) - do_run_request - end - - it "should build a new HTTP GET request" do - Net::HTTP::Get.should_receive(:new).with("/?foo=bar", - { 'Accept' => 'application/json' } - ).and_return(@request_mock) - do_run_request - end - - it "should build a new HTTP POST request" do - Net::HTTP::Post.should_receive(:new).with("/", - { 'Accept' => 'application/json', "Content-Type" => 'application/json' } - ).and_return(@request_mock) - do_run_request(:POST, @data_mock) - end - - it "should build a new HTTP PUT request" do - Net::HTTP::Put.should_receive(:new).with("/", - { 'Accept' => 'application/json', "Content-Type" => 'application/json' } - ).and_return(@request_mock) - do_run_request(:PUT, @data_mock) - end - - it "should build a new HTTP DELETE request" do - Net::HTTP::Delete.should_receive(:new).with("/?foo=bar", - { 'Accept' => 'application/json' } - ).and_return(@request_mock) - do_run_request(:DELETE) - end - - describe "with HTTP Basic Authentication info in the URL" do - before(:each) do - @url_mock.stub!(:user).and_return('frodo') - @url_mock.stub!(:password).and_return('odorf') + + it "should return the body of the response on success" do + do_run_request.should eql("ninja") end - - %w(Get Post Put Delete).each do |verb| - it "should authenticate HTTP #{verb.upcase} requests" do - Net::HTTP::const_get(verb).stub!(:new).and_return(@request_mock) - @request_mock.should_receive(:basic_auth).with('frodo', 'odorf') - do_run_request(verb.upcase.to_sym) - end + + it "should inflate the body as to an object if JSON is returned" do + @http_response_mock.stub!(:[]).with('content-type').and_return("application/json") + JSON.should_receive(:parse).with("ninja").and_return(true) + do_run_request + end + + it "should call run_request again on a Redirect response" do + @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(true) + @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path) + lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError) end - end - it "should raise an error if the method is not GET/PUT/POST/DELETE" do - lambda { do_run_request(:MONKEY) }.should raise_error(ArgumentError) - end - - it "should run an http request" do - @http_mock.should_receive(:request).and_return(@http_response_mock) - do_run_request - end - - it "should return the body of the response on success" do - do_run_request.should eql("ninja") - end - - it "should inflate the body as to an object if JSON is returned" do - @http_response_mock.stub!(:[]).with('content-type').and_return("application/json") - JSON.should_receive(:parse).with("ninja").and_return(true) - do_run_request - end - - it "should call run_request again on a Redirect response" do - @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(true) - @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path) - lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError) - end + it "should call run_request again on a Permanent Redirect response" do + @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(true) + @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path) + lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError) + end + + it "should raise an exception on an unsuccessful request" do + @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false) + @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(false) + @http_response_mock.should_receive(:error!) + do_run_request + end + + it "should build a new HTTP GET request without the application/json accept header for raw reqs" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", {}).and_return(@request_mock) + do_run_request(:GET, false, 10, true) + end + + it "should create a tempfile for the output of a raw request" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + Tempfile.should_receive(:new).with("chef-rest").and_return(@tf_mock) + do_run_request(:GET, false, 10, true).should eql(@tf_mock) + end + + it "should read the body of the response in chunks on a raw request" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @http_response_mock.should_receive(:read_body).and_return(true) + do_run_request(:GET, false, 10, true) + end + + it "should populate the tempfile with the value of the raw request" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @http_response_mock.stub!(:read_body).and_yield("ninja") + @tf_mock.should_receive(:write, "ninja").once.and_return(true) + do_run_request(:GET, false, 10, true) + end + + it "should close the tempfile if we're doing a raw request" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @tf_mock.should_receive(:close).once.and_return(true) + do_run_request(:GET, false, 10, true) + end + + it "should not raise a divide by zero exception if the size is 0" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" }) + @http_response_mock.stub!(:read_body).and_yield('') + lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError) + end + + it "should not raise a divide by zero exception if the Content-Length is 0" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "0" }) + @http_response_mock.stub!(:read_body).and_yield("ninja") + lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError) + end + + it "should call read_body without a block if the request is not raw" do + @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) + @http_response_mock.should_receive(:read_body) + do_run_request(:GET, false, 10, false) + end - it "should call run_request again on a Permanent Redirect response" do - @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(true) - @http_response_mock.stub!(:[]).with('location').and_return(@url_mock.path) - lambda { do_run_request(method=:GET, data=false, limit=1) }.should raise_error(ArgumentError) - end - - it "should raise an exception on an unsuccessful request" do - @http_response_mock.stub!(:kind_of?).with(Net::HTTPSuccess).and_return(false) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPFound).and_return(false) - @http_response_mock.stub!(:kind_of?).with(Net::HTTPMovedPermanently).and_return(false) - @http_response_mock.should_receive(:error!) - do_run_request - end - - it "should build a new HTTP GET request without the application/json accept header for raw reqs" do - Net::HTTP::Get.should_receive(:new).with("/?foo=bar", {}).and_return(@request_mock) - do_run_request(:GET, false, 10, true) - end - - it "should create a tempfile for the output of a raw request" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - Tempfile.should_receive(:new).with("chef-rest").and_return(@tf_mock) - do_run_request(:GET, false, 10, true).should eql(@tf_mock) - end - - it "should read the body of the response in chunks on a raw request" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.should_receive(:read_body).and_return(true) - do_run_request(:GET, false, 10, true) - end - - it "should populate the tempfile with the value of the raw request" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.stub!(:read_body).and_yield("ninja") - @tf_mock.should_receive(:write, "ninja").once.and_return(true) - do_run_request(:GET, false, 10, true) - end - - it "should close the tempfile if we're doing a raw request" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @tf_mock.should_receive(:close).once.and_return(true) - do_run_request(:GET, false, 10, true) - end - - it "should not raise a divide by zero exception if the size is 0" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "5" }) - @http_response_mock.stub!(:read_body).and_yield('') - lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError) - end - - it "should not raise a divide by zero exception if the Content-Length is 0" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.stub!(:header).and_return({ 'Content-Length' => "0" }) - @http_response_mock.stub!(:read_body).and_yield("ninja") - lambda { do_run_request(:GET, false, 10, true) }.should_not raise_error(ZeroDivisionError) - end - - it "should call read_body without a block if the request is not raw" do - @http_mock.stub!(:request).and_yield(@http_response_mock).and_return(@http_response_mock) - @http_response_mock.should_receive(:read_body) - do_run_request(:GET, false, 10, false) end end + diff --git a/chef/spec/unit/run_list_spec.rb b/chef/spec/unit/run_list_spec.rb index 6e63d9edd4..deafa785af 100644 --- a/chef/spec/unit/run_list_spec.rb +++ b/chef/spec/unit/run_list_spec.rb @@ -175,7 +175,7 @@ describe Chef::RunList do describe "from couchdb" do it "should load the role from couchdb" do - Chef::Role.should_receive(:load).with("stubby") + Chef::Role.should_receive(:load) @run_list.expand("couchdb") end end diff --git a/chef/spec/unit/search/query_spec.rb b/chef/spec/unit/search/query_spec.rb new file mode 100644 index 0000000000..8cb1a87832 --- /dev/null +++ b/chef/spec/unit/search/query_spec.rb @@ -0,0 +1,105 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) +require 'chef/search/query' + +describe Chef::Search::Query do + before(:each) do + @rest = mock("Chef::REST", :null_object => true) + Chef::REST.stub!(:new).and_return(@rest) + @query = Chef::Search::Query.new + end + + describe "initialize" do + it "should return a Chef::Search::Query" do + @query.should be_a_kind_of(Chef::Search::Query) + end + end + + describe "search" do + before(:each) do + @response = { + "rows" => [ + { "id" => "for you" }, + { "id" => "hip hop" }, + { "id" => "thought was down by law for you" }, + { "id" => "kept it hard core for you" }, + ], + "start" => 0, + "total" => 4 + } + @rest.stub!(:get_rest).and_return(@response) + end + + it "should accept a type as the first argument" do + lambda { @query.search("foo") }.should_not raise_error(ArgumentError) + lambda { @query.search(:foo) }.should_not raise_error(ArgumentError) + lambda { @query.search(Hash.new) }.should raise_error(ArgumentError) + end + + it "should query for every object of a type by default" do + @rest.should_receive(:get_rest).with("search/foo?q=%2A%3A%2A&sort=&start=0&rows=20").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo) + end + + it "should allow a custom query" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla%3Adundee&sort=&start=0&rows=20").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee") + end + + it "should let you set a sort order" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla%3Adundee&sort=id+desc&start=0&rows=20").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc") + end + + it "should let you set a starting object" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla%3Adundee&sort=id+desc&start=2&rows=20").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc", 2) + end + + it "should let you set how many rows to return" do + @rest.should_receive(:get_rest).with("search/foo?q=gorilla%3Adundee&sort=id+desc&start=2&rows=40").and_return(@response) + @query = Chef::Search::Query.new + @query.search(:foo, "gorilla:dundee", "id desc", 2, 40) + end + + it "should return the raw rows, start, and total if no block is passed" do + rows, start, total = @query.search(:foo) + rows.should equal(@response["rows"]) + start.should equal(@response["start"]) + total.should equal(@response["total"]) + end + + it "should call a block for each object in the response" do + @call_me = mock("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + @query.search(:foo) { |r| @call_me.do(r) } + end + + it "should page through the responses" do + @call_me = mock("blocky") + @response["rows"].each { |r| @call_me.should_receive(:do).with(r) } + @query.search(:foo, "*:*", nil, 0, 1) { |r| @call_me.do(r) } + end + end +end diff --git a/chef/spec/unit/search/result_spec.rb b/chef/spec/unit/search/result_spec.rb deleted file mode 100644 index c72b46fa70..0000000000 --- a/chef/spec/unit/search/result_spec.rb +++ /dev/null @@ -1,84 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2009 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) -require 'chef/search/result' - -describe Chef::Search::Result do - before(:each) do - @sr = Chef::Search::Result.new - end - - describe "initialize" do - it "should return a Chef::Search::Result" do - @sr.should be_a_kind_of(Chef::Search::Result) - end - end - - describe "hash like behavior" do - it "should let you set and retrieve data" do - @sr[:one] = "two" - @sr[:one].should == "two" - end - - it "should let you enumerate with each" do - @sr[:one] = "two" - @sr[:three] = "four" - seen = Hash.new - @sr.each do |k,v| - seen[k] = v - end - seen["one"].should == "two" - seen["three"].should == "four" - end - end - - describe "auto-inflate to a nested hash" do - it "should allow for _ seperated keys to be auto-inflated to nested hashes" do - @sr["one_two_three"] = "four" - @sr["one"]["two"]["three"].should == "four" - end - end - - describe "to_json" do - it "should serialize to json" do - @sr[:one] = "two" - @sr[:three] = "four" - @sr.to_json.should =~ /"one":"two"/ - @sr.to_json.should =~ /"three":"four"/ - @sr.to_json.should =~ /"json_class":"Chef::Search::Result"/ - end - end - - describe "json_create" do - before(:each) do - @sr[:one] = "two" - @sr[:three] = "four" - @new_sr = JSON.parse(@sr.to_json) - end - - it "should create a new Chef::Search::Result" do - @new_sr.should be_a_kind_of(Chef::Search::Result) - end - - it "have all the keys of the original Chef::Search::Result" do - @new_sr["one"].should == "two" - @new_sr["three"].should == "four" - end - end -end diff --git a/chef/tasks/rspec.rb b/chef/tasks/rspec.rb index 44e9ea2864..45a6faa1c6 100644 --- a/chef/tasks/rspec.rb +++ b/chef/tasks/rspec.rb @@ -8,7 +8,7 @@ task :default => :spec desc "Run all specs in spec directory" Spec::Rake::SpecTask.new(:spec) do |t| - t.spec_opts = ['--options', "\"#{CHEF_ROOT}/spec/spec.opts\""] + t.spec_opts = ['--format', 'specdoc', '--options', "\"#{CHEF_ROOT}/spec/spec.opts\""] t.spec_files = FileList['spec/**/*_spec.rb'] end diff --git a/cucumber.yml b/cucumber.yml index 03ab26c20c..f2b579cd13 100644 --- a/cucumber.yml +++ b/cucumber.yml @@ -1,22 +1,34 @@ default: -f pretty features -r features/steps -r features/support -api: --tags api --format pretty -r features/steps -r features/support features -api_roles: --tags roles --format pretty -r features/steps -r features/support features +api: --tags @api --format pretty -r features/steps -r features/support features +api_cookbooks: --tags @api,@cookbooks --format pretty -r features/steps -r features/support features +api_cookbooks_tarballs: --tags @api,@cookbooks,@tarballs --format pretty -r features/steps -r features/support features +api_roles: --tags @api_roles --format pretty -r features/steps -r features/support features api_roles_create: --tags roles_create --format pretty -r features/steps -r features/support features api_roles_delete: --tags roles_delete --format pretty -r features/steps -r features/support features api_roles_list: --tags roles_list --format pretty -r features/steps -r features/support features api_roles_show: --tags roles_show --format pretty -r features/steps -r features/support features api_roles_update: --tags roles_update --format pretty -r features/steps -r features/support features -api_nodes: --tags nodes --format pretty -r features/steps -r features/support features +api_nodes: --tags @api_nodes --format pretty -r features/steps -r features/support features api_nodes_create: --tags nodes_create --format pretty -r features/steps -r features/support features api_nodes_delete: --tags nodes_delete --format pretty -r features/steps -r features/support features api_nodes_list: --tags nodes_list --format pretty -r features/steps -r features/support features api_nodes_show: --tags nodes_show --format pretty -r features/steps -r features/support features api_nodes_update: --tags nodes_update --format pretty -r features/steps -r features/support features -client: --tags client --format pretty -r features/steps -r features/support features +api_data: --tags @api_data --format pretty -r features/steps -r features/support features +api_data_delete: --tags @api_data_delete --format pretty -r features/steps -r features/support features +api_data_item: --tags @api_data_item --format pretty -r features/steps -r features/support features +api_search: --tags @api_search --format pretty -r features/steps -r features/support features +api_search_list: --tags @api_search_list --format pretty -r features/steps -r features/support features +api_search_show: --tags @api_search_show --format pretty -r features/steps -r features/support features +client: --tags @client --format pretty -r features/steps -r features/support features client_roles: --tags client_roles --format pretty -r features/steps -r features/support features -search: --tags search --format pretty -r features/steps -r features/support features -language: --tags language --format pretty -r features/steps -r features/support features -recipe_inclusion: --tags recipe_inclusion --format pretty -r features/steps -r features/support features -provider_remote_file: --tags provider,remote_file --format pretty -r features/steps -r features/support features -provider_package_macports: --tags macports --format pretty -r features/steps -r features/support features +search: --tags @search --format pretty -r features/steps -r features/support features +provider: --tags @provider --format pretty -r features/steps -r features/support features +provider_directory: --tags @provider_directory --format pretty -r features/steps -r features/support features +provider_execute: --tags @provider_execute --format pretty -r features/steps -r features/support features +provider_file: --tags @provider_file --format pretty -r features/steps -r features/support features +provider_remote_file: --tags @remote_file --format pretty -r features/steps -r features/support features +provider_package_macports: --tags @macports --format pretty -r features/steps -r features/support features +language: --tags @language --format pretty -r features/steps -r features/support features +cookbooks: --tags @cookbooks --format pretty -r features/steps -r features/support features diff --git a/features/api/cookbooks/list_cookbooks_api.feature b/features/api/cookbooks/list_cookbooks_api.feature new file mode 100644 index 0000000000..8888cfa3ce --- /dev/null +++ b/features/api/cookbooks/list_cookbooks_api.feature @@ -0,0 +1,19 @@ +@api @cookbooks @list_cookbooks +Feature: List cookbooks via the REST API + In order to know what cookbooks are loaded on the Chef Server + As a Developer + I want to list all the cookbooks + + Scenario: List cookbooks + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks' + Then the inflated responses key 'manage_files' should exist + And the inflated responses key 'manage_files' should match 'http://[^/]+/organizations/clownco/cookbooks/manage_files' + And the inflated responses key 'delayed_notifications' should exist + And the inflated responses key 'delayed_notifications' should match 'http://[^/]+/organizations/clownco/cookbooks/delayed_notifications' + + Scenario: List cookbooks with a wrong private key + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks' using a wrong private key + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/cookbooks/show_cookbook_api.feature b/features/api/cookbooks/show_cookbook_api.feature new file mode 100644 index 0000000000..48e19a2981 --- /dev/null +++ b/features/api/cookbooks/show_cookbook_api.feature @@ -0,0 +1,32 @@ +@api @cookbooks @show_cookbook +Feature: Show a cookbook via the REST API + In order to know what the details are for a cookbook + As a Developer + I want to show the details for a specific cookbook + + Scenario: Show a cookbook + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks/show_cookbook' + Then the inflated responses key 'name' should match 'show_cookbook' + Then the inflated responses key 'files' should match '^\[.+\]$' as json + Then the inflated responses key 'recipes' should match '^\[.+\]$' as json + Then the inflated responses key 'metadata' should match '^\{.+\}$' as json + Then the inflated responses key 'attributes' should match '^\[.+\]$' as json + Then the inflated responses key 'libraries' should match '^\[.+\]$' as json + Then the inflated responses key 'definitions' should match '^\[.+\]$' as json + Then the inflated responses key 'templates' should match '^\[.+\]$' as json + + Scenario: Show a missing cookbook + Given a 'registration' named 'bobo' exists + When I download the 'frabjabtasticaliciousmonkeyman' cookbook + Then the response code should be '404' + + Scenario: Show a cookbook with a wrong private key + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks/show_cookbook' using a wrong private key + Then I should get a '401 "Unauthorized"' exception + + Scenario: Show a cookbook without authenticating + When I 'GET' the path '/cookbooks/show_cookbook' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/cookbooks/show_cookbook_attributes_api.feature b/features/api/cookbooks/show_cookbook_attributes_api.feature new file mode 100644 index 0000000000..8e4fc0cd1a --- /dev/null +++ b/features/api/cookbooks/show_cookbook_attributes_api.feature @@ -0,0 +1,21 @@ +@api @cookbooks +Feature: Show a cookbooks attribute files via the REST API + In order to know what the details are for a cookbooks attribute files + As a Developer + I want to show the attribute files for a specific cookbook + + Scenario: Show a cookbooks attribute files + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks/show_cookbook/attributes' + Then the inflated response should match '^[.+]$' as json + + Scenario: Show a missing cookbook + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks/frabjabtasticaliciousmonkeyman/attributes' + Then I should get a '404 "Not Found"' exception + + Scenario: Show a cookbooks attribute files with a wrong private key + Given a 'registration' named 'bobo' exists + When I 'GET' the path '/cookbooks/show_cookbook/attributes' using a wrong private key + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/data/create_data_bag_api.feature b/features/api/data/create_data_bag_api.feature new file mode 100644 index 0000000000..c76018b2f0 --- /dev/null +++ b/features/api/data/create_data_bag_api.feature @@ -0,0 +1,26 @@ +@api @data @api_data +Feature: Create a data bag via the REST API + In order to create data bags programatically + As a Devleoper + I want to create data bags via the REST API + + Scenario: Create a new data bag + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' + When I authenticate as 'bobo' + And I 'POST' the 'data_bag' to the path '/data' + And the inflated responses key 'uri' should match '^http://.+/data/users$' + + Scenario: Create a data bag that already exists + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' + When I authenticate as 'bobo' + And I 'POST' the 'data_bag' to the path '/data' + And I 'POST' the 'data_bag' to the path '/data' + Then I should get a '403 "Forbidden"' exception + + Scenario: Create a new data bag without authenticating + Given a 'data_bag' named 'webserver' + When I 'POST' the 'data_bag' to the path '/data' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/data/create_data_bag_item_api.feature b/features/api/data/create_data_bag_item_api.feature new file mode 100644 index 0000000000..ebd9637b2b --- /dev/null +++ b/features/api/data/create_data_bag_item_api.feature @@ -0,0 +1,29 @@ +@api @data @api_data @api_data_item +Feature: Create a data bag item via the REST API + In order to store an item in a data bag programatically + As a Devleoper + I want to store data bag items via the REST API + + Scenario: Create a new data bag item + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' + When I authenticate as 'bobo' + And I 'PUT' the 'data_bag_item' to the path '/data/users/francis' + Then the inflated responses key 'id' should match '^francis$' + + Scenario: Update a data bag item that already exists + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'francis_extra' + When I authenticate as 'bobo' + And I 'PUT' the 'data_bag_item' to the path '/data/users/francis' + Then the inflated responses key 'id' should match '^francis$' + And the inflated responses key 'extra' should match '^majority$' + + Scenario: Create a new data bag without authenticating + Given a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' + When I 'PUT' the 'data_bag_item' to the path '/data/users/francis' + Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/data/delete_data_bag_api.feature b/features/api/data/delete_data_bag_api.feature new file mode 100644 index 0000000000..560d1e3ea6 --- /dev/null +++ b/features/api/data/delete_data_bag_api.feature @@ -0,0 +1,34 @@ +@api @data @api_data @api_data_delete +Feature: Delete a Data Bag via the REST API + In order to remove a Data Bag + As a Developer + I want to delete a Data Bag via the REST API + + Scenario: Delete a Data Bag + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'DELETE' the path '/data/users' + Then the inflated response should respond to 'name' with 'users' + + Scenario: Delete a Data Bag that does not exist + Given a 'registration' named 'bobo' exists + And there are no Data Bags + When I authenticate as 'bobo' + When I 'DELETE' the path '/data/users' + Then I should get a '404 "Not Found"' exception + + Scenario: Delete a Data Bag that has items in it + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + When I authenticate as 'bobo' + And I 'DELETE' the path '/data/users' + Then the inflated response should respond to 'name' with 'users' + And the data_bag named 'users' should not have an item named 'francis' + + Scenario: Delete a Data Bag without authenticating + Given a 'data_bag' named 'users' exists + When I 'DELETE' the path '/data/users' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/data/delete_data_bag_item.feature b/features/api/data/delete_data_bag_item.feature new file mode 100644 index 0000000000..057c2e55ec --- /dev/null +++ b/features/api/data/delete_data_bag_item.feature @@ -0,0 +1,27 @@ +@api @data @api_data @api_data_item +Feature: Delete a Data Bag Item via the REST API + In order to remove a Data Bag Item + As a Developer + I want to delete a Data Bag Item via the REST API + + Scenario: Delete a Data Bag Item + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + When I authenticate as 'bobo' + And I 'DELETE' the path '/data/users/francis' + Then the inflated responses key 'id' should match '^francis$' + + Scenario: Delete a Data Bag Item that does not exist + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + When I 'DELETE' the path '/data/users/francis' + Then I should get a '404 "Not Found"' exception + + Scenario: Delete a Data Bag Item without authenticating + Given a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + When I 'DELETE' the path '/data/users/francis' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/data/list_data_bags.feature b/features/api/data/list_data_bags.feature new file mode 100644 index 0000000000..97929d7747 --- /dev/null +++ b/features/api/data/list_data_bags.feature @@ -0,0 +1,34 @@ +@api @data @api_data +Feature: List data bags via the REST API + In order to know what data bags exists programatically + As a Developer + I want to list all the data bags + + Scenario: List data bags when none have been created + Given a 'registration' named 'bobo' exists + And there are no data bags + When I authenticate as 'bobo' + And I 'GET' the path '/data' + Then the inflated response should be an empty array + + Scenario: List data bags when one has been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data' + Then the inflated response should include '^http://.+/data/users$' + + Scenario: List data bags when two have been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag' named 'rubies' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data' + Then the inflated response should be '2' items long + And the inflated response should include '^http://.+/data/users$' + And the inflated response should include '^http://.+/data/rubies$' + + Scenario: List data bags when you are not authenticated + When I 'GET' the path '/data' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/data/show_data_bag_api.feature b/features/api/data/show_data_bag_api.feature new file mode 100644 index 0000000000..d27f64093a --- /dev/null +++ b/features/api/data/show_data_bag_api.feature @@ -0,0 +1,44 @@ +@api @data @api_data +Feature: Show a data_bag via the REST API + In order to know what the details are for a data_bag + As a Developer + I want to show the details for a specific data_bag + + Scenario: Show a data_bag with no entries in it + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data/users' + Then the inflated response should be an empty array + + Scenario: Show a data_bag with one entry in it + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data/users' + Then the inflated response should include '/data/users/francis' + + Scenario: Show a data_bag with two entries in it + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data/users' + Then the inflated response should include '/data/users/francis' + And the inflated response should include '/data/users/axl_rose' + + Scenario: Show a missing data_bag + Given a 'registration' named 'bobo' exists + And there are no data_bags + When I authenticate as 'bobo' + And I 'GET' the path '/data/users' + Then I should get a '404 "Not Found"' exception + + Scenario: Show a data_bag without authenticating + Given a 'data_bag' named 'users' exists + And I 'GET' the path '/data/users' + Then I should get a '401 "Unauthorized"' exception + + diff --git a/features/api/data/show_data_bag_item_api.feature b/features/api/data/show_data_bag_item_api.feature new file mode 100644 index 0000000000..fd0e474224 --- /dev/null +++ b/features/api/data/show_data_bag_item_api.feature @@ -0,0 +1,28 @@ +@api @data @api_data @api_data_item +Feature: Show a data_bag item via the REST API + In order to know what the data is for an item in a data_bag + As a Developer + I want to retrieve an item from a data_bag + + Scenario: Show a data_bag item + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data/users/francis' + Then the inflated responses key 'id' should match '^francis$' + + Scenario: Show a missing data_bag item + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'GET' the path '/data/users/francis' + Then I should get a '404 "Not Found"' exception + + Scenario: Show a data_bag item without authenticating + Given a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And I 'GET' the path '/data/users/francis' + Then I should get a '401 "Unauthorized"' exception + + diff --git a/features/api/nodes/cookbook_sync_api.feature b/features/api/nodes/cookbook_sync_api.feature new file mode 100644 index 0000000000..3d39796166 --- /dev/null +++ b/features/api/nodes/cookbook_sync_api.feature @@ -0,0 +1,25 @@ +@api @nodes @cookbook_sync @api_nodes +Feature: Synchronize cookbooks to the edge + In order to configure my nodes centrally + As a Developer + I want to synchronize the cookbooks from the server to the edge nodes + + Scenario: Retrieve the list of cookbook files to synchronize + Given a 'registration' named 'bobo' exists + And a 'node' named 'sync' exists + When I 'GET' the path '/nodes/sync/cookbooks' + And the inflated responses key 'node_cookbook_sync' should exist + And the inflated responses key 'node_cookbook_sync' should match '"recipes":' as json + And the inflated responses key 'node_cookbook_sync' should match 'default.rb' as json + And the inflated responses key 'node_cookbook_sync' should match '"definitions":' as json + And the inflated responses key 'node_cookbook_sync' should match 'def_file.rb' as json + And the inflated responses key 'node_cookbook_sync' should match '"libraries":' as json + And the inflated responses key 'node_cookbook_sync' should match 'lib_file.rb' as json + And the inflated responses key 'node_cookbook_sync' should match '"attributes":' as json + And the inflated responses key 'node_cookbook_sync' should match 'attr_file.rb' as json + + Scenario: Retrieve the list of cookbook files to synchronize with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'node' named 'sync' exists + When I 'GET' the path '/nodes/sync/cookbooks' using a wrong private key + Then I should get a '401 "Unauthorized"' exception
\ No newline at end of file diff --git a/features/api/nodes/create_node_api.feature b/features/api/nodes/create_node_api.feature index 5012a8a357..ec5bbb73be 100644 --- a/features/api/nodes/create_node_api.feature +++ b/features/api/nodes/create_node_api.feature @@ -1,4 +1,4 @@ -@api @nodes @nodes_create +@api @api_nodes @nodes_create Feature: Create a node via the REST API In order to create nodes programatically As a Devleoper @@ -7,19 +7,19 @@ Feature: Create a node via the REST API Scenario: Create a new node Given a 'registration' named 'bobo' exists And a 'node' named 'webserver' - When I authenticate as 'bobo' - And I 'POST' the 'node' to the path '/nodes' + When I 'POST' the 'node' to the path '/nodes' And the inflated responses key 'uri' should match '^http://.+/nodes/webserver$' Scenario: Create a node that already exists Given a 'registration' named 'bobo' exists And an 'node' named 'webserver' - When I authenticate as 'bobo' - And I 'POST' the 'node' to the path '/nodes' + When I 'POST' the 'node' to the path '/nodes' And I 'POST' the 'node' to the path '/nodes' Then I should get a '403 "Forbidden"' exception - - Scenario: Create a new node without authenticating - Given a 'node' named 'webserver' - When I 'POST' the 'node' to the path '/nodes' + + Scenario: Create a node with a wrong private key + Given a 'registration' named 'bobo' exists + And an 'node' named 'webserver' + When I 'POST' the 'node' to the path '/nodes' using a wrong private key Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/nodes/delete_node_api.feature b/features/api/nodes/delete_node_api.feature index 7addc6e790..46c027561d 100644 --- a/features/api/nodes/delete_node_api.feature +++ b/features/api/nodes/delete_node_api.feature @@ -1,4 +1,4 @@ -@api @nodes @nodes_delete +@api @api_nodes @nodes_delete Feature: Delete a node via the REST API In order to remove a node As a Developer @@ -7,19 +7,18 @@ Feature: Delete a node via the REST API Scenario: Delete a node Given a 'registration' named 'bobo' exists And a 'node' named 'webserver' exists - When I authenticate as 'bobo' - And I 'DELETE' the path '/nodes/webserver' + When I 'DELETE' the path '/nodes/webserver' Then the inflated response should respond to 'name' with 'webserver' Scenario: Delete a node that does not exist Given a 'registration' named 'bobo' exists And there are no nodes - When I authenticate as 'bobo' When I 'DELETE' the path '/nodes/webserver' Then I should get a '404 "Not Found"' exception - Scenario: Delete a node without authenticating - Given a 'node' named 'webserver' - When I 'DELETE' the path '/nodes/webserver' + Scenario: Delete a node with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'node' named 'webserver' exists + When I 'DELETE' the path '/nodes/webserver' using a wrong private key Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/nodes/list_nodes_api.feature b/features/api/nodes/list_nodes_api.feature index 4489c4d49a..7127720947 100644 --- a/features/api/nodes/list_nodes_api.feature +++ b/features/api/nodes/list_nodes_api.feature @@ -1,4 +1,4 @@ -@api @nodes @nodes_list +@api @api_nodes @nodes_list Feature: List nodes via the REST API In order to know what nodes exists programatically As a Developer @@ -7,28 +7,26 @@ Feature: List nodes via the REST API Scenario: List nodes when none have been created Given a 'registration' named 'bobo' exists And there are no nodes - When I authenticate as 'bobo' - And I 'GET' the path '/nodes' + When I 'GET' the path '/nodes' Then the inflated response should be an empty array Scenario: List nodes when one has been created Given a 'registration' named 'bobo' exists Given a 'node' named 'webserver' exists - When I authenticate as 'bobo' - And I 'GET' the path '/nodes' + When I 'GET' the path '/nodes' Then the inflated response should include '^http://.+/nodes/webserver$' - + Scenario: List nodes when two have been created Given a 'registration' named 'bobo' exists And a 'node' named 'webserver' exists And a 'node' named 'dbserver' exists - When I authenticate as 'bobo' - And I 'GET' the path '/nodes' + When I 'GET' the path '/nodes' Then the inflated response should be '2' items long And the inflated response should include '^http://.+/nodes/webserver$' And the inflated response should include '^http://.+/nodes/dbserver$' - Scenario: List nodes when you are not authenticated - When I 'GET' the path '/nodes' + Scenario: List nodes none have been created with a wrong private key + Given a 'registration' named 'bobo' exists + And there are no cookbooks + When I 'GET' the path '/nodes' using a wrong private key Then I should get a '401 "Unauthorized"' exception - diff --git a/features/api/nodes/show_node_api.feature b/features/api/nodes/show_node_api.feature index 8a2ab2990d..e26d127a06 100644 --- a/features/api/nodes/show_node_api.feature +++ b/features/api/nodes/show_node_api.feature @@ -1,4 +1,4 @@ -@api @nodes @nodes_show +@api @api_nodes @nodes_show Feature: Show a node via the REST API In order to know what the details are for a node As a Developer @@ -7,19 +7,18 @@ Feature: Show a node via the REST API Scenario: Show a node Given a 'registration' named 'bobo' exists And a 'node' named 'webserver' exists - When I authenticate as 'bobo' - And I 'GET' the path '/nodes/webserver' + When I 'GET' the path '/nodes/webserver' Then the inflated response should respond to 'name' with 'webserver' Scenario: Show a missing node Given a 'registration' named 'bobo' exists And there are no nodes - When I authenticate as 'bobo' - And I 'GET' the path '/nodes/bobo' + When I 'GET' the path '/nodes/bobo' Then I should get a '404 "Not Found"' exception - Scenario: Show a node without authenticating - Given a 'node' named 'webserver' exists - And I 'GET' the path '/nodes/webserver' + Scenario: Show a node with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'node' named 'webserver' exists + When I 'GET' the path '/nodes/webserver' using a wrong private key Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/nodes/update_node_api.feature b/features/api/nodes/update_node_api.feature index ecf512f64f..4f6ad41f1b 100644 --- a/features/api/nodes/update_node_api.feature +++ b/features/api/nodes/update_node_api.feature @@ -1,4 +1,4 @@ -@api @nodes @nodes_update +@api @api_nodes @nodes_update Feature: Update a node In order to keep my node data up-to-date As a Developer @@ -8,7 +8,6 @@ Feature: Update a node Given a 'registration' named 'bobo' exists And a 'node' named 'webserver' exists And sending the method '<method>' to the 'node' with '<updated_value>' - When I authenticate as 'bobo' When I 'PUT' the 'node' to the path '/nodes/webserver' Then the inflated response should respond to '<method>' with '<updated_value>' When I 'GET' the path '/nodes/webserver' @@ -19,9 +18,10 @@ Feature: Update a node | run_list | [ "recipe[one]", "recipe[two]" ] | | snakes | really arent so bad | - Scenario: Update a node without authenticating - Given a 'node' named 'webserver' - And sending the method 'snakes' to the 'node' with 'night train' - When I 'PUT' the 'node' to the path '/nodes/webserver' - Then I should get a '401 "Unauthorized"' exception + Scenario Outline: Update a node with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'node' named 'webserver' exists + And sending the method 'run_list' to the 'node' with '[ "recipe[one]", "recipe[two]" ]' + When I 'PUT' the 'node' to the path '/nodes/webserver' using a wrong private key + Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/roles/create_role_api.feature b/features/api/roles/create_role_api.feature index 7b04b64294..4282400c7f 100644 --- a/features/api/roles/create_role_api.feature +++ b/features/api/roles/create_role_api.feature @@ -1,4 +1,4 @@ -@api @roles @roles_create +@api @api_roles @roles_create Feature: Create a role via the REST API In order to create roles programatically As a Devleoper @@ -7,20 +7,18 @@ Feature: Create a role via the REST API Scenario: Create a new role Given a 'registration' named 'bobo' exists And a 'role' named 'webserver' - When I authenticate as 'bobo' - And I 'POST' the 'role' to the path '/roles' + When I 'POST' the 'role' to the path '/roles' And the inflated responses key 'uri' should match '^http://.+/roles/webserver$' Scenario: Create a role that already exists Given a 'registration' named 'bobo' exists And an 'role' named 'webserver' - When I authenticate as 'bobo' - And I 'POST' the 'role' to the path '/roles' + When I 'POST' the 'role' to the path '/roles' And I 'POST' the 'role' to the path '/roles' Then I should get a '403 "Forbidden"' exception - Scenario: Create a new role without authenticating - Given a 'role' named 'webserver' - When I 'POST' the 'role' to the path '/roles' + Scenario: Create a new role with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'role' named 'webserver' + When I 'POST' the 'role' to the path '/roles' using a wrong private key Then I should get a '401 "Unauthorized"' exception - diff --git a/features/api/roles/delete_role_api.feature b/features/api/roles/delete_role_api.feature index c6504741f9..0dab4128af 100644 --- a/features/api/roles/delete_role_api.feature +++ b/features/api/roles/delete_role_api.feature @@ -1,4 +1,4 @@ -@api @roles @roles_delete +@api @api_roles @roles_delete Feature: Delete a Role via the REST API In order to remove a role As a Developer @@ -7,19 +7,18 @@ Feature: Delete a Role via the REST API Scenario: Delete a Role Given a 'registration' named 'bobo' exists And a 'role' named 'webserver' exists - When I authenticate as 'bobo' - And I 'DELETE' the path '/roles/webserver' + When I 'DELETE' the path '/roles/webserver' Then the inflated response should respond to 'name' with 'webserver' Scenario: Delete a Role that does not exist Given a 'registration' named 'bobo' exists And there are no roles - When I authenticate as 'bobo' When I 'DELETE' the path '/roles/webserver' Then I should get a '404 "Not Found"' exception - - Scenario: Delete a Role without authenticating - Given a 'role' named 'webserver' - When I 'DELETE' the path '/roles/webserver' + + Scenario: Delete a Role with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'role' named 'webserver' exists + When I 'DELETE' the path '/roles/webserver' using a wrong private key Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/roles/list_roles_api.feature b/features/api/roles/list_roles_api.feature index edc96e0869..72251d2251 100644 --- a/features/api/roles/list_roles_api.feature +++ b/features/api/roles/list_roles_api.feature @@ -1,4 +1,4 @@ -@api @roles @roles_list +@api @api_roles @roles_list Feature: List roles via the REST API In order to know what roles exists programatically As a Developer @@ -6,30 +6,29 @@ Feature: List roles via the REST API Scenario: List roles when none have been created Given a 'registration' named 'bobo' exists - And there are no roles - When I authenticate as 'bobo' - And I 'GET' the path '/roles' - Then the inflated response should be an empty array + And there are no roles + When I 'GET' the path '/roles' + Then the inflated response should be '1' items long Scenario: List roles when one has been created Given a 'registration' named 'bobo' exists Given a 'role' named 'webserver' exists - When I authenticate as 'bobo' - And I 'GET' the path '/roles' + When I 'GET' the path '/roles' Then the inflated response should include '^http://.+/roles/webserver$' Scenario: List roles when two have been created Given a 'registration' named 'bobo' exists And a 'role' named 'webserver' exists And a 'role' named 'db' exists - When I authenticate as 'bobo' - And I 'GET' the path '/roles' + When I 'GET' the path '/roles' Then the inflated response should be '3' items long And the inflated response should include '^http://.+/roles/role_test$' And the inflated response should include '^http://.+/roles/webserver$' And the inflated response should include '^http://.+/roles/db$' - Scenario: List roles when you are not authenticated - When I 'GET' the path '/roles' + Scenario: List roles when none have been created with a wrong private key + Given a 'registration' named 'bobo' exists + And there are no roles + When I 'GET' the path '/roles' using a wrong private key Then I should get a '401 "Unauthorized"' exception diff --git a/features/api/roles/show_roles_api.feature b/features/api/roles/show_roles_api.feature index 70fe4aad60..5a7a5e2734 100644 --- a/features/api/roles/show_roles_api.feature +++ b/features/api/roles/show_roles_api.feature @@ -1,4 +1,4 @@ -@api @roles @roles_show +@api @api_roles @roles_show Feature: Show a role via the REST API In order to know what the details are for a Role As a Developer @@ -7,20 +7,18 @@ Feature: Show a role via the REST API Scenario: Show a role Given a 'registration' named 'bobo' exists And a 'role' named 'webserver' exists - When I authenticate as 'bobo' - And I 'GET' the path '/roles/webserver' + When I 'GET' the path '/roles/webserver' Then the inflated response should respond to 'name' with 'webserver' Scenario: Show a missing role Given a 'registration' named 'bobo' exists And there are no roles - When I authenticate as 'bobo' - And I 'GET' the path '/roles/bobo' + When I 'GET' the path '/roles/bobo' Then I should get a '404 "Not Found"' exception - Scenario: Show a role without authenticating - Given a 'role' named 'webserver' exists - And I 'GET' the path '/roles/webserver' + Scenario: Show a role with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'role' named 'webserver' exists + When I 'GET' the path '/roles/webserver' using a wrong private key Then I should get a '401 "Unauthorized"' exception - diff --git a/features/api/roles/update_roles_api.feature b/features/api/roles/update_roles_api.feature index 90a6282577..ea72f68c05 100644 --- a/features/api/roles/update_roles_api.feature +++ b/features/api/roles/update_roles_api.feature @@ -1,4 +1,4 @@ -@api @roles @roles_update +@api @api_roles @roles_update Feature: Update a role In order to keep my role data up-to-date As a Developer @@ -8,7 +8,6 @@ Feature: Update a role Given a 'registration' named 'bobo' exists And a 'role' named 'webserver' exists And sending the method '<method>' to the 'role' with '<updated_value>' - When I authenticate as 'bobo' When I 'PUT' the 'role' to the path '/roles/webserver' Then the inflated response should respond to '<method>' with '<updated_value>' When I 'GET' the path '/roles/webserver' @@ -21,9 +20,9 @@ Feature: Update a role | default_attributes | { "a": "d" } | | override_attributes | { "c": "e" } | - Scenario: Update a role without authenticating - Given a 'role' named 'webserver' - And sending the method 'description' to the 'role' with 'Is easy' - When I 'PUT' the 'role' to the path '/roles/webserver' + Scenario Outline: Update a role with a wrong private key + Given a 'registration' named 'bobo' exists + And a 'role' named 'webserver' exists + And sending the method '<method>' to the 'role' with '<updated_value>' + When I 'PUT' the 'role' to the path '/roles/webserver' using a wrong private key Then I should get a '401 "Unauthorized"' exception - diff --git a/features/api/search/list_search.feature b/features/api/search/list_search.feature new file mode 100644 index 0000000000..198f065f12 --- /dev/null +++ b/features/api/search/list_search.feature @@ -0,0 +1,29 @@ +@api @data @api_search @api_search_list +Feature: List search endpoints via the REST API + In order to know what search endpoints exist programatically + As a Developer + I want to list all the search indexes + + Scenario: List search indexes when no data bags have been created + Given a 'registration' named 'bobo' exists + And there are no data bags + When I authenticate as 'bobo' + And I 'GET' the path '/search' + Then the inflated response should include '^http://(.+)/search/node$' + And the inflated response should include '^http://(.+)/search/role$' + And the inflated response should be '2' items long + + Scenario: List search indexes when a data bag has been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'GET' the path '/search' + Then the inflated response should include '^http://(.+)/search/node$' + And the inflated response should include '^http://(.+)/search/role$' + And the inflated response should include '^http://(.+)/search/users$' + And the inflated response should be '3' items long + + Scenario: List search indexes when you are not authenticated + When I 'GET' the path '/search' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/api/search/show_search.feature b/features/api/search/show_search.feature new file mode 100644 index 0000000000..292d043b0d --- /dev/null +++ b/features/api/search/show_search.feature @@ -0,0 +1,115 @@ +@api @data @api_search @api_search_show +Feature: Search data via the REST API + In order to know about objects in the system + As a Developer + I want to search the objects + + Scenario: Search for objects when none have been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + When I authenticate as 'bobo' + And I 'GET' the path '/search/users' + Then the inflated responses key 'rows' should be '0' items long + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '0' + + Scenario: Search for objects when one has been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'francis' + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '1' + + Scenario: Search for objects when two have been created + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'francis' + And the inflated responses key 'rows' item '1' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '1' key 'id' should be 'axl_rose' + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '2' + + Scenario: Search for objects with a manual ascending sort order + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users?sort=id+asc' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'axl_rose' + And the inflated responses key 'rows' item '1' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '1' key 'id' should be 'francis' + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '2' + + Scenario: Search for objects with a manual descending sort order + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users?sort=id+desc' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'francis' + And the inflated responses key 'rows' item '1' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '1' key 'id' should be 'axl_rose' + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '2' + + Scenario: Search for objects and page through the results + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users?rows=1&sort=id+asc' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'axl_rose' + And the inflated responses key 'rows' should be '1' items long + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '2' + When I 'GET' the path '/search/users?rows=1&start=1&sort=id+asc' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'francis' + And the inflated responses key 'rows' should be '1' items long + And the inflated responses key 'start' should be the integer '1' + And the inflated responses key 'total' should be the integer '2' + + Scenario: Search for a subset of objects + Given a 'registration' named 'bobo' exists + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + And I wait for '10' seconds + When I authenticate as 'bobo' + And I 'GET' the path '/search/users?q=id:axl_rose' + Then the inflated responses key 'rows' item '0' should be a kind of 'Chef::DataBagItem' + And the inflated responses key 'rows' item '0' key 'id' should be 'axl_rose' + And the inflated responses key 'start' should be the integer '0' + And the inflated responses key 'total' should be the integer '1' + + Scenario: Search for a type of object that does not exist + Given a 'registration' named 'bobo' exists + When I authenticate as 'bobo' + And I 'GET' the path '/search/funkensteins' + Then I should get a '404 "Not Found"' exception + + Scenario: Search for objects when you are not authenticated + When I 'GET' the path '/search/users' + Then I should get a '401 "Unauthorized"' exception + diff --git a/features/data/config/client.rb b/features/data/config/client.rb index ab10d750df..5e51b26c82 100644 --- a/features/data/config/client.rb +++ b/features/data/config/client.rb @@ -6,9 +6,15 @@ log_location STDOUT file_cache_path File.join(tmpdir, "cache") ssl_verify_mode :verify_none registration_url "http://127.0.0.1:4000" -openid_url "http://127.0.0.1:4001" +openid_url "http://127.0.0.1:4000" template_url "http://127.0.0.1:4000" remotefile_url "http://127.0.0.1:4000" search_url "http://127.0.0.1:4000" -role_url "http://127.0.0.1:4000" -couchdb_database 'chef_integration' +role_url "http://127.0.0.1:4000" +client_url "http://127.0.0.1:4000" +chef_server_url "http://127.0.0.1:4000" +validation_client_name "validator" +systmpdir = File.expand_path(File.join(Dir.tmpdir, "chef_integration")) +validation_key File.join(systmpdir, "validation.pem") +client_key File.join(systmpdir, "client.pem") + diff --git a/features/data/config/server.rb b/features/data/config/server.rb index 51a7bacef0..cc7db3a232 100644 --- a/features/data/config/server.rb +++ b/features/data/config/server.rb @@ -6,17 +6,30 @@ log_location STDOUT file_cache_path File.join(tmpdir, "cache") ssl_verify_mode :verify_none registration_url "http://127.0.0.1:4000" -openid_url "http://127.0.0.1:4001" +openid_url "http://127.0.0.1:4000" template_url "http://127.0.0.1:4000" remotefile_url "http://127.0.0.1:4000" search_url "http://127.0.0.1:4000" -role_url "http://127.0.0.1:4000" +role_url "http://127.0.0.1:4000" +chef_server_url "http://127.0.0.1:4000" +client_url "http://127.0.0.1:4000" cookbook_path File.join(supportdir, "cookbooks") openid_store_path File.join(tmpdir, "openid", "store") openid_cstore_path File.join(tmpdir, "openid", "cstore") search_index_path File.join(tmpdir, "search_index") role_path File.join(supportdir, "roles") -validation_token 'ceelo' couchdb_database 'chef_integration' +systmpdir = File.expand_path(File.join(Dir.tmpdir, "chef_integration")) + +validation_client_name "validator" +validation_key File.join(systmpdir, "validation.pem") +client_key File.join(systmpdir, "client.pem") + +solr_jetty_path File.join(supportdir, "solr", "jetty") +solr_heap_size "250M" +solr_data_path File.join(supportdir, "solr", "data") +solr_home_path File.join(supportdir, "solr", "home") +solr_heap_size "256M" + Chef::Log::Formatter.show_time = true diff --git a/features/data/cookbooks/integration_setup/recipes/default.rb b/features/data/cookbooks/integration_setup/recipes/default.rb index 0ada2aa485..fc913b4502 100644 --- a/features/data/cookbooks/integration_setup/recipes/default.rb +++ b/features/data/cookbooks/integration_setup/recipes/default.rb @@ -19,7 +19,7 @@ directory node[:int][:tmpdir] do owner "root" - mode 1777 + mode "1777" action :create end diff --git a/features/data/cookbooks/node_cookbook_sync/README.rdoc b/features/data/cookbooks/node_cookbook_sync/README.rdoc new file mode 100644 index 0000000000..8d774805b9 --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/README.rdoc @@ -0,0 +1,8 @@ += DESCRIPTION: + += REQUIREMENTS: + += ATTRIBUTES: + += USAGE: + diff --git a/features/data/cookbooks/node_cookbook_sync/attributes/attr_file.rb b/features/data/cookbooks/node_cookbook_sync/attributes/attr_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/attributes/attr_file.rb diff --git a/features/data/cookbooks/node_cookbook_sync/definitions/def_file.rb b/features/data/cookbooks/node_cookbook_sync/definitions/def_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/definitions/def_file.rb diff --git a/features/data/cookbooks/node_cookbook_sync/libraries/lib_file.rb b/features/data/cookbooks/node_cookbook_sync/libraries/lib_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/libraries/lib_file.rb diff --git a/features/data/cookbooks/node_cookbook_sync/metadata.rb b/features/data/cookbooks/node_cookbook_sync/metadata.rb new file mode 100644 index 0000000000..850a993fbb --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/metadata.rb @@ -0,0 +1,6 @@ +maintainer "Opscode" +maintainer_email "do_not_reply@opscode.com" +license "Apache 2.0" +description "Installs/Configures node_cookbook_sync" +long_description IO.read(File.join(File.dirname(__FILE__), 'README.rdoc')) +version "0.1" diff --git a/features/data/cookbooks/node_cookbook_sync/recipes/default.rb b/features/data/cookbooks/node_cookbook_sync/recipes/default.rb new file mode 100644 index 0000000000..7912164bf3 --- /dev/null +++ b/features/data/cookbooks/node_cookbook_sync/recipes/default.rb @@ -0,0 +1,18 @@ +# +# Cookbook Name:: node_cookbook_sync +# Recipe:: default +# +# Copyright 2009, Opscode +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/features/data/cookbooks/search/recipes/search_data.rb b/features/data/cookbooks/search/recipes/search_data.rb index 6113da356f..6deefed9b3 100644 --- a/features/data/cookbooks/search/recipes/search_data.rb +++ b/features/data/cookbooks/search/recipes/search_data.rb @@ -17,11 +17,12 @@ # limitations under the License. # -node.save -sleep 5 -search(:node, "*") do |entry| - Chef::Log.error(entry.inspect) - entry["search_files"].each do |filename| - file "#{node[:tmpdir]}/#{filename}" - end +# We have to sleep at least 10 seconds to confirm that the data has made it +# into the index. We can only rely on this because we are in a test environment +# in real-land Chef, the index is only eventually consistent.. and may take a +# variable amount of time. +sleep 10 +search(:users, "*:*") do |entry| + file "#{node[:tmpdir]}/#{entry["id"]}" end + diff --git a/features/data/cookbooks/search/recipes/search_data_noblock.rb b/features/data/cookbooks/search/recipes/search_data_noblock.rb new file mode 100644 index 0000000000..6fb9de6d81 --- /dev/null +++ b/features/data/cookbooks/search/recipes/search_data_noblock.rb @@ -0,0 +1,32 @@ +# +# Cookbook Name:: search +# Recipe:: default +# +# Copyright 2009, Opscode +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# We have to sleep at least 10 seconds to confirm that the data has made it +# into the index. We can only rely on this because we are in a test environment +# in real-land Chef, the index is only eventually consistent.. and may take a +# variable amount of time. + + +sleep 10 +objects, start, rows = search(:users, "*:*") + +objects.each do |entry| + file "#{node[:tmpdir]}/#{entry["id"]}" +end + diff --git a/features/data/cookbooks/show_cookbook/README.rdoc b/features/data/cookbooks/show_cookbook/README.rdoc new file mode 100644 index 0000000000..8d774805b9 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/README.rdoc @@ -0,0 +1,8 @@ += DESCRIPTION: + += REQUIREMENTS: + += ATTRIBUTES: + += USAGE: + diff --git a/features/data/cookbooks/show_cookbook/attributes/attr_file.rb b/features/data/cookbooks/show_cookbook/attributes/attr_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/attributes/attr_file.rb diff --git a/features/data/cookbooks/show_cookbook/definitions/def_file.rb b/features/data/cookbooks/show_cookbook/definitions/def_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/definitions/def_file.rb diff --git a/features/data/cookbooks/show_cookbook/files/default/prime_time.txt b/features/data/cookbooks/show_cookbook/files/default/prime_time.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/files/default/prime_time.txt diff --git a/features/data/cookbooks/show_cookbook/files/host-latte/prime_time.txt b/features/data/cookbooks/show_cookbook/files/host-latte/prime_time.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/files/host-latte/prime_time.txt diff --git a/features/data/cookbooks/show_cookbook/files/mac_os_x-10.5/prime_time.txt b/features/data/cookbooks/show_cookbook/files/mac_os_x-10.5/prime_time.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/files/mac_os_x-10.5/prime_time.txt diff --git a/features/data/cookbooks/show_cookbook/files/mac_os_x/prime_time.txt b/features/data/cookbooks/show_cookbook/files/mac_os_x/prime_time.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/files/mac_os_x/prime_time.txt diff --git a/features/data/cookbooks/show_cookbook/libraries/lib_file.rb b/features/data/cookbooks/show_cookbook/libraries/lib_file.rb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/libraries/lib_file.rb diff --git a/features/data/cookbooks/show_cookbook/metadata.rb b/features/data/cookbooks/show_cookbook/metadata.rb new file mode 100644 index 0000000000..6f06805f62 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/metadata.rb @@ -0,0 +1,6 @@ +maintainer "Opscode" +maintainer_email "do_not_reply@opscode.com" +license "Apache 2.0" +description "Installs/Configures show_cookbook" +long_description IO.read(File.join(File.dirname(__FILE__), 'README.rdoc')) +version "1.0" diff --git a/features/data/cookbooks/show_cookbook/recipes/default.rb b/features/data/cookbooks/show_cookbook/recipes/default.rb new file mode 100644 index 0000000000..6f972192cb --- /dev/null +++ b/features/data/cookbooks/show_cookbook/recipes/default.rb @@ -0,0 +1,18 @@ +# +# Cookbook Name:: show_cookbook +# Recipe:: default +# +# Copyright 2009, Opscode +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/features/data/cookbooks/show_cookbook/templates/default/prime_time.txt.erb b/features/data/cookbooks/show_cookbook/templates/default/prime_time.txt.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/templates/default/prime_time.txt.erb diff --git a/features/data/cookbooks/show_cookbook/templates/host-latte/prime_time.txt.erb b/features/data/cookbooks/show_cookbook/templates/host-latte/prime_time.txt.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/templates/host-latte/prime_time.txt.erb diff --git a/features/data/cookbooks/show_cookbook/templates/mac_os_x-10.5/prime_time.txt.erb b/features/data/cookbooks/show_cookbook/templates/mac_os_x-10.5/prime_time.txt.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/templates/mac_os_x-10.5/prime_time.txt.erb diff --git a/features/data/cookbooks/show_cookbook/templates/mac_os_x/prime_time.txt.erb b/features/data/cookbooks/show_cookbook/templates/mac_os_x/prime_time.txt.erb new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/features/data/cookbooks/show_cookbook/templates/mac_os_x/prime_time.txt.erb diff --git a/features/language/delayed_notifications.feature b/features/language/delayed_notifications.feature index bc874f0f5e..511711c930 100644 --- a/features/language/delayed_notifications.feature +++ b/features/language/delayed_notifications.feature @@ -1,3 +1,4 @@ +@language Feature: Delayed Notifications In order to not impact the system we are configuring unduly As a developer diff --git a/features/provider/directory/create_directories.feature b/features/provider/directory/create_directories.feature index 0d2ab35ab9..b2ac1b157c 100644 --- a/features/provider/directory/create_directories.feature +++ b/features/provider/directory/create_directories.feature @@ -1,3 +1,4 @@ +@provider @provider_directory Feature: Create Directories In order to save time As a Developer diff --git a/features/provider/directory/delete_directories.feature b/features/provider/directory/delete_directories.feature index 1867e6c43c..e98ec2140e 100644 --- a/features/provider/directory/delete_directories.feature +++ b/features/provider/directory/delete_directories.feature @@ -1,3 +1,4 @@ +@provider @provider_directory Feature: Delete Directories In order to save time As a Developer diff --git a/features/provider/execute/run_commands.feature b/features/provider/execute/run_commands.feature index 96a7a36da9..28dd70482a 100644 --- a/features/provider/execute/run_commands.feature +++ b/features/provider/execute/run_commands.feature @@ -1,3 +1,4 @@ +@provider @provider_execute Feature: Run Commands In order to utilize the plethora of useful command line utilities As a Developer diff --git a/features/provider/file/manage_files.feature b/features/provider/file/manage_files.feature index 4034fb0f31..762fff3c62 100644 --- a/features/provider/file/manage_files.feature +++ b/features/provider/file/manage_files.feature @@ -1,3 +1,4 @@ +@provider @provider_file Feature: Manage Files In order to save time As a Developer diff --git a/features/search/search_data.feature b/features/search/search_data.feature index 26b256b077..f6544b7f2e 100644 --- a/features/search/search_data.feature +++ b/features/search/search_data.feature @@ -4,11 +4,25 @@ Feature: Search Data As a Developer I want to search the data - Scenario: Search the node index + Scenario: Search the user index Given a validated node And it includes the recipe 'search::search_data' + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists When I run the chef-client Then the run should exit '0' - And a file named 'search_one.txt' should exist - And a file named 'search_two.txt' should exist + And a file named 'francis' should exist + And a file named 'axl_rose' should exist + + Scenario: Search the user index without a block + Given a validated node + And it includes the recipe 'search::search_data_noblock' + And a 'data_bag' named 'users' exists + And a 'data_bag_item' named 'francis' exists + And a 'data_bag_item' named 'axl_rose' exists + When I run the chef-client + Then the run should exit '0' + And a file named 'francis' should exist + And a file named 'axl_rose' should exist diff --git a/features/steps/cookbook_steps.rb b/features/steps/cookbook_steps.rb index 202b4c0eda..1c5c0276ae 100644 --- a/features/steps/cookbook_steps.rb +++ b/features/steps/cookbook_steps.rb @@ -1,5 +1,6 @@ # # Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Chris Walters (<cw@opscode.com>) # Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 # @@ -51,3 +52,4 @@ When /^I run the rake task to generate cookbook metadata$/ do end end end + diff --git a/features/steps/couchdb_steps.rb b/features/steps/couchdb_steps.rb deleted file mode 100644 index be8b84db0a..0000000000 --- a/features/steps/couchdb_steps.rb +++ /dev/null @@ -1,33 +0,0 @@ -# -# Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Before do - system("mkdir -p #{tmpdir}") - cdb = Chef::CouchDB.new(Chef::Config[:couchdb_url]) - cdb.create_db - Chef::Node.create_design_document - Chef::Role.create_design_document - Chef::Role.sync_from_disk_to_couchdb - Chef::OpenIDRegistration.create_design_document -end - -After do - r = Chef::REST.new(Chef::Config[:couchdb_url]) - r.delete_rest("#{Chef::Config[:couchdb_database]}/") - system("rm -rf #{tmpdir}") -end diff --git a/features/steps/fixture_steps.rb b/features/steps/fixture_steps.rb index 6c9e8c9ed1..5239e5d762 100644 --- a/features/steps/fixture_steps.rb +++ b/features/steps/fixture_steps.rb @@ -1,19 +1,53 @@ +require 'ostruct' + Before do @fixtures = { + 'signing_caller' =>{ + :user_id=>'bobo', :secret_key => "/tmp/poop.pem" + }, 'registration' => { 'bobo' => Proc.new do - r = Chef::OpenIDRegistration.new - r.name = "bobo" - r.set_password('tclown') - r.validated = true - r.admin = true - r + + OpenStruct.new({ :save => true }) + #Chef::CouchDB.new(Chef::Config[:couchdb_url], "chef_integration")) + end + }, + 'data_bag' => { + 'users' => Proc.new do + b = Chef::DataBag.new(Chef::CouchDB.new(nil, "chef_integration")) + b.name "users" + b + end, + 'rubies' => Proc.new do + b = Chef::DataBag.new(Chef::CouchDB.new(nil, "chef_integration")) + b.name "rubies" + b + end + }, + 'data_bag_item' => { + 'francis' => Proc.new do + i = Chef::DataBagItem.new(Chef::CouchDB.new(nil, "chef_integration")) + i.data_bag "users" + i.raw_data = { "id" => "francis" } + i + end, + 'francis_extra' => Proc.new do + i = Chef::DataBagItem.new(Chef::CouchDB.new(nil, "chef_integration")) + i.data_bag "users" + i.raw_data = { "id" => "francis", "extra" => "majority" } + i + end, + 'axl_rose' => Proc.new do + i = Chef::DataBagItem.new(Chef::CouchDB.new(nil, "chef_integration")) + i.data_bag "users" + i.raw_data = { "id" => "axl_rose" } + i end }, 'role' => { 'webserver' => Proc.new do - r = Chef::Role.new + r = Chef::Role.new(Chef::CouchDB.new(nil, "chef_integration")) r.name "webserver" r.description "monkey" r.recipes("role::webserver", "role::base") @@ -22,7 +56,7 @@ Before do r end, 'db' => Proc.new do - r = Chef::Role.new + r = Chef::Role.new(Chef::CouchDB.new(nil, "chef_integration")) r.name "db" r.description "monkey" r.recipes("role::db", "role::base") @@ -33,7 +67,7 @@ Before do }, 'node' => { 'webserver' => Proc.new do - n = Chef::Node.new + n = Chef::Node.new(Chef::CouchDB.new(nil, "chef_integration")) n.name 'webserver' n.run_list << "tacos" n.snakes "on a plane" @@ -41,17 +75,34 @@ Before do n end, 'dbserver' => Proc.new do - n = Chef::Node.new + n = Chef::Node.new(Chef::CouchDB.new(nil, "chef_integration")) n.name 'dbserver' n.run_list << "oracle" n.just "kidding - who uses oracle?" n + end, + 'sync' => Proc.new do + n = Chef::Node.new(Chef::CouchDB.new(nil, "chef_integration")) + n.name 'sync' + n.run_list << "node_cookbook_sync" + n end } } @stash = {} end +def sign_request(http_method, private_key, user_id, body = "") + timestamp = Time.now.utc.iso8601 + sign_obj = Mixlib::Auth::SignedHeaderAuth.signing_object( + :http_method=>http_method, + :body=>body, + :user_id=>user_id, + :timestamp=>timestamp) + signed = sign_obj.sign(private_key).merge({:host => "localhost"}) + signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo} +end + def get_fixture(stash_name, stash_key) fixy = @fixtures[stash_name][stash_key] if fixy.kind_of?(Proc) @@ -62,20 +113,35 @@ def get_fixture(stash_name, stash_key) end Given /^an? '(.+)' named '(.+)'$/ do |stash_name, stash_key| - @stash[stash_name] = get_fixture(stash_name, stash_key) + # BUGBUG: I need to reference fixtures individually, but the fixtures, as written, store under the type, not the fixture's identifier and I don't currently have time to re-write the tests + + key = case stash_name + when 'file','hash' + stash_key + else + stash_name + end + @stash[key] = get_fixture(stash_name, stash_key) end -Given /^an? '(.+)' named '(.+)' exists$/ do |stash_name, stash_key| +Given /^an? '(.+)' named '(.+)' exists$/ do |stash_name, stash_key| @stash[stash_name] = get_fixture(stash_name, stash_key) - if @stash[stash_name].respond_to?(:save) - @stash[stash_name].save - else - request("/#{stash_name.pluralize}", { - :method => "POST", - "HTTP_ACCEPT" => 'application/json', - "CONTENT_TYPE" => 'application/json', - :input => @stash[stash_name].to_json - }) + + if stash_name == 'registration' + r = Chef::REST.new(Chef::Config[:registration_url], Chef::Config[:validation_user], Chef::Config[:validation_key]) + r.register("bobo", "#{tmpdir}/bobo.pem") + @rest = Chef::REST.new(Chef::Config[:registration_url], 'bobo', "#{tmpdir}/bobo.pem") + else + if @stash[stash_name].respond_to?(:save)#stash_name == "registration" + @stash[stash_name].save + else + request("#{stash_name.pluralize}", { + :method => "POST", + "HTTP_ACCEPT" => 'application/json', + "CONTENT_TYPE" => 'application/json', + :input => @stash[stash_name].to_json + }.merge(sign_request("POST", OpenSSL::PKey::RSA.new(IO.read("#{tmpdir}/client.pem")), "bobo"))) + end end end @@ -98,3 +164,7 @@ Given /^there are no (.+)$/ do |stash_name| Chef::Role.list(true).each { |r| r.destroy } end end + +Given /^I wait for '(\d+)' seconds$/ do |time| + sleep time.to_i +end diff --git a/features/steps/node_steps.rb b/features/steps/node_steps.rb index d1aa9a0045..3f4f76b870 100644 --- a/features/steps/node_steps.rb +++ b/features/steps/node_steps.rb @@ -20,15 +20,15 @@ # Given ### Given /^a validated node$/ do - client.validation_token = Chef::Config[:validation_token] = 'ceelo' + client.determine_node_name client.register - client.authenticate client.build_node client.node.recipes << "integration_setup" end Given /^it includes the recipe '(.+)'$/ do |recipe| self.recipe = recipe + Chef::Log.error("It's like this we have: #{Chef::Config[:chef_server_url]}") client.node.recipes << recipe client.save_node end diff --git a/features/steps/request_steps.rb b/features/steps/request_steps.rb index 156fc88b3a..892195e80e 100644 --- a/features/steps/request_steps.rb +++ b/features/steps/request_steps.rb @@ -1,21 +1,55 @@ -When /^I '(.+)' the path '(.+)'$/ do |http_method, request_uri| +When /^I '([^']*)' (?:to )?the path '([^']*)'$/ do |http_method, request_uri| begin self.response = rest.send("#{http_method}_rest".downcase.to_sym, request_uri) - self.inflated_response = self.response + self.inflated_response = self.response rescue + Chef::Log.debug("Caught exception in request: #{$!.message}") self.exception = $! end end +When /^I '([^']*)' to the path '(.+)'$/ do |http_method, request_uri| + When "I '#{http_method}' the path '#{request_uri}'" +end + +When /^I '(.+)' the path '(.+)' using a wrong private key$/ do |http_method, request_uri| + key = OpenSSL::PKey::RSA.generate(2048) + File.open(File.join(tmpdir, 'false_key.pem'), "w") { |f| f.print key } + @rest = Chef::REST.new(Chef::Config[:chef_server_url], 'snakebite' , File.join(tmpdir, 'false_key.pem')) + + When "I '#{http_method}' the path '#{request_uri}'" +end + When /^I '(.+)' the '(.+)' to the path '(.+)'$/ do |http_method, stash_key, request_uri| begin - self.response = rest.send("#{http_method}_rest".downcase.to_sym, request_uri, stash[stash_key]) + self.response = rest.send("#{http_method.to_s.downcase}_rest".downcase.to_sym, request_uri, stash[stash_key]) self.inflated_response = response rescue self.exception = $! end end +When /^I '(.+)' the '(.+)' to the path '(.+)' using a wrong private key$/ do |http_method, stash_key, request_uri| + key = OpenSSL::PKey::RSA.generate(2048) + File.open(File.join(tmpdir, 'false_key.pem'), "w") { |f| f.print key } + @rest = Chef::REST.new(Chef::Config[:chef_server_url], 'snakebite' , File.join(tmpdir, 'false_key.pem')) + + When "I '#{http_method}' the '#{stash_key}' to the path '#{request_uri}'" +end + +When /^I delete local private key/ do + Chef::FileCache.delete("private_key.pem") +end + +When /^I register '(.+)'$/ do |user| + begin + rest = Chef::REST.new(Chef::Config[:registration_url]) + rest.register("bobo") + rescue + self.exception = $! + end +end + When /^I authenticate as '(.+)'$/ do |reg| begin rest.authenticate(reg, 'tclown') @@ -24,3 +58,59 @@ When /^I authenticate as '(.+)'$/ do |reg| end end + + + +# When /^I '(.+)' the path '(.+)'$/ do |http_method, request_uri| +# begin +# #if http_method.downcase == 'get' +# # self.response = @rest.get_rest(request_uri) +# #else +# #puts "test test test \n\n\n\n\n\n\n" +# @response = @rest.send("#{http_method}_rest".downcase.to_sym, request_uri) +# #end +# puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" +# puts @response +# puts @response['content-type'] +# #puts self.response +# #puts self.response.inspect +# #self.inflated_response = self.response +# @inflated_response = @response#JSON.parse(response.body.to_s) +# puts "~~~~~~~~INFLATED RESPONSE~~~~~~~~~~~~" +# puts @inflated_response +# rescue +# self.exception = $! +# end +# end +# +# When /^I '(.+)' the '(.+)' to the path '(.+)'$/ do |http_method, stash_key, request_uri| +# begin +# #if http_method.downcase == 'post' +# # puts "post request" +# # self.response = @rest.post_rest(request_uri, @stash[stash_key]) +# # puts self.response +# #else +# puts "This is the request -- @stash[stash_key]:" +# puts @stash[stash_key].to_s +# @response = @rest.send("#{http_method}_rest".downcase.to_sym, request_uri, @stash[stash_key]) +# #end +# puts "This is the response:" +# #puts self.response.body.to_s +# puts @response +# #self.inflated_response = response +# @inflated_response = @response#JSON.parse(self.response.body.to_s) +# puts "~~~~~~~~INFLATED RESPONSE~~~~~~~~~~~~" +# puts @inflated_response +# rescue +# self.exception = $! +# end +# end +# +# When /^I authenticate as '(.+)'$/ do |reg| +# begin +# rest.authenticate(reg, 'tclown') +# rescue +# self.exception = $! +# end +# end +# diff --git a/features/steps/response_steps.rb b/features/steps/response_steps.rb index 3f3ef28459..0c0fc2d6ab 100644 --- a/features/steps/response_steps.rb +++ b/features/steps/response_steps.rb @@ -3,70 +3,101 @@ Then /^I should get a '(.+)' exception$/ do |exception| end Then /^the response code should be '(.+)'$/ do |response_code| - response.status.should == response_code.to_i + self.response.status.should == response_code.to_i +end + +Then /^the inflated responses key '(.+)' should be the integer '(\d+)'$/ do |key, int| + inflated_response[key].should == int.to_i end Then /^the inflated responses key '(.+)' should match '(.+)'$/ do |key, regex| - inflated_response[key].should =~ /#{regex}/ + puts self.inflated_response.inspect if ENV['DEBUG'] + self.inflated_response[key].should =~ /#{regex}/m +end + +Then /^the inflated responses key '(.+)' should match '(.+)' as json$/ do |key, regex| + puts self.inflated_response.inspect if ENV["DEBUG"] + self.inflated_response[key].to_json.should =~ /#{regex}/m +end + +Then /^the inflated responses key '(.+)' item '(\d+)' should be a kind of '(.+)'$/ do |key, index, constant| + inflated_response[key][index.to_i].should be_a_kind_of(eval(constant)) +end + +Then /^the inflated responses key '(.+)' item '(\d+)' key '(.+)' should be '(.+)'$/ do |key, index, sub_key, to_equal| + inflated_response[key][index.to_i][sub_key].should == to_equal +end + +Then /^the inflated responses key '(.+)' should be '(\d+)' items long$/ do |key, length| + inflated_response[key].length.should == length.to_i end Then /^the inflated responses key '(.+)' should not exist$/ do |key| - inflated_response.has_key?(key).should == false + self.inflated_response.has_key?(key).should == false end Then /^the inflated responses key '(.+)' should exist$/ do |key| - inflated_response.has_key?(key).should == true + self.inflated_response.has_key?(key).should == true end Then /^the inflated response should be an empty array$/ do - inflated_response.should == [] + self.inflated_response.should == [] end Then /^the inflated response should include '(.+)'$/ do |entry| - inflated_response.detect { |n| n =~ /#{entry}/ }.should be(true) + self.inflated_response.detect { |n| n =~ /#{entry}/ }.should be(true) end Then /^the inflated response should be '(.+)' items long$/ do |length| - inflated_response.length.should == length.to_i + self.inflated_response.length.should == length.to_i end Then /^the '(.+)' header should match '(.+)'$/ do |header, regex| - response.headers[header].should =~ /#{regex}/ + self.response.headers[header].should =~ /#{regex}/ end Then /^the inflated responses key '(.+)' should include '(.+)'$/ do |key, regex| - inflated_response[key].detect { |n| n =~ /#{regex}/ }.should be(true) + self.inflated_response[key].detect { |n| n =~ /#{regex}/ }.should be(true) end Then /^the inflated response should match the '(.+)'$/ do |stash_name| stash[stash_name].each do |k,v| - inflated_response[k.to_s].should == v + self.inflated_response[k.to_s].should == v end end Then /^the inflated response should be the '(.+)'$/ do |stash_key| - stash[stash_key].should == inflated_response + stash[stash_key].should == self.inflated_response end Then /^the inflated response should be a kind of '(.+)'$/ do |thing| - inflated_response.should be_a_kind_of(thing) + self.inflated_response.should be_a_kind_of(thing) end Then /^the inflated response should respond to '(.+)' with '(.+)'$/ do |method, to_match| to_match = JSON.parse(to_match) if to_match =~ /^\[|\{/ - inflated_response.send(method.to_sym).should == to_match + self.inflated_response.send(method.to_sym).should == to_match end Then /^the inflated response should respond to '(.+)' and match '(.+)'$/ do |method, to_match| - inflated_response.send(method.to_sym).should == to_match + self.inflated_response.send(method.to_sym).should == to_match end Then /^the fields in the inflated response should match the '(.+)'$/ do |stash_name| - inflated_response.each do |k,v| + self.inflated_response.each do |k,v| unless k =~ /^_/ || k == 'couchrest-type' stash[stash_name][k.to_sym].should == v end end end +Then /^the data_bag named '(.+)' should not have an item named '(.+)'$/ do |data_bag, item| + exists = true + begin + Chef::DataBagItem.load(data_bag, item, @couchdb) + rescue + exists = false + end + exists.should == false +end diff --git a/features/steps/run_client_steps.rb b/features/steps/run_client_steps.rb index 815c5c7148..25726f59aa 100644 --- a/features/steps/run_client_steps.rb +++ b/features/steps/run_client_steps.rb @@ -24,7 +24,7 @@ When /^I run the chef\-client$/ do @chef_args ||= "" @config_file ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'client.rb')) status = Chef::Mixin::Command.popen4( - "chef-client -l #{@log_level} -c #{@config_file} #{@chef_args}") do |p, i, o, e| + "#{File.join(File.dirname(__FILE__), "..", "..", "chef", "bin", "chef-client")} -l #{@log_level} -c #{@config_file} #{@chef_args}") do |p, i, o, e| @stdout = o.gets(nil) @stderr = e.gets(nil) end @@ -76,7 +76,7 @@ openid_url "http://127.0.0.1:4001" template_url "http://127.0.0.1:4000" remotefile_url "http://127.0.0.1:4000" search_url "http://127.0.0.1:4000" -couchdb_database 'chef_integration' +couchdb_database 'chef' CONFIG @config_file = File.expand_path(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'client-with-logging.rb')) @@ -86,12 +86,11 @@ CONFIG self.cleanup_files << @config_file - @status = Chef::Mixin::Command.popen4("chef-client -c #{@config_file}") do |p, i, o, e| + + @status = Chef::Mixin::Command.popen4("#{File.join(File.dirname(__FILE__), "..", "..", "chef", "bin", "chef-client")} -l #{@log_level} -c #{@config_file} #{@chef_args}") do |p, i, o, e| @stdout = o.gets(nil) @stderr = e.gets(nil) end - - end ### diff --git a/features/steps/run_solo.rb b/features/steps/run_solo.rb index e5397b853d..b2b244cfb6 100644 --- a/features/steps/run_solo.rb +++ b/features/steps/run_solo.rb @@ -26,7 +26,7 @@ When /^I run chef-solo with the '(.+)' recipe$/ do |recipe_name| cleanup_files << config_file binary_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'chef', 'bin', 'chef-solo')) - command = "chef-solo -c #{config_file} -j #{dna_file}" + command = "#{binary_path} -c #{config_file} -j #{dna_file}" command += " -l debug" if ENV['LOG_LEVEL'] == 'debug' # Run it diff --git a/features/steps/webrat_steps.rb b/features/steps/webrat_steps.rb index c7f78a5406..b152a1501d 100644 --- a/features/steps/webrat_steps.rb +++ b/features/steps/webrat_steps.rb @@ -36,4 +36,4 @@ end When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field| attach_file(field, path) end -
\ No newline at end of file + diff --git a/features/support/env.rb b/features/support/env.rb index 2a5b217d29..ca60753df2 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -16,7 +16,7 @@ # limitations under the License. # -%w{chef chef-server chef-server-slice}.each do |inc_dir| +%w{chef chef-server chef-server-slice chef-solr}.each do |inc_dir| $: << File.join(File.dirname(__FILE__), '..', '..', inc_dir, 'lib') end @@ -25,22 +25,92 @@ require 'spec' require 'chef' require 'chef/config' require 'chef/client' +require 'chef/data_bag' +require 'chef/data_bag_item' +require 'chef/solr' require 'tmpdir' require 'merb-core' require 'merb_cucumber/world/webrat' +require 'opscode/audit' +require 'chef/streaming_cookbook_uploader' def Spec.run? ; true; end -Chef::Config.from_file(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'server.rb')) -Chef::Config[:log_level] = :error -Ohai::Config[:log_level] = :error +ENV['LOG_LEVEL'] ||= 'error' + +def setup_logging + Chef::Config.from_file(File.join(File.dirname(__FILE__), '..', 'data', 'config', 'server.rb')) + Merb.logger.auto_flush = true + if ENV['DEBUG'] == 'true' || ENV['LOG_LEVEL'] == 'debug' + Chef::Config[:log_level] = :debug + Chef::Log.level(:debug) + Merb.logger.set_log(STDOUT, :debug) + else + Chef::Config[:log_level] = ENV['LOG_LEVEL'].to_sym + Chef::Log.level(ENV['LOG_LEVEL'].to_sym) + Merb.logger.set_log(STDOUT, ENV['LOG_LEVEL'].to_sym) + end + Nanite::Log.logger = Mixlib::Auth::Log.logger = Ohai::Log.logger = Chef::Log.logger +end + +def setup_nanite + Chef::Config[:nanite_identity] = "chef-integration-test" + Chef::Nanite.in_event { Chef::Log.debug("Nanite is up!") } + Chef::Log.debug("Waiting for Nanites to register with us as a mapper") + sleep 10 +end + +def delete_databases + c = Chef::REST.new(Chef::Config[:couchdb_url], nil, nil) + %w{chef_integration}.each do |db| + begin + c.delete_rest("#{db}/") + rescue + end + end +end -if ENV['DEBUG'] = 'true' - Merb.logger.set_log(STDOUT, :debug) if ENV['DEBUG'] = 'true' -else - Merb.logger.set_log(STDOUT, :error) +def create_databases + Chef::Log.info("Creating bootstrap databases") + cdb = Chef::CouchDB.new(Chef::Config[:couchdb_url], "chef_integration") + cdb.create_db + Chef::Node.create_design_document + Chef::Role.create_design_document + Chef::DataBag.create_design_document + Chef::Role.sync_from_disk_to_couchdb end + +def create_validation +# TODO: Create the validation certificate here + File.open("#{Dir.tmpdir}/validation.pem", "w") do |f| + f.print response["private_key"] + end +end + +def prepare_replicas + c = Chef::REST.new(Chef::Config[:couchdb_url], nil, nil) + c.put_rest("chef_integration_safe/", nil) + c.post_rest("_replicate", { "source" => "#{Chef::Config[:couchdb_url]}/chef_integration", "target" => "#{Chef::Config[:couchdb_url]}/chef_integration_safe" }) + c.delete_rest("chef_integration") +end + +at_exit do + c = Chef::REST.new(Chef::Config[:couchdb_url], nil, nil) + c.delete_rest("chef_integration_safe") + File.unlink(File.join(Dir.tmpdir, "validation.pem")) +end + +### +# Pre-testing setup +### +setup_logging +setup_nanite +delete_databases +create_databases +create_validation +prepare_replicas + Merb.start_environment( :merb_root => File.join(File.dirname(__FILE__), "..", "..", "chef-server"), :testing => true, @@ -55,6 +125,11 @@ Spec::Runner.configure do |config| config.include(Merb::Test::ControllerHelper) end +Chef::Log.info("Ready to run tests") + +### +# The Cucumber World +### module ChefWorld attr_accessor :recipe, :cookbook, :response, :inflated_response, :log_level, :chef_args, :config_file, :stdout, :stderr, :status, :exception @@ -64,7 +139,7 @@ module ChefWorld end def rest - @rest ||= Chef::REST.new('http://localhost:4000') + @rest ||= Chef::REST.new('http://localhost:4000/organizations/clownco', nil, nil) end def tmpdir @@ -91,7 +166,23 @@ end World(ChefWorld) +Before do + system("mkdir -p #{tmpdir}") + system("cp -r #{File.join(Dir.tmpdir, "validation.pem")} #{File.join(tmpdir, "validation.pem")}") + Chef::CouchDB.new(Chef::Config[:couchdb_url], "chef_integration").create_db + c = Chef::REST.new(Chef::Config[:couchdb_url], nil, nil) + c.post_rest("_replicate", { + "source" => "#{Chef::Config[:couchdb_url]}/chef_integration_safe", + "target" => "#{Chef::Config[:couchdb_url]}/chef_integration" + }) +end + After do + r = Chef::REST.new(Chef::Config[:couchdb_url], nil, nil) + r.delete_rest("chef_integration/") + s = Chef::Solr.new + s.solr_delete_by_query("*:*") + s.solr_commit cleanup_files.each do |file| system("rm #{file}") end @@ -104,5 +195,6 @@ After do end data_tmp = File.join(File.dirname(__FILE__), "..", "data", "tmp") system("rm -rf #{data_tmp}/*") + system("rm -rf #{tmpdir}") end diff --git a/opscode-start b/opscode-start new file mode 100755 index 0000000000..b2cddf6cef --- /dev/null +++ b/opscode-start @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FindBin; + +my $src_dir = $FindBin::Bin; +system("sudo echo"); + +my $osascript = <<EOF; + tell application "iTerm" + set myterm to (make new terminal) + tell myterm + launch session "Default session" + tell the last session + set name to "CouchDB" + write text "cd $src_dir && sudo rake dev:features:start:couchdb" + end tell + launch session "Default session" + tell the last session + set name to "RabbitMQ" + write text "cd $src_dir && sudo rake dev:features:start:rabbitmq" + end tell + launch session "Default session" + tell the last session + set name to "Solr" + write text "cd $src_dir && sudo rake dev:features:start:chef_solr" + end tell + launch session "Default session" + tell the last session + set name to "Dynomite" + write text "cd $src_dir && sudo rake dev:features:start:dynomite" + end tell + launch session "Default session" + tell the last session + set name to "Opscode GUID" + delay 10 + write text "cd $src_dir && sudo rake dev:features:start:opscode_guid" + end tell + launch session "Default session" + tell the last session + set name to "Certificate" + write text "cd $src_dir && sudo rake dev:features:start:certificate" + end tell + launch session "Default session" + tell the last session + set name to "Opscode Authz" + write text "cd $src_dir && sudo rake dev:features:start:opscode_authz" + end tell + launch session "Default session" + tell the last session + set name to "Opscode Audit" + write text "cd $src_dir && sudo rake dev:features:start:opscode_audit" + end tell + launch session "Default session" + tell the last session + set name to "Opscode Account" + write text "cd $src_dir && sudo rake dev:features:start:opscode_account" + end tell + launch session "Default session" + tell the last session + set name to "Chef Solr Indexer" + write text "cd $src_dir && sudo rake dev:features:start:chef_solr_indexer" + end tell + launch session "Default session" + tell the last session + set name to "Chef Server" + write text "cd $src_dir && sudo rake dev:features:start:chef_server" + end tell + end tell + end tell +EOF + +open(CMD, "| osascript"); +print CMD $osascript; +close(CMD); |