Merge lp:~ptressel/sahana-eden/devel2 into lp:sahana-eden
- devel2
- Merge into trunk
Proposed by
Pat Tressel
Status: | Merged |
---|---|
Merged at revision: | 3023 |
Proposed branch: | lp:~ptressel/sahana-eden/devel2 |
Merge into: | lp:sahana-eden |
Diff against target: |
440 lines (+310/-58) 5 files modified
.bzrignore (+1/-0) deployment-templates/models/000_config.py (+5/-2) models/000_1st_run.py (+123/-52) models/00_settings.py (+1/-4) private/update_check/eden_update_check.py (+180/-0) |
To merge this branch: | bzr merge lp:~ptressel/sahana-eden/devel2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Fran Boon | Approve | ||
Review via email: mp+85119@code.launchpad.net |
Commit message
Description of the change
Here's an updated update check. Cleanup plus some functional changes: Any template file can have an "edited" flag. (Non-Python files can put these in comments.) Adds "version" flags for template files, so it can tell when the template is ahead of the in-use file. Allows triggering an update check with an url query.
To post a comment you must log in.
Revision history for this message
Pat Tressel (ptressel) wrote : | # |
Revision history for this message
Fran Boon (flavour) wrote : | # |
I have merged in most of these elements, thanks.
So far I have left out:
* 000_config VERSION check
- since currently this will break all existing installs without any current reason to do so (we can introduce this if/when that file has incompatible/
* update_check controller function (I don't see the usecase for it yet)
I also trimmed the comments.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2011-12-06 16:57:57 +0000 |
3 | +++ .bzrignore 2011-12-09 12:27:24 +0000 |
4 | @@ -9,6 +9,7 @@ |
5 | databases/* |
6 | errors/* |
7 | models/000_config.py |
8 | +models/0000_update_check.py |
9 | sessions/* |
10 | static/scripts/tools/httpserver.log |
11 | uploads/survey/translations/* |
12 | |
13 | === modified file 'deployment-templates/models/000_config.py' |
14 | --- deployment-templates/models/000_config.py 2011-11-30 17:15:49 +0000 |
15 | +++ deployment-templates/models/000_config.py 2011-12-09 12:27:24 +0000 |
16 | @@ -13,8 +13,11 @@ |
17 | |
18 | # Remind admin to edit this file |
19 | FINISHED_EDITING_CONFIG_FILE = False # change to True after you finish editing this file |
20 | -if not FINISHED_EDITING_CONFIG_FILE: |
21 | - raise HTTP(501, body="Please edit models/000_config.py first") |
22 | + |
23 | +# This value will be changed when a new version is supplied. This will be used |
24 | +# to alert the admin that there are changes to merge in to the site's |
25 | +# customized copy of the file. |
26 | +VERSION_CONFIG = 1 |
27 | |
28 | # Database settings |
29 | deployment_settings.database.db_type = "sqlite" |
30 | |
31 | === modified file 'models/000_1st_run.py' |
32 | --- models/000_1st_run.py 2011-11-27 17:45:11 +0000 |
33 | +++ models/000_1st_run.py 2011-12-09 12:27:24 +0000 |
34 | @@ -2,6 +2,7 @@ |
35 | """ |
36 | 1st RUN: |
37 | |
38 | + - Run update check if needed. |
39 | - Import the S3 Framework Extensions |
40 | - If needed, copy deployment specific templates to the live installation. |
41 | Developers: note that the templates are version-controlled, while their |
42 | @@ -10,6 +11,128 @@ |
43 | If you add something new to these files, you should also |
44 | make the change at deployment-templates and commit it. |
45 | """ |
46 | +# ----------------------------------------------------------------------------- |
47 | + |
48 | +# Perform update checks: These are tests for proper configuration that are run |
49 | +# when the need is indicated by incrementing the CURRENT_UPDATE_CHECK_ID. |
50 | +# The previously checked id is written into the update check "canary" file, |
51 | +# 0000_update_check.py. If the file doesn't exist, or the ids don't match, the |
52 | +# update checks are run. Checks can also be run by doing a query with |
53 | +# "update_check" as the controller, e.g.: http://eden.example.com/update_check |
54 | +# In this case, after the check completes the controller will be set to "index". |
55 | +# |
56 | +# @ToDo: Scrape all the how-to and what's-this comments, plus ToDos, into a |
57 | +# wiki page, then reference that here. |
58 | +# @ToDo: If the deployment includes their own add-on modules, they may need an |
59 | +# independent update check id -- consider how that could be provided (e.g. |
60 | +# a 0000_site_update_check. |
61 | +# @ToDo: On a heavily used site, it is possible that more than one request will |
62 | +# find a mismatch in the update check id, and run the checks. If this is a |
63 | +# concern, consider adding an interlock and flag for a fatal result, e.g. |
64 | +# written into the canary file. On the other hand, if there is a fatal error, |
65 | +# the site admins should disable the site and fix the problem, so multiple |
66 | +# complaints of 500 errors might yield faster action. |
67 | + |
68 | +CURRENT_UPDATE_CHECK_ID = 1 |
69 | +update_check_needed = False |
70 | +if request.controller == "update_check": |
71 | + update_check_needed = True |
72 | +else: |
73 | + try: |
74 | + if CANARY_UPDATE_CHECK_ID != CURRENT_UPDATE_CHECK_ID: |
75 | + update_check_needed = True |
76 | + except NameError: |
77 | + update_check_needed = True |
78 | +if update_check_needed: |
79 | + # Run update checks -- these are the update_check() functions from each |
80 | + # Python file in private/update_check that has such a function. |
81 | + # |
82 | + # These functions will have the Web2py environment available, but will run |
83 | + # before any site configuration has been read or database opened. If either |
84 | + # are needed for a check, the function can read 000_config.py, or open the |
85 | + # database -- we are not concerned about speed at this point, as this is a |
86 | + # "once"-only event after an update (for some value of "once" -- see ToDo |
87 | + # above), and it's likely the sysadmin getting hit. |
88 | + # @ToDo: To facilitate doing a database connectivity check, we might package |
89 | + # the code in 00_db.py that opens the database into a function. |
90 | + # |
91 | + # update_check functions will receive the current globals (mainly those |
92 | + # supplied by Web2py) as their sole argument. They should return a dict with |
93 | + # either or both of these key / value pairs: |
94 | + # {"error_messages": [...], "warning_messages": [...]} |
95 | + # where the values are lists of message strings. |
96 | + # |
97 | + # Errors, reported in the error_messages list, are conditions that would |
98 | + # prevent Eden from running, cause damage to data, or indicate configuration |
99 | + # is incomplete. If any errors are reported, execution will end with a 500 |
100 | + # error. The supplied error messages are sent with the 500 error and printed |
101 | + # on the console. Each message will be prefixed with ERROR:. |
102 | + # |
103 | + # Warnings, reported in the warning_messages list, do not stop execution, |
104 | + # and are only printed on the console. Each message will be prefixed with |
105 | + # WARNING:. |
106 | + # @ToDo: The only material reason for reporting warnings in the update check |
107 | + # is to get them all out in one batch. Otherwise, we might as well just let |
108 | + # them be reported in place where the missing feature is wanted. Since |
109 | + # warnings are not fatal and do not force the site to update, unlike errors, |
110 | + # tests for their features must be left in the code where they're used. We |
111 | + # could remove the warning messages there, though. |
112 | + import os |
113 | + from gluon.fileutils import listdir |
114 | + update_check_path_parts = [ |
115 | + "applications", request.application, "private", "update_check"] |
116 | + update_check_path = os.path.join(*update_check_path_parts) |
117 | + update_check_import_path = ".".join(update_check_path_parts) |
118 | + errors = [] |
119 | + warnings = [] |
120 | + # Supply the current (Web2py) environment. Pick out only the items that are |
121 | + # safe for the check functions to combine with their own environments, i.e. |
122 | + # not anything of the form __x__. |
123 | + environment = dict((k, v) for (k, v) in globals().iteritems() if not k.startswith("__")) |
124 | + for filename in listdir(update_check_path, expression = ".*\.py$"): |
125 | + try: |
126 | + exec "from %s.%s import update_check" % \ |
127 | + (update_check_import_path, filename[0:-3]) |
128 | + except ImportError: |
129 | + continue |
130 | + messages = update_check(environment) |
131 | + errors.extend(messages.get("error_messages", [])) |
132 | + warnings.extend(messages.get("warning_messages", [])) |
133 | + |
134 | + # Temporary catch-all check for dependency errors. This does not satisfy |
135 | + # the goal of calling out all the setup errors at once -- it will die on |
136 | + # the first fatal error encountered. |
137 | + try: |
138 | + import s3 as s3base |
139 | + except Exception, errmsg: |
140 | + errors.extend(errmsg) |
141 | + |
142 | + # Report (non-fatal) warnings. |
143 | + if warnings: |
144 | + prefix = "\n" + T("WARNING: ") |
145 | + msg = prefix + prefix.join(warnings) |
146 | + import sys |
147 | + print >> sys.stderr, msg |
148 | + # Report errors and stop. |
149 | + if errors: |
150 | + prefix = "\n" + T("ERROR: ") |
151 | + msg = prefix + prefix.join(errors) |
152 | + import sys |
153 | + print >> sys.stderr, msg |
154 | + raise HTTP(500, body=msg) |
155 | + |
156 | + # If we are still here, create or update the canary file. |
157 | + from gluon import portalocker |
158 | + canary = open( |
159 | + "applications/%s/models/0000_update_check.py" % request.application, "w") |
160 | + portalocker.lock(canary, portalocker.LOCK_EX) |
161 | + statement = "CANARY_UPDATE_CHECK_ID = %s" % CURRENT_UPDATE_CHECK_ID |
162 | + canary.write(statement) |
163 | + canary.close() |
164 | + if request.controller == "update_check": |
165 | + redirect(URL(c="default", f="index")) |
166 | + |
167 | +# ----------------------------------------------------------------------------- |
168 | import os |
169 | from gluon import current |
170 | from gluon.storage import Storage |
171 | @@ -19,64 +142,12 @@ |
172 | |
173 | # Import the S3 Framework |
174 | import s3 as s3base |
175 | - |
176 | -# ----------------------------------------------------------------------------- |
177 | # Keep all S3 framework-level elements stored in response.s3, so as to avoid |
178 | # polluting global namespace & to make it clear which part of the framework is |
179 | # being interacted with. |
180 | # Avoid using this where a method parameter could be used: http://en.wikipedia.org/wiki/Anti_pattern#Programming_anti-patterns |
181 | response.s3 = Storage() |
182 | s3 = response.s3 |
183 | - |
184 | -# Minimum usable Web2py version: If Eden depends on a feature in Web2py that |
185 | -# only becomes available with a specific Web2py version, copy the text from |
186 | -# that Web2py's VERSION file here. |
187 | -s3.web2py_minimum_version = "Version 1.99.2 (2011-09-26 00:51:34) stable" |
188 | -# Check that the installed Web2py is sufficiently recent. The datetime string, |
189 | -# which changes with individual commits, is finer-grained than the released version. |
190 | -web2py_version_ok = True |
191 | -try: |
192 | - from gluon.fileutils import parse_version |
193 | -except ImportError: |
194 | - web2py_version_ok = False |
195 | -if web2py_version_ok: |
196 | - web2py_minimum_datetime = parse_version(s3.web2py_minimum_version)[3] |
197 | - web2py_installed_datetime = request.global_settings.web2py_version[3] |
198 | - web2py_version_ok = web2py_installed_datetime >= web2py_minimum_datetime |
199 | -if not web2py_version_ok: |
200 | - error = "ERROR: The installed version of Web2py is too old for the installed version of Eden.\nPlease upgrade Web2py to at least version: %s" % \ |
201 | - s3.web2py_minimum_version |
202 | - import sys |
203 | - print >> sys.stderr, error |
204 | - raise HTTP(501, body=error) |
205 | - |
206 | -# ----------------------------------------------------------------------------- |
207 | -template_src = os.path.join("applications", request.application, "deployment-templates") |
208 | -template_dst = os.path.join("applications", request.application) |
209 | - |
210 | -template_files = ( |
211 | - "models/000_config.py", |
212 | - # Deprecated by Scheduler |
213 | - #"cron/crontab" |
214 | -) |
215 | - |
216 | -copied_from_template = [] |
217 | - |
218 | -for t in template_files: |
219 | - dst_path = os.path.join(template_dst, t) |
220 | - try: |
221 | - os.stat(dst_path) |
222 | - except OSError: |
223 | - # not found, copy from template |
224 | - import shutil |
225 | - shutil.copy(os.path.join(template_src, t), dst_path) |
226 | - copied_from_template.append(t) |
227 | - |
228 | -if copied_from_template: |
229 | - raise HTTP(501, body="The following files were copied from templates and should be edited: %s" % |
230 | - ", ".join(copied_from_template)) |
231 | - |
232 | -# ----------------------------------------------------------------------------- |
233 | response.s3.gis = Storage() # Defined early for use by S3Config. |
234 | deployment_settings = s3base.S3Config() |
235 | current.deployment_settings = deployment_settings |
236 | |
237 | === modified file 'models/00_settings.py' |
238 | --- models/00_settings.py 2011-12-05 14:29:54 +0000 |
239 | +++ models/00_settings.py 2011-12-09 12:27:24 +0000 |
240 | @@ -257,10 +257,7 @@ |
241 | # Auth |
242 | ###### |
243 | |
244 | -try: |
245 | - auth.settings.password_min_length = 4 |
246 | -except: |
247 | - raise HTTP(501, body="Error: Running Web2Py < 3654, so please upgrade") |
248 | +auth.settings.password_min_length = 4 |
249 | auth.settings.expiration = 28800 # seconds |
250 | |
251 | #auth.settings.username_field = True |
252 | |
253 | === added file 'private/__init__.py' |
254 | === added directory 'private/update_check' |
255 | === added file 'private/update_check/__init__.py' |
256 | === added file 'private/update_check/eden_update_check.py' |
257 | --- private/update_check/eden_update_check.py 1970-01-01 00:00:00 +0000 |
258 | +++ private/update_check/eden_update_check.py 2011-12-09 12:27:24 +0000 |
259 | @@ -0,0 +1,180 @@ |
260 | +# -*- coding: utf-8 -*- |
261 | +""" |
262 | + Check whether the configuration is sufficient to run Eden. |
263 | +""" |
264 | + |
265 | +def update_check(environment): |
266 | + # Get Web2py environment into our globals. |
267 | + globals().update(**environment) |
268 | + |
269 | + import os |
270 | + app_path_parts = ["applications", request.application] |
271 | + app_path = os.path.join(*app_path_parts) |
272 | + |
273 | + # Fatal configuration errors. |
274 | + errors = [] |
275 | + # Non-fatal warnings. |
276 | + warnings = [] |
277 | + |
278 | + # Minimum usable Web2py version: If Eden depends on a feature in Web2py, |
279 | + # there are several ways one can test for that feature's availability. |
280 | + # One is to check for the existence of some class or global variable the |
281 | + # the feature provides uniquely. Lacking that, one can copy the text from |
282 | + # VERSION in a version of Web2py at or after where the feature becomes |
283 | + # available, then compare the timestamps in that version string with the |
284 | + # running Web2py's version. |
285 | + |
286 | + # Currently, the minimum usable Web2py is determined by the existence of |
287 | + # the global "current". |
288 | + try: |
289 | + from gluon import current |
290 | + except ImportError: |
291 | + errors.append( |
292 | + "The installed version of Web2py is too old -- it does not define current." |
293 | + "\nPlease upgrade Web2py to a more recent version.") |
294 | + # @ToDo: Could find the version or revision where current becomes available |
295 | + # and say "upgrade to at least ..." |
296 | + |
297 | + # Web2py's Scheduler was revamped and became usable at the following |
298 | + # version. Scheduler isn't strictly required, so for the moment, this is |
299 | + # only a warning. In future, the version, message to print, and whether |
300 | + # this is a warning or error can all change -- the test mechanism itself |
301 | + # remains the same. |
302 | + web2py_minimum_version = "Version 1.99.2 (2011-09-26 00:51:34) stable" |
303 | + # Check that the installed Web2py is sufficiently recent. The datetime |
304 | + # string, which changes with individual commits, is finer-grained than the |
305 | + # released version. (Note: If the version check is not needed just now, |
306 | + # don't delete the code -- just comment it out. That avoids having someone |
307 | + # redo the work of digging the info out of Web2py. Thank you.) |
308 | + web2py_version_ok = True |
309 | + try: |
310 | + from gluon.fileutils import parse_version |
311 | + except ImportError: |
312 | + web2py_version_ok = False |
313 | + if web2py_version_ok: |
314 | + web2py_minimum_datetime = parse_version(web2py_minimum_version)[3] |
315 | + web2py_installed_datetime = request.global_settings.web2py_version[3] |
316 | + web2py_version_ok = web2py_installed_datetime >= web2py_minimum_datetime |
317 | + if not web2py_version_ok: |
318 | + warnings.append( |
319 | + "The installed version of Web2py is too old to provide the Scheduler," |
320 | + "\nso scheduled tasks will not be available. If you need scheduled tasks," |
321 | + "\nplease upgrade Web2py to at least version: %s" % \ |
322 | + web2py_minimum_version) |
323 | + |
324 | + # -------------------------------------------------------------------------- |
325 | + template_src = os.path.join(app_path, "deployment-templates") |
326 | + template_dst = app_path |
327 | + |
328 | + template_files = ( |
329 | + os.path.join("models", "000_config.py"), |
330 | + # Deprecated by Scheduler |
331 | + #"cron/crontab" |
332 | + ) |
333 | + |
334 | + copied_from_template = [] |
335 | + |
336 | + for t in template_files: |
337 | + src_path = os.path.join(template_src, t) |
338 | + dst_path = os.path.join(template_dst, t) |
339 | + try: |
340 | + os.stat(dst_path) |
341 | + except OSError: |
342 | + # not found, copy from template |
343 | + import shutil |
344 | + shutil.copy(src_path, dst_path) |
345 | + copied_from_template.append(t) |
346 | + else: |
347 | + # Found the file in the destination -- check if it's up to date and |
348 | + # has been edited. |
349 | + # |
350 | + # Each file can have a flag for whether it's been edited. This |
351 | + # should be of the form: |
352 | + # FINISHED_EDITING_\w*\s*=\s*(True|False) |
353 | + # Any suffix is ok after FINISHED_EDITING_ and the "assignment" can |
354 | + # be in a comment if the file is not a Python file. |
355 | + # |
356 | + # Likewise, each file can have a version number. This should be of |
357 | + # the form: |
358 | + # VERSION_\w*\s*=\s*[0-9]+ |
359 | + # No ordering is implied for the "number" -- it's matched as a |
360 | + # string -- but it will be easier to maintain if it's incremented |
361 | + # for new versions. |
362 | + # |
363 | + # No spurious errors will be reported for files that don't have |
364 | + # edited or version flags. |
365 | + # |
366 | + # @Note the versions are not directly tied to the update check id |
367 | + # in 000_1st_run. A file's version would be changed when the file |
368 | + # changes, and a file change might need an update check, but an |
369 | + # update check might be required for some other reason. |
370 | + import re |
371 | + edited_pattern = r"FINISHED_EDITING_\w*\s*=\s*(True|False)" |
372 | + version_pattern = r"VERSION_\w*\s*=\s*([0-9]+)" |
373 | + edited_matcher = re.compile(edited_pattern).match |
374 | + version_matcher = re.compile(version_pattern).match |
375 | + has_edited = False |
376 | + has_version = False |
377 | + |
378 | + # Get version and edited from the in-use copy of the file. |
379 | + with open(dst_path) as f: |
380 | + for line in f: |
381 | + if not has_edited: |
382 | + edited_result = edited_matcher(line) |
383 | + if edited_result: |
384 | + has_edited = True |
385 | + edited = edited_result.group(1) |
386 | + if not has_version: |
387 | + version_result = version_matcher(line) |
388 | + if version_result: |
389 | + has_version = True |
390 | + version = version_result.group(1) |
391 | + if has_edited and has_version: |
392 | + break |
393 | + if has_edited and (edited != "True"): |
394 | + errors.append("Please edit %s before starting the system." % t) |
395 | + |
396 | + # Get version from deployment-templates. |
397 | + template_has_version = False |
398 | + with open(src_path) as f: |
399 | + for line in f: |
400 | + version_result = version_matcher(line) |
401 | + if version_result: |
402 | + template_has_version = True |
403 | + template_version = version_result.group(1) |
404 | + break |
405 | + if (has_version != template_has_version) or \ |
406 | + has_version and (version != template_version): |
407 | + errors.append( |
408 | + "Version of %s does not match that in deployment-templates.\n" % t + |
409 | + "Please merge in changes from the template.") |
410 | + |
411 | + if copied_from_template: |
412 | + errors.append( |
413 | + "The following files were copied from templates and should be edited: %s" % |
414 | + ", ".join(copied_from_template)) |
415 | + |
416 | + # @ToDo specifically about 000_config: |
417 | + # Right now we only want these flags, but later we may want more of |
418 | + # the config values. For that, execfile() would be appropriate so |
419 | + # it can be evaluated in an appropriate global / local environment. |
420 | + # However, that fails with what appears to be a compile() error: |
421 | + # TypeError: must be string without null bytes, not str |
422 | + # Are there actually null bytes in 000_config.py, and are they |
423 | + # somehow just fine when Web2py compiles it? Is this because of the |
424 | + # Unicode chars -- do some of them contain zero bytes? I read the |
425 | + # file as bytes and didn't find nulls, so suspect the error is a red |
426 | + # herring. Time to download the Python source code... |
427 | + # |
428 | + # @Note: What about import to read values from 000_config? It isn't |
429 | + # designed to be imported, but rather to have its code executed in a |
430 | + # pre-existing environment (it has unsatisfied references if one |
431 | + # attempts to import it) so can't import it to get the value of the |
432 | + # "edited" flag. It could be made importable by adding some (fairly |
433 | + # time consuming) environment setup. Or turned into a function, as |
434 | + # per this file. |
435 | + # |
436 | + # @Note: Perhaps web setup could provide config settings in some |
437 | + # more accessible form. |
438 | + |
439 | + return {"error_messages": errors, "warning_messages": warnings} |
440 | \ No newline at end of file |
I gather there was a private (review) comment objecting to the quantity of (code) comments, and on the grounds of that invisible review comment, no review has been done by the actual reviewer (whoever is going to do that...). The comments in the code are there for the benefit of the reviewer, as they explain some of why things were done as they are. They are intended to be moved to the wiki, as appropriate. Some are ToDos. The very first ToDo is:
# @ToDo: Scrape all the how-to and what's-this comments, plus ToDos, into a
# wiki page, then reference that here.
With the comments inline, the reviewer can see easily what they refer to. Can the reviewer please have a look, and decide at least whether they are willing to waste round trips asking why things were done as they are, if the comments are removed prior to the review?