Merge lp:~ptressel/sahana-eden/devel2 into lp:sahana-eden

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
Reviewer Review Type Date Requested Status
Fran Boon Approve
Review via email: mp+85119@code.launchpad.net

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 :

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?

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/mandatory changes added, which is less frequent than it used to be)
* 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