spice 0.8.0 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,81 +1,94 @@
1
1
  require 'yajl'
2
+ require 'uri'
3
+
4
+ require 'spice/request'
5
+ require 'spice/connection/authentication'
6
+ require 'spice/connection/search'
7
+ require 'spice/connection/clients'
8
+ require 'spice/connection/cookbooks'
9
+ require 'spice/connection/data_bags'
10
+ require 'spice/connection/environments'
11
+ require 'spice/connection/nodes'
12
+ require 'spice/connection/roles'
13
+ require 'spice/connection/search'
2
14
 
3
15
  module Spice
4
16
  class Connection
5
- attr_accessor :client_name, :key_file, :auth_credentials, :server_url, :url_path
17
+ include Toy::Store
18
+ store :memory, {}
6
19
 
7
- def initialize(options={})
8
- endpoint = URI.parse(options[:server_url])
9
- @server_url = options[:server_url]
10
- @host = endpoint.host
11
- @port = endpoint.port
12
- @scheme = endpoint.scheme
13
- @url_path = endpoint.path
14
- @auth_credentials = Authentication.new(options[:client_name], options[:key_file])
15
- @sign_on_redirect, @sign_request = true, true
16
- end
20
+ include Spice::Connection::Clients
21
+ include Spice::Connection::Cookbooks
22
+ include Spice::Connection::DataBags
23
+ include Spice::Connection::Environments
24
+ include Spice::Connection::Nodes
25
+ include Spice::Connection::Roles
26
+ include Spice::Connection::Search
27
+ include Spice::Connection::Authentication
28
+ include Spice::Connection::Search
29
+ include Spice::Request
30
+
31
+ attribute :client_name, String
32
+ attribute :key_file, String
33
+ attribute :key, String
34
+ attribute :server_url, String
35
+ attribute :sign_on_redirect, Boolean, :default => true
36
+ attribute :sign_request, Boolean, :default => true
37
+ attribute :endpoint, String
17
38
 
18
- def get(path, headers={})
19
- begin
20
- response = RestClient.get(
21
- "#{@server_url}#{path}",
22
- build_headers(:GET, "#{@url_path}#{path}", headers)
23
- )
24
- return Yajl.load(response.body)
25
-
26
- rescue => e
27
- e.response
28
- end
29
- end
39
+ validates_presence_of :client_name, :key_file, :server_url
30
40
 
31
- def post(path, payload, headers={})
32
- begin
33
- response = RestClient.post(
34
- "#{@server_url}#{path}",
35
- Yajl.dump(payload),
36
- build_headers(:POST, "#{@url_path}#{path}", headers, Yajl.dump(payload))
37
- )
38
- return Yajl.load(response.body)
39
-
40
- rescue => e
41
- e.response
42
- end
41
+ def connection
42
+ self
43
43
  end
44
44
 
45
- def put(path, payload, headers={})
46
- begin
47
- response = RestClient.put(
48
- "#{@server_url}#{path}",
49
- Yajl.dump(payload),
50
- build_headers(:PUT, "#{@url_path}#{path}", headers, Yajl.dump(payload))
51
- )
52
- return Yajl.load(response.body)
53
-
54
- rescue => e
55
- e.response
56
- end
45
+ def parsed_url
46
+ URI.parse(server_url)
57
47
  end
48
+
49
+ def get(path)
50
+ response = request(:headers => build_headers(
51
+ :GET,
52
+ "#{parsed_url.path}#{path}")
53
+ ).get(
54
+ "#{server_url}#{path}"
55
+ )
56
+ return response
57
+ end # def get
58
58
 
59
- def delete(path, headers={})
60
- begin
61
- response = RestClient.delete(
62
- "#{@server_url}#{path}",
63
- build_headers(:DELETE, "#{@url_path}#{path}", headers)
64
- )
65
- return Yajl.load(response.body)
66
-
67
- rescue => e
68
- e.response
69
- end
70
- end
59
+ def post(path, payload)
60
+ response = request(:headers => build_headers(
61
+ :POST,
62
+ "#{parsed_url.path}#{path}",
63
+ Yajl.dump(payload))
64
+ ).post(
65
+ "#{server_url}#{path}",
66
+ Yajl.dump(payload)
67
+ )
68
+ return response
69
+ end # def post
71
70
 
72
- def sign_requests?
73
- auth_credentials.sign_requests? && @sign_request
74
- end
71
+ def put(path, payload, headers={})
72
+ response = request(:headers => build_headers(
73
+ :PUT,
74
+ "#{parsed_url.path}#{path}",
75
+ Yajl.dump(payload))
76
+ ).put(
77
+ "#{server_url}#{path}",
78
+ Yajl.dump(payload)
79
+ )
80
+ return response
81
+ end # def put
75
82
 
76
- def search(index, options={})
77
- Spice::Search.search(index, options)
78
- end
83
+ def delete(path, headers={})
84
+ response = request(:headers => build_headers(
85
+ :DELETE,
86
+ "#{parsed_url.path}#{path}")
87
+ ).delete(
88
+ "#{server_url}#{path}"
89
+ )
90
+ return response
91
+ end # def delete
79
92
 
80
93
  private
81
94
 
@@ -84,18 +97,20 @@ module Spice
84
97
  :http_method => method,
85
98
  :path => path.gsub(/\?.*$/, ''),
86
99
  :body => json_body,
87
- :host => "#{@host}:#{@port}"
100
+ :host => "#{parsed_url.host}:#{parsed_url.port}"
88
101
  }
89
102
  request_params[:body] ||= ""
90
- auth_credentials.signature_headers(request_params)
91
- end
103
+ signature_headers(request_params)
104
+ end # def authentication_headers
92
105
 
93
- def build_headers(method, path, headers={}, json_body=false)
94
- headers['Accept'] = "application/json"
95
- headers["Content-Type"] = 'application/json' if json_body
106
+ def build_headers(method, path, json_body=nil)
107
+ headers={}
108
+ # headers['Accept'] = "application/json"
109
+ # headers["Content-Type"] = 'application/json' if json_body
96
110
  headers['Content-Length'] = json_body.bytesize.to_s if json_body
97
111
  headers.merge!(authentication_headers(method, path, json_body)) if sign_requests?
98
112
  headers
99
- end
100
- end
101
- end
113
+ end # def build_headers
114
+
115
+ end # class Connection
116
+ end # module Spice
@@ -0,0 +1,47 @@
1
+ module Spice
2
+ class Connection
3
+ module Authentication
4
+
5
+ def sign_requests?
6
+ !!key_file
7
+ end
8
+
9
+ def signature_headers(request_params={})
10
+ load_signing_key if sign_requests?
11
+ request_params = request_params.dup
12
+ request_params[:timestamp] = Time.now.utc.iso8601
13
+ request_params[:user_id] = client_name
14
+ host = request_params.delete(:host) || "localhost"
15
+
16
+ sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(request_params)
17
+ signed = sign_obj.sign(@key).merge({:host => host})
18
+ signed.inject({}){|memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo}
19
+ # Platform requires X-Chef-Version header
20
+ version = { "X-Chef-Version" => Spice.chef_version }
21
+ signed.merge!(version)
22
+ end
23
+
24
+ private
25
+
26
+ def load_signing_key
27
+ begin
28
+ raw_key = File.read(key_file).strip
29
+ rescue SystemCallError, IOError => e
30
+ raise IOError, "Unable to read #{key_file}"
31
+ end
32
+ assert_valid_key_format!(raw_key)
33
+ @key = OpenSSL::PKey::RSA.new(raw_key)
34
+ end
35
+
36
+ def assert_valid_key_format!(raw_key)
37
+ unless (raw_key =~ /\A-----BEGIN RSA PRIVATE KEY-----$/) &&
38
+ (raw_key =~ /^-----END RSA PRIVATE KEY-----\Z/)
39
+ msg = "The file #{key_file} does not contain a correctly formatted private key.\n"
40
+ msg << "The key file should begin with '-----BEGIN RSA PRIVATE KEY-----' and end with '-----END RSA PRIVATE KEY-----'"
41
+ raise ArgumentError, msg
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ module Spice
2
+ class Connection
3
+ module Clients
4
+ def clients(options={})
5
+ connection.search('client', options)
6
+ end
7
+
8
+ def client(name)
9
+ attributes = connection.get("/clients/#{name}").body
10
+ Spice::Client.new(attributes)
11
+ end
12
+
13
+ end # module Clients
14
+ end # class Connection
15
+ end # module Spice
@@ -0,0 +1,42 @@
1
+ module Spice
2
+ class Connection
3
+ module Cookbooks
4
+ def cookbooks(options={})
5
+ if Gem::Version.new(Spice.chef_version) >= Gem::Version.new("0.10.0")
6
+ cookbooks = []
7
+ connection.get("/cookbooks").body.each_pair do |key, value|
8
+ versions = value['versions'].map{ |v| v['version'] }
9
+ cookbooks << Spice::Cookbook.new(:name => key, :versions => versions)
10
+ end
11
+ cookbooks
12
+ else
13
+ connection.get("/cookbooks").body.keys.map do |cookbook|
14
+ attributes = connection.get("/cookbooks/#{cookbook}").body.to_a[0]
15
+ Spice::Cookbook.new(:name => attributes[0], :versions => attributes[1])
16
+ end
17
+ end
18
+ end
19
+
20
+ def cookbook(name)
21
+ if Gem::Version.new(Spice.chef_version) >= Gem::Version.new("0.10.0")
22
+ cookbook = connection.get("/cookbooks/#{name}").body
23
+ puts cookbook[name]
24
+ versions = cookbook[name]['versions'].map{ |v| v['version'] }
25
+
26
+ Spice::Cookbook.new(:name => name, :versions => versions)
27
+ else
28
+ connection.get("/cookbooks").body.keys.map do |cookbook|
29
+ attributes = connection.get("/cookbooks/#{cookbook}").body.to_a[0]
30
+ Spice::Cookbook.new(:name => attributes[0], :versions => attributes[1])
31
+ end
32
+ end
33
+ end
34
+
35
+ def cookbook_version(name, version)
36
+ attributes = connection.get("/cookbooks/#{name}/#{version}").body
37
+ Spice::CookbookVersion.new(attributes)
38
+ end
39
+
40
+ end # module Cookbooks
41
+ end # class Connection
42
+ end # module Spice
@@ -0,0 +1,35 @@
1
+ module Spice
2
+ class Connection
3
+ module DataBags
4
+ def data_bags
5
+ connection.get("/data").body.keys.map do |data_bag|
6
+ items = connection.search(data_bag)
7
+ Spice::DataBag.new(:name => data_bag, :items => items)
8
+ end
9
+ end
10
+
11
+ alias :data :data_bags
12
+
13
+ def data_bag(name)
14
+ items = connection.search(name)
15
+ Spice::DataBag.new(:name => name, :items => items)
16
+ end
17
+
18
+ def data_bag_item(name, id)
19
+ data = connection.get("/data/#{name}/#{id}")
20
+ data.delete('id')
21
+ Spice::DataBagItem.new(:_id => id, :data => data, :name => name)
22
+ end
23
+
24
+ private
25
+
26
+ def get_data_bag_items(name)
27
+ connection.get("/data/#{name}").body.keys.map do |id|
28
+ data = connection.get("/data/#{name}/#{id}").body
29
+ Spice::DataBagItem.new(:_id => id, :data => data, :name => name)
30
+ end
31
+ end
32
+
33
+ end # module DataBags
34
+ end # class Connection
35
+ end # module Spice
@@ -0,0 +1,16 @@
1
+ module Spice
2
+ class Connection
3
+ module Environments
4
+ def environments(options={})
5
+ connection.search('environment', options)
6
+ end
7
+
8
+ def environment(name)
9
+ attributes = connection.get("/environments/#{name}").body
10
+ attributes['attrs'] = attributes.delete('attributes')
11
+ Spice::Environment.new(attributes)
12
+ end
13
+
14
+ end # module Environments
15
+ end # class Connection
16
+ end # module Spice
@@ -0,0 +1,15 @@
1
+ module Spice
2
+ class Connection
3
+ module Nodes
4
+ def nodes(options={})
5
+ connection.search('node', options)
6
+ end
7
+
8
+ def node(name)
9
+ node_attributes = connection.get("/nodes/#{name}").body
10
+ Spice::Node.new(node_attributes)
11
+ end
12
+
13
+ end # module Nodes
14
+ end # class Connection
15
+ end # module Spice
@@ -0,0 +1,15 @@
1
+ module Spice
2
+ class Connection
3
+ module Roles
4
+ def roles(options={})
5
+ connection.search('role', options)
6
+ end
7
+
8
+ def role(name)
9
+ attributes = connection.get("/roles/#{name}").body
10
+ Spice::Role.new(attributes)
11
+ end
12
+
13
+ end # module Roles
14
+ end # class Connection
15
+ end # module Spice
@@ -0,0 +1,49 @@
1
+ require 'cgi'
2
+
3
+ module Spice
4
+ class Connection
5
+ module Search
6
+ def search(index, options={})
7
+ options = {:q => options} if options.is_a? String
8
+ options.symbolize_keys!
9
+
10
+ options[:q] ||= '*:*'
11
+ options[:sort] ||= "X_CHEF_id_CHEF_X asc"
12
+ options[:start] ||= 0
13
+ options[:rows] ||= 1000
14
+
15
+ # clean up options hash
16
+ options.delete_if{|k,v| !%w(q sort start rows).include?(k.to_s)}
17
+
18
+ params = options.collect{ |k, v| "#{k}=#{CGI::escape(v.to_s)}"}.join("&")
19
+ case index
20
+ when 'node'
21
+ connection.get("/search/#{CGI::escape(index.to_s)}?#{params}").body['rows'].map do |node|
22
+ Spice::Node.new(node)
23
+ end
24
+ when 'role'
25
+ connection.get("/search/#{CGI::escape(index.to_s)}?#{params}").body['rows'].map do |role|
26
+ Spice::Role.new(role)
27
+ end
28
+ when 'client'
29
+ connection.get("/search/#{CGI::escape(index.to_s)}?#{params}").body['rows'].map do |client|
30
+ Spice::Client.new(client)
31
+ end
32
+ when 'environment'
33
+ connection.get("/search/#{CGI::escape(index.to_s)}?#{params}").body['rows'].map do |env|
34
+ env['attrs'] = env.delete('attributes')
35
+ Spice::Environment.new(env)
36
+ end
37
+ else
38
+ # assume it's a data bag
39
+ connection.get("/search/#{CGI::escape(index.to_s)}?#{params}").body['rows'].map do |db|
40
+ data = db['raw_data']
41
+ id = data.delete('id')
42
+ Spice::DataBagItem.new(:_id => id, :data => data, :name => index)
43
+ end
44
+ end
45
+ end # def search
46
+
47
+ end # module Search
48
+ end # class Connection
49
+ end # module Spice
@@ -1,33 +1,16 @@
1
+ require 'spice/persistence'
2
+
1
3
  module Spice
2
- class Cookbook < Spice::Chef
3
- def self.all(options={})
4
- connection.get("/cookbooks")
5
- end
6
-
7
- def self.[](name)
8
- connection.get("/cookbooks/#{name}")
9
- end
10
-
11
- def self.show(options={})
12
- raise ArgumentError, "Option :name must be present" unless options[:name]
13
- name = options.delete(:name)
14
- connection.get("/cookbooks/#{name}")
15
- end
16
-
17
- def self.create(options={})
18
- connection.post("/cookbooks", options)
19
- end
20
-
21
- def self.update(options={})
22
- raise ArgumentError, "Option :name must be present" unless options[:name]
23
- name = options.delete(:name)
24
- connection.put("/cookbooks/#{name}", options)
25
- end
26
-
27
- def self.delete(options={})
28
- raise ArgumentError, "Option :name must be present" unless options[:name]
29
- name = options.delete(:name)
30
- connection.delete("/cookbooks/#{name}", options)
31
- end
4
+ class Cookbook
5
+ include Toy::Store
6
+ include Spice::Persistence
7
+ extend Spice::Persistence
8
+ store :memory, {}
9
+ endpoint "cookbooks"
10
+
11
+ attribute :name, String
12
+ attribute :versions, Array, :default => []
13
+
14
+ validates_presence_of :name
32
15
  end
33
16
  end