recipiez 0.0.1
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/History.txt +1 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +13 -0
- data/lib/activecollab_notifier.rb +58 -0
- data/lib/basecamp.rb +486 -0
- data/lib/basecamp_notifier.rb +34 -0
- data/recipes/apache.rb +41 -0
- data/recipes/chef.rb +8 -0
- data/recipes/deployment_recipiez.rb +295 -0
- data/recipes/logrotate.rb +9 -0
- data/recipes/monit.rb +46 -0
- data/recipes/nginx.rb +35 -0
- data/recipes/render.rb +6 -0
- data/recipes/templates/apache_monit.erb +11 -0
- data/recipes/templates/logrotate.erb +9 -0
- data/recipes/templates/monit_config.erb +7 -0
- data/recipes/templates/mysql_monit.erb +6 -0
- data/recipes/templates/nginx_monit.erb +9 -0
- data/recipes/templates/nginx_vhost.erb +135 -0
- data/recipes/templates/passenger_vhost.erb +13 -0
- data/recipes/templates/php_handler.erb +1 -0
- data/recipes/templates/php_vhost.erb +12 -0
- data/recipes/templates/recipiez.yml.example +8 -0
- data/recipes/templates/sshd_monit.erb +5 -0
- data/recipes/templates/thin_monit.erb +16 -0
- data/recipes/thin.rb +37 -0
- data/recipes/tolk.rb +13 -0
- metadata +72 -0
data/History.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* Initial development - 9/1/08
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Alastair Brunton
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
|
3
|
+
module ActiveCollab
|
4
|
+
|
5
|
+
class Notifier
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
|
9
|
+
if keys_missing?(options)
|
10
|
+
raise StandardError, "You have not supplied all the required arguments in the hash. (base_url, project_id, ticket_id, email, password)"
|
11
|
+
end
|
12
|
+
|
13
|
+
@base_url = options[:base_url]
|
14
|
+
@project_id = options[:project_id]
|
15
|
+
@ticket_id = options[:ticket_id]
|
16
|
+
@mech = Mechanize.new
|
17
|
+
@email = options[:email]
|
18
|
+
@password = options[:password]
|
19
|
+
end
|
20
|
+
|
21
|
+
def say(message)
|
22
|
+
login
|
23
|
+
submit_ticket_comment(message)
|
24
|
+
end
|
25
|
+
|
26
|
+
def keys_missing?(options)
|
27
|
+
[:base_url, :project_id, :ticket_id, :email, :password].each do |key|
|
28
|
+
return true unless options.has_key?(key)
|
29
|
+
end
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def login
|
36
|
+
|
37
|
+
@mech.get(@base_url + "/login") do |login_page|
|
38
|
+
form = login_page.forms.first
|
39
|
+
|
40
|
+
form['login[email]'] = @email
|
41
|
+
form['login[password]'] = @password
|
42
|
+
|
43
|
+
form.click_button
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def submit_ticket_comment(message)
|
48
|
+
@mech.get(@base_url + "/projects/#{@project_id}/tickets/#{@ticket_id}") do |page|
|
49
|
+
page.form_with(:action => /\/projects\/#{@project_id}\/comments\/add/) do |comment_form|
|
50
|
+
comment_form['comment[body]'] = message
|
51
|
+
comment_form.click_button
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/basecamp.rb
ADDED
@@ -0,0 +1,486 @@
|
|
1
|
+
# the following are all standard ruby libraries
|
2
|
+
require 'net/https'
|
3
|
+
require 'yaml'
|
4
|
+
require 'date'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'rubygems'
|
9
|
+
require 'xmlsimple'
|
10
|
+
rescue LoadError
|
11
|
+
abort <<-ERROR
|
12
|
+
The 'xml-simple' library could not be loaded. If you have RubyGems installed
|
13
|
+
you can install xml-simple by doing "gem install xml-simple".
|
14
|
+
ERROR
|
15
|
+
end
|
16
|
+
|
17
|
+
# An interface to the Basecamp web-services API. Usage is straightforward:
|
18
|
+
#
|
19
|
+
# session = Basecamp.new('your.basecamp.com', 'username', 'password')
|
20
|
+
# puts "projects: #{session.projects.length}"
|
21
|
+
class Basecamp
|
22
|
+
|
23
|
+
# A wrapper to encapsulate the data returned by Basecamp, for easier access.
|
24
|
+
class Record #:nodoc:
|
25
|
+
attr_reader :type
|
26
|
+
|
27
|
+
def initialize(type, hash)
|
28
|
+
@type = type
|
29
|
+
@hash = hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](name)
|
33
|
+
name = dashify(name)
|
34
|
+
case @hash[name]
|
35
|
+
when Hash then
|
36
|
+
@hash[name] = (@hash[name].keys.length == 1 && Array === @hash[name].values.first) ?
|
37
|
+
@hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) } :
|
38
|
+
Record.new(name, @hash[name])
|
39
|
+
else @hash[name]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def id
|
44
|
+
@hash["id"]
|
45
|
+
end
|
46
|
+
|
47
|
+
def attributes
|
48
|
+
@hash.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
def respond_to?(sym)
|
52
|
+
super || @hash.has_key?(dashify(sym))
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(sym, *args)
|
56
|
+
if args.empty? && !block_given? && respond_to?(sym)
|
57
|
+
self[sym]
|
58
|
+
else
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
"\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
|
65
|
+
end
|
66
|
+
|
67
|
+
def inspect
|
68
|
+
to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def dashify(name)
|
74
|
+
name.to_s.tr("_", "-")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# A wrapper to represent a file that should be uploaded. This is used so that
|
79
|
+
# the form/multi-part encoder knows when to encode a field as a file, versus
|
80
|
+
# when to encode it as a simple field.
|
81
|
+
class FileUpload
|
82
|
+
attr_reader :filename, :content
|
83
|
+
|
84
|
+
def initialize(filename, content)
|
85
|
+
@filename = filename
|
86
|
+
@content = content
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_accessor :use_xml
|
91
|
+
|
92
|
+
# Connects
|
93
|
+
def initialize(url, user_name, password, use_ssl = false)
|
94
|
+
@use_xml = false
|
95
|
+
@user_name, @password = user_name, password
|
96
|
+
connect!(url, use_ssl)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Return the list of all accessible projects.
|
100
|
+
def projects
|
101
|
+
records "project", "/project/list"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the list of message categories for the given project
|
105
|
+
def message_categories(project_id)
|
106
|
+
records "post-category", "/projects/#{project_id}/post_categories"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the list of file categories for the given project
|
110
|
+
def file_categories(project_id)
|
111
|
+
records "attachment-category", "/projects/#{project_id}/attachment_categories"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return information for the company with the given id
|
115
|
+
def company(id)
|
116
|
+
record "/contacts/company/#{id}"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return an array of the people in the given company. If the project-id is
|
120
|
+
# given, only people who have access to the given project will be returned.
|
121
|
+
def people(company_id, project_id=nil)
|
122
|
+
url = project_id ? "/projects/#{project_id}" : ""
|
123
|
+
url << "/contacts/people/#{company_id}"
|
124
|
+
records "person", url
|
125
|
+
end
|
126
|
+
|
127
|
+
# Return information about the person with the given id
|
128
|
+
def person(id)
|
129
|
+
record "/contacts/person/#{id}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return information about the message(s) with the given id(s). The API
|
133
|
+
# limits you to requesting 25 messages at a time, so if you need to get more
|
134
|
+
# than that, you'll need to do it in multiple requests.
|
135
|
+
def message(*ids)
|
136
|
+
result = records("post", "/msg/get/#{ids.join(",")}")
|
137
|
+
result.length == 1 ? result.first : result
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns a summary of all messages in the given project (and category, if
|
141
|
+
# specified). The summary is simply the title and category of the message,
|
142
|
+
# as well as the number of attachments (if any).
|
143
|
+
def message_list(project_id, category_id=nil)
|
144
|
+
url = "/projects/#{project_id}/msg"
|
145
|
+
url << "/cat/#{category_id}" if category_id
|
146
|
+
url << "/archive"
|
147
|
+
|
148
|
+
records "post", url
|
149
|
+
end
|
150
|
+
|
151
|
+
# Create a new message in the given project. The +message+ parameter should
|
152
|
+
# be a hash. The +email_to+ parameter must be an array of person-id's that
|
153
|
+
# should be notified of the post.
|
154
|
+
#
|
155
|
+
# If you want to add attachments to the message, the +attachments+ parameter
|
156
|
+
# should be an array of hashes, where each has has a :name key (optional),
|
157
|
+
# and a :file key (required). The :file key must refer to a Basecamp::FileUpload
|
158
|
+
# instance.
|
159
|
+
#
|
160
|
+
# msg = session.post_message(158141,
|
161
|
+
# { :title => "Requirements",
|
162
|
+
# :body => "Here are the requirements documents you asked for.",
|
163
|
+
# :category_id => 2301121 },
|
164
|
+
# [john.id, martha.id],
|
165
|
+
# [ { :name => "Primary Requirements",
|
166
|
+
# :file => Basecamp::FileUpload.new('primary.doc", File.read('primary.doc')) },
|
167
|
+
# { :file => Basecamp::FileUpload.new('other.doc', File.read('other.doc')) } ])
|
168
|
+
def post_message(project_id, message, notify=[], attachments=[])
|
169
|
+
prepare_attachments(attachments)
|
170
|
+
record "/projects/#{project_id}/msg/create",
|
171
|
+
:post => message,
|
172
|
+
:notify => notify,
|
173
|
+
:attachments => attachments
|
174
|
+
end
|
175
|
+
|
176
|
+
# Edit the message with the given id. The +message+ parameter should
|
177
|
+
# be a hash. The +email_to+ parameter must be an array of person-id's that
|
178
|
+
# should be notified of the post.
|
179
|
+
#
|
180
|
+
# The +attachments+ parameter, if used, should be the same as described for
|
181
|
+
# #post_message.
|
182
|
+
def update_message(id, message, notify=[], attachments=[])
|
183
|
+
prepare_attachments(attachments)
|
184
|
+
record "/msg/update/#{id}",
|
185
|
+
:post => message,
|
186
|
+
:notify => notify,
|
187
|
+
:attachments => attachments
|
188
|
+
end
|
189
|
+
|
190
|
+
# Deletes the message with the given id, and returns it.
|
191
|
+
def delete_message(id)
|
192
|
+
record "/msg/delete/#{id}"
|
193
|
+
end
|
194
|
+
|
195
|
+
# Return a list of the comments for the specified message.
|
196
|
+
def comments(post_id)
|
197
|
+
records "comment", "/msg/comments/#{post_id}"
|
198
|
+
end
|
199
|
+
|
200
|
+
# Retrieve a specific comment
|
201
|
+
def comment(id)
|
202
|
+
record "/msg/comment/#{id}"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Add a new comment to a message. +comment+ must be a hash describing the
|
206
|
+
# comment. You can add attachments to the comment, too, by giving them in
|
207
|
+
# an array. See the #post_message method for a description of how to do that.
|
208
|
+
def create_comment(post_id, comment, attachments=[])
|
209
|
+
prepare_attachments(attachments)
|
210
|
+
record "/msg/create_comment", :comment => comment.merge(:post_id => post_id),
|
211
|
+
:attachments => attachments
|
212
|
+
end
|
213
|
+
|
214
|
+
# Update the given comment. Attachments follow the same format as #post_message.
|
215
|
+
def update_comment(id, comment, attachments=[])
|
216
|
+
prepare_attachments(attachments)
|
217
|
+
record "/msg/update_comment", :comment_id => id,
|
218
|
+
:comment => comment, :attachments => attachments
|
219
|
+
end
|
220
|
+
|
221
|
+
# Deletes (and returns) the given comment.
|
222
|
+
def delete_comment(id)
|
223
|
+
record "/msg/delete_comment/#{id}"
|
224
|
+
end
|
225
|
+
|
226
|
+
# =========================================================================
|
227
|
+
# TODO LISTS AND ITEMS
|
228
|
+
# =========================================================================
|
229
|
+
|
230
|
+
# Marks the given item completed.
|
231
|
+
def complete_item(id)
|
232
|
+
record "/todos/complete_item/#{id}"
|
233
|
+
end
|
234
|
+
|
235
|
+
# Marks the given item uncompleted.
|
236
|
+
def uncomplete_item(id)
|
237
|
+
record "/todos/uncomplete_item/#{id}"
|
238
|
+
end
|
239
|
+
|
240
|
+
# Creates a new to-do item.
|
241
|
+
def create_item(list_id, content, responsible_party=nil, notify=true)
|
242
|
+
record "/todos/create_item/#{list_id}",
|
243
|
+
:content => content, :responsible_party => responsible_party,
|
244
|
+
:notify => notify
|
245
|
+
end
|
246
|
+
|
247
|
+
# Creates a new list using the given hash of list metadata.
|
248
|
+
def create_list(project_id, list)
|
249
|
+
record "/projects/#{project_id}/todos/create_list", list
|
250
|
+
end
|
251
|
+
|
252
|
+
# Deletes the given item from it's parent list.
|
253
|
+
def delete_item(id)
|
254
|
+
record "/todos/delete_item/#{id}"
|
255
|
+
end
|
256
|
+
|
257
|
+
# Deletes the given list and all of its items.
|
258
|
+
def delete_list(id)
|
259
|
+
record "/todos/delete_list/#{id}"
|
260
|
+
end
|
261
|
+
|
262
|
+
# Retrieves the specified list, and all of its items.
|
263
|
+
def get_list(id)
|
264
|
+
record "/todos/list/#{id}"
|
265
|
+
end
|
266
|
+
|
267
|
+
# Return all lists for a project. If complete is true, only completed lists
|
268
|
+
# are returned. If complete is false, only uncompleted lists are returned.
|
269
|
+
def lists(project_id, complete=nil)
|
270
|
+
records "todo-list", "/projects/#{project_id}/todos/lists", :complete => complete
|
271
|
+
end
|
272
|
+
|
273
|
+
# Repositions an item to be at the given position in its list
|
274
|
+
def move_item(id, to)
|
275
|
+
record "/todos/move_item/#{id}", :to => to
|
276
|
+
end
|
277
|
+
|
278
|
+
# Repositions a list to be at the given position in its project
|
279
|
+
def move_list(id, to)
|
280
|
+
record "/todos/move_list/#{id}", :to => to
|
281
|
+
end
|
282
|
+
|
283
|
+
# Updates the given item
|
284
|
+
def update_item(id, content, responsible_party=nil, notify=true)
|
285
|
+
record "/todos/update_item/#{id}",
|
286
|
+
:item => { :content => content }, :responsible_party => responsible_party,
|
287
|
+
:notify => notify
|
288
|
+
end
|
289
|
+
|
290
|
+
# Updates the given list's metadata
|
291
|
+
def update_list(id, list)
|
292
|
+
record "/todos/update_list/#{id}", :list => list
|
293
|
+
end
|
294
|
+
|
295
|
+
# =========================================================================
|
296
|
+
# MILESTONES
|
297
|
+
# =========================================================================
|
298
|
+
|
299
|
+
# Complete the milestone with the given id
|
300
|
+
def complete_milestone(id)
|
301
|
+
record "/milestones/complete/#{id}"
|
302
|
+
end
|
303
|
+
|
304
|
+
# Create a new milestone for the given project. +data+ must be hash of the
|
305
|
+
# values to set, including +title+, +deadline+, +responsible_party+, and
|
306
|
+
# +notify+.
|
307
|
+
def create_milestone(project_id, data)
|
308
|
+
create_milestones(project_id, [data]).first
|
309
|
+
end
|
310
|
+
|
311
|
+
# As #create_milestone, but can create multiple milestones in a single
|
312
|
+
# request. The +milestones+ parameter must be an array of milestone values as
|
313
|
+
# descrbed in #create_milestone.
|
314
|
+
def create_milestones(project_id, milestones)
|
315
|
+
records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
|
316
|
+
end
|
317
|
+
|
318
|
+
# Destroys the milestone with the given id.
|
319
|
+
def delete_milestone(id)
|
320
|
+
record "/milestones/delete/#{id}"
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns a list of all milestones for the given project, optionally filtered
|
324
|
+
# by whether they are completed, late, or upcoming.
|
325
|
+
def milestones(project_id, find="all")
|
326
|
+
records "milestone", "/projects/#{project_id}/milestones/list", :find => find
|
327
|
+
end
|
328
|
+
|
329
|
+
# Uncomplete the milestone with the given id
|
330
|
+
def uncomplete_milestone(id)
|
331
|
+
record "/milestones/uncomplete/#{id}"
|
332
|
+
end
|
333
|
+
|
334
|
+
# Updates an existing milestone.
|
335
|
+
def update_milestone(id, data, move=false, move_off_weekends=false)
|
336
|
+
record "/milestones/update/#{id}", :milestone => data,
|
337
|
+
:move_upcoming_milestones => move,
|
338
|
+
:move_upcoming_milestones_off_weekends => move_off_weekends
|
339
|
+
end
|
340
|
+
|
341
|
+
# Make a raw web-service request to Basecamp. This will return a Hash of
|
342
|
+
# Arrays of the response, and may seem a little odd to the uninitiated.
|
343
|
+
def request(path, parameters = {}, second_try = false)
|
344
|
+
response = post(path, convert_body(parameters), "Content-Type" => content_type)
|
345
|
+
|
346
|
+
if response.code.to_i / 100 == 2
|
347
|
+
result = XmlSimple.xml_in(response.body, 'keeproot' => true,
|
348
|
+
'contentkey' => '__content__', 'forcecontent' => true)
|
349
|
+
typecast_value(result)
|
350
|
+
elsif response.code == "302" && !second_try
|
351
|
+
connect!(@url, !@use_ssl)
|
352
|
+
request(path, parameters, true)
|
353
|
+
else
|
354
|
+
raise "#{response.message} (#{response.code})"
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# A convenience method for wrapping the result of a query in a Record
|
359
|
+
# object. This assumes that the result is a singleton, not a collection.
|
360
|
+
def record(path, parameters={})
|
361
|
+
result = request(path, parameters)
|
362
|
+
(result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
|
363
|
+
end
|
364
|
+
|
365
|
+
# A convenience method for wrapping the result of a query in Record
|
366
|
+
# objects. This assumes that the result is a collection--any singleton
|
367
|
+
# result will be wrapped in an array.
|
368
|
+
def records(node, path, parameters={})
|
369
|
+
result = request(path, parameters).values.first or return []
|
370
|
+
result = result[node] or return []
|
371
|
+
result = [result] unless Array === result
|
372
|
+
result.map { |row| Record.new(node, row) }
|
373
|
+
end
|
374
|
+
|
375
|
+
private
|
376
|
+
|
377
|
+
def connect!(url, use_ssl)
|
378
|
+
@use_ssl = use_ssl
|
379
|
+
@url = url
|
380
|
+
@connection = Net::HTTP.new(url, use_ssl ? 443 : 80)
|
381
|
+
@connection.use_ssl = @use_ssl
|
382
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl
|
383
|
+
end
|
384
|
+
|
385
|
+
def convert_body(body)
|
386
|
+
body = use_xml ? body.to_xml : body.to_yaml
|
387
|
+
end
|
388
|
+
|
389
|
+
def content_type
|
390
|
+
use_xml ? "application/xml" : "application/x-yaml"
|
391
|
+
end
|
392
|
+
|
393
|
+
def post(path, body, header={})
|
394
|
+
request = Net::HTTP::Post.new(path, header.merge('Accept' => 'application/xml'))
|
395
|
+
request.basic_auth(@user_name, @password)
|
396
|
+
@connection.request(request, body)
|
397
|
+
end
|
398
|
+
|
399
|
+
def store_file(contents)
|
400
|
+
response = post("/upload", contents, 'Content-Type' => 'application/octet-stream',
|
401
|
+
'Accept' => 'application/xml')
|
402
|
+
|
403
|
+
if response.code == "200"
|
404
|
+
result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'forcearray' => false)
|
405
|
+
return result["upload"]["id"]
|
406
|
+
else
|
407
|
+
raise "Could not store file: #{response.message} (#{response.code})"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def typecast_value(value)
|
412
|
+
case value
|
413
|
+
when Hash
|
414
|
+
if value.has_key?("__content__")
|
415
|
+
content = translate_entities(value["__content__"]).strip
|
416
|
+
case value["type"]
|
417
|
+
when "integer" then content.to_i
|
418
|
+
when "boolean" then content == "true"
|
419
|
+
when "datetime" then Time.parse(content)
|
420
|
+
when "date" then Date.parse(content)
|
421
|
+
else content
|
422
|
+
end
|
423
|
+
# a special case to work-around a bug in XmlSimple. When you have an empty
|
424
|
+
# tag that has an attribute, XmlSimple will not add the __content__ key
|
425
|
+
# to the returned hash. Thus, we check for the presense of the 'type'
|
426
|
+
# attribute to look for empty, typed tags, and simply return nil for
|
427
|
+
# their value.
|
428
|
+
elsif value.keys == %w(type)
|
429
|
+
nil
|
430
|
+
elsif value["nil"] == "true"
|
431
|
+
nil
|
432
|
+
# another special case, introduced by the latest rails, where an array
|
433
|
+
# type now exists. This is parsed by XmlSimple as a two-key hash, where
|
434
|
+
# one key is 'type' and the other is the actual array value.
|
435
|
+
elsif value.keys.length == 2 && value["type"] == "array"
|
436
|
+
value.delete("type")
|
437
|
+
typecast_value(value)
|
438
|
+
else
|
439
|
+
value.empty? ? nil : value.inject({}) do |h,(k,v)|
|
440
|
+
h[k] = typecast_value(v)
|
441
|
+
h
|
442
|
+
end
|
443
|
+
end
|
444
|
+
when Array
|
445
|
+
value.map! { |i| typecast_value(i) }
|
446
|
+
case value.length
|
447
|
+
when 0 then nil
|
448
|
+
when 1 then value.first
|
449
|
+
else value
|
450
|
+
end
|
451
|
+
else
|
452
|
+
raise "can't typecast #{value.inspect}"
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def translate_entities(value)
|
457
|
+
value.gsub(/</, "<").
|
458
|
+
gsub(/>/, ">").
|
459
|
+
gsub(/"/, '"').
|
460
|
+
gsub(/'/, "'").
|
461
|
+
gsub(/&/, "&")
|
462
|
+
end
|
463
|
+
|
464
|
+
def prepare_attachments(list)
|
465
|
+
(list || []).each do |data|
|
466
|
+
upload = data[:file]
|
467
|
+
id = store_file(upload.content)
|
468
|
+
data[:file] = { :file => id,
|
469
|
+
:content_type => "application/octet-stream",
|
470
|
+
:original_filename => upload.filename }
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# A minor hack to let Xml-Simple serialize symbolic keys in hashes
|
476
|
+
class Symbol
|
477
|
+
def [](*args)
|
478
|
+
to_s[*args]
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
class Hash
|
483
|
+
def to_xml
|
484
|
+
XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
|
485
|
+
end
|
486
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# A class to update basecamp after a successful deployment
|
2
|
+
require 'basecamp'
|
3
|
+
class BasecampNotifier
|
4
|
+
|
5
|
+
def initialize(application, rails_env, options = {}, current_rev = 0, rev_log = '')
|
6
|
+
|
7
|
+
if keys_missing?(options)
|
8
|
+
raise StandardError, 'You have not supplied all arguments in the hash (:username, :password, :domain, :category_id, :project_id)'
|
9
|
+
end
|
10
|
+
|
11
|
+
@project_id = options[:project_id]
|
12
|
+
@category_id = options[:category_id]
|
13
|
+
@current_revision = current_rev
|
14
|
+
@application = application
|
15
|
+
@rails_env = rails_env
|
16
|
+
@revision_log = rev_log
|
17
|
+
@basecamp = Basecamp.new(options[:domain], options[:username], options[:password], true)
|
18
|
+
end
|
19
|
+
|
20
|
+
def keys_missing?(options)
|
21
|
+
[:domain, :username, :password, :category_id, :project_id].each do |key|
|
22
|
+
return true unless options.has_key?(key)
|
23
|
+
end
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def notify
|
28
|
+
message = {:title => "#{@application} rev. #{@current_revision} deployed to #{@rails_env}",
|
29
|
+
:body => "#{@application} has been successfully deployed to #{@rails_env} <br /> #{@revision_log}",
|
30
|
+
:category_id => @category_id}
|
31
|
+
@basecamp.post_message(@project_id, message)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/recipes/apache.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
namespace :apache do
|
2
|
+
|
3
|
+
desc "Setup passenger vhost"
|
4
|
+
task :passenger_vhost do
|
5
|
+
logger.info "generating .conf file"
|
6
|
+
logger.info "placing #{application}.conf on remote server"
|
7
|
+
apache_conf = "/etc/apache2/sites-available/#{application}"
|
8
|
+
put render("passenger_vhost", binding), "#{application}.conf"
|
9
|
+
sudo "mv #{application}.conf #{apache_conf}"
|
10
|
+
sudo "a2ensite #{application}"
|
11
|
+
sudo "/etc/init.d/apache2 reload"
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
desc "PHP Vhost Setup"
|
16
|
+
task :php_vhost do
|
17
|
+
|
18
|
+
unless defined? apache_port
|
19
|
+
set :apache_port, '80'
|
20
|
+
end
|
21
|
+
|
22
|
+
logger.info "generating .conf file"
|
23
|
+
logger.info "placing #{application}.conf on remote server"
|
24
|
+
apache_conf = "/etc/apache2/sites-available/#{application}"
|
25
|
+
put render("php_vhost", binding), "#{application}.conf"
|
26
|
+
sudo "mv #{application}.conf #{apache_conf}"
|
27
|
+
sudo "a2ensite #{application}"
|
28
|
+
sudo "/etc/init.d/apache2 reload"
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
desc "enable php"
|
33
|
+
task :enable_php do
|
34
|
+
put render("php_handler", binding), "phphandler.conf"
|
35
|
+
apache_handler = "/etc/apache2/conf.d/phphandler"
|
36
|
+
sudo "mv phphandler.conf #{apache_handler}"
|
37
|
+
sudo "/etc/init.d/apache2 force-reload"
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
end
|