From af843190f9be71469c4a20ef8f9021b292c05588 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 9 Jun 2008 18:53:32 -0700 Subject: Huge amount of work, covering openid, clients, and all sorts of server stuff --- lib/chef/client.rb | 123 +++++++++++++++++++++++++++ lib/chef/compile.rb | 7 +- lib/chef/config.rb | 12 ++- lib/chef/couchdb.rb | 132 +++++++++++++++++++++++++++++ lib/chef/file_store.rb | 9 +- lib/chef/node.rb | 111 ++++++++++++++++++------- lib/chef/openid_registration.rb | 179 ++++++++++++++++++++++++++++++++++++++++ lib/chef/platform.rb | 3 +- lib/chef/resource.rb | 9 +- lib/chef/resource_collection.rb | 5 +- lib/chef/rest.rb | 99 +++++++++++++++++++++- 11 files changed, 643 insertions(+), 46 deletions(-) create mode 100644 lib/chef/client.rb create mode 100644 lib/chef/couchdb.rb create mode 100644 lib/chef/openid_registration.rb (limited to 'lib') diff --git a/lib/chef/client.rb b/lib/chef/client.rb new file mode 100644 index 0000000000..2cd11bb2e0 --- /dev/null +++ b/lib/chef/client.rb @@ -0,0 +1,123 @@ +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 HJK Solutions, LLC +# License:: GNU General Public License version 2 or later +# +# This program and entire repository is free software; you can +# redistribute it and/or modify it under the terms of the GNU +# General Public License as published by the Free Software +# Foundation; either version 2 of the License, or any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +require File.join(File.dirname(__FILE__), "mixin", "params_validate") + +require 'rubygems' +require 'facter' + +class Chef + class Client + + attr_accessor :node, :registration + + def initialize() + @node = nil + @safe_name = nil + @registration = nil + @rest = Chef::REST.new(Chef::Config[:registration_url]) + end + + def run + build_node + register + authenticate + save_node + converge + end + + def build_node + node_name = Facter["fqdn"].value ? Facter["fqdn"].value : Facter["hostname"].value + @safe_name = node_name.gsub(/\./, '_') + begin + @node = @rest.get_rest("nodes/#{@safe_name}") + rescue Net::HTTPServerException => e + unless e.message =~ /^404/ + raise e + end + end + unless @node + @node ||= Chef::Node.new + @node.name(node_name) + end + Facter.each do |field, value| + @node[field] = value + end + @node + end + + def register + @registration = nil + begin + @registration = @rest.get_rest("registrations/#{@safe_name}") + rescue Net::HTTPServerException => e + unless e.message =~ /^404/ + raise e + end + end + + if @registration + reg = Chef::FileStore.load("registration", @safe_name) + @secret = reg["secret"] + else + create_registration + end + end + + def create_registration + @secret = random_password(40) + Chef::FileStore.store("registration", @safe_name, { "secret" => @secret }) + @rest.post_rest("registrations", { :id => @safe_name, :password => @secret }) + end + + def authenticate + 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 + + def save_node + @rest.put_rest("nodes/#{@safe_name}", @node) + end + + def converge + results = @rest.get_rest("nodes/#{@safe_name}/compile") + results["collection"].resources.each do |r| + r.collection = results["collection"] + end + cr = Chef::Runner.new(results["node"], results["collection"]) + cr.converge + end + + protected + 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 \ No newline at end of file diff --git a/lib/chef/compile.rb b/lib/chef/compile.rb index 5cd200b098..800d89c6b0 100644 --- a/lib/chef/compile.rb +++ b/lib/chef/compile.rb @@ -35,8 +35,11 @@ class Chef end def load_node(name) - Chef::Log.debug("Loading Chef Node #{name}") - @node = Chef::Node.find(name) + Chef::Log.debug("Loading Chef Node #{name} from CouchDB") + @node = Chef::Node.load(name) + Chef::Log.debug("Loading Recipe for Chef Node #{name}") + @node.find_file(name) + @node end def load_definitions() diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 807631aedb..192b510d17 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -39,10 +39,16 @@ class Chef @configuration = { :cookbook_path => [ "/etc/chef/site-cookbook", "/etc/chef/cookbook" ], :node_path => "/etc/chef/node", - :file_store_path => "/var/lib/chef/store", - :search_index_path => "/var/lib/chef/search_index", + :file_store_path => "/var/chef/store", + :search_index_path => "/var/chef/search_index", :log_level => :info, - :log_location => STDOUT + :log_location => STDOUT, + :openid_providers => nil, + :ssl_verify_mode => :verify_none, + :rest_timeout => 60, + :couchdb_url => "http://localhost:5984", + :registration_url => "http://localhost:4000", + :openid_url => "http://localhost:4001", } class << self diff --git a/lib/chef/couchdb.rb b/lib/chef/couchdb.rb new file mode 100644 index 0000000000..c5605080a6 --- /dev/null +++ b/lib/chef/couchdb.rb @@ -0,0 +1,132 @@ +require File.join(File.dirname(__FILE__), "mixin", "params_validate") +require 'digest/sha2' +require 'json' + +class Chef + class CouchDB + include Chef::Mixin::ParamsValidate + + def initialize(url=nil) + url ||= Chef::Config[:couchdb_url] + @rest = Chef::REST.new(url) + end + + def create_db + @database_list = @rest.get_rest("_all_dbs") + unless @database_list.detect { |db| db == "chef" } + response = @rest.put_rest("chef", Hash.new) + end + "chef" + end + + def create_design_document(name, data) + to_update = true + begin + old_doc = @rest.get_rest("chef/_design%2F#{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") + end + if to_update + @rest.put_rest("chef/_design%2F#{name}", data) + end + true + end + + def store(obj_type, name, object) + validate( + { + :obj_type => obj_type, + :name => name, + :object => object, + }, + { + :object => { :respond_to => :to_json }, + } + ) + @rest.put_rest("chef/#{obj_type}_#{safe_name(name)}", object) + end + + def load(obj_type, name) + validate( + { + :obj_type => obj_type, + :name => name, + }, + { + :obj_type => { :kind_of => String }, + :name => { :kind_of => String }, + } + ) + @rest.get_rest("chef/#{obj_type}_#{safe_name(name)}") + end + + def delete(obj_type, name, rev=nil) + validate( + { + :obj_type => obj_type, + :name => name, + }, + { + :obj_type => { :kind_of => String }, + :name => { :kind_of => String }, + } + ) + unless rev + last_obj = @rest.get_rest("chef/#{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/#{obj_type}_#{safe_name(name)}?rev=#{rev}") + end + + def list(view, inflate=false) + validate( + { + :view => view, + }, + { + :view => { :kind_of => String } + } + ) + if inflate + @rest.get_rest("chef/_view/#{view}/all") + else + @rest.get_rest("chef/_view/#{view}/all_id") + end + end + + def has_key?(obj_type, name) + validate( + { + :obj_type => obj_type, + :name => name, + }, + { + :obj_type => { :kind_of => String }, + :name => { :kind_of => String }, + } + ) + begin + @rest.get_rest("chef/#{obj_type}_#{safe_name(name)}") + true + rescue + false + end + end + + private + def safe_name(name) + name.gsub(/\./, "_") + end + + end +end \ No newline at end of file diff --git a/lib/chef/file_store.rb b/lib/chef/file_store.rb index 1c2b02ed2f..4981cec802 100644 --- a/lib/chef/file_store.rb +++ b/lib/chef/file_store.rb @@ -38,7 +38,6 @@ class Chef ) store_path = create_store_path(obj_type, name) raise "Cannot find #{store_path} for #{obj_type} #{name}!" unless File.exists?(store_path) - object = JSON.parse(IO.read(store_path)) end @@ -59,7 +58,7 @@ class Chef end end - def list(obj_type) + def list(obj_type, inflate=false) validate( { :obj_type => obj_type, @@ -71,7 +70,11 @@ class Chef keys = Array.new Dir[File.join(Chef::Config[:file_store_path], obj_type, '**', '*')].each do |f| if File.file?(f) - keys << File.basename(f) + if inflate + keys << load(obj_type, File.basename(f)) + else + keys << File.basename(f) + end end end keys diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 236bd2f9e1..d7350ac2fa 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -30,49 +30,53 @@ require 'json' class Chef class Node - attr_accessor :attribute, :recipe_list + attr_accessor :attribute, :recipe_list, :couchdb_rev include Chef::Mixin::CheckHelper include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + DESIGN_DOCUMENT = { + "version" => 3, + "language" => "javascript", + "views" => { + "all" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "node") { + emit(doc.name, doc); + } + } + EOJS + }, + "all_id" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "node") { + emit(doc.name, doc.name); + } + } + EOJS + }, + }, + } + # Create a new Chef::Node object. def initialize() @name = nil @attribute = Hash.new @recipe_list = Array.new + @couchdb_rev = nil + @couchdb = Chef::CouchDB.new end - # Find a Chef::Node by fqdn. Will search first for Chef::Config["node_path"]/fqdn.rb, then - # hostname.rb, then default.rb. + # Find a recipe for this Chef::Node by fqdn. Will search first for + # Chef::Config["node_path"]/fqdn.rb, then hostname.rb, then default.rb. # # Returns a new Chef::Node object. # # Raises an ArgumentError if it cannot find the node. - def self.find(fqdn) - node_file = self.find_file(fqdn) - unless node_file - raise ArgumentError, "Cannot find a node matching #{fqdn}, not even with default.rb!" - end - chef_node = Chef::Node.new() - chef_node.from_file(node_file) - chef_node - end - - # Returns an array of nodes available, based on the list of files present. - def self.list - results = Array.new - Dir[File.join(Chef::Config[:node_path], "*.rb")].sort.each do |file| - mr = file.match(/^.+\/(.+)\.rb$/) - node_name = mr[1] - results << node_name - end - results - end - - # Returns the file name we would use to build a node. Returns nil if it cannot find - # a file for this node. - def self.find_file(fqdn) + def find_file(fqdn) node_file = nil host_parts = fqdn.split(".") hostname = host_parts[0] @@ -84,6 +88,10 @@ class Chef elsif File.exists?(File.join(Chef::Config[:node_path], "default.rb")) node_file = File.join(Chef::Config[:node_path], "default.rb") end + unless node_file + raise ArgumentError, "Cannot find a node matching #{fqdn}, not even with default.rb!" + end + self.from_file(node_file) end # Set the name of this Node, or return the current name. @@ -169,14 +177,18 @@ class Chef def to_json(*a) attributes = Hash.new recipes = Array.new - { + result = { "name" => @name, 'json_class' => self.class.name, "attributes" => @attribute, + "chef_type" => "node", "recipes" => @recipe_list, - }.to_json(*a) + } + result["_rev"] = @couchdb_rev if @couchdb_rev + result.to_json(*a) end + # Create a Chef::Node from JSON def self.json_create(o) node = new node.name(o["name"]) @@ -186,10 +198,49 @@ class Chef o["recipes"].each do |r| node.recipes << r end - + node.couchdb_rev = o["_rev"] if o.has_key?("_rev") 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) + if inflate + rs["rows"].collect { |r| r["value"] } + else + rs["rows"].collect { |r| r["key"] } + end + end + + # Load a node by name from CouchDB + def self.load(name) + Chef::CouchDB.new.load("node", name) + end + + # Remove this node from the CouchDB + def destroy + Chef::Queue.send_msg(:queue, :node_remove, self) + @couchdb.delete("node", @name, @couchdb_rev) + end + + # Save this node to the CouchDB + def save + Chef::Queue.send_msg(:queue, :node_index, self) + results = @couchdb.store("node", @name, self) + @couchdb_rev = results["rev"] + end + + # Whether or not there is an OpenID Registration with this key. + def self.has_key?(name) + Chef::CouchDB.new.has_key?("node", name) + end + + # Set up our CouchDB design document + def self.create_design_document + Chef::CouchDB.new.create_design_document("nodes", DESIGN_DOCUMENT) + end + # As a string def to_s "node[#{@name}]" diff --git a/lib/chef/openid_registration.rb b/lib/chef/openid_registration.rb new file mode 100644 index 0000000000..a887e05c2c --- /dev/null +++ b/lib/chef/openid_registration.rb @@ -0,0 +1,179 @@ +# +# Chef::Node::OpenIDRegistration +# +# Author:: Adam Jacob () +# Copyright:: Copyright (c) 2008 HJK Solutions, LLC +# License:: GNU General Public License version 2 or later +# +# This program and entire repository is free software; you can +# redistribute it and/or modify it under the terms of the GNU +# General Public License as published by the Free Software +# Foundation; either version 2 of the License, or any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +require 'rubygems' +require 'json' + +class Chef + class OpenIDRegistration + + attr_accessor :name, :salt, :validated, :password, :couchdb_rev + + include Chef::Mixin::ParamsValidate + + DESIGN_DOCUMENT = { + "version" => 3, + "language" => "javascript", + "views" => { + "all" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "openid_registration") { + emit(doc.name, doc); + } + } + EOJS + }, + "all_id" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "openid_registration") { + emit(doc.name, doc.name); + } + } + EOJS + }, + "validated" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "openid_registration") { + if (doc.validated == true) { + emit(doc.name, doc); + } + } + } + EOJS + }, + "unvalidated" => { + "map" => <<-EOJS + function(doc) { + if (doc.chef_type == "openid_registration") { + if (doc.validated == false) { + emit(doc.name, doc); + } + } + } + EOJS + }, + }, + } + + # Create a new Chef::OpenIDRegistration object. + def initialize() + @name = nil + @salt = nil + @password = nil + @validated = false + @couchdb_rev = nil + @couchdb = Chef::CouchDB.new + end + + def name=(n) + @name = n.gsub(/\./, '_') + end + + # Set the password for this object. + def set_password(password) + @salt = generate_salt + @password = encrypt_password(@salt, password) + end + + # Serialize this object as a hash + def to_json(*a) + attributes = Hash.new + recipes = Array.new + result = { + 'name' => @name, + 'json_class' => self.class.name, + 'salt' => @salt, + 'password' => @password, + 'validated' => @validated, + 'chef_type' => 'openid_registration', + } + result["_rev"] = @couchdb_rev if @couchdb_rev + result.to_json(*a) + end + + # Create a Chef::Node from JSON + def self.json_create(o) + me = new + me.name = o["name"] + me.salt = o["salt"] + me.password = o["password"] + me.validated = o["validated"] + me.couchdb_rev = o["_rev"] if o.has_key?("_rev") + me + end + + # List all the Chef::OpenIDRegistration objects in the CouchDB. If inflate is set to true, you will get + # the full list of all registration objects. Otherwise, you'll just get the IDs + def self.list(inflate=false) + rs = Chef::CouchDB.new.list("registrations", inflate) + if inflate + rs["rows"].collect { |r| r["value"] } + else + rs["rows"].collect { |r| r["key"] } + end + end + + # Load an OpenIDRegistration by name from CouchDB + def self.load(name) + Chef::CouchDB.new.load("openid_registration", name) + end + + # Whether or not there is an OpenID Registration with this key. + def self.has_key?(name) + Chef::CouchDB.new.has_key?("openid_registration", name) + end + + # Remove this node from the CouchDB + def destroy + @couchdb.delete("openid_registration", @name, @couchdb_rev) + end + + # Save this node to the CouchDB + def save + results = @couchdb.store("openid_registration", @name, self) + @couchdb_rev = results["rev"] + end + + # Set up our CouchDB design document + def self.create_design_document + Chef::CouchDB.new.create_design_document("registrations", DESIGN_DOCUMENT) + end + + protected + + def generate_salt + salt = Time.now.to_s + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + 1.upto(30) { |i| salt << chars[rand(chars.size-1)] } + salt + end + + def encrypt_password(salt, password) + Digest::SHA1.hexdigest("--#{salt}--#{password}--") + end + + end +end \ No newline at end of file diff --git a/lib/chef/platform.rb b/lib/chef/platform.rb index 2e5899a5af..55e85ae354 100644 --- a/lib/chef/platform.rb +++ b/lib/chef/platform.rb @@ -73,11 +73,12 @@ class Chef pmap = Chef::Platform.find(platform, version) rtkey = resource_type if resource_type.kind_of?(Chef::Resource) - rtkey = resource_type.resource_name + rtkey = resource_type.resource_name.to_sym end 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/lib/chef/resource.rb b/lib/chef/resource.rb index 705738fe59..74f27bcb82 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -28,8 +28,8 @@ class Chef include Chef::Mixin::CheckHelper include Chef::Mixin::ParamsValidate - attr_accessor :actions, :params, :provider, :updated, :allowed_actions - attr_reader :resource_name, :collection, :source_line + attr_accessor :actions, :params, :provider, :updated, :allowed_actions, :collection + attr_reader :resource_name, :source_line def initialize(name, collection=nil) @name = name @@ -124,10 +124,11 @@ class Chef self.instance_variables.each do |iv| instance_vars[iv] = self.instance_variable_get(iv) unless iv == "@collection" end - { + results = { 'json_class' => self.class.name, 'instance_vars' => instance_vars - }.to_json(*a) + } + results.to_json(*a) end def self.json_create(o) diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb index 362781ea0e..e53a759460 100644 --- a/lib/chef/resource_collection.rb +++ b/lib/chef/resource_collection.rb @@ -118,10 +118,11 @@ class Chef self.instance_variables.each do |iv| instance_vars[iv] = self.instance_variable_get(iv) end - { + results = { 'json_class' => self.class.name, 'instance_vars' => instance_vars - }.to_json(*a) + } + results.to_json(*a) end def self.json_create(o) diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index 1ca18e4ef5..f71c203b3a 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -26,7 +26,104 @@ require 'json' class Chef class REST - def initialize(url, username) + def initialize(url) + @url = url + @cookies = Hash.new + end + + # Send an HTTP GET request to the path + def get_rest(path) + run_request(:GET, create_url(path)) + end + + # Send an HTTP DELETE request to the path + def delete_rest(path) + run_request(:DELETE, create_url(path)) + end + + # Send an HTTP POST request to the path + def post_rest(path, json) + run_request(:POST, create_url(path), json) + end + + # Send an HTTP PUT request to the path + def put_rest(path, json) + run_request(:PUT, create_url(path), json) + end + + def create_url(path) + if path =~ /^(http|https):\/\// + URI.parse(path) + else + URI.parse("#{@url}/#{path}") + end + 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 + # .to_json) and finally, the limit of HTTP Redirects to follow (10). + # + # Typically, you won't use this method -- instead, you'll use one of + # 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) + raise ArgumentError, 'HTTP redirect too deep' if limit == 0 + + http = Net::HTTP.new(url.host, url.port) + if url.scheme == "https" + http.use_ssl = true + if Chef::Config[:ssl_verify_mode] == :verify_none + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + http.read_timeout = Chef::Config[:rest_timeout] + headers = { + 'Accept' => "application/json", + } + if @cookies["#{url.host}:#{url.port}"] + headers['Cookie'] = @cookies["#{url.host}:#{url.port}"] + end + req = nil + case method + when :GET + req_path = "#{url.path}" + req_path << "?#{url.query}" if url.query + 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 + when :PUT + headers["Content-Type"] = 'application/json' if data + req = Net::HTTP::Put.new(url.path, headers) + req.body = data.to_json if data + when :DELETE + req_path = "#{url.path}" + req_path << "?#{url.query}" if url.query + req = Net::HTTP::Delete.new(req_path, headers) + else + raise ArgumentError, "You must provide :GET, :PUT, :POST or :DELETE as the method" + end + res = http.request(req) + if res.kind_of?(Net::HTTPSuccess) + if res['set-cookie'] + @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] + end + if res['content-type'] == "application/json" + JSON.parse(res.body) + else + res.body + end + elsif res.kind_of?(Net::HTTPRedirection) + if res['set-cookie'] + @cookies["#{url.host}:#{url.port}"] = res['set-cookie'] + end + run_request(:GET, create_url(res['location']), false, limit - 1) + else + res.error! + end end end -- cgit v1.2.1