Merge lp:~adeuring/charms/precise/juju-reports/juju-reports-new-revno-in-new-dir into lp:~juju-qa/charms/precise/juju-reports/trunk

Proposed by Abel Deuring
Status: Merged
Merged at revision: 48
Proposed branch: lp:~adeuring/charms/precise/juju-reports/juju-reports-new-revno-in-new-dir
Merge into: lp:~juju-qa/charms/precise/juju-reports/trunk
Diff against target: 294 lines (+109/-56)
2 files modified
hooks/common.py (+109/-47)
hooks/install (+0/-9)
To merge this branch: bzr merge lp:~adeuring/charms/precise/juju-reports/juju-reports-new-revno-in-new-dir
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+242230@code.launchpad.net

Description of the change

A (partial) fix for bug 1357390 and 1357391. The assumption is that these bugs occur when a a new juju-reports revision is installed: Some libraries needed by cron script are not available during the upgrade.

The main idea of this change: When a new revision is set, this revision is stored in a new directory, named juju-reports-REVNO. When "make prodinstall" finishes, a symlink named juju-reports is created that points to juju-reports-REVNO.

This works fine for revno upgrades, but it does not address another issue: When the charm config variable "dist-clean" is true, setting any config value will still cause cronscript failures until "make prodinstall" finishes.

This could be fixed in a follow-up branch, which could probably also move the stop() call in update_from_config() closer to the start() call.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

Why use a symlink? Why not just rename the new project dir into place?

review: Needs Fixing
Revision history for this message
Abel Deuring (adeuring) wrote :

If we rename the directoy, many scripts will break. As an example from a test run, the script bin/cbtest:

#!/home/ubuntu/juju-reports-170/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'jujureports==0.0','console_scripts','cbtest'
__requires__ = 'jujureports==0.0'
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.exit(
        load_entry_point('jujureports==0.0', 'console_scripts', 'cbtest')()
    )

51. By Abel Deuring

Use a new source directory in each call of update_source(); exception-based logic to distiguish between PROJECT_DIR being a directory or a symlink or not existing.

52. By Abel Deuring

reduce downtime during a config-changed hook: Call stop() immediaetly before make_project_link()

Revision history for this message
Abel Deuring (adeuring) wrote :

Another small change that became possible because update_soruce() now always uses a new directory: stop() can be called much later: Just before make_project_link(). This reduces the downtime when the config-changed hook is running.

Revision history for this message
Aaron Bentley (abentley) wrote :

Thanks for explaining why we need to use symlinks here.

Thanks for switching to EAFP. I disagree that Look Before You Leap vs Easier to Ask Forgiveness than permission is simply a matter of taste; EAFP is more correct because it does not have race conditions, and typically more efficient. In the symlink case, the symlink could be moved or deleted between the os.path.lexists, os.path.islink and os.readlink calls, so EAFP is less correct. It also requires three times as many system calls, so it is less efficient.

Now, it's true that two extra system calls don't really matter. But using three times as many system calls in order to be less correct does not make sense to me.

It also looks like you're pulling the branch from scratch, instead of taking advantage of the data in the pre-existing branch. That's going to make updates slower. Instead, you could use a shared repository, or you could branch from the old branch instead of using "bzr init".

I am also concerned that update_source is now going to do work for every config change. It was intended to only do work when the branch revision changed. Do we need to start by comparing revision-ids at the top of update_source and exiting early if there's no change?

review: Needs Fixing
53. By Abel Deuring

make_project_link(): use OSError.errno to figure out the exact error condition.

54. By Abel Deuring

Run install_production() only when the revno or the branch URL changes.

55. By Abel Deuring

Use a shared repository for the juju-reports branches.

56. By Abel Deuring

Do not remove an existing directory /juju-reports in the install hook.

Revision history for this message
Abel Deuring (adeuring) wrote :

The charm now uses a shared repository. I've also added some logic to check if the revno has changed between two calls of the config-changed hook: The current revno and parent branch URL are stored a file "last-source-revision"; update_source() and install_production() are now called only when the current config has other branch URL/revo values than before.

The config parameter "dist-clean" is has not real usage in the strict sense since juju-reports is now always built in a new directory; I've added logic so that update_source() and install_production() are always called when dist-clean is true. Similary, the revno value -1 cause a new build in each config-chaned run. (This should be improved: We can check if the parent branch has a new revision.)

Finally I noticed that the install hook still removed the directory $home/juju-reports. This is done in make_project_link(), so I removed it.

57. By Abel Deuring

do not automatically re-install juju-reports when the revno is set to -1

Revision history for this message
Aaron Bentley (abentley) wrote :

I think it is simpler and more accurate to check revision-id, as I suggested in my review, rather than revno and branch.

It is simpler because Bazaar tracks revision-id itself, so you don't need to add new config files. It is more accurate because changes to revno and branch do not necessarily imply that revision-id has changed. source can change to a different location with the same branch contents. revno can change from -1 to a positive value and still refer to the same revision-id. And when branch and revno are unchanged, this does not guarantee that revision-id is unchanged; if revno is -1, it will change as the branch is updated.

So I don't think this is the right way to handle this, but I also don't think this will cause practical problems because
1. we don't use -1 in production
2. we don't change source in production, and even if we did, it would just mean one extra period of downtime.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/common.py'
--- hooks/common.py 2014-11-10 14:31:29 +0000
+++ hooks/common.py 2014-11-21 16:47:34 +0000
@@ -4,10 +4,10 @@
4import errno4import errno
5import os5import os
6from pwd import getpwnam6from pwd import getpwnam
7import re7import shutil
8from StringIO import StringIO
9import subprocess8import subprocess
10import sys9from tempfile import mkdtemp
10import yaml
1111
12from Cheetah.Template import Template12from Cheetah.Template import Template
1313
@@ -18,28 +18,29 @@
18HOME = '/home/ubuntu'18HOME = '/home/ubuntu'
19PROJECT_DIR = os.path.join(HOME, 'juju-reports')19PROJECT_DIR = os.path.join(HOME, 'juju-reports')
20INI = 'production.ini'20INI = 'production.ini'
2121REVISION_INFO_FILE = os.path.join(HOME, 'last-source-revision')
2222
23def get_ini_path():23
24 return os.path.join(PROJECT_DIR, INI)24def get_ini_path(source_path):
2525 return os.path.join(source_path, INI)
2626
27def get_ini():27
28def get_ini(source_path):
28 config = ConfigParser()29 config = ConfigParser()
29 config.read(get_ini_path())30 config.read(get_ini_path(source_path))
30 return config31 return config
3132
3233
33def get_template_path(name):34def get_template_path(name, source_path):
34 return os.path.join(PROJECT_DIR, 'resources/{}.template'.format(name))35 return os.path.join(source_path, 'resources/{}.template'.format(name))
3536
3637
37def install_ini(mongo_url, gh_token, jujuci_url, token, error_email,38def install_ini(mongo_url, gh_token, jujuci_url, token, error_email,
38 error_email_from, cloud_health_failure_threshold):39 error_email_from, cloud_health_failure_threshold, source_path):
39 config = ConfigParser()40 config = ConfigParser()
40 path = get_template_path(INI)41 path = get_template_path(INI, source_path)
41 if not os.path.exists(path):42 if not os.path.exists(path):
42 path = get_ini_path()43 path = get_ini_path(source_path)
43 config.read(path)44 config.read(path)
44 config.set('app:main', 'mongo.url', mongo_url)45 config.set('app:main', 'mongo.url', mongo_url)
45 config.set('app:main', 'gh_token', gh_token)46 config.set('app:main', 'gh_token', gh_token)
@@ -51,12 +52,12 @@
51 config.set(52 config.set(
52 'app:main', 'cloud_health_failure_threshold',53 'app:main', 'cloud_health_failure_threshold',
53 cloud_health_failure_threshold)54 cloud_health_failure_threshold)
54 config.write(open(get_ini_path(), 'w'))55 config.write(open(get_ini_path(source_path), 'w'))
55 return config56 return config
5657
5758
58def configure_s3(config):59def configure_s3(config, source_path):
59 template_path = get_template_path('juju-qa.s3cfg')60 template_path = get_template_path('juju-qa.s3cfg', source_path)
60 try:61 try:
61 template_cm = open(template_path)62 template_cm = open(template_path)
62 except IOError as e:63 except IOError as e:
@@ -65,7 +66,7 @@
65 return66 return
66 with template_cm as template_file:67 with template_cm as template_file:
67 template = template_file.read()68 template = template_file.read()
68 config_path = os.path.join(PROJECT_DIR, 'juju-qa.s3cfg')69 config_path = os.path.join(source_path, 'juju-qa.s3cfg')
69 s3config = template.format(access_key=config['aws-access-key'],70 s3config = template.format(access_key=config['aws-access-key'],
70 secret_key=config['aws-secret-key'])71 secret_key=config['aws-secret-key'])
71 update_file(config_path, s3config)72 update_file(config_path, s3config)
@@ -99,9 +100,9 @@
99 hookenv.close_port(port)100 hookenv.close_port(port)
100101
101102
102def call_script(script_path, root=False):103def call_script(script_path, source_path, root=False):
103 env = os.environ.copy()104 env = os.environ.copy()
104 ini = get_ini_path()105 ini = get_ini_path(source_path)
105 env['HOME'] = HOME106 env['HOME'] = HOME
106 env['INI'] = ini107 env['INI'] = ini
107 if root:108 if root:
@@ -110,20 +111,20 @@
110 args = ['su', 'ubuntu', '-p', '-c']111 args = ['su', 'ubuntu', '-p', '-c']
111112
112 args.append(script_path)113 args.append(script_path)
113 subprocess.check_call(args, env=env, cwd=PROJECT_DIR)114 subprocess.check_call(args, env=env, cwd=source_path)
114 return env115 return env
115116
116117
117def stop():118def stop():
118 try:119 try:
119 call_script('scripts/stop', root=True)120 call_script('scripts/stop', PROJECT_DIR, root=True)
120 except OSError as e:121 except OSError as e:
121 if e.errno != errno.ENOENT:122 if e.errno != errno.ENOENT:
122 raise123 raise
123124
124125
125def start():126def start(source_path):
126 call_script('scripts/run')127 call_script('scripts/run', source_path)
127128
128129
129def open_secret(path, uid=None, gid=None):130def open_secret(path, uid=None, gid=None):
@@ -152,26 +153,39 @@
152153
153154
154def update_source(source_url, revno):155def update_source(source_url, revno):
155 # Make it easier to develop on a deploy
156 call_bzr(['bind', '-d', PROJECT_DIR, source_url])
157
158 # Grab the code156 # Grab the code
157 repository = os.path.join(HOME, 'juju-reports-repo')
158 pwd = getpwnam('ubuntu')
159 try:
160 os.mkdir(repository)
161 except OSError as e:
162 if e.errno != errno.EEXIST:
163 raise
164 pass
165 else:
166 call_bzr(['init-repo', repository])
167 os.chown(repository, pwd.pw_uid, pwd.pw_gid)
168 target_dir = mkdtemp(prefix='juju-reports-%s-' % revno, dir=repository)
159 revno = 'revno:%s' % revno169 revno = 'revno:%s' % revno
170 os.chown(target_dir, pwd.pw_uid, pwd.pw_gid)
171 call_bzr(['init', target_dir])
172 os.chown(target_dir, pwd.pw_uid, pwd.pw_gid)
173
174 # Make it easier to develop on a deploy
175 call_bzr(['bind', '-d', target_dir, source_url])
160176
161 bzr_cmd = ['pull', '--overwrite', source_url, '-r', revno, '-d',177 bzr_cmd = ['pull', '--overwrite', source_url, '-r', revno, '-d',
162 PROJECT_DIR, '--remember', '--local']178 target_dir, '--remember', '--local']
163 call_bzr(bzr_cmd)179 call_bzr(bzr_cmd)
164180 return target_dir
165 # Restore the templates if they are missing/altered.181
166 call_bzr(['revert', os.path.join(PROJECT_DIR, 'resources')])182
167183def install_production(dist_clean, develop, source_path):
168184 subprocess.check_call(['make', 'deploydeps'], cwd=source_path)
169def install_production(dist_clean, develop):
170 subprocess.check_call(['make', 'deploydeps'], cwd=PROJECT_DIR)
171 if dist_clean:185 if dist_clean:
172 subprocess.check_call(['make', 'distclean'], cwd=PROJECT_DIR)186 subprocess.check_call(['make', 'distclean'], cwd=source_path)
173 target = 'install' if develop else 'prodinstall'187 target = 'install' if develop else 'prodinstall'
174 subprocess.check_call(['make', target], cwd=PROJECT_DIR)188 subprocess.check_call(['make', target], cwd=source_path)
175 subprocess.check_call(['apt-get', 'autoremove', '--yes'])189 subprocess.check_call(['apt-get', 'autoremove', '--yes'])
176190
177191
@@ -260,28 +274,73 @@
260 fileobj.write(new_contents)274 fileobj.write(new_contents)
261275
262276
277def make_project_link(source_path, branch_url, revno):
278 try:
279 old_sources = os.readlink(PROJECT_DIR)
280 except OSError as err:
281 if err.errno == errno.EINVAL:
282 shutil.rmtree(PROJECT_DIR)
283 old_sources = None
284 elif err.errno == errno.ENOENT:
285 old_sources = None
286 else:
287 raise
288 else:
289 if old_sources == source_path:
290 return
291 os.remove(PROJECT_DIR)
292 os.symlink(source_path, PROJECT_DIR)
293 if old_sources is not None:
294 shutil.rmtree(old_sources)
295 with open(REVISION_INFO_FILE, 'w') as f:
296 yaml.dump((branch_url, revno), f)
297
298
299def get_last_revision():
300 try:
301 file_context = open(REVISION_INFO_FILE, 'r')
302 except IOError as e:
303 if e.errno == errno.ENOENT:
304 return None, None
305 else:
306 raise
307 with file_context:
308 return yaml.load(file_context)
309
310
263def update_from_config():311def update_from_config():
264 ini = get_ini()312 ini = get_ini(PROJECT_DIR)
265 mongo_url = get_mongo_url()313 mongo_url = get_mongo_url()
266 config = hookenv.config()314 config = hookenv.config()
267 set_port(ini, False)315 set_port(ini, False)
268 stop()
269 configure_lp(config['lp-key'])316 configure_lp(config['lp-key'])
270 if config['source'] == '':317 if config['source'] == '':
271 hookenv.log('Incomplete config: source')318 hookenv.log('Incomplete config: source')
272 return319 return
273 update_source(config['source'], config['revno'])320 last_source_url, last_revision = get_last_revision()
274 oauth_path = os.path.join(PROJECT_DIR, 'jujureports/lp_credentials.txt')321 needs_install_update = (
322 (last_source_url, last_revision) != (config['source'], config['revno'])
323 or config['dist-clean'])
324 if needs_install_update:
325 source_path = update_source(
326 config['source'], config['revno'])
327 else:
328 source_path = PROJECT_DIR
329 oauth_path = os.path.join(source_path, 'jujureports/lp_credentials.txt')
275 update_file(oauth_path, config['lp-oauth'] + '\n')330 update_file(oauth_path, config['lp-oauth'] + '\n')
276 install_production(config['dist-clean'], config['develop-install'])331 if needs_install_update:
332 install_production(
333 config['dist-clean'], config['develop-install'], source_path)
277 if mongo_url == '':334 if mongo_url == '':
278 hookenv.log('No mongodb set up.')335 hookenv.log('No mongodb set up.')
336 if needs_install_update:
337 make_project_link(source_path, config['source'], config['revno'])
279 return338 return
280 ini = install_ini(mongo_url, config['gh-token'], config['jujuci-url'],339 ini = install_ini(mongo_url, config['gh-token'], config['jujuci-url'],
281 config['charm-bundle-test-token'],340 config['charm-bundle-test-token'],
282 config['error-email'], config['error-email-from'],341 config['error-email'], config['error-email-from'],
283 config['cloud-health-failure-threshold'])342 config['cloud-health-failure-threshold'], source_path)
284 configure_s3(config)343 configure_s3(config, source_path)
285 unset = [key for key in [344 unset = [key for key in [
286 'lp-key', 'lp-oauth', 'gh-token', 'charm-bundle-test-token',345 'lp-key', 'lp-oauth', 'gh-token', 'charm-bundle-test-token',
287 'error-email', 'error-email-from'346 'error-email', 'error-email-from'
@@ -292,6 +351,9 @@
292 update_cronjob(None)351 update_cronjob(None)
293 else:352 else:
294 install_cronjob(config['cron-interval'], config['error-email'])353 install_cronjob(config['cron-interval'], config['error-email'])
295 start()354 stop()
355 if needs_install_update:
356 make_project_link(source_path, config['source'], config['revno'])
357 start(source_path)
296 set_port(ini, True)358 set_port(ini, True)
297 update_website(ini)359 update_website(ini)
298360
=== modified file 'hooks/install'
--- hooks/install 2014-08-25 14:31:05 +0000
+++ hooks/install 2014-11-21 16:47:34 +0000
@@ -7,11 +7,6 @@
7home="/home/ubuntu"7home="/home/ubuntu"
8project_dir="$home/juju-reports"8project_dir="$home/juju-reports"
99
10# Set up the project directory
11if [ -d $project_dir ]; then
12 rm -rf $project_dir
13fi
14mkdir -p $project_dir
15mkdir -p $home/logs10mkdir -p $home/logs
16chown ubuntu:ubuntu $home/logs11chown ubuntu:ubuntu $home/logs
1712
@@ -31,10 +26,6 @@
31cat templates/known_hosts >> /home/ubuntu/.ssh/known_hosts26cat templates/known_hosts >> /home/ubuntu/.ssh/known_hosts
32chown ubuntu:ubuntu /home/ubuntu/.ssh/known_hosts27chown ubuntu:ubuntu /home/ubuntu/.ssh/known_hosts
3328
34# Get the code.
35cd $project_dir
36bzr init
37chown ubuntu:ubuntu $project_dir
38if [ -f /tmp/app.log ]; then29if [ -f /tmp/app.log ]; then
39 chown ubuntu:ubuntu /tmp/app.log30 chown ubuntu:ubuntu /tmp/app.log
40fi31fi

Subscribers

People subscribed via source and target branches

to all changes: