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.
- 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
|