Merge lp:~abentley/charms/precise/juju-reports/major-updates into lp:~juju-qa/charms/precise/juju-reports/trunk

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 25
Proposed branch: lp:~abentley/charms/precise/juju-reports/major-updates
Merge into: lp:~juju-qa/charms/precise/juju-reports/trunk
Diff against target: 351 lines (+158/-93)
9 files modified
config.yaml (+4/-0)
hooks/common.py (+142/-11)
hooks/config-changed (+2/-63)
hooks/database-relation-broken (+2/-2)
hooks/database-relation-changed (+2/-5)
hooks/database-relation-departed (+2/-2)
hooks/install (+1/-9)
hooks/start (+3/-0)
hooks/upgrade-charm (+0/-1)
To merge this branch: bzr merge lp:~abentley/charms/precise/juju-reports/major-updates
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+218863@code.launchpad.net

Commit message

Major updates to charm.

Description of the change

Major updates to charm.
1. all code is common code. Anything that runs python does
2. 'dist-clean' is introduced so that devs can turn of automatic "make dist-clean" and update much faster.
3. open-port and close-port are done as part of config now, not database-relation-changed
4. open / close port use the configured ports.
5. almost all use of in_juju_reports is removed.
6. restart is removed in favour of stop (before updating source) and start (after updating source).
7. LP key is written in update_source, not install hook.
8. port is closed when the service cannot operate.
9. Launchpad user is juju-qa-bot
10. start hook fails gracefully.
11. config is parsed as yaml instead of being looked up one-by-one. This also preserves value types.
12. use os.unlink instead of subprocess.call(['rm', ...])
13. stop early if mongodb url is not known.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

In update_source() you use a try/except block...I expected a context manger. The lines aren't wrong, I just expect context managers for resources in code that YOU write.

In that same function, I see we login as the bot. This has bothered me for some time. We don't logout. Should we? Am I paranoid. I ask because I am pondering a jenkin-juju-ci subordinate charm and I feel safer if the charm can login to get private branches, but logs out when done. The user is not left with powers that have no legitimate need.

In install_cronjob() file() is used. The function is deprecated. Does this work?
    open('/etc/cron.d/ubuntu', 'w').write(str(t))

review: Needs Information (code)
Revision history for this message
Aaron Bentley (abentley) wrote :

> In update_source() you use a try/except block...I expected a context manger.
> The lines aren't wrong, I just expect context managers for resources in code
> that YOU write.

I think I just figured that it was specific to the context, not something that could be generalized usefully. It also does sys.exit, which would be poor behaviour for a library function (and you can't return in a context manager).

> In that same function, I see we login as the bot.

It actually just sets the Launchpad user ID. It's not like Launchpadlib. It doesn't establish OAuth credentials or anything.

> This has bothered me for
> some time. We don't logout. Should we? Am I paranoid. I ask because I am
> pondering a jenkin-juju-ci subordinate charm and I feel safer if the charm can
> login to get private branches, but logs out when done. The user is not left
> with powers that have no legitimate need.

The actual secret is the SSH key, in this case. We can delete it after pulling, if we think that's useful. But it's security-by-obscurity-- the SSH key will still be accessible in the box, just not on the filesystem.

To embrace the principle of least privilege, we'd want to grant this machine access only to the one branch it needs. But that means a separate account, and I don't like doing that.

>
> In install_cronjob() file() is used. The function is deprecated. Does this
> work?
> open('/etc/cron.d/ubuntu', 'w').write(str(t))

Oh, it's not marked as deprecated in the Python 2 docs, but I see it's not in Python 3. Learn something every day. I'll change it.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for explaining he situation and replacing the call to file(). r=me

review: Approve (code)
26. By Aaron Bentley

Use 'open' instead of 'file'.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2014-01-03 16:39:13 +0000
3+++ config.yaml 2014-05-08 21:35:11 +0000
4@@ -20,3 +20,7 @@
5 type: string
6 default: ""
7 description: "SSH key for accessing private code on launchpad."
8+ dist-clean:
9+ type: boolean
10+ default: true
11+ description: "If enabled, run dist-clean when updating source code."
12
13=== modified file 'hooks/common.py'
14--- hooks/common.py 2014-04-18 19:19:28 +0000
15+++ hooks/common.py 2014-05-08 21:35:11 +0000
16@@ -1,6 +1,14 @@
17-from ConfigParser import ConfigParser
18+from ConfigParser import (
19+ ConfigParser,
20+ NoSectionError)
21+import errno
22 import os
23+from StringIO import StringIO
24 import subprocess
25+import sys
26+
27+from Cheetah.Template import Template
28+import yaml
29
30
31 CHARM_DIR = os.getcwd()
32@@ -17,20 +25,143 @@
33 return dec_fn
34
35
36+def get_ini_path():
37+ return os.path.join(PROJECT_DIR, INI)
38+
39+
40+def get_ini():
41+ config = ConfigParser()
42+ config.read(get_ini_path())
43+ return config
44+
45+
46 def update_or_install_ini(mongo_url=None):
47- ini_file = os.path.join(PROJECT_DIR, INI)
48- config = ConfigParser()
49- config.read([ini_file])
50+ config = get_ini()
51 if mongo_url is not None:
52 config.set('app:main', 'mongo.url', mongo_url)
53- config.write(file(ini_file, 'w'))
54-
55-
56-@in_juju_reports
57-def restart():
58+ config.write(open(get_ini_path(), 'w'))
59+ return config
60+
61+
62+def set_port(ini, open_port=True):
63+ port = ini_get(ini, 'server:main', 'port')
64+ if port is None:
65+ return
66+ cmd = 'open-port' if open_port else 'close-port'
67+ subprocess.check_call([cmd, '%s/tcp' % port])
68+
69+
70+def call_script(script_path):
71 env = os.environ.copy()
72 ini = os.path.join(PROJECT_DIR, INI)
73 env['HOME'] = HOME
74 env['INI'] = ini
75- subprocess.check_call(['scripts/stop'], env=env)
76- subprocess.check_call(['scripts/run'], env=env)
77+ subprocess.check_call([script_path], env=env, cwd=PROJECT_DIR)
78+ return env
79+
80+
81+def stop():
82+ try:
83+ call_script('scripts/stop')
84+ except OSError as e:
85+ if e.errno != errno.ENOENT:
86+ raise
87+
88+
89+def start():
90+ call_script('scripts/run')
91+
92+
93+def get_config():
94+ config = yaml.safe_load(StringIO(subprocess.check_output('config-get')))
95+ for key in ['lp-key', 'source']:
96+ if config[key] == '':
97+ raise IncompleteConfig('%s not specified.' % key)
98+ return config
99+
100+
101+class IncompleteConfig(ValueError):
102+ """Error class indicating that ssh is not set to a valid value."""
103+
104+
105+@in_juju_reports
106+def update_source(source_url, revno, ssh_key):
107+ fd = os.open('/root/.ssh/lp_rsa', os.O_WRONLY | os.O_TRUNC | os.O_CREAT,
108+ 0600)
109+ try:
110+ os.write(fd, ssh_key)
111+ finally:
112+ os.close(fd)
113+ # Login to LP
114+ bzr_cmd = ['bzr', 'lp-login', 'juju-qa-bot']
115+ subprocess.check_call(bzr_cmd)
116+
117+ # Grab the code
118+ revno = 'revno:%s' % revno
119+ bzr_cmd = ['bzr', 'pull', '--overwrite', source_url, '-r', revno, '-d',
120+ PROJECT_DIR]
121+ subprocess.check_call(bzr_cmd)
122+
123+ # Remove the unnecessary INI files--if something tries to use them we need
124+ # a failure.
125+ for ini in ['test.ini', 'development.ini']:
126+ try:
127+ os.unlink(os.path.join(PROJECT_DIR, ini))
128+ except OSError as e:
129+ if e.errno != errno.ENOENT:
130+ raise
131+
132+
133+@in_juju_reports
134+def install_production(dist_clean):
135+ subprocess.check_call(['make', 'deploydeps'])
136+ if dist_clean:
137+ subprocess.check_call(['make', 'distclean'])
138+ subprocess.check_call(['make', 'prodinstall'])
139+ subprocess.check_call(['apt-get', 'autoremove', '--yes'])
140+
141+
142+def install_cronjob(interval, error_email):
143+ t = Template(file='templates/crontab.tmpl')
144+ t.interval = interval
145+ if error_email is not None:
146+ t.error_email = 'MAILTO=%s' % error_email
147+ else:
148+ t.error_email = ''
149+ t.reports_home = PROJECT_DIR
150+ open('/etc/cron.d/ubuntu', 'w').write(str(t))
151+
152+
153+def ini_get(ini, section, key):
154+ try:
155+ return ini.get(section, key)
156+ except NoSectionError:
157+ return
158+
159+
160+def update_from_config(mongo_url=None):
161+ ini = get_ini()
162+ if mongo_url is None:
163+ mongo_url = ini_get(ini, 'app:main', 'mongo.url')
164+ try:
165+ try:
166+ if mongo_url is None:
167+ raise IncompleteConfig('mongo url is missing.')
168+ config = get_config()
169+ except:
170+ set_port(ini, False)
171+ stop()
172+ raise
173+ except IncompleteConfig as e:
174+ subprocess.call(['juju-log', 'Incomplete config: %s' % e])
175+ sys.exit(0)
176+ set_port(ini, False)
177+ stop()
178+ update_source(config['source'], config['revno'], config['lp-key'])
179+ # ini may be updated by update_source
180+ ini = get_ini()
181+ install_production(config['dist-clean'])
182+ ini = update_or_install_ini(mongo_url)
183+ install_cronjob(config['cron-interval'], config['error-email'])
184+ start()
185+ set_port(ini, True)
186
187=== modified file 'hooks/config-changed'
188--- hooks/config-changed 2014-04-18 19:19:28 +0000
189+++ hooks/config-changed 2014-05-08 21:35:11 +0000
190@@ -1,66 +1,5 @@
191 #!/usr/bin/env python
192-import os
193-import subprocess
194-
195-from Cheetah.Template import Template
196-
197-from common import (
198- in_juju_reports,
199- PROJECT_DIR,
200- restart,
201- update_or_install_ini,
202-)
203-
204-
205-def rm_ini(ini_name):
206- if ini_name in os.listdir('.'):
207- subprocess.check_call(['rm', ini_name])
208-
209-
210-@in_juju_reports
211-def update_source(source_url, revno):
212- # Login to LP
213- bzr_cmd = ['bzr', 'lp-login', 'jcsackett']
214- subprocess.check_call(bzr_cmd)
215-
216- # Grab the code
217- if revno == -1:
218- revno = 'HEAD'
219- revno = 'revno:%s' % revno
220- bzr_cmd = ['bzr', 'pull', '--overwrite', source_url, '-r', revno]
221- subprocess.check_call(bzr_cmd)
222-
223- #Remove the unnecessary INI files--if something tries to use them we need a
224- #failure.
225- rm_ini('test.ini')
226- rm_ini('development.ini')
227-
228-
229-@in_juju_reports
230-def install_production():
231- subprocess.check_call(['make', 'deploydeps'])
232- subprocess.check_call(['make', 'distclean'])
233- subprocess.check_call(['make', 'prodinstall'])
234-
235-
236-def install_cronjob(interval, error_email):
237- t = Template(file='templates/crontab.tmpl')
238- t.interval = interval
239- if error_email is not None:
240- t.error_email = 'MAILTO=%s' % error_email
241- else:
242- t.error_email = ''
243- t.reports_home = PROJECT_DIR
244- file('/etc/cron.d/ubuntu', 'w').write(str(t))
245-
246+from common import update_from_config
247
248 if __name__ == '__main__':
249- source_url = subprocess.check_output(['config-get', 'source']).strip()
250- revno = subprocess.check_output(['config-get', 'revno']).strip()
251- update_source(source_url, revno)
252- update_or_install_ini()
253- install_production()
254- restart()
255- interval = subprocess.check_output(['config-get', 'cron-interval']).strip()
256- error_email = subprocess.check_output(['config-get', 'error-email']).strip()
257- install_cronjob(interval, error_email)
258+ update_from_config()
259
260=== modified file 'hooks/database-relation-broken'
261--- hooks/database-relation-broken 2014-04-02 20:02:37 +0000
262+++ hooks/database-relation-broken 2014-05-08 21:35:11 +0000
263@@ -1,6 +1,6 @@
264 #!/usr/bin/env python
265-from common import update_or_install_ini
266+from common import update_from_config
267
268
269 if __name__ == '__main__':
270- update_or_install_ini()
271+ update_from_config()
272
273=== modified file 'hooks/database-relation-changed'
274--- hooks/database-relation-changed 2014-02-11 20:19:28 +0000
275+++ hooks/database-relation-changed 2014-05-08 21:35:11 +0000
276@@ -4,8 +4,7 @@
277 import sys
278
279 from common import (
280- restart,
281- update_or_install_ini
282+ update_from_config
283 )
284
285
286@@ -19,6 +18,4 @@
287 # We don't have the environment data we're supposed to have; by
288 # convention we exit silently.
289 sys.exit()
290- update_or_install_ini(mongo_url)
291- subprocess.check_call(['open-port', '6543/tcp'])
292- restart()
293+ update_from_config(mongo_url)
294
295=== modified file 'hooks/database-relation-departed'
296--- hooks/database-relation-departed 2014-04-02 20:02:37 +0000
297+++ hooks/database-relation-departed 2014-05-08 21:35:11 +0000
298@@ -1,6 +1,6 @@
299 #!/usr/bin/env python
300-from common import update_or_install_ini
301+from common import update_from_config
302
303
304 if __name__ == '__main__':
305- update_or_install_ini()
306+ update_from_config()
307
308=== modified file 'hooks/install'
309--- hooks/install 2014-01-30 16:17:47 +0000
310+++ hooks/install 2014-05-08 21:35:11 +0000
311@@ -12,18 +12,10 @@
312 fi
313 mkdir -p $project_dir
314
315-# Set up the machinery to be able to pull the project's private branch.
316-# Install the ssh key for launchpad.
317-cat << EOT > /root/.ssh/lp_rsa
318-`config-get lp-key`
319-EOT
320-chmod 600 /root/.ssh/lp_rsa
321-
322-# Tell ssh to use the above key for accessing launchpad.
323+# Tell ssh what key to use for accessing launchpad.
324 cat << EOT > /root/.ssh/config
325 Host bazaar.launchpad.net
326 IdentityFile ~/.ssh/lp_rsa
327- User jcsackett
328 EOT
329
330 # Make sure launchpad is a known host so ssh isn't prompted to accept it.
331
332=== modified file 'hooks/start'
333--- hooks/start 2014-02-11 20:19:28 +0000
334+++ hooks/start 2014-05-08 21:35:11 +0000
335@@ -4,5 +4,8 @@
336 project_dir="$home/juju-reports"
337 ini="$project_dir/production.ini"
338 cd $project_dir
339+if [ ! -f $project_dir/scripts/stop ]; then
340+ exit 0
341+fi
342 su ubuntu -c "HOME=$home PROJECT_DIR=$project_dir INI=$ini scripts/stop"
343 su ubuntu -c "HOME=$home PROJECT_DIR=$project_dir INI=$ini scripts/run"
344
345=== modified file 'hooks/upgrade-charm'
346--- hooks/upgrade-charm 2013-12-19 14:02:42 +0000
347+++ hooks/upgrade-charm 2014-05-08 21:35:11 +0000
348@@ -1,3 +1,2 @@
349 #!/bin/bash
350 hooks/install
351-hooks/config-changed

Subscribers

People subscribed via source and target branches

to all changes: