poise-application-python 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.kitchen.travis.yml +9 -0
  4. data/.kitchen.yml +10 -0
  5. data/.travis.yml +20 -0
  6. data/.yardopts +3 -0
  7. data/Berksfile +35 -0
  8. data/CHANGELOG.md +71 -0
  9. data/Gemfile +37 -0
  10. data/LICENSE +201 -0
  11. data/README.md +334 -0
  12. data/Rakefile +17 -0
  13. data/SUPPORTERS.md +81 -0
  14. data/chef/templates/celeryconfig.py.erb +5 -0
  15. data/chef/templates/settings.py.erb +13 -0
  16. data/lib/poise_application_python.rb +23 -0
  17. data/lib/poise_application_python/app_mixin.rb +67 -0
  18. data/lib/poise_application_python/cheftie.rb +17 -0
  19. data/lib/poise_application_python/error.rb +25 -0
  20. data/lib/poise_application_python/resources.rb +26 -0
  21. data/lib/poise_application_python/resources/celery_beat.rb +43 -0
  22. data/lib/poise_application_python/resources/celery_config.rb +109 -0
  23. data/lib/poise_application_python/resources/celery_worker.rb +77 -0
  24. data/lib/poise_application_python/resources/django.rb +355 -0
  25. data/lib/poise_application_python/resources/gunicorn.rb +127 -0
  26. data/lib/poise_application_python/resources/pip_requirements.rb +47 -0
  27. data/lib/poise_application_python/resources/python.rb +57 -0
  28. data/lib/poise_application_python/resources/python_execute.rb +89 -0
  29. data/lib/poise_application_python/resources/python_package.rb +62 -0
  30. data/lib/poise_application_python/resources/virtualenv.rb +75 -0
  31. data/lib/poise_application_python/service_mixin.rb +57 -0
  32. data/lib/poise_application_python/version.rb +19 -0
  33. data/poise-application-python.gemspec +45 -0
  34. data/test/cookbooks/application_python_test/attributes/default.rb +17 -0
  35. data/test/cookbooks/application_python_test/metadata.rb +20 -0
  36. data/test/cookbooks/application_python_test/recipes/default.rb +83 -0
  37. data/test/cookbooks/application_python_test/recipes/django.rb +32 -0
  38. data/test/cookbooks/application_python_test/recipes/flask.rb +25 -0
  39. data/test/gemfiles/chef-12.gemfile +19 -0
  40. data/test/gemfiles/master.gemfile +27 -0
  41. data/test/integration/default/serverspec/default_spec.rb +81 -0
  42. data/test/integration/default/serverspec/django_spec.rb +56 -0
  43. data/test/integration/default/serverspec/flask_spec.rb +39 -0
  44. data/test/spec/app_mixin_spec.rb +69 -0
  45. data/test/spec/resources/celery_config_spec.rb +58 -0
  46. data/test/spec/resources/django_spec.rb +303 -0
  47. data/test/spec/resources/gunicorn_spec.rb +96 -0
  48. data/test/spec/resources/python_execute_spec.rb +46 -0
  49. data/test/spec/resources/python_spec.rb +44 -0
  50. data/test/spec/resources/virtualenv_spec.rb +44 -0
  51. data/test/spec/spec_helper.rb +19 -0
  52. metadata +216 -0
@@ -0,0 +1,355 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'uri'
18
+
19
+ require 'chef/provider'
20
+ require 'chef/resource'
21
+ require 'poise'
22
+ require 'poise_application'
23
+ require 'poise_python'
24
+
25
+ require 'poise_application_python/app_mixin'
26
+ require 'poise_application_python/error'
27
+
28
+
29
+ module PoiseApplicationPython
30
+ module Resources
31
+ # (see Django::Resource)
32
+ # @since 4.0.0
33
+ module Django
34
+ # Aliases for Django database engine names. Based on https://github.com/kennethreitz/dj-database-url/blob/master/dj_database_url.py
35
+ # Copyright 2014, Kenneth Reitz.
36
+ ENGINE_ALIASES = {
37
+ 'postgres' => 'django.db.backends.postgresql_psycopg2',
38
+ 'postgresql' => 'django.db.backends.postgresql_psycopg2',
39
+ 'pgsql' => 'django.db.backends.postgresql_psycopg2',
40
+ 'postgis' => 'django.contrib.gis.db.backends.postgis',
41
+ 'mysql2' => 'django.db.backends.mysql',
42
+ 'mysqlgis' => 'django.contrib.gis.db.backends.mysql',
43
+ 'spatialite' => 'django.contrib.gis.db.backends.spatialite',
44
+ 'sqlite' => 'django.db.backends.sqlite3',
45
+ }
46
+
47
+ # An `application_django` resource to configure Django applications.
48
+ #
49
+ # @since 4.0.0
50
+ # @provides application_django
51
+ # @action deploy
52
+ # @example
53
+ # application '/srv/myapp' do
54
+ # git '...'
55
+ # pip_requirements
56
+ # django do
57
+ # database do
58
+ # host node['db_host']
59
+ # end
60
+ # end
61
+ # gunicorn do
62
+ # port 8080
63
+ # end
64
+ # end
65
+ class Resource < Chef::Resource
66
+ include PoiseApplicationPython::AppMixin
67
+ provides(:application_django)
68
+ actions(:deploy)
69
+
70
+ # @!attribute allowed_hosts
71
+ # Value for `ALLOWED_HOSTS` in the Django settings.
72
+ # @return [String, Array<String>]
73
+ attribute(:allowed_hosts, kind_of: [String, Array], default: lazy { [] })
74
+ # @!attribute collectstatic
75
+ # Set to false to disable running manage.py collectstatic during
76
+ # deployment.
77
+ # @todo This could auto-detect based on config vars in settings?
78
+ # @return [Boolean]
79
+ attribute(:collectstatic, equal_to: [true, false], default: true)
80
+ # @!attribute database
81
+ # Option collector attribute for Django database configuration.
82
+ # @return [Hash]
83
+ # @example Setting via block
84
+ # database do
85
+ # engine 'postgresql'
86
+ # database 'blog'
87
+ # end
88
+ # @example Setting via URL
89
+ # database 'postgresql://localhost/blog'
90
+ attribute(:database, option_collector: true, parser: :parse_database_url, forced_keys: %i{name})
91
+ # @!attribute debug
92
+ # Enable debug mode for Django.
93
+ # @note
94
+ # If you use this in production you will get everything you deserve.
95
+ # @return [Boolean]
96
+ attribute(:debug, equal_to: [true, false], default: false)
97
+ # @!attribute group
98
+ # Owner for the Django application, defaults to application group.
99
+ # @return [String]
100
+ attribute(:group, kind_of: String, default: lazy { parent && parent.group })
101
+ # @!attribute local_settings
102
+ # Template content attribute for the contents of local_settings.py.
103
+ # @todo Redo this doc to cover the actual attributes created.
104
+ # @return [Poise::Helpers::TemplateContent]
105
+ attribute(:local_settings, template: true, default_source: 'settings.py.erb', default_options: lazy { default_local_settings_options })
106
+ # @!attribute local_settings_path
107
+ # Path to write local settings to. If given as a relative path,
108
+ # will be expanded against {#path}. Set to false to disable writing
109
+ # local settings. Defaults to local_settings.py next to
110
+ # {#setting_module}.
111
+ # @return [String, nil false]
112
+ attribute(:local_settings_path, kind_of: [String, NilClass, FalseClass], default: lazy { default_local_settings_path })
113
+ # @!attribute migrate
114
+ # Run database migrations. This is a bad idea for real apps. Please
115
+ # do not use it.
116
+ # @return [Boolean]
117
+ attribute(:migrate, equal_to: [true, false], default: false)
118
+ # @!attribute manage_path
119
+ # Path to manage.py. Defaults to scanning for the nearest manage.py
120
+ # to {#path}.
121
+ # @return [String]
122
+ attribute(:manage_path, kind_of: String, default: lazy { default_manage_path })
123
+ # @!attribute owner
124
+ # Owner for the Django application, defaults to application owner.
125
+ # @return [String]
126
+ attribute(:owner, kind_of: String, default: lazy { parent && parent.owner })
127
+ # @!attribute secret_key
128
+ # Value for `SECRET_KEY` in the Django settings. If unset, no key is
129
+ # added to the local settings.
130
+ # @return [String, false]
131
+ attribute(:secret_key, kind_of: [String, FalseClass])
132
+ # @!attribute settings_module
133
+ # Django settings module in dotted notation. Set to false to disable
134
+ # anything related to settings. Defaults to scanning for the nearest
135
+ # settings.py to {#path}.
136
+ # @return [Boolean]
137
+ attribute(:settings_module, kind_of: [String, NilClass, FalseClass], default: lazy { default_settings_module })
138
+ # @!attribute syncdb
139
+ # Run database sync. This is a bad idea for real apps. Please do not
140
+ # use it.
141
+ # @return [Boolean]
142
+ attribute(:syncdb, equal_to: [true, false], default: false)
143
+ # @!attribute wsgi_module
144
+ # WSGI application module in dotted notation. Set to false to disable
145
+ # anything related to WSGI. Defaults to scanning for the nearest
146
+ # wsgi.py to {#path}.
147
+ # @return [Boolean]
148
+ attribute(:wsgi_module, kind_of: [String, NilClass, FalseClass], default: lazy { default_wsgi_module })
149
+
150
+ private
151
+
152
+ # Default value for {#local_settings_options}. Adds Django settings data
153
+ # from the resource to be rendered in the local settings template.
154
+ #
155
+ # @return [Hash]
156
+ def default_local_settings_options
157
+ {}.tap do |options|
158
+ options[:allowed_hosts] = Array(allowed_hosts)
159
+ options[:databases] = {}
160
+ options[:databases]['default'] = database.inject({}) do |memo, (key, value)|
161
+ key = key.to_s.upcase
162
+ # Deal with engine aliases here too, just in case.
163
+ value = resolve_engine(value) if key == 'ENGINE'
164
+ memo[key] = value
165
+ memo
166
+ end
167
+ options[:debug] = debug
168
+ options[:secret_key] = secret_key
169
+ end
170
+ end
171
+
172
+ # Default value for {#local_settings_path}, local_settings.py next to
173
+ # the configured {#settings_module}.
174
+ #
175
+ # @return [String, nil]
176
+ def default_local_settings_path
177
+ # If no settings module, no default local settings.
178
+ return unless settings_module
179
+ settings_path = PoisePython::Utils.module_to_path(settings_module, path)
180
+ ::File.expand_path(::File.join('..', 'local_settings.py'), settings_path)
181
+ end
182
+
183
+ # Default value for {#manage_path}, searches for manage.py in the
184
+ # application path.
185
+ #
186
+ # @return [String, nil]
187
+ def default_manage_path
188
+ find_file('manage.py')
189
+ end
190
+
191
+ # Default value for {#settings_module}, searches for settings.py in the
192
+ # application path.
193
+ #
194
+ # @return [String, nil]
195
+ def default_settings_module
196
+ settings_path = find_file('settings.py')
197
+ if settings_path
198
+ PoisePython::Utils.path_to_module(settings_path, path)
199
+ else
200
+ nil
201
+ end
202
+ end
203
+
204
+ # Default value for {#wsgi_module}, searchs for wsgi.py in the
205
+ # application path.
206
+ #
207
+ # @return [String, nil]
208
+ def default_wsgi_module
209
+ wsgi_path = find_file('wsgi.py')
210
+ if wsgi_path
211
+ PoisePython::Utils.path_to_module(wsgi_path, path)
212
+ else
213
+ nil
214
+ end
215
+ end
216
+
217
+ # Format a URL for DATABASES.
218
+ #
219
+ # @return [Hash]
220
+ def parse_database_url(url)
221
+ parsed = URI(url)
222
+ {}.tap do |db|
223
+ # Store this for use later in #set_state, and maybe future use by
224
+ # Django in some magic world where operability happens.
225
+ db[:URL] = url
226
+ db[:ENGINE] = resolve_engine(parsed.scheme)
227
+ # Strip the leading /.
228
+ path = parsed.path ? parsed.path[1..-1] : parsed.path
229
+ # If we are using SQLite, make it an absolute path.
230
+ path = ::File.expand_path(path, self.path) if db[:ENGINE].include?('sqlite')
231
+ db[:NAME] = path if path && !path.empty?
232
+ db[:USER] = parsed.user if parsed.user && !parsed.user.empty?
233
+ db[:PASSWORD] = parsed.password if parsed.password && !parsed.password.empty?
234
+ db[:HOST] = parsed.host if parsed.host && !parsed.host.empty?
235
+ db[:PORT] = parsed.port if parsed.port && !parsed.port.empty?
236
+ end
237
+ end
238
+
239
+ # Search for a file somewhere under the application path. Prefers files
240
+ # closer to the root, then sort alphabetically for stability.
241
+ #
242
+ # @param name [String] Filename to search for.
243
+ # @return [String, nil]
244
+ def find_file(name)
245
+ num_separators = lambda do |path|
246
+ if ::File::ALT_SEPARATOR && path.include?(::File::ALT_SEPARATOR)
247
+ # :nocov:
248
+ path.count(::File::ALT_SEPARATOR)
249
+ # :nocov:
250
+ else
251
+ path.count(::File::SEPARATOR)
252
+ end
253
+ end
254
+ Dir[::File.join(path, '**', name)].min do |a, b|
255
+ cmp = num_separators.call(a) <=> num_separators.call(b)
256
+ if cmp == 0
257
+ cmp = a <=> b
258
+ end
259
+ cmp
260
+ end
261
+ end
262
+
263
+ # Resolve Django database engine from shortname to dotted module.
264
+ #
265
+ # @param name [String, nil] Engine name.
266
+ # @return [String, nil]
267
+ def resolve_engine(name)
268
+ if name && !name.empty? && !name.include?('.')
269
+ ENGINE_ALIASES[name] || "django.db.backends.#{name}"
270
+ else
271
+ name
272
+ end
273
+ end
274
+
275
+ end
276
+
277
+ # Provider for `application_django`.
278
+ #
279
+ # @since 4.0.0
280
+ # @see Resource
281
+ # @provides application_django
282
+ class Provider < Chef::Provider
283
+ include PoiseApplicationPython::AppMixin
284
+ provides(:application_django)
285
+
286
+ # `deploy` action for `application_django`. Ensure all configuration
287
+ # files are created and other deploy tasks resolved.
288
+ #
289
+ # @return [void]
290
+ def action_deploy
291
+ set_state
292
+ notifying_block do
293
+ write_config
294
+ run_syncdb
295
+ run_migrate
296
+ run_collectstatic
297
+ end
298
+ end
299
+
300
+ private
301
+
302
+ # Set app_state variables for future services et al.
303
+ def set_state
304
+ # Set environment variables for later services.
305
+ new_resource.app_state_environment[:DJANGO_SETTINGS_MODULE] = new_resource.settings_module if new_resource.settings_module
306
+ new_resource.app_state_environment[:DATABASE_URL] = new_resource.database[:URL] if new_resource.database[:URL]
307
+ # Set the app module.
308
+ new_resource.app_state[:python_wsgi_module] = new_resource.wsgi_module if new_resource.wsgi_module
309
+ end
310
+
311
+ # Create the database using the older syncdb command.
312
+ def run_syncdb
313
+ manage_py_execute('syncdb', '--noinput') if new_resource.syncdb
314
+ end
315
+
316
+ # Create the database using the newer migrate command. This should work
317
+ # for either South or the built-in migrations support.
318
+ def run_migrate
319
+ manage_py_execute('migrate', '--noinput') if new_resource.migrate
320
+ end
321
+
322
+ # Run the asset pipeline.
323
+ def run_collectstatic
324
+ manage_py_execute('collectstatic', '--noinput') if new_resource.collectstatic
325
+ end
326
+
327
+ # Create the local config settings.
328
+ def write_config
329
+ # Allow disabling the local settings.
330
+ return unless new_resource.local_settings_path
331
+ file new_resource.local_settings_path do
332
+ content new_resource.local_settings_content
333
+ mode '640'
334
+ owner new_resource.owner
335
+ group new_resource.group
336
+ end
337
+ end
338
+
339
+ # Run a manage.py command using `python_execute`.
340
+ def manage_py_execute(*cmd)
341
+ raise PoiseApplicationPython::Error.new("Unable to find a find a manage.py for #{new_resource}, please set manage_path") unless new_resource.manage_path
342
+ python_execute "manage.py #{cmd.join(' ')}" do
343
+ python_from_parent new_resource
344
+ command [::File.expand_path(new_resource.manage_path, new_resource.path)] + cmd
345
+ cwd new_resource.path
346
+ environment new_resource.app_state_environment
347
+ group new_resource.group
348
+ user new_resource.owner
349
+ end
350
+ end
351
+
352
+ end
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,127 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'shellwords'
18
+
19
+ require 'chef/provider'
20
+ require 'chef/resource'
21
+ require 'poise'
22
+
23
+ require 'poise_application_python/service_mixin'
24
+
25
+
26
+ module PoiseApplicationPython
27
+ module Resources
28
+ # (see Gunicorn::Resource)
29
+ # @since 4.0.0
30
+ module Gunicorn
31
+ class Resource < Chef::Resource
32
+ include PoiseApplicationPython::ServiceMixin
33
+ provides(:application_gunicorn)
34
+
35
+ attribute(:app_module, kind_of: [String, NilClass], default: lazy { default_app_module })
36
+ attribute(:bind, kind_of: [String, Array], default: '0.0.0.0:80')
37
+ attribute(:config, kind_of: [String, NilClass])
38
+ attribute(:preload_app, equal_to: [true, false], default: false)
39
+ attribute(:version, kind_of: [String, TrueClass, FalseClass], default: true)
40
+
41
+ # Helper to set {#bind} with just a port number.
42
+ #
43
+ # @param val [String, Integer] Port number to use.
44
+ # @return [void]
45
+ def port(val)
46
+ bind("0.0.0.0:#{val}")
47
+ end
48
+
49
+ private
50
+
51
+ # Compute the default application module to pass to gunicorn. This
52
+ # checks the app state and then looks for commonly used filenames.
53
+ # Raises an exception if no default can be found.
54
+ #
55
+ # @return [String]
56
+ def default_app_module
57
+ # If set in app_state, use that.
58
+ return app_state[:python_wsgi_module] if app_state[:python_wsgi_module]
59
+ files = Dir.exist?(path) ? Dir.entries(path) : []
60
+ # Try to find a known filename.
61
+ candidate_file = %w{wsgi.py main.py app.py application.py}.find {|file| files.include?(file) }
62
+ # Try the first Python file. Do I really want this?
63
+ candidate_file ||= files.find {|file| file.end_with?('.py') }
64
+ if candidate_file
65
+ ::File.basename(candidate_file, '.py')
66
+ else
67
+ nil
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ class Provider < Chef::Provider
74
+ include PoiseApplicationPython::ServiceMixin
75
+ provides(:application_gunicorn)
76
+
77
+ def action_enable
78
+ notifying_block do
79
+ install_gunicorn
80
+ end
81
+ super
82
+ end
83
+
84
+ private
85
+
86
+ def install_gunicorn
87
+ return unless new_resource.version
88
+ python_package 'gunicorn' do
89
+ python_from_parent new_resource
90
+ version new_resource.version if new_resource.version.is_a?(String)
91
+ end
92
+ end
93
+
94
+ def gunicorn_command_options
95
+ # Based on http://docs.gunicorn.org/en/latest/settings.html
96
+ [].tap do |cmd|
97
+ # What options are common enough to deal with here?
98
+ # %w{config backlog workers worker_class threads worker_connections timeout graceful_timeout keepalive}.each do |opt|
99
+ %w{config}.each do |opt|
100
+ val = new_resource.send(opt)
101
+ if val && !(val.respond_to?(:empty?) && val.empty?)
102
+ cmd_opt = opt.gsub(/_/, '-')
103
+ cmd << "--#{cmd_opt} #{Shellwords.escape(val)}"
104
+ end
105
+ end
106
+ # Can be given multiple times.
107
+ Array(new_resource.bind).each do |bind|
108
+ cmd << "--bind #{bind}" if bind
109
+ end
110
+ # --preload doesn't take an argument and the name doesn't match.
111
+ if new_resource.preload_app
112
+ cmd << '--preload'
113
+ end
114
+ end
115
+ end
116
+
117
+ # (see PoiseApplication::ServiceMixin#service_options)
118
+ def service_options(resource)
119
+ super
120
+ raise PoiseApplicationPython::Error.new("Unable to determine app module for #{new_resource}") unless new_resource.app_module
121
+ resource.command("#{new_resource.python} -m gunicorn.app.wsgiapp #{gunicorn_command_options.join(' ')} #{new_resource.app_module}")
122
+ end
123
+
124
+ end
125
+ end
126
+ end
127
+ end