cookbook-client 0.1.0
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/NOTICE +13 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/bin/spoon +233 -0
- data/cookbook-client.gemspec +69 -0
- data/examples/couchdb/metadata.rb +16 -0
- data/examples/couchdb/recipes/default.rb +35 -0
- data/examples/couchdb2/metadata.rb +16 -0
- data/examples/couchdb2/recipes/default.rb +35 -0
- data/examples/couchdb3/metadata.rb +16 -0
- data/examples/couchdb3/recipes/default.rb +35 -0
- data/examples/couchdb4/metadata.rb +16 -0
- data/examples/couchdb4/recipes/default.rb +35 -0
- data/examples/couchdb5/metadata.rb +16 -0
- data/examples/couchdb5/recipes/default.rb +35 -0
- data/examples/rest-delete.rb +24 -0
- data/examples/test-auth-against-erlang.rb +20 -0
- data/examples/test-hash-find.rb +22 -0
- data/examples/test-ssl.rb +20 -0
- data/examples/test-using-restclient-multipart.rb +28 -0
- data/examples/test_signature_verification.rb +26 -0
- data/examples/testrestoc.rb +62 -0
- data/examples/upload-using-cw_multipart.rb +28 -0
- data/lib/chef/cookbook_client.rb +196 -0
- data/lib/chef/remote_cookbook.rb +114 -0
- data/lib/chef/streaming_cookbook_uploader.rb +177 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/spoon/spoon_spec.rb +240 -0
- metadata +93 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'opscode/rest'
|
5
|
+
|
6
|
+
client = Opscode::REST.new
|
7
|
+
|
8
|
+
uri = 'http://localhost:5959/objects/1'
|
9
|
+
secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
|
10
|
+
user_id = 'timh2'
|
11
|
+
|
12
|
+
user_secret = OpenSSL::PKey::RSA.new(File.read(secret_filename))
|
13
|
+
|
14
|
+
res = client.request(:get,
|
15
|
+
uri,
|
16
|
+
:authenticate => true,
|
17
|
+
:user_id => user_id,
|
18
|
+
:user_secret => user_secret)
|
19
|
+
puts "res from GET is: #{res}"
|
20
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
#!/usr/bin/env ruby -w
|
3
|
+
|
4
|
+
# Test collectino.find
|
5
|
+
hash = {
|
6
|
+
'something_else' => 'value',
|
7
|
+
'another_thing' => 'hmm',
|
8
|
+
'tarball' => File.new('/etc/passwd'),
|
9
|
+
'yet_another' => '..',
|
10
|
+
}
|
11
|
+
|
12
|
+
Hash
|
13
|
+
res = hash.find { |k,v| v.is_a?(File) }
|
14
|
+
puts "res is #{res[1]}"
|
15
|
+
|
16
|
+
|
17
|
+
# test gsub
|
18
|
+
str = "my-testing/0_7_0"
|
19
|
+
str = str.gsub(/_/, '.')
|
20
|
+
puts "str is #{str}"
|
21
|
+
|
22
|
+
Hash
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
url = 'https://s3.amazonaws.com/opscode-community/cookbook_versions/tarballs/151/original/teamspeak.tar.gz'
|
5
|
+
uri = URI.parse url
|
6
|
+
|
7
|
+
http_client = Net::HTTP.new(uri.host, uri.port)
|
8
|
+
if uri.scheme == 'https'
|
9
|
+
http_client.use_ssl = true
|
10
|
+
http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
11
|
+
end
|
12
|
+
|
13
|
+
res = http_client.request_get(uri.path)
|
14
|
+
|
15
|
+
#http = Net::HTTP.new(uri.host, uri.port)
|
16
|
+
#http.use_ssl = true if uri.scheme == 'https'
|
17
|
+
#res = http.get
|
18
|
+
|
19
|
+
puts "res #{res}"
|
20
|
+
puts "res.body is #{res.body}"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rest_client'
|
5
|
+
require 'mixlib/authentication/signedheaderauth'
|
6
|
+
|
7
|
+
uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
|
8
|
+
user_secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
|
9
|
+
user_secret = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
|
10
|
+
|
11
|
+
args = {
|
12
|
+
:file => File.new('couchdb2.tar.gz'),
|
13
|
+
:user_id => 'timh2',
|
14
|
+
:http_method => :post,
|
15
|
+
:timestamp => Time.now.utc.iso8601
|
16
|
+
}
|
17
|
+
client = Mixlib::Authentication::SignedHeaderAuth.signing_object(args)
|
18
|
+
puts "SignedHeaderAuth client is #{client}"
|
19
|
+
|
20
|
+
headers = client.sign(user_secret)
|
21
|
+
puts "headers is #{headers}"
|
22
|
+
|
23
|
+
res = RestClient.post(uri,
|
24
|
+
{:tarball => File.read('couchdb2.tar.gz'), :cookbook => '{"category":"good"}'},
|
25
|
+
#File.new('couchdb2.tar.gz'),
|
26
|
+
headers)
|
27
|
+
puts "POST res is #{res}"
|
28
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'mixlib/authentication/signatureverification'
|
5
|
+
|
6
|
+
sigv = Mixlib::Authentication::SignatureVerification.new
|
7
|
+
|
8
|
+
# for timh2:
|
9
|
+
# -----BEGIN RSA PUBLIC KEY-----
|
10
|
+
# MIIBCgKCAQEA0wqGeC4/OSMQwFCp0/6o21dUlSrbWFFFYdVC1wY/ADgpnAbnZGDO
|
11
|
+
# URrrmQ8zw2WBMGpZ2RvhaToj7SOlCUOJmM99W5Ou9E3pOFvjRWgWjW0LAe48SIWP
|
12
|
+
# t2WtdtI4YBzyS6Dp9Oxdcm5TGGJa7QMbIgaU9VD0Jr8yFDWE9QdqAsy+NBSwM13c
|
13
|
+
# CQuBqHtSoJMk9SgzwQyFSm6GCQcliNh5wupKqhNNYCdYOAeJQqJKn25aK9/4+eLu
|
14
|
+
# 8F8bVD1MznOBes089+CoY50sy1N2iZnxwftZklmGLJ1tB5C8+vNwB91Qzp9csUtr
|
15
|
+
# prTkgeW/9cqybTwTm30pj5yrW6A271pkVwIDAQAB
|
16
|
+
# -----END RSA PUBLIC KEY-----
|
17
|
+
|
18
|
+
# for timh3:
|
19
|
+
# -----BEGIN RSA PUBLIC KEY-----
|
20
|
+
# MIIBCgKCAQEA0wqGeC4/OSMQwFCp0/6o21dUlSrbWFFFYdVC1wY/ADgpnAbnZGDO
|
21
|
+
# URrrmQ8zw2WBMGpZ2RvhaToj7SOlCUOJmM99W5Ou9E3pOFvjRWgWjW0LAe48SIWP
|
22
|
+
# t2WtdtI4YBzyS6Dp9Oxdcm5TGGJa7QMbIgaU9VD0Jr8yFDWE9QdqAsy+NBSwM13c
|
23
|
+
# CQuBqHtSoJMk9SgzwQyFSm6GCQcliNh5wupKqhNNYCdYOAeJQqJKn25aK9/4+eLu
|
24
|
+
# 8F8bVD1MznOBes089+CoY50sy1N2iZnxwftZklmGLJ1tB5C8+vNwB91Qzp9csUtr
|
25
|
+
# prTkgeW/9cqybTwTm30pj5yrW6A271pkVwIDAQAB
|
26
|
+
# -----END RSA PUBLIC KEY-----
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(__FILE__), "..", "opscode-chef", "chef", "lib")
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'cgi'
|
7
|
+
require 'json'
|
8
|
+
require 'opscode/rest'
|
9
|
+
require 'mixlib/log'
|
10
|
+
require 'chef/log'
|
11
|
+
require 'streaming_cookbook_uploader'
|
12
|
+
|
13
|
+
Opscode::REST::Config[:log_level] = :debug
|
14
|
+
|
15
|
+
client = Opscode::REST.new
|
16
|
+
|
17
|
+
#uri = 'http://opscode-community.pivotallabs.com/api/v1/cookbooks'
|
18
|
+
#user_id = 'timh'
|
19
|
+
#secret_key_filename = '/Users/tim/.ssh/pivotallabs-secret.txt'
|
20
|
+
|
21
|
+
uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
|
22
|
+
user_id = 'timh2'
|
23
|
+
secret_key_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
|
24
|
+
|
25
|
+
user_secret = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
|
26
|
+
filename_to_upload = 'couchdb3.tar.gz'
|
27
|
+
|
28
|
+
if 1
|
29
|
+
########## streaming cookbook uploader
|
30
|
+
########## results in 'invalid authenticity token' exception (pages of rails
|
31
|
+
########## exception) from far end:
|
32
|
+
streaming_client = Chef::StreamingCookbookUploader.new
|
33
|
+
res = streaming_client.post(uri, user_id, secret_key_filename,
|
34
|
+
{ 'tarball' => File.new(filename_to_upload),
|
35
|
+
'cookbook' => '{"category":"good"}' })
|
36
|
+
puts "res is #{res}"
|
37
|
+
|
38
|
+
elsif nil
|
39
|
+
########## nuo's alternative
|
40
|
+
########## results in 'unauthorized' from far end
|
41
|
+
headers = {:accept=>"application/json", :content_type=>'application/json'}
|
42
|
+
options = {:authenticate=> true,
|
43
|
+
:user_secret=>user_secret,
|
44
|
+
:user_id=>user_id,
|
45
|
+
:headers=>headers,
|
46
|
+
:payload=>File.read(filename_to_upload)
|
47
|
+
}
|
48
|
+
res = client.request(:post,uri,options)
|
49
|
+
puts "res is #{res}"
|
50
|
+
|
51
|
+
elsif 1
|
52
|
+
########## my original
|
53
|
+
########## results in 'unauthorized' from far end
|
54
|
+
res = client.post(uri,
|
55
|
+
:payload => File.read(filename_to_upload),
|
56
|
+
:authenticate => true,
|
57
|
+
:user_id => user_id,
|
58
|
+
:user_secret => user_secret,
|
59
|
+
:headers => {
|
60
|
+
:content_type => 'application/octet-stream' })
|
61
|
+
puts "res is #{res}"
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(__FILE__), "..", "opscode-chef", "chef", "lib")
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'rest_client'
|
7
|
+
require 'mixlib/authentication/signedheaderauth'
|
8
|
+
require 'mixlib/log'
|
9
|
+
require 'chef/log'
|
10
|
+
require 'streaming_cookbook_uploader'
|
11
|
+
|
12
|
+
uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
|
13
|
+
user_id = 'timh2'
|
14
|
+
user_secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
|
15
|
+
user_secret = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
|
16
|
+
|
17
|
+
# def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
|
18
|
+
# make_request(:post, to_url, user_id, secret_key_filename, params, headers)
|
19
|
+
# end
|
20
|
+
|
21
|
+
client = Chef::StreamingCookbookUploader.new
|
22
|
+
|
23
|
+
res = client.post(uri, user_id, user_secret_filename, {
|
24
|
+
:tarball => File.open('couchdb2.tar.gz'),
|
25
|
+
:cookbook => '{"category":"databases"}'
|
26
|
+
})
|
27
|
+
puts "POST res is #{res}"
|
28
|
+
|
@@ -0,0 +1,196 @@
|
|
1
|
+
|
2
|
+
require 'chef/streaming_cookbook_uploader'
|
3
|
+
require 'chef/remote_cookbook'
|
4
|
+
require 'opscode/rest'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
module Chef
|
8
|
+
class CookbookClient
|
9
|
+
def initialize(hostname)
|
10
|
+
@hostname = hostname
|
11
|
+
@client = Opscode::REST.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def do_search(term)
|
15
|
+
uri = make_uri('api/v1/search', {'q' => term})
|
16
|
+
parse_json_cookbook_list @client.get(uri)
|
17
|
+
end
|
18
|
+
|
19
|
+
def do_list()
|
20
|
+
params = {}
|
21
|
+
params['items'] = 999999
|
22
|
+
|
23
|
+
uri = make_uri('api/v1/cookbooks', params)
|
24
|
+
|
25
|
+
parse_json_cookbook_list @client.get(uri)
|
26
|
+
end
|
27
|
+
|
28
|
+
# parse json output back from pivotallabs. assumes a hash comes back, which
|
29
|
+
# contains a key 'items', which itself is an array. that array has cookbook
|
30
|
+
# hashes in it.
|
31
|
+
# e.g. http://opscode-community.pivotallabs.com/api/v1/cookbooks/
|
32
|
+
def parse_json_cookbook_list(json_hash)
|
33
|
+
cookbooks_res = []
|
34
|
+
|
35
|
+
items = json_hash['items']
|
36
|
+
if !items.nil? && items.kind_of?(Array)
|
37
|
+
items.each do |item|
|
38
|
+
cookbook = RemoteCookbook.from_json_list(item)
|
39
|
+
|
40
|
+
cookbooks_res.push cookbook
|
41
|
+
end
|
42
|
+
else
|
43
|
+
raise Exception, "items should be an Array, but it's #{items}"
|
44
|
+
end
|
45
|
+
cookbooks_res
|
46
|
+
end
|
47
|
+
|
48
|
+
# Retrieve details and an enumeration of versions for the given cookbook.
|
49
|
+
def do_details(cookbook_name)
|
50
|
+
cookbook_uri = make_uri "api/v1/cookbooks/#{cookbook_name}"
|
51
|
+
|
52
|
+
json_hash = @client.get(cookbook_uri)
|
53
|
+
|
54
|
+
cookbook_res = RemoteCookbook.from_json_details(cookbook_uri, json_hash)
|
55
|
+
cookbook_res.versions.each do |ver|
|
56
|
+
ver_json_hash = @client.get(ver.uri)
|
57
|
+
|
58
|
+
# populate additional fields of the version object with the result
|
59
|
+
# from the versioned JSON call.
|
60
|
+
ver.set_from_json_hash @hostname, ver_json_hash
|
61
|
+
end
|
62
|
+
|
63
|
+
cookbook_res
|
64
|
+
end
|
65
|
+
|
66
|
+
# Download the tarball for a given cookbook. If the version isn't specified,
|
67
|
+
# grab the latest. Returns the file
|
68
|
+
def do_download(cookbook_name, cookbook_version = nil)
|
69
|
+
# Fetch the details of the cookbook we're about to download, so we can
|
70
|
+
# look at its versions. If there's no such cookbook, rest client will
|
71
|
+
# throw an exception.
|
72
|
+
remote_cookbook = do_details cookbook_name
|
73
|
+
|
74
|
+
# if user specified a version, look it up, otherwise use the latest.
|
75
|
+
found_version =
|
76
|
+
if cookbook_version
|
77
|
+
remote_cookbook.find_version_by_user_version(cookbook_version)
|
78
|
+
else
|
79
|
+
remote_cookbook.latest_version
|
80
|
+
end
|
81
|
+
|
82
|
+
# Download the cookbook if we were able to find a valid version.
|
83
|
+
if found_version
|
84
|
+
if found_version.tarball_uri =~ /.*\/([^\/]+)$/
|
85
|
+
out_filename = $1
|
86
|
+
end
|
87
|
+
if out_filename.nil?
|
88
|
+
raise ArgumentError, "do_download: can't figure out the filename pattern for #{found_version.tarball_uri}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# TODO: should be using a streaming form, but am using the string all-at-once
|
92
|
+
# version for now.
|
93
|
+
uri = URI.parse found_version.tarball_uri
|
94
|
+
http_client = Net::HTTP.new(uri.host, uri.port)
|
95
|
+
if uri.scheme == 'https'
|
96
|
+
http_client.use_ssl = true
|
97
|
+
http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
98
|
+
end
|
99
|
+
|
100
|
+
http_res = http_client.request_get(uri.path)
|
101
|
+
out = File.open(out_filename, 'wb')
|
102
|
+
out.write(http_res.body)
|
103
|
+
out.close
|
104
|
+
|
105
|
+
out_filename
|
106
|
+
else
|
107
|
+
raise Exception, "No such version of cookbook '#{cookbook_name}': #{cookbook_version}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename)
|
112
|
+
cookbook_uploader = StreamingCookbookUploader.new
|
113
|
+
uri = make_uri "api/v1/cookbooks"
|
114
|
+
|
115
|
+
category_string = { 'category'=>cookbook_category }.to_json
|
116
|
+
|
117
|
+
http_resp = cookbook_uploader.post(uri, user_id, user_secret_filename, {
|
118
|
+
:tarball => File.open(cookbook_filename),
|
119
|
+
:cookbook => category_string
|
120
|
+
})
|
121
|
+
|
122
|
+
res = JSON.parse(http_resp.body)
|
123
|
+
if http_resp.code.to_i != 201
|
124
|
+
if !res['error_messages'].nil?
|
125
|
+
if res['error_messages'][0] =~ /Version already exists/
|
126
|
+
raise "Version already exists"
|
127
|
+
else
|
128
|
+
raise Exception, res
|
129
|
+
end
|
130
|
+
else
|
131
|
+
raise Exception, "Error uploading: #{res}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
#puts "do_upload: POST res is #{res}"
|
136
|
+
res
|
137
|
+
end
|
138
|
+
|
139
|
+
# Delete the given cookbook, using the given credentials
|
140
|
+
def do_delete(cookbook_name, user_id, user_secret_filename)
|
141
|
+
cookbook_to_delete = do_details(cookbook_name)
|
142
|
+
if cookbook_to_delete.nil?
|
143
|
+
raise ArgumentError, "No such cookbook to delete #{cookbook_name}"
|
144
|
+
end
|
145
|
+
|
146
|
+
user_secret_rsa = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
|
147
|
+
|
148
|
+
client_options = {
|
149
|
+
:authenticate => true,
|
150
|
+
:user_id => user_id,
|
151
|
+
:user_secret => user_secret_rsa
|
152
|
+
}
|
153
|
+
|
154
|
+
# delete the whole cookbook
|
155
|
+
@client.request(:delete, cookbook_to_delete.uri, client_options)
|
156
|
+
end
|
157
|
+
|
158
|
+
def do_deprecate(cookbook_name, replacement_cookbook_name, user_id, user_secret_filename)
|
159
|
+
cookbook_to_deprecate = do_details(cookbook_name)
|
160
|
+
if cookbook_to_deprecate.nil?
|
161
|
+
raise ArgumentError, "No such cookbook to deprecate #{cookbook_name}"
|
162
|
+
end
|
163
|
+
|
164
|
+
user_secret_rsa = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
|
165
|
+
|
166
|
+
deprecate_uri = "#{cookbook_to_deprecate.uri}/deprecation"
|
167
|
+
deprecation_json = { 'replacement_cookbook_name' => replacement_cookbook_name }.to_json
|
168
|
+
|
169
|
+
client_options = {
|
170
|
+
:authenticate => true,
|
171
|
+
:user_id => user_id,
|
172
|
+
:user_secret => user_secret_rsa,
|
173
|
+
:payload => deprecation_json,
|
174
|
+
}
|
175
|
+
|
176
|
+
# delete the whole cookbook
|
177
|
+
res = @client.post(deprecate_uri, client_options)
|
178
|
+
end
|
179
|
+
|
180
|
+
# make a url given the hostname we were set up with, the base path,
|
181
|
+
# and any parameters that were included.
|
182
|
+
def make_uri(base, params = {})
|
183
|
+
if params && !params.empty?
|
184
|
+
path_components = params.each.collect do |key, value|
|
185
|
+
key = CGI.escape(key.to_s)
|
186
|
+
value = CGI.escape(value.to_s)
|
187
|
+
"#{key}=#{value}"
|
188
|
+
end
|
189
|
+
path_components_str = '?' + path_components.join('&')
|
190
|
+
end
|
191
|
+
"http://#{@hostname}/#{base}#{path_components_str}"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
@@ -0,0 +1,114 @@
|
|
1
|
+
|
2
|
+
module Chef
|
3
|
+
class RemoteCookbookVersion
|
4
|
+
attr_accessor :uri
|
5
|
+
attr_accessor :tarball_uri
|
6
|
+
|
7
|
+
def initialize(uri)
|
8
|
+
@uri = uri
|
9
|
+
end
|
10
|
+
|
11
|
+
def version
|
12
|
+
# http://www.example.com/api/v1/cookbooks/apache/versions/1_0
|
13
|
+
version = ""
|
14
|
+
if /.+\/([^\/]+)$/ =~ @uri
|
15
|
+
version = $1
|
16
|
+
end
|
17
|
+
version.gsub(/_/, '.')
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_from_json_hash(hostname, json_hash)
|
21
|
+
tarball_path = json_hash['file']
|
22
|
+
|
23
|
+
if tarball_path =~ /http/
|
24
|
+
# absolute path. use it verbatim.
|
25
|
+
@tarball_uri = tarball_path
|
26
|
+
else
|
27
|
+
# relative path. use the hostname we already have and the path in 'file'
|
28
|
+
# chop off leading slash for relative paths
|
29
|
+
if tarball_path =~ /^\/(.+)$/
|
30
|
+
tarball_path = $1
|
31
|
+
end
|
32
|
+
|
33
|
+
@tarball_uri = "http://#{hostname}/#{tarball_path}"
|
34
|
+
end
|
35
|
+
|
36
|
+
#created_at = Date.parse json_hash['created_at']
|
37
|
+
#updated_at = Date.parse json_hash['updated_at']
|
38
|
+
#average_rating = json_hash['average_rating']
|
39
|
+
#license = json_hash['license']
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class RemoteCookbook
|
45
|
+
attr_accessor :uri, :maintainer, :name, :description
|
46
|
+
attr_accessor :deprecated
|
47
|
+
attr_accessor :versions, :latest_version
|
48
|
+
|
49
|
+
def initialize(params = {})
|
50
|
+
@uri = params[:uri]
|
51
|
+
@maintainer = params[:maintainer]
|
52
|
+
@name = params[:name]
|
53
|
+
@description = params[:description]
|
54
|
+
|
55
|
+
@versions = Array.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# these kind of responses come back from 'search' and 'list'
|
59
|
+
def self.from_json_list(json_hash)
|
60
|
+
res = new
|
61
|
+
|
62
|
+
res.uri = json_hash['cookbook']
|
63
|
+
res.maintainer = json_hash['cookbook_maintainer']
|
64
|
+
res.name = json_hash['cookbook_name']
|
65
|
+
res.description = json_hash['cookbook_description']
|
66
|
+
|
67
|
+
res
|
68
|
+
end
|
69
|
+
|
70
|
+
# this type of response comes back from directly looking up a cookbook
|
71
|
+
def self.from_json_details(uri, json_hash)
|
72
|
+
unless json_hash.kind_of?(Hash)
|
73
|
+
raise "from_json_show needs a hash"
|
74
|
+
end
|
75
|
+
|
76
|
+
res = new
|
77
|
+
|
78
|
+
res.uri = uri
|
79
|
+
res.name = json_hash['name']
|
80
|
+
res.maintainer = json_hash['maintainer']
|
81
|
+
res.description = json_hash['description']
|
82
|
+
res.deprecated = json_hash['deprecated'] == 'true'
|
83
|
+
# res.external_url = json_hash['external_url']
|
84
|
+
# res.category = json_hash['category']
|
85
|
+
# res.average_rating = json_hash['average_rating']
|
86
|
+
# res.created_at = Date.parse json_hash['created_at']
|
87
|
+
# res.updated_at = Date.parse json_hash['updated_at']
|
88
|
+
|
89
|
+
versions = json_hash['versions']
|
90
|
+
if (!versions.nil?) && (versions.kind_of? Array)
|
91
|
+
versions.each do |ver|
|
92
|
+
res.versions.push(RemoteCookbookVersion.new ver)
|
93
|
+
end
|
94
|
+
res.latest_version = res.find_version_by_uri json_hash['latest_version']
|
95
|
+
end
|
96
|
+
|
97
|
+
res
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_version_by_uri(uri)
|
101
|
+
@versions.find { |ver| ver.uri == uri }
|
102
|
+
end
|
103
|
+
|
104
|
+
def find_version_by_user_version(version)
|
105
|
+
@versions.find { |ver| ver.version == version }
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
"RemoteCookbook #{@name}: description '#{@description}', maint #{@maintainer}, uri #{@uri}"
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'net/http'
|
3
|
+
require 'mixlib/authentication/signedheaderauth'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
# inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
|
7
|
+
module Chef
|
8
|
+
class StreamingCookbookUploader
|
9
|
+
DefaultHeaders = { 'accept' => 'application/json' }
|
10
|
+
|
11
|
+
def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
|
12
|
+
make_request(:post, to_url, user_id, secret_key_filename, params, headers)
|
13
|
+
end
|
14
|
+
|
15
|
+
def put(to_url, user_id, secret_key_filename, params = {}, headers = {})
|
16
|
+
make_request(:put, to_url, user_id, secret_key_filename, params, headers)
|
17
|
+
end
|
18
|
+
|
19
|
+
def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {})
|
20
|
+
boundary = '----RubyMultipartClient' + rand(1000000).to_s + 'ZZZZZ'
|
21
|
+
parts = []
|
22
|
+
content_file = nil
|
23
|
+
content_body = nil
|
24
|
+
|
25
|
+
timestamp = Time.now.utc.iso8601
|
26
|
+
secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
|
27
|
+
|
28
|
+
unless params.nil? || params.empty?
|
29
|
+
params.each do |key, value|
|
30
|
+
if value.kind_of?(File)
|
31
|
+
content_file = value
|
32
|
+
filepath = value.path
|
33
|
+
filename = File.basename(filepath)
|
34
|
+
parts << StringPart.new( "--" + boundary + "\r\n" +
|
35
|
+
"Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" +
|
36
|
+
"Content-Type: application/octet-stream\r\n\r\n")
|
37
|
+
parts << StreamPart.new(value, File.size(filepath))
|
38
|
+
parts << StringPart.new("\r\n")
|
39
|
+
else
|
40
|
+
content_body = value.to_s
|
41
|
+
parts << StringPart.new( "--" + boundary + "\r\n" +
|
42
|
+
"Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n")
|
43
|
+
parts << StringPart.new(content_body + "\r\n")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
parts << StringPart.new("--" + boundary + "--\r\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
body_stream = MultipartStream.new(parts)
|
50
|
+
|
51
|
+
timestamp = Time.now.utc.iso8601
|
52
|
+
|
53
|
+
signing_options = {
|
54
|
+
:http_method=>http_verb,
|
55
|
+
:user_id=>user_id,
|
56
|
+
:timestamp=>timestamp
|
57
|
+
}
|
58
|
+
(content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || ""))
|
59
|
+
|
60
|
+
headers.merge!(Mixlib::Authentication::SignedHeaderAuth.signing_object(signing_options).sign(secret_key))
|
61
|
+
|
62
|
+
content_file.rewind if content_file
|
63
|
+
|
64
|
+
# net/http doesn't like symbols for header keys, so we'll to_s each one just in case
|
65
|
+
headers = DefaultHeaders.merge(Hash[*headers.map{ |k,v| [k.to_s, v] }.flatten])
|
66
|
+
|
67
|
+
url = URI.parse(to_url)
|
68
|
+
req = case http_verb
|
69
|
+
when :put
|
70
|
+
Net::HTTP::Put.new(url.path, headers)
|
71
|
+
when :post
|
72
|
+
Net::HTTP::Post.new(url.path, headers)
|
73
|
+
end
|
74
|
+
req.content_length = body_stream.size
|
75
|
+
req.content_type = 'multipart/form-data; boundary=' + boundary unless parts.empty?
|
76
|
+
req.body_stream = body_stream
|
77
|
+
|
78
|
+
http = Net::HTTP.new(url.host, url.port)
|
79
|
+
if url.scheme == "https"
|
80
|
+
http.use_ssl = true
|
81
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
82
|
+
end
|
83
|
+
res = http.request(req)
|
84
|
+
#res = http.start {|http_proc| http_proc.request(req) }
|
85
|
+
|
86
|
+
# alias status to code and to_s to body for test purposes
|
87
|
+
# TODO: stop the following madness!
|
88
|
+
class << res
|
89
|
+
alias :to_s :body
|
90
|
+
|
91
|
+
# BUGBUG this makes the response compatible with what respsonse_steps expects to test headers (response.headers[] -> response[])
|
92
|
+
def headers
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
def status
|
97
|
+
code.to_i
|
98
|
+
end
|
99
|
+
end
|
100
|
+
res
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class StreamPart
|
105
|
+
def initialize(stream, size)
|
106
|
+
@stream, @size = stream, size
|
107
|
+
end
|
108
|
+
|
109
|
+
def size
|
110
|
+
@size
|
111
|
+
end
|
112
|
+
|
113
|
+
# read the specified amount from the stream
|
114
|
+
def read(offset, how_much)
|
115
|
+
@stream.read(how_much)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class StringPart
|
120
|
+
def initialize(str)
|
121
|
+
@str = str
|
122
|
+
end
|
123
|
+
|
124
|
+
def size
|
125
|
+
@str.length
|
126
|
+
end
|
127
|
+
|
128
|
+
# read the specified amount from the string startiung at the offset
|
129
|
+
def read(offset, how_much)
|
130
|
+
@str[offset, how_much]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class MultipartStream
|
135
|
+
def initialize(parts)
|
136
|
+
@parts = parts
|
137
|
+
@part_no = 0
|
138
|
+
@part_offset = 0
|
139
|
+
end
|
140
|
+
|
141
|
+
def size
|
142
|
+
@parts.inject(0) {|size, part| size + part.size}
|
143
|
+
end
|
144
|
+
|
145
|
+
def read(how_much)
|
146
|
+
return nil if @part_no >= @parts.size
|
147
|
+
|
148
|
+
how_much_current_part = @parts[@part_no].size - @part_offset
|
149
|
+
|
150
|
+
how_much_current_part = if how_much_current_part > how_much
|
151
|
+
how_much
|
152
|
+
else
|
153
|
+
how_much_current_part
|
154
|
+
end
|
155
|
+
|
156
|
+
how_much_next_part = how_much - how_much_current_part
|
157
|
+
|
158
|
+
current_part = @parts[@part_no].read(@part_offset, how_much_current_part)
|
159
|
+
|
160
|
+
# recurse into the next part if the current one was not large enough
|
161
|
+
if how_much_next_part > 0
|
162
|
+
@part_no += 1
|
163
|
+
@part_offset = 0
|
164
|
+
next_part = read(how_much_next_part)
|
165
|
+
current_part + if next_part
|
166
|
+
next_part
|
167
|
+
else
|
168
|
+
''
|
169
|
+
end
|
170
|
+
else
|
171
|
+
@part_offset += how_much_current_part
|
172
|
+
current_part
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|