hosted-chef 0.5.1 → 0.5.2
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/README.md +1 -1
- data/bin/hosted-chef +2 -6
- data/lib/hosted-chef.rb +20 -366
- data/lib/hosted-chef/api_client.rb +120 -0
- data/lib/hosted-chef/cli.rb +117 -0
- data/lib/hosted-chef/config_installer.rb +85 -0
- data/lib/hosted-chef/controller.rb +71 -0
- data/lib/hosted-chef/version.rb +1 -1
- metadata +12 -8
data/README.md
CHANGED
@@ -9,7 +9,7 @@ This script currently resets both your user API key and your
|
|
9
9
|
Organization's validation key, so it's only recommended for new users
|
10
10
|
with freshly created organizations. If that's you, run:
|
11
11
|
|
12
|
-
gem install hosted
|
12
|
+
gem install hosted-chef
|
13
13
|
hosted-chef -u USERNAME -o ORGANIZATION
|
14
14
|
# enter your password at the prompt.
|
15
15
|
|
data/bin/hosted-chef
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
rescue LoadError
|
6
|
-
$:.unshift(File.expand_path("../../lib", __FILE__))
|
7
|
-
require 'hosted-chef'
|
8
|
-
end
|
3
|
+
$:.unshift(File.expand_path("../../lib", __FILE__))
|
4
|
+
require 'hosted-chef'
|
9
5
|
|
10
6
|
HostedChef::Controller.new.run
|
11
7
|
|
data/lib/hosted-chef.rb
CHANGED
@@ -4,376 +4,30 @@ require 'optparse'
|
|
4
4
|
require 'rexml/document'
|
5
5
|
require 'fileutils'
|
6
6
|
|
7
|
-
|
7
|
+
begin
|
8
|
+
require 'rubygems'
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
8
12
|
require 'restclient'
|
9
13
|
require 'highline'
|
10
14
|
|
11
|
-
|
15
|
+
require 'hosted-chef/version'
|
16
|
+
require 'hosted-chef/api_client'
|
17
|
+
require 'hosted-chef/cli'
|
18
|
+
require 'hosted-chef/config_installer'
|
19
|
+
require 'hosted-chef/controller'
|
20
|
+
|
21
|
+
|
22
|
+
#== HostedChef
|
23
|
+
# A command line application for interacting with Opscode's Hosted Chef using
|
24
|
+
# password authentication. The goal of this program is simply to automate tasks
|
25
|
+
# for which the normal API authentication mechanism cannot be (easily) used.
|
26
|
+
#
|
27
|
+
# Developers interested in this code should note that the code is not intended
|
28
|
+
# for library use. In particular, classes contain methods that call exit or
|
29
|
+
# request user input.
|
12
30
|
module HostedChef
|
13
|
-
|
14
|
-
class InvalidPassword < RuntimeError
|
15
|
-
end
|
16
|
-
|
17
|
-
class Options
|
18
|
-
attr_writer :password
|
19
|
-
|
20
|
-
def password
|
21
|
-
@password ||= HighLine.new.ask("Your Hosted Chef password: ") {|q| q.echo = "*"}
|
22
|
-
end
|
23
|
-
|
24
|
-
attr_writer :username
|
25
|
-
|
26
|
-
def username
|
27
|
-
@username ||= HighLine.new.ask("Your Hosted Chef username? ")
|
28
|
-
end
|
29
|
-
|
30
|
-
attr_writer :orgname
|
31
|
-
|
32
|
-
def orgname
|
33
|
-
@orgname ||= HighLine.new.ask("Your Hosted Chef organization? ")
|
34
|
-
end
|
35
|
-
|
36
|
-
# force evaluation of options
|
37
|
-
def ask_for_missing_opts
|
38
|
-
username
|
39
|
-
password
|
40
|
-
orgname
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
44
|
-
|
45
|
-
class ConfigValidator
|
46
|
-
|
47
|
-
extend Forwardable
|
48
|
-
|
49
|
-
def_delegators :@options, :username, :password, :orgname
|
50
|
-
|
51
|
-
def initialize(api_client, options)
|
52
|
-
@api_client, @options = api_client, options
|
53
|
-
end
|
54
|
-
|
55
|
-
def validate!
|
56
|
-
puts "Validating your account information..."
|
57
|
-
validate_username
|
58
|
-
validate_password
|
59
|
-
validate_orgname
|
60
|
-
end
|
61
|
-
|
62
|
-
def validate_username
|
63
|
-
RestClient.head("http://community.opscode.com/users/#{username}")
|
64
|
-
puts "* Username '#{username}' is valid."
|
65
|
-
rescue RestClient::ResourceNotFound
|
66
|
-
STDERR.puts "The user '#{username}' does not exist. Check your spelling, or visit http://www.opscode.com/hosted-chef/ to sign up."
|
67
|
-
exit 1
|
68
|
-
end
|
69
|
-
|
70
|
-
def validate_password
|
71
|
-
@api_client.login_cookies
|
72
|
-
puts "* Password is correct"
|
73
|
-
rescue InvalidPassword
|
74
|
-
STDERR.puts "Could not login with the password you gave. Check your "\
|
75
|
-
"typing, or visit http://community.opscode.com/password_reset_requests/new"\
|
76
|
-
" to reset your password."
|
77
|
-
exit 1
|
78
|
-
end
|
79
|
-
|
80
|
-
def validate_orgname
|
81
|
-
page = RestClient.get("https://manage.opscode.com/organizations/#{orgname}", :cookies => @api_client.login_cookies)
|
82
|
-
# TODO: use actual HTTP response codes.
|
83
|
-
if page =~ /not found/i
|
84
|
-
STDERR.puts "The organization '#{orgname}' does not exist. Check your"\
|
85
|
-
" spelling, or visit https://manage.opscode.com/organizations to create your organization"
|
86
|
-
exit 1
|
87
|
-
elsif page =~ /permission denied/i
|
88
|
-
STDERR.puts "You are not associated with the organization '#{orgname}'"\
|
89
|
-
" or you do not have sufficient privileges to download its validator"\
|
90
|
-
" key. Ask an administrator of this org to invite you."
|
91
|
-
exit 1
|
92
|
-
end
|
93
|
-
puts "* Organization name '#{orgname}' is correct"
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
class ArgvParser
|
98
|
-
|
99
|
-
attr_reader :options
|
100
|
-
|
101
|
-
def initialize(argv)
|
102
|
-
@options = Options.new
|
103
|
-
@argv = argv
|
104
|
-
end
|
105
|
-
|
106
|
-
def parse
|
107
|
-
parser.parse!
|
108
|
-
options
|
109
|
-
end
|
110
|
-
|
111
|
-
def parser
|
112
|
-
OptionParser.new do |o|
|
113
|
-
o.on("-u", "--user USERNAME", "Your Hosted Chef username") do |username|
|
114
|
-
options.username = username
|
115
|
-
end
|
116
|
-
|
117
|
-
o.on("-p", "--password PASSWORD", "Your Hosted Chef password") do |passwd|
|
118
|
-
options.password = passwd
|
119
|
-
end
|
120
|
-
|
121
|
-
o.on('-o', "--organization ORGANIZATION", "Your Hosted Chef Organization") do |orgname|
|
122
|
-
options.orgname = orgname
|
123
|
-
end
|
124
|
-
|
125
|
-
o.on('-h', "--help") do
|
126
|
-
puts o
|
127
|
-
exit 1
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
end
|
133
|
-
|
134
|
-
#--
|
135
|
-
# Someday we'll return JSON when you ask for it, and then the naming of this
|
136
|
-
# class won't be a passive aggressive joke.
|
137
|
-
class ApiClient
|
138
|
-
|
139
|
-
extend Forwardable
|
140
|
-
|
141
|
-
def_delegators :@options, :username, :password, :orgname
|
142
|
-
|
143
|
-
def initialize(options)
|
144
|
-
@options = options
|
145
|
-
end
|
146
|
-
|
147
|
-
def login_authenticity_token
|
148
|
-
@auth_token ||= begin
|
149
|
-
login_page = RestClient.get('https://manage.opscode.com')
|
150
|
-
extract_csrf_token_from(login_page)
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def user_key_authenticity_token
|
155
|
-
@user_key_authenticity_token ||= begin
|
156
|
-
new_key_page = RestClient.get("https://community.opscode.com/users/#{username}/user_key/new", :cookies => login_cookies)
|
157
|
-
@login_cookies = new_key_page.cookies
|
158
|
-
extract_csrf_token_from(new_key_page)
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def validator_key_authenticity_token
|
163
|
-
@validator_key_authenticity_token ||= begin
|
164
|
-
org_list_page = RestClient.get("https://manage.opscode.com/organizations", :cookies => login_cookies)
|
165
|
-
@login_cookies = org_list_page.cookies
|
166
|
-
extract_csrf_token_from(org_list_page)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def login_cookies
|
171
|
-
@login_cookies ||= begin
|
172
|
-
post_options = {:name => username,
|
173
|
-
:password => password,
|
174
|
-
:authenticity_token => login_authenticity_token,
|
175
|
-
:multipart => true}
|
176
|
-
|
177
|
-
RestClient.post("https://manage.opscode.com/login_exec", post_options) do |response, req, result, &block|
|
178
|
-
if response.headers[:location] == "https://manage.opscode.com/login" # bad passwd
|
179
|
-
raise InvalidPassword
|
180
|
-
else
|
181
|
-
response.cookies
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def knife_config
|
188
|
-
RestClient.get("https://manage.opscode.com/organizations/#{orgname}/_knife_config", :cookies => login_cookies)
|
189
|
-
end
|
190
|
-
|
191
|
-
def user_key
|
192
|
-
post_options = {
|
193
|
-
:authenticity_token => user_key_authenticity_token,
|
194
|
-
:multipart => true
|
195
|
-
}
|
196
|
-
RestClient.post("https://community.opscode.com/users/#{username}/user_key", post_options, {:cookies => login_cookies})
|
197
|
-
end
|
198
|
-
|
199
|
-
def validator_key
|
200
|
-
#https://manage.opscode.com/organizations/prodbench2/_regenerate_key
|
201
|
-
post_options = {
|
202
|
-
#:multipart => true,
|
203
|
-
:authenticity_token => validator_key_authenticity_token,
|
204
|
-
:_method => "put"
|
205
|
-
}
|
206
|
-
headers = {
|
207
|
-
:Referer => "https://manage.opscode.com/organizations",
|
208
|
-
:cookies => login_cookies
|
209
|
-
}
|
210
|
-
uri = "https://manage.opscode.com/organizations/#{orgname}/_regenerate_key"
|
211
|
-
RestClient.post(uri, post_options,headers)
|
212
|
-
end
|
213
|
-
private
|
214
|
-
|
215
|
-
def extract_csrf_token_from(page)
|
216
|
-
# TODO: serve the authenticity token over JSON so we don't have to
|
217
|
-
# do this.
|
218
|
-
if md = page.match(/(\<meta content=\"[^\"]+\" name=\"csrf-token\"[\s]*\/\>)/)
|
219
|
-
page_element = md[1]
|
220
|
-
xml = REXML::Document.new(page_element)
|
221
|
-
xml.elements.first.attributes["content"]
|
222
|
-
elsif md = page.match(/(<input[^\>]*name=\"authenticity_token\"[^\>]+\>)/)
|
223
|
-
page_element = md[1]
|
224
|
-
xml = REXML::Document.new(page_element)
|
225
|
-
xml.elements.first.attributes["value"]
|
226
|
-
else
|
227
|
-
raise "Can't find CSRF token on the page:\n#{page}"
|
228
|
-
end
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
class ConfigInstaller
|
233
|
-
attr_reader :install_type
|
234
|
-
|
235
|
-
def initialize(api_client, options)
|
236
|
-
@api_client, @options = api_client, options
|
237
|
-
@install_dir, @install_type = nil, nil
|
238
|
-
end
|
239
|
-
|
240
|
-
def validate!
|
241
|
-
install_dir
|
242
|
-
end
|
243
|
-
|
244
|
-
def install_dir
|
245
|
-
unless @install_dir
|
246
|
-
@install_dir, @install_type = select_install_dir
|
247
|
-
end
|
248
|
-
@install_dir
|
249
|
-
end
|
250
|
-
|
251
|
-
def install_all
|
252
|
-
puts "\nInstalling..."
|
253
|
-
|
254
|
-
puts "> mkdir -p #{install_dir}"
|
255
|
-
FileUtils.mkdir_p(install_dir)
|
256
|
-
|
257
|
-
knife_rb = "#{@install_dir}/knife.rb"
|
258
|
-
puts "> create #{knife_rb} 0644"
|
259
|
-
File.open(knife_rb, File::RDWR|File::CREAT, 0644) {|f|
|
260
|
-
f << @api_client.knife_config
|
261
|
-
}
|
262
|
-
|
263
|
-
user_pem = "#{@install_dir}/#{@options.username}.pem"
|
264
|
-
puts "> create #{user_pem} 0600"
|
265
|
-
File.open(user_pem, File::RDWR|File::CREAT, 0600) {|f|
|
266
|
-
f << @api_client.user_key
|
267
|
-
}
|
268
|
-
|
269
|
-
validator_pem = "#{@install_dir}/#{@options.orgname}-validator.pem"
|
270
|
-
puts "> create #{validator_pem} 0600"
|
271
|
-
File.open(validator_pem, File::RDWR|File::CREAT, 0600) {|f|
|
272
|
-
f << @api_client.validator_key
|
273
|
-
}
|
274
|
-
end
|
275
|
-
|
276
|
-
private
|
277
|
-
|
278
|
-
def select_install_dir
|
279
|
-
home_dot_chef = File.expand_path("~/.chef")
|
280
|
-
cwd_dot_chef = File.expand_path(".chef")
|
281
|
-
|
282
|
-
if ENV['USER'] && !File.exist?(home_dot_chef)
|
283
|
-
[home_dot_chef, :default]
|
284
|
-
elsif !File.exist?(cwd_dot_chef)
|
285
|
-
[cwd_dot_chef, :non_default]
|
286
|
-
else
|
287
|
-
install_locations = [home_dot_chef, cwd_dot_chef].uniq
|
288
|
-
if install_locations.size == 1
|
289
|
-
STDERR.puts(<<-NOWHERE_TO_GO)
|
290
|
-
ERROR: You already have a Chef configuration in your home directory
|
291
|
-
(#{home_dot_chef})
|
292
|
-
|
293
|
-
If you really want to replace this configuration, you can remove it,
|
294
|
-
otherwise cd to a different directory to generate a new config.
|
295
|
-
NOWHERE_TO_GO
|
296
|
-
else
|
297
|
-
STDERR.puts(<<-NOWHERE_TO_GO)
|
298
|
-
ERROR: You already have a Chef configuration in your home directory and
|
299
|
-
your current working directory.
|
300
|
-
(#{home_dot_chef} and #{cwd_dot_chef})
|
301
|
-
|
302
|
-
If you wish to replace one of these you can remove it, otherwise you can
|
303
|
-
cd to a different directory to create a new config.
|
304
|
-
NOWHERE_TO_GO
|
305
|
-
exit 1
|
306
|
-
end
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
class Controller
|
312
|
-
|
313
|
-
attr_reader :argv
|
314
|
-
|
315
|
-
def initialize(argv=ARGV)
|
316
|
-
@argv = argv
|
317
|
-
@options = ArgvParser.new(argv).parse
|
318
|
-
@api_client = ApiClient.new(@options)
|
319
|
-
@validator = ConfigValidator.new(@api_client, @options)
|
320
|
-
@config_installer = ConfigInstaller.new(@api_client, @options)
|
321
|
-
end
|
322
|
-
|
323
|
-
def greeting
|
324
|
-
puts(<<-WELCOME)
|
325
|
-
Welcome to Opscode Hosted Chef. We're going to download your API keys
|
326
|
-
and knife config from Opscode and install them on your system.
|
327
|
-
|
328
|
-
Before you continue, please be aware that this program will update your API key
|
329
|
-
and your organization's validation key. The existing keys for these accounts
|
330
|
-
will be expired and no longer valid.
|
331
|
-
|
332
|
-
Your config will be created in #{@config_installer.install_dir}
|
333
|
-
|
334
|
-
WELCOME
|
335
|
-
|
336
|
-
unless HighLine.new.agree("Ready to get started? (yes/no): ")
|
337
|
-
STDERR.puts "goodbye."
|
338
|
-
exit 1
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
def setup_and_validate
|
343
|
-
@validator.validate!
|
344
|
-
end
|
345
|
-
|
346
|
-
def fetch_and_install
|
347
|
-
@config_installer.install_all
|
348
|
-
end
|
349
|
-
|
350
|
-
def goodbye
|
351
|
-
puts(<<-RESOURCES)
|
352
|
-
|
353
|
-
You're ready to go! To verify your configuration, try running
|
354
|
-
`knife client list`
|
355
|
-
|
356
|
-
If you should run into trouble check the following resources:
|
357
|
-
* Knife built-in manuals: run `knife help`
|
358
|
-
* Documentation at http://wiki.opscode.com
|
359
|
-
* Get support from Opscode: http://help.opscode.com/
|
360
|
-
RESOURCES
|
361
|
-
end
|
362
|
-
|
363
|
-
|
364
|
-
def run
|
365
|
-
@config_installer.validate!
|
366
|
-
greeting
|
367
|
-
@options.ask_for_missing_opts
|
368
|
-
setup_and_validate
|
369
|
-
fetch_and_install
|
370
|
-
goodbye
|
371
|
-
rescue Interrupt, EOFError
|
372
|
-
puts "\nexiting..."
|
373
|
-
exit! 1
|
374
|
-
end
|
375
|
-
|
376
|
-
end
|
377
31
|
end
|
378
32
|
|
379
33
|
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module HostedChef
|
2
|
+
|
3
|
+
# Talks to Hosted Chef over HTTP. Right now, uses screen scraping to fetch
|
4
|
+
# CSRF tokens from HTML pages, but I'm optimistic that we'll at least send
|
5
|
+
# these as JSON in the future.
|
6
|
+
class ApiClient
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@options, :username, :password, :orgname
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
# Rails csrf token for the login page.
|
17
|
+
def login_authenticity_token
|
18
|
+
@auth_token ||= begin
|
19
|
+
login_page = RestClient.get('https://manage.opscode.com')
|
20
|
+
extract_csrf_token_from(login_page)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Rails csrf token for the user profile page on community.opscode.com
|
25
|
+
def user_key_authenticity_token
|
26
|
+
@user_key_authenticity_token ||= begin
|
27
|
+
new_key_page = RestClient.get("https://community.opscode.com/users/#{username}/user_key/new", :cookies => login_cookies)
|
28
|
+
@login_cookies = new_key_page.cookies
|
29
|
+
extract_csrf_token_from(new_key_page)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Rails csrf token for the organization list page on manage.opscode.com
|
34
|
+
def validator_key_authenticity_token
|
35
|
+
@validator_key_authenticity_token ||= begin
|
36
|
+
org_list_page = RestClient.get("https://manage.opscode.com/organizations", :cookies => login_cookies)
|
37
|
+
@login_cookies = org_list_page.cookies
|
38
|
+
extract_csrf_token_from(org_list_page)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Memoized cookies for authN with manage and community sites.
|
43
|
+
def login_cookies
|
44
|
+
@login_cookies ||= begin
|
45
|
+
post_options = {:name => username,
|
46
|
+
:password => password,
|
47
|
+
:authenticity_token => login_authenticity_token,
|
48
|
+
:multipart => true}
|
49
|
+
|
50
|
+
RestClient.post("https://manage.opscode.com/login_exec", post_options) do |response, req, result, &block|
|
51
|
+
if response.headers[:location] == "https://manage.opscode.com/login" # bad passwd
|
52
|
+
raise InvalidPassword
|
53
|
+
else
|
54
|
+
response.cookies
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Fetches the knife.rb for the given user/organization pair from manage.
|
61
|
+
def knife_config
|
62
|
+
RestClient.get("https://manage.opscode.com/organizations/#{orgname}/_knife_config", :cookies => login_cookies)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Fetches the user's RSA API key from community.opscode.com.
|
66
|
+
#
|
67
|
+
# NB: Updates state on the server such that any existing key becomes
|
68
|
+
# invalid.
|
69
|
+
def user_key
|
70
|
+
post_options = {
|
71
|
+
:authenticity_token => user_key_authenticity_token,
|
72
|
+
:multipart => true
|
73
|
+
}
|
74
|
+
RestClient.post("https://community.opscode.com/users/#{username}/user_key", post_options, {:cookies => login_cookies})
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fetches the organization's validator client's key from manage.
|
78
|
+
#
|
79
|
+
# NB: This updates state on the server such that any existing key for this
|
80
|
+
# client is no longer valid.
|
81
|
+
def validator_key
|
82
|
+
#https://manage.opscode.com/organizations/prodbench2/_regenerate_key
|
83
|
+
post_options = {
|
84
|
+
#:multipart => true,
|
85
|
+
:authenticity_token => validator_key_authenticity_token,
|
86
|
+
:_method => "put"
|
87
|
+
}
|
88
|
+
headers = {
|
89
|
+
:Referer => "https://manage.opscode.com/organizations",
|
90
|
+
:cookies => login_cookies
|
91
|
+
}
|
92
|
+
uri = "https://manage.opscode.com/organizations/#{orgname}/_regenerate_key"
|
93
|
+
RestClient.post(uri, post_options,headers)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Screen scrape the HTML in +page+ and find the csrf token. Obviously this
|
99
|
+
# is suboptimal, but REXML can't parse HTML (AFAICT) and we don't want to
|
100
|
+
# depend on libxml2, so really nice solutions (like nokogiri+mechanize) are
|
101
|
+
# out. Hopefully we (opscode) will have time soon to build a saner
|
102
|
+
# password-based auth mechanism and no one will know that I parsed HTML
|
103
|
+
# with regexes :P
|
104
|
+
def extract_csrf_token_from(page)
|
105
|
+
# TODO: serve the authenticity token over JSON so we don't have to
|
106
|
+
# do this.
|
107
|
+
if md = page.match(/(\<meta content=\"[^\"]+\" name=\"csrf-token\"[\s]*\/\>)/)
|
108
|
+
page_element = md[1]
|
109
|
+
xml = REXML::Document.new(page_element)
|
110
|
+
xml.elements.first.attributes["content"]
|
111
|
+
elsif md = page.match(/(<input[^\>]*name=\"authenticity_token\"[^\>]+\>)/)
|
112
|
+
page_element = md[1]
|
113
|
+
xml = REXML::Document.new(page_element)
|
114
|
+
xml.elements.first.attributes["value"]
|
115
|
+
else
|
116
|
+
raise "Can't find CSRF token on the page:\n#{page}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module HostedChef
|
2
|
+
class InvalidPassword < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class Options
|
6
|
+
attr_writer :password
|
7
|
+
|
8
|
+
def password
|
9
|
+
@password ||= HighLine.new.ask("Your Hosted Chef password: ") {|q| q.echo = "*"}
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_writer :username
|
13
|
+
|
14
|
+
def username
|
15
|
+
@username ||= HighLine.new.ask("Your Hosted Chef username? ")
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_writer :orgname
|
19
|
+
|
20
|
+
def orgname
|
21
|
+
@orgname ||= HighLine.new.ask("Your Hosted Chef organization? ")
|
22
|
+
end
|
23
|
+
|
24
|
+
# force evaluation of options
|
25
|
+
def ask_for_missing_opts
|
26
|
+
username
|
27
|
+
password
|
28
|
+
orgname
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class ConfigValidator
|
34
|
+
|
35
|
+
extend Forwardable
|
36
|
+
|
37
|
+
def_delegators :@options, :username, :password, :orgname
|
38
|
+
|
39
|
+
def initialize(api_client, options)
|
40
|
+
@api_client, @options = api_client, options
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate!
|
44
|
+
puts "Validating your account information..."
|
45
|
+
validate_username
|
46
|
+
validate_password
|
47
|
+
validate_orgname
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_username
|
51
|
+
RestClient.head("http://community.opscode.com/users/#{username}")
|
52
|
+
puts "* Username '#{username}' is valid."
|
53
|
+
rescue RestClient::ResourceNotFound
|
54
|
+
STDERR.puts "The user '#{username}' does not exist. Check your spelling, or visit http://www.opscode.com/hosted-chef/ to sign up."
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_password
|
59
|
+
@api_client.login_cookies
|
60
|
+
puts "* Password is correct"
|
61
|
+
rescue InvalidPassword
|
62
|
+
STDERR.puts "Could not login with the password you gave. Check your "\
|
63
|
+
"typing, or visit http://community.opscode.com/password_reset_requests/new"\
|
64
|
+
" to reset your password."
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_orgname
|
69
|
+
page = RestClient.get("https://www.opscode.com/account/organizations/#{orgname}/plan", :cookies => @api_client.login_cookies)
|
70
|
+
# TODO: use actual HTTP response codes.
|
71
|
+
puts "* Organization name '#{orgname}' is correct"
|
72
|
+
rescue RestClient::ResourceNotFound
|
73
|
+
STDERR.puts "The organization '#{orgname}' does not exist or you dont "\
|
74
|
+
"have permission to access it. Check your spelling, or visit "\
|
75
|
+
"https://manage.opscode.com/organizations to create your organization"
|
76
|
+
exit 1
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class ArgvParser
|
81
|
+
|
82
|
+
attr_reader :options
|
83
|
+
|
84
|
+
def initialize(argv)
|
85
|
+
@options = Options.new
|
86
|
+
@argv = argv
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse
|
90
|
+
parser.parse!
|
91
|
+
options
|
92
|
+
end
|
93
|
+
|
94
|
+
def parser
|
95
|
+
OptionParser.new do |o|
|
96
|
+
o.on("-u", "--user USERNAME", "Your Hosted Chef username") do |username|
|
97
|
+
options.username = username
|
98
|
+
end
|
99
|
+
|
100
|
+
o.on("-p", "--password PASSWORD", "Your Hosted Chef password") do |passwd|
|
101
|
+
options.password = passwd
|
102
|
+
end
|
103
|
+
|
104
|
+
o.on('-o', "--organization ORGANIZATION", "Your Hosted Chef Organization") do |orgname|
|
105
|
+
options.orgname = orgname
|
106
|
+
end
|
107
|
+
|
108
|
+
o.on('-h', "--help") do
|
109
|
+
puts o
|
110
|
+
exit 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module HostedChef
|
2
|
+
|
3
|
+
# Handles the business of selecting the install location and writing out the
|
4
|
+
# API keys and knife config.
|
5
|
+
class ConfigInstaller
|
6
|
+
attr_reader :install_type
|
7
|
+
|
8
|
+
def initialize(api_client, options)
|
9
|
+
@api_client, @options = api_client, options
|
10
|
+
@install_dir, @install_type = nil, nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate!
|
14
|
+
install_dir
|
15
|
+
end
|
16
|
+
|
17
|
+
def install_dir
|
18
|
+
unless @install_dir
|
19
|
+
@install_dir, @install_type = select_install_dir
|
20
|
+
end
|
21
|
+
@install_dir
|
22
|
+
end
|
23
|
+
|
24
|
+
def install_all
|
25
|
+
puts "\nInstalling..."
|
26
|
+
|
27
|
+
puts "> mkdir -p #{install_dir}"
|
28
|
+
FileUtils.mkdir_p(install_dir)
|
29
|
+
|
30
|
+
knife_rb = "#{@install_dir}/knife.rb"
|
31
|
+
puts "> create #{knife_rb} 0644"
|
32
|
+
File.open(knife_rb, File::RDWR|File::CREAT, 0644) {|f|
|
33
|
+
f << @api_client.knife_config
|
34
|
+
}
|
35
|
+
|
36
|
+
user_pem = "#{@install_dir}/#{@options.username}.pem"
|
37
|
+
puts "> create #{user_pem} 0600"
|
38
|
+
File.open(user_pem, File::RDWR|File::CREAT, 0600) {|f|
|
39
|
+
f << @api_client.user_key
|
40
|
+
}
|
41
|
+
|
42
|
+
validator_pem = "#{@install_dir}/#{@options.orgname}-validator.pem"
|
43
|
+
puts "> create #{validator_pem} 0600"
|
44
|
+
File.open(validator_pem, File::RDWR|File::CREAT, 0600) {|f|
|
45
|
+
f << @api_client.validator_key
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def select_install_dir
|
52
|
+
home_dot_chef = File.expand_path("~/.chef")
|
53
|
+
cwd_dot_chef = File.expand_path(".chef")
|
54
|
+
|
55
|
+
if ENV['USER'] && !File.exist?(home_dot_chef)
|
56
|
+
[home_dot_chef, :default]
|
57
|
+
elsif !File.exist?(cwd_dot_chef)
|
58
|
+
[cwd_dot_chef, :non_default]
|
59
|
+
else
|
60
|
+
install_locations = [home_dot_chef, cwd_dot_chef].uniq
|
61
|
+
if install_locations.size == 1
|
62
|
+
STDERR.puts(<<-NOWHERE_TO_GO)
|
63
|
+
ERROR: You already have a Chef configuration in your home directory
|
64
|
+
(#{home_dot_chef})
|
65
|
+
|
66
|
+
If you really want to replace this configuration, you can remove it,
|
67
|
+
otherwise cd to a different directory to generate a new config.
|
68
|
+
NOWHERE_TO_GO
|
69
|
+
else
|
70
|
+
STDERR.puts(<<-NOWHERE_TO_GO)
|
71
|
+
ERROR: You already have a Chef configuration in your home directory and
|
72
|
+
your current working directory.
|
73
|
+
(#{home_dot_chef} and #{cwd_dot_chef})
|
74
|
+
|
75
|
+
If you wish to replace one of these you can remove it, otherwise you can
|
76
|
+
cd to a different directory to create a new config.
|
77
|
+
NOWHERE_TO_GO
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module HostedChef
|
2
|
+
|
3
|
+
# Controller and Presentation (yes, I know) for the Hosted Chef user setup
|
4
|
+
# task (currently the only task).
|
5
|
+
class Controller
|
6
|
+
|
7
|
+
attr_reader :argv
|
8
|
+
|
9
|
+
def initialize(argv=ARGV)
|
10
|
+
@argv = argv
|
11
|
+
@options = ArgvParser.new(argv).parse
|
12
|
+
@api_client = ApiClient.new(@options)
|
13
|
+
@validator = ConfigValidator.new(@api_client, @options)
|
14
|
+
@config_installer = ConfigInstaller.new(@api_client, @options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def greeting
|
18
|
+
puts(<<-WELCOME)
|
19
|
+
Welcome to Opscode Hosted Chef. We're going to download your API keys
|
20
|
+
and knife config from Opscode and install them on your system.
|
21
|
+
|
22
|
+
Before you continue, please be aware that this program will update your API key
|
23
|
+
and your organization's validation key. The existing keys for these accounts
|
24
|
+
will be expired and no longer valid.
|
25
|
+
|
26
|
+
Your config will be created in #{@config_installer.install_dir}
|
27
|
+
|
28
|
+
WELCOME
|
29
|
+
|
30
|
+
unless HighLine.new.agree("Ready to get started? (yes/no): ")
|
31
|
+
STDERR.puts "goodbye."
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_and_validate
|
37
|
+
@validator.validate!
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_and_install
|
41
|
+
@config_installer.install_all
|
42
|
+
end
|
43
|
+
|
44
|
+
def goodbye
|
45
|
+
puts(<<-RESOURCES)
|
46
|
+
|
47
|
+
You're ready to go! To verify your configuration, try running
|
48
|
+
`knife client list`
|
49
|
+
|
50
|
+
If you should run into trouble check the following resources:
|
51
|
+
* Knife built-in manuals: run `knife help`
|
52
|
+
* Documentation at http://wiki.opscode.com
|
53
|
+
* Get support from Opscode: http://help.opscode.com/
|
54
|
+
RESOURCES
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def run
|
59
|
+
@config_installer.validate!
|
60
|
+
greeting
|
61
|
+
@options.ask_for_missing_opts
|
62
|
+
setup_and_validate
|
63
|
+
fetch_and_install
|
64
|
+
goodbye
|
65
|
+
rescue Interrupt, EOFError
|
66
|
+
puts "\nexiting..."
|
67
|
+
exit! 1
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
data/lib/hosted-chef/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hosted-chef
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-01-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rest-client
|
16
|
-
requirement: &
|
16
|
+
requirement: &70232472699880 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70232472699880
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: highline
|
27
|
-
requirement: &
|
27
|
+
requirement: &70232472699460 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70232472699460
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &70232472699020 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,7 +43,7 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70232472699020
|
47
47
|
description: Configures your workstation for Hosted Chef
|
48
48
|
email: info@opscode.com
|
49
49
|
executables:
|
@@ -55,6 +55,10 @@ extra_rdoc_files:
|
|
55
55
|
files:
|
56
56
|
- LICENSE
|
57
57
|
- README.md
|
58
|
+
- lib/hosted-chef/api_client.rb
|
59
|
+
- lib/hosted-chef/cli.rb
|
60
|
+
- lib/hosted-chef/config_installer.rb
|
61
|
+
- lib/hosted-chef/controller.rb
|
58
62
|
- lib/hosted-chef/version.rb
|
59
63
|
- lib/hosted-chef.rb
|
60
64
|
- bin/hosted-chef
|