spice 0.8.0 → 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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