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.
- data/.gitignore +1 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +6 -6
- data/README.md +5 -63
- data/lib/spice.rb +56 -151
- data/lib/spice/client.rb +23 -36
- data/lib/spice/config.rb +52 -0
- data/lib/spice/connection.rb +89 -74
- data/lib/spice/connection/authentication.rb +47 -0
- data/lib/spice/connection/clients.rb +15 -0
- data/lib/spice/connection/cookbooks.rb +42 -0
- data/lib/spice/connection/data_bags.rb +35 -0
- data/lib/spice/connection/environments.rb +16 -0
- data/lib/spice/connection/nodes.rb +15 -0
- data/lib/spice/connection/roles.rb +15 -0
- data/lib/spice/connection/search.rb +49 -0
- data/lib/spice/cookbook.rb +13 -30
- data/lib/spice/cookbook_version.rb +43 -0
- data/lib/spice/data_bag.rb +17 -121
- data/lib/spice/data_bag_item.rb +35 -0
- data/lib/spice/environment.rb +15 -79
- data/lib/spice/error.rb +30 -0
- data/lib/spice/node.rb +19 -36
- data/lib/spice/persistence.rb +42 -0
- data/lib/spice/request.rb +27 -0
- data/lib/spice/request/auth.rb +14 -0
- data/lib/spice/response/client_error.rb +30 -0
- data/lib/spice/response/parse_json.rb +24 -0
- data/lib/spice/role.rb +18 -25
- data/lib/spice/version.rb +1 -1
- data/spec/spec_helper.rb +0 -1
- data/spice.gemspec +4 -3
- metadata +65 -42
- data/lib/spice/core_ext/hash.rb +0 -21
- data/lib/spice/search.rb +0 -23
data/lib/spice/connection.rb
CHANGED
@@ -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
|
-
|
17
|
+
include Toy::Store
|
18
|
+
store :memory, {}
|
6
19
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
32
|
-
|
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
|
46
|
-
|
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
|
60
|
-
|
61
|
-
|
62
|
-
"#{
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
73
|
-
|
74
|
-
|
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
|
77
|
-
|
78
|
-
|
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 => "#{
|
100
|
+
:host => "#{parsed_url.host}:#{parsed_url.port}"
|
88
101
|
}
|
89
102
|
request_params[:body] ||= ""
|
90
|
-
|
91
|
-
end
|
103
|
+
signature_headers(request_params)
|
104
|
+
end # def authentication_headers
|
92
105
|
|
93
|
-
def build_headers(method, path,
|
94
|
-
headers
|
95
|
-
headers[
|
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
|
-
|
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
|
data/lib/spice/cookbook.rb
CHANGED
@@ -1,33 +1,16 @@
|
|
1
|
+
require 'spice/persistence'
|
2
|
+
|
1
3
|
module Spice
|
2
|
-
class Cookbook
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|