poise-application-python 4.0.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.
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