Merge lp:~hloeung/jenkins-agent-charm/reactive-rewrite-remove-bundled-charmhelpers into lp:jenkins-agent-charm
- reactive-rewrite-remove-bundled-charmhelpers
- Merge into trunk
Proposed by
Haw Loeung
Status: | Merged |
---|---|
Approved by: | Paul Gear |
Approved revision: | 23 |
Merged at revision: | 23 |
Proposed branch: | lp:~hloeung/jenkins-agent-charm/reactive-rewrite-remove-bundled-charmhelpers |
Merge into: | lp:jenkins-agent-charm |
Diff against target: |
5326 lines (+0/-5072) 44 files modified
hooks/install.d/README.md (+0/-7) hooks/install.d/charmhelpers/canonical_ci/cron.py (+0/-55) hooks/install.d/charmhelpers/canonical_ci/gerrit.py (+0/-220) hooks/install.d/charmhelpers/canonical_ci/jenkins.py (+0/-41) hooks/install.d/charmhelpers/canonical_ci/nrpe.py (+0/-66) hooks/install.d/charmhelpers/canonical_ci/ssh.py (+0/-37) hooks/install.d/charmhelpers/canonical_ci/volume.py (+0/-223) hooks/install.d/charmhelpers/cli/README.rst (+0/-57) hooks/install.d/charmhelpers/cli/__init__.py (+0/-147) hooks/install.d/charmhelpers/cli/commands.py (+0/-2) hooks/install.d/charmhelpers/cli/host.py (+0/-14) hooks/install.d/charmhelpers/contrib/ansible/__init__.py (+0/-101) hooks/install.d/charmhelpers/contrib/charmhelpers/IMPORT (+0/-4) hooks/install.d/charmhelpers/contrib/charmhelpers/__init__.py (+0/-184) hooks/install.d/charmhelpers/contrib/charmsupport/IMPORT (+0/-14) hooks/install.d/charmhelpers/contrib/charmsupport/nrpe.py (+0/-218) hooks/install.d/charmhelpers/contrib/charmsupport/volumes.py (+0/-156) hooks/install.d/charmhelpers/contrib/hahelpers/apache.py (+0/-58) hooks/install.d/charmhelpers/contrib/hahelpers/ceph.py (+0/-294) hooks/install.d/charmhelpers/contrib/hahelpers/cluster.py (+0/-181) hooks/install.d/charmhelpers/contrib/jujugui/IMPORT (+0/-4) hooks/install.d/charmhelpers/contrib/jujugui/utils.py (+0/-602) hooks/install.d/charmhelpers/contrib/network/ovs/__init__.py (+0/-72) hooks/install.d/charmhelpers/contrib/openstack/context.py (+0/-294) hooks/install.d/charmhelpers/contrib/openstack/templates/__init__.py (+0/-2) hooks/install.d/charmhelpers/contrib/openstack/templates/ceph.conf (+0/-11) hooks/install.d/charmhelpers/contrib/openstack/templates/haproxy.cfg (+0/-37) hooks/install.d/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+0/-23) hooks/install.d/charmhelpers/contrib/openstack/templating.py (+0/-261) hooks/install.d/charmhelpers/contrib/openstack/utils.py (+0/-276) hooks/install.d/charmhelpers/contrib/saltstack/__init__.py (+0/-149) hooks/install.d/charmhelpers/contrib/ssl/__init__.py (+0/-79) hooks/install.d/charmhelpers/contrib/storage/linux/loopback.py (+0/-62) hooks/install.d/charmhelpers/contrib/storage/linux/lvm.py (+0/-88) hooks/install.d/charmhelpers/contrib/storage/linux/utils.py (+0/-25) hooks/install.d/charmhelpers/contrib/templating/pyformat.py (+0/-13) hooks/install.d/charmhelpers/core/hookenv.py (+0/-340) hooks/install.d/charmhelpers/core/host.py (+0/-241) hooks/install.d/charmhelpers/fetch/__init__.py (+0/-209) hooks/install.d/charmhelpers/fetch/archiveurl.py (+0/-48) hooks/install.d/charmhelpers/fetch/bzrurl.py (+0/-49) hooks/install.d/charmhelpers/payload/__init__.py (+0/-1) hooks/install.d/charmhelpers/payload/archive.py (+0/-57) hooks/install.d/charmhelpers/payload/execd.py (+0/-50) |
To merge this branch: | bzr merge lp:~hloeung/jenkins-agent-charm/reactive-rewrite-remove-bundled-charmhelpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Gear (community) | Approve | ||
Review via email: mp+363898@code.launchpad.net |
Commit message
Removed no longer needed and used hooks/install.d
Description of the change
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 23
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === removed directory 'hooks' | |||
2 | === removed directory 'hooks/install.d' | |||
3 | === removed file 'hooks/install.d/README.md' | |||
4 | --- hooks/install.d/README.md 2014-05-15 16:16:04 +0000 | |||
5 | +++ hooks/install.d/README.md 1970-01-01 00:00:00 +0000 | |||
6 | @@ -1,7 +0,0 @@ | |||
7 | 1 | # hooks/install.d | ||
8 | 2 | |||
9 | 3 | This directory can be used to extend the function of the jenkins master | ||
10 | 4 | charm without changing any of the base hooks. | ||
11 | 5 | |||
12 | 6 | Files must be executable otherwise the install hook (which is also run | ||
13 | 7 | on upgrade-charm and config-changed hooks) will not execute them. | ||
14 | 8 | 0 | ||
15 | === removed directory 'hooks/install.d/charmhelpers' | |||
16 | === removed file 'hooks/install.d/charmhelpers/__init__.py' | |||
17 | === removed directory 'hooks/install.d/charmhelpers/canonical_ci' | |||
18 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/__init__.py' | |||
19 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/cron.py' | |||
20 | --- hooks/install.d/charmhelpers/canonical_ci/cron.py 2015-08-19 19:17:42 +0000 | |||
21 | +++ hooks/install.d/charmhelpers/canonical_ci/cron.py 1970-01-01 00:00:00 +0000 | |||
22 | @@ -1,55 +0,0 @@ | |||
23 | 1 | import os | ||
24 | 2 | |||
25 | 3 | from charmhelpers.core.hookenv import log, INFO | ||
26 | 4 | |||
27 | 5 | |||
28 | 6 | def write_cronjob(content, job_name=''): | ||
29 | 7 | f = os.environ["JUJU_UNIT_NAME"].replace("/", "_") | ||
30 | 8 | cron_path = os.path.join('/etc', 'cron.d', f) | ||
31 | 9 | if job_name: | ||
32 | 10 | cron_path+='_'+job_name | ||
33 | 11 | |||
34 | 12 | f = open(cron_path, "w") | ||
35 | 13 | f.write(content) | ||
36 | 14 | f.close() | ||
37 | 15 | os.chmod(cron_path, 0755) | ||
38 | 16 | log("Wrote cronjob to %s." % cron_path, INFO) | ||
39 | 17 | |||
40 | 18 | |||
41 | 19 | # generic backup job creation | ||
42 | 20 | def schedule_backup(sources, ci_user, target, schedule, retention_count): | ||
43 | 21 | log("Creating backup cronjob for sources: %s." % sources, INFO) | ||
44 | 22 | |||
45 | 23 | # if doesn't exist, create backup directory and scripts directory | ||
46 | 24 | if not os.path.exists(target): | ||
47 | 25 | os.makedirs(target) | ||
48 | 26 | os.chmod(target, 0755) | ||
49 | 27 | |||
50 | 28 | script = os.path.join(os.environ['CHARM_DIR'], | ||
51 | 29 | "scripts/backup_job") | ||
52 | 30 | backup_string = ",".join(sources) | ||
53 | 31 | |||
54 | 32 | # create the cronjob file that will call the script | ||
55 | 33 | content = ("%s %s %s %s %s %s\n" % | ||
56 | 34 | (schedule, ci_user, script, backup_string, target, retention_count)) | ||
57 | 35 | write_cronjob(content) | ||
58 | 36 | |||
59 | 37 | |||
60 | 38 | def schedule_repo_updates(schedule, ci_user, ci_config_dir, jobs_config_dir): | ||
61 | 39 | log("Creating cronjob to update CI repo config.", INFO) | ||
62 | 40 | |||
63 | 41 | #XXX: matsubara perhaps would be better to bzr pull and then | ||
64 | 42 | # trigger jjb.update_jenkins() | ||
65 | 43 | update_command = ( | ||
66 | 44 | "/usr/bin/bzr update %s && " | ||
67 | 45 | "/usr/local/bin/jenkins-jobs --flush-cache update %s" % ( | ||
68 | 46 | ci_config_dir, jobs_config_dir)) | ||
69 | 47 | |||
70 | 48 | content = "%s %s %s\n" % (schedule, ci_user, update_command) | ||
71 | 49 | write_cronjob(content) | ||
72 | 50 | |||
73 | 51 | |||
74 | 52 | def schedule_generic_job(schedule, user, name, job): | ||
75 | 53 | content = "%s %s %s\n" % (schedule, user, job) | ||
76 | 54 | write_cronjob(content, job_name=name) | ||
77 | 55 | |||
78 | 56 | 0 | ||
79 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/gerrit.py' | |||
80 | --- hooks/install.d/charmhelpers/canonical_ci/gerrit.py 2015-08-19 19:17:42 +0000 | |||
81 | +++ hooks/install.d/charmhelpers/canonical_ci/gerrit.py 1970-01-01 00:00:00 +0000 | |||
82 | @@ -1,220 +0,0 @@ | |||
83 | 1 | import logging | ||
84 | 2 | import os | ||
85 | 3 | import paramiko | ||
86 | 4 | import sys | ||
87 | 5 | import subprocess | ||
88 | 6 | import json | ||
89 | 7 | |||
90 | 8 | from charmhelpers.core.hookenv import ( | ||
91 | 9 | log as _log, | ||
92 | 10 | ERROR, | ||
93 | 11 | ) | ||
94 | 12 | |||
95 | 13 | _connection = None | ||
96 | 14 | GERRIT_DAEMON = "/etc/init.d/gerrit" | ||
97 | 15 | |||
98 | 16 | logging.basicConfig(level=logging.INFO) | ||
99 | 17 | |||
100 | 18 | |||
101 | 19 | def log(msg, level=None): | ||
102 | 20 | # wrap log calls and distribute to correct logger | ||
103 | 21 | # depending if this code is being run by a hook | ||
104 | 22 | # or an external script. | ||
105 | 23 | if os.getenv('JUJU_AGENT_SOCKET'): | ||
106 | 24 | _log(msg, level=level) | ||
107 | 25 | else: | ||
108 | 26 | logging.info(msg) | ||
109 | 27 | |||
110 | 28 | |||
111 | 29 | def get_ssh(host, user, port, key_file): | ||
112 | 30 | global _connection | ||
113 | 31 | if _connection: | ||
114 | 32 | return _connection | ||
115 | 33 | |||
116 | 34 | _connection = paramiko.SSHClient() | ||
117 | 35 | _connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | ||
118 | 36 | _connection.connect(host, username=user, port=port, key_filename=key_file) | ||
119 | 37 | |||
120 | 38 | return _connection | ||
121 | 39 | |||
122 | 40 | |||
123 | 41 | # start gerrit application | ||
124 | 42 | def start_gerrit(): | ||
125 | 43 | try: | ||
126 | 44 | subprocess.check_call([GERRIT_DAEMON, "start"]) | ||
127 | 45 | except: | ||
128 | 46 | pass | ||
129 | 47 | |||
130 | 48 | |||
131 | 49 | # stop gerrit application | ||
132 | 50 | def stop_gerrit(): | ||
133 | 51 | try: | ||
134 | 52 | subprocess.check_call([GERRIT_DAEMON, "stop"]) | ||
135 | 53 | except: | ||
136 | 54 | pass | ||
137 | 55 | |||
138 | 56 | |||
139 | 57 | class GerritException(Exception): | ||
140 | 58 | def __init__(self, msg): | ||
141 | 59 | log('Failed to execute gerrit command: %s' % msg) | ||
142 | 60 | super(GerritException, self).__init__(msg) | ||
143 | 61 | |||
144 | 62 | |||
145 | 63 | class GerritClient(object): | ||
146 | 64 | def __init__(self, host, user, port, key_file): | ||
147 | 65 | self.ssh = get_ssh(host, user, port, key_file) | ||
148 | 66 | |||
149 | 67 | def _run_cmd(self, cmd): | ||
150 | 68 | stdin, stdout, stderr = self.ssh.exec_command(cmd) | ||
151 | 69 | return (stdout.read(), stderr.read()) | ||
152 | 70 | |||
153 | 71 | def create_user(self, user, name, group, ssh_key): | ||
154 | 72 | log('Creating gerrit new user %s in group %s.' % (user, group)) | ||
155 | 73 | cmd = ('gerrit create-account %(user)s --full-name "%(name)s" ' | ||
156 | 74 | '--group "%(group)s" --ssh-key ' | ||
157 | 75 | '"%(ssh_key)s"' % locals()) | ||
158 | 76 | stdout, stderr = self._run_cmd(cmd) | ||
159 | 77 | if not stdout and not stderr: | ||
160 | 78 | log('Created new gerrit user %s in group %s.' % (user, group)) | ||
161 | 79 | |||
162 | 80 | if stderr.startswith('fatal'): | ||
163 | 81 | if 'already exists' not in stderr: | ||
164 | 82 | # different error | ||
165 | 83 | log('Error creating account', ERROR) | ||
166 | 84 | sys.exit(1) | ||
167 | 85 | else: | ||
168 | 86 | # retrieve user id and update keys | ||
169 | 87 | account_id = None | ||
170 | 88 | cmd = ('gerrit gsql --format json -c "SELECT account_id ' | ||
171 | 89 | 'FROM account_external_ids WHERE external_id=\'username:%s\'"' | ||
172 | 90 | % (user)) | ||
173 | 91 | stdout, stderr = self._run_cmd(cmd) | ||
174 | 92 | if not stderr: | ||
175 | 93 | # load and decode json, extract account id | ||
176 | 94 | lines = stdout.splitlines() | ||
177 | 95 | if len(lines)>0: | ||
178 | 96 | res = json.loads(lines[0]) | ||
179 | 97 | try: | ||
180 | 98 | account_id = res['columns']['account_id'] | ||
181 | 99 | except: | ||
182 | 100 | pass | ||
183 | 101 | |||
184 | 102 | # if found, update ssh keys | ||
185 | 103 | if account_id: | ||
186 | 104 | cmd = ('gerrit gsql -c "DELETE FROM account_ssh_keys ' | ||
187 | 105 | 'WHERE account_id=%s' % account_id) | ||
188 | 106 | stdout, stderr = self._run_cmd(cmd) | ||
189 | 107 | |||
190 | 108 | # insert new key | ||
191 | 109 | cmd = ('gerrit gsql -c "INSERT INTO account_ssh_keys ' | ||
192 | 110 | '(ssh_public_key, valid, account_id, seq) VALUES (\'%s\', \'Y\', ' | ||
193 | 111 | '\'%s\', 0)" ' % (ssh_key, account_id)) | ||
194 | 112 | stdout, stderr = self._run_cmd(cmd) | ||
195 | 113 | |||
196 | 114 | # reboot gerrit to refresh accounts | ||
197 | 115 | stop_gerrit() | ||
198 | 116 | start_gerrit() | ||
199 | 117 | |||
200 | 118 | def create_users_batch(self, group, users): | ||
201 | 119 | for user in users: | ||
202 | 120 | # sets container user, name, ssh, openid | ||
203 | 121 | login = user[0] | ||
204 | 122 | name = user[1] | ||
205 | 123 | email = user[2] | ||
206 | 124 | ssh = user[3] | ||
207 | 125 | openid = user[4] | ||
208 | 126 | |||
209 | 127 | cmd = (u'gerrit create-account %s --full-name "%s" ' | ||
210 | 128 | u'--group "%s" --email "%s"' % | ||
211 | 129 | (login, name, group, email)) | ||
212 | 130 | stdout, stderr = self._run_cmd(cmd) | ||
213 | 131 | |||
214 | 132 | if stderr.startswith('fatal'): | ||
215 | 133 | if 'already exists' not in stderr: | ||
216 | 134 | sys.exit(1) | ||
217 | 135 | |||
218 | 136 | # retrieve user id | ||
219 | 137 | account_id = None | ||
220 | 138 | cmd = ('gerrit gsql --format json -c "SELECT account_id ' | ||
221 | 139 | 'FROM account_external_ids WHERE external_id=\'username:%s\'"' | ||
222 | 140 | % (login)) | ||
223 | 141 | stdout, stderr = self._run_cmd(cmd) | ||
224 | 142 | if not stderr: | ||
225 | 143 | # load and decode json, extract account id | ||
226 | 144 | lines = stdout.splitlines() | ||
227 | 145 | if len(lines)>0: | ||
228 | 146 | res = json.loads(lines[0]) | ||
229 | 147 | try: | ||
230 | 148 | account_id = res['columns']['account_id'] | ||
231 | 149 | except: | ||
232 | 150 | pass | ||
233 | 151 | |||
234 | 152 | # if found, update ssh keys and openid | ||
235 | 153 | if account_id: | ||
236 | 154 | # remove old keys and add new | ||
237 | 155 | if len(ssh)>0: | ||
238 | 156 | cmd = ('gerrit gsql -c "DELETE FROM account_ssh_keys ' | ||
239 | 157 | 'WHERE account_id=%s AND ssh_public_key NOT IN (%s)"' % | ||
240 | 158 | (account_id, (', '.join('\''+item+'\'' for item in ssh)) )) | ||
241 | 159 | else: | ||
242 | 160 | cmd = ('gerrit gsql -c "DELETE FROM account_ssh_keys ' | ||
243 | 161 | 'WHERE account_id=%s' % account_id) | ||
244 | 162 | |||
245 | 163 | stdout, stderr = self._run_cmd(cmd) | ||
246 | 164 | |||
247 | 165 | num_key = 0 | ||
248 | 166 | for ssh_key in ssh: | ||
249 | 167 | # insert new keys | ||
250 | 168 | cmd = ('gerrit gsql -c "INSERT INTO account_ssh_keys ' | ||
251 | 169 | '(ssh_public_key, valid, account_id, seq) SELECT ' | ||
252 | 170 | '%(ssh_key)s, %(valid)s, %(account_id)s, %(num_key)s ' | ||
253 | 171 | 'WHERE NOT EXISTS (SELECT ' | ||
254 | 172 | 'account_id FROM account_ssh_keys WHERE ' | ||
255 | 173 | 'account_id=%(account_id)s AND ssh_public_key=%(ssh_key)s)"' % | ||
256 | 174 | {'ssh_key': '\''+ssh_key+'\'', 'valid':'\'Y\'', | ||
257 | 175 | 'account_id': '\''+account_id+'\'', 'num_key': num_key}) | ||
258 | 176 | num_key+=1 | ||
259 | 177 | stdout, stderr = self._run_cmd(cmd) | ||
260 | 178 | |||
261 | 179 | # replace external id | ||
262 | 180 | if openid: | ||
263 | 181 | openid = openid.replace('login.launchpad.net', 'login.ubuntu.com') | ||
264 | 182 | cmd = ('gerrit gsql -c "DELETE FROM account_external_ids ' | ||
265 | 183 | 'WHERE account_id=%s AND external_id NOT IN (%s) AND ' | ||
266 | 184 | 'external_id LIKE \'http%%\'"' % (account_id, '\''+openid+'\'')) | ||
267 | 185 | stdout, stderr = self._run_cmd(cmd) | ||
268 | 186 | |||
269 | 187 | # replace launchpad for ubuntu account | ||
270 | 188 | cmd = ('gerrit gsql -c "INSERT INTO account_external_ids ' | ||
271 | 189 | '(account_id, email_address, external_id) SELECT ' | ||
272 | 190 | '%(account_id)s, %(email_address)s, %(external_id)s WHERE ' | ||
273 | 191 | 'NOT EXISTS (SELECT account_id FROM account_external_ids ' | ||
274 | 192 | 'WHERE account_id=%(account_id)s AND external_id=%(external_id)s)"' % | ||
275 | 193 | {'account_id':'\''+account_id+'\'', | ||
276 | 194 | 'email_address':'\''+str(email)+'\'', | ||
277 | 195 | 'external_id': '\''+openid+'\''}) | ||
278 | 196 | stdout, stderr = self._run_cmd(cmd) | ||
279 | 197 | |||
280 | 198 | |||
281 | 199 | def create_project(self, project): | ||
282 | 200 | log('Creating gerrit project %s' % project) | ||
283 | 201 | cmd = ('gerrit create-project %s' % project) | ||
284 | 202 | stdout, stderr = self._run_cmd(cmd) | ||
285 | 203 | if not stdout and not stderr: | ||
286 | 204 | log('Created new project %s.' % project) | ||
287 | 205 | return True | ||
288 | 206 | else: | ||
289 | 207 | log('Error creating project %s, skipping project creation' % | ||
290 | 208 | project) | ||
291 | 209 | return False | ||
292 | 210 | |||
293 | 211 | def create_group(self, group): | ||
294 | 212 | log('Creating gerrit group %s' % group) | ||
295 | 213 | cmd = ('gerrit create-group %s' % group) | ||
296 | 214 | stdout, stderr = self._run_cmd(cmd) | ||
297 | 215 | if not stdout and not stderr: | ||
298 | 216 | log('Created new group %s.' % group) | ||
299 | 217 | |||
300 | 218 | def flush_cache(self): | ||
301 | 219 | cmd = ('gerrit flush-caches') | ||
302 | 220 | stdout, stderr = self._run_cmd(cmd) | ||
303 | 221 | 0 | ||
304 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/jenkins.py' | |||
305 | --- hooks/install.d/charmhelpers/canonical_ci/jenkins.py 2015-08-19 19:17:42 +0000 | |||
306 | +++ hooks/install.d/charmhelpers/canonical_ci/jenkins.py 1970-01-01 00:00:00 +0000 | |||
307 | @@ -1,41 +0,0 @@ | |||
308 | 1 | import logging | ||
309 | 2 | import os | ||
310 | 3 | import paramiko | ||
311 | 4 | import sys | ||
312 | 5 | import subprocess | ||
313 | 6 | import json | ||
314 | 7 | |||
315 | 8 | from charmhelpers.core.hookenv import ( | ||
316 | 9 | log as _log, | ||
317 | 10 | ERROR, | ||
318 | 11 | ) | ||
319 | 12 | |||
320 | 13 | JENKINS_DAEMON = "/etc/init.d/jenkins" | ||
321 | 14 | |||
322 | 15 | logging.basicConfig(level=logging.INFO) | ||
323 | 16 | |||
324 | 17 | |||
325 | 18 | def log(msg, level=None): | ||
326 | 19 | # wrap log calls and distribute to correct logger | ||
327 | 20 | # depending if this code is being run by a hook | ||
328 | 21 | # or an external script. | ||
329 | 22 | if os.getenv('JUJU_AGENT_SOCKET'): | ||
330 | 23 | _log(msg, level=level) | ||
331 | 24 | else: | ||
332 | 25 | logging.info(msg) | ||
333 | 26 | |||
334 | 27 | |||
335 | 28 | # start jenkins application | ||
336 | 29 | def start_jenkins(): | ||
337 | 30 | try: | ||
338 | 31 | subprocess.check_call([JENKINS_DAEMON, "start"]) | ||
339 | 32 | except: | ||
340 | 33 | pass | ||
341 | 34 | |||
342 | 35 | |||
343 | 36 | # stop jenkins application | ||
344 | 37 | def stop_jenkins(): | ||
345 | 38 | try: | ||
346 | 39 | subprocess.check_call([JENKINS_DAEMON, "stop"]) | ||
347 | 40 | except: | ||
348 | 41 | pass | ||
349 | 42 | 0 | ||
350 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/nrpe.py' | |||
351 | --- hooks/install.d/charmhelpers/canonical_ci/nrpe.py 2015-08-19 19:17:42 +0000 | |||
352 | +++ hooks/install.d/charmhelpers/canonical_ci/nrpe.py 1970-01-01 00:00:00 +0000 | |||
353 | @@ -1,66 +0,0 @@ | |||
354 | 1 | from charmhelpers.core.hookenv import log | ||
355 | 2 | |||
356 | 3 | |||
357 | 4 | HTTP_CHECK = """ | ||
358 | 5 | command[%(name)s]=/usr/lib/nagios/plugins/check_http | ||
359 | 6 | --hostname=%(hostname)s --port=%(port)s | ||
360 | 7 | """.strip().replace('\n', ' ') | ||
361 | 8 | |||
362 | 9 | |||
363 | 10 | TCP_CHECK = """ | ||
364 | 11 | command[%(name)s]=/usr/lib/nagios/plugins/check_tcp | ||
365 | 12 | --hostname=%(hostname)s --port=%(port)s | ||
366 | 13 | """.strip().replace('\n', ' ') | ||
367 | 14 | |||
368 | 15 | |||
369 | 16 | NRPE_SERVICE_ENTRY = """ | ||
370 | 17 | define service { | ||
371 | 18 | use active-service | ||
372 | 19 | host_name %(nagios_hostname)s | ||
373 | 20 | service_description %(nagios_hostname)s %(check_name)s | ||
374 | 21 | check_command check_nrpe!%(check_name)s | ||
375 | 22 | servicegroups %(nagios_servicegroup)s | ||
376 | 23 | } | ||
377 | 24 | |||
378 | 25 | """ | ||
379 | 26 | |||
380 | 27 | |||
381 | 28 | NRPE_CHECKS = { | ||
382 | 29 | 'http': HTTP_CHECK, | ||
383 | 30 | 'tcp': TCP_CHECK, | ||
384 | 31 | } | ||
385 | 32 | |||
386 | 33 | |||
387 | 34 | CONF_HEADER = "#"*80 + "\n# This file is Juju managed\n" + "#"*80 + '\n' | ||
388 | 35 | |||
389 | 36 | |||
390 | 37 | def nrpe_service_config(check_name, nagios_hostname, nagios_servicegroup): | ||
391 | 38 | """ | ||
392 | 39 | Generates a single snippet of nagios config for a monitored service. | ||
393 | 40 | Does not verify whether the check to use is actually configured. | ||
394 | 41 | """ | ||
395 | 42 | return NRPE_SERVICE_ENTRY % locals() | ||
396 | 43 | |||
397 | 44 | |||
398 | 45 | def nrpe_check(check_type, name, hostname, port, **kwargs): | ||
399 | 46 | """ | ||
400 | 47 | Generates a single NRPE check command for a given type. | ||
401 | 48 | |||
402 | 49 | name, hostname and port are currently required for all. | ||
403 | 50 | |||
404 | 51 | Any kwargs will be expanded to additional --k=v arguments, | ||
405 | 52 | or --k argument if value is True. | ||
406 | 53 | """ | ||
407 | 54 | try: | ||
408 | 55 | cmd = NRPE_CHECKS[check_type] | ||
409 | 56 | except KeyError: | ||
410 | 57 | e = 'Unsupported NRPE check type: %s.' % check_type | ||
411 | 58 | log(e) | ||
412 | 59 | raise Exception(e) | ||
413 | 60 | cmd = cmd % locals() | ||
414 | 61 | for k, v in kwargs.iteritems(): | ||
415 | 62 | if v is True: | ||
416 | 63 | cmd += ' --%s' % k | ||
417 | 64 | else: | ||
418 | 65 | cmd += ' --%s=%s' % (k, v) | ||
419 | 66 | return cmd | ||
420 | 67 | 0 | ||
421 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/ssh.py' | |||
422 | --- hooks/install.d/charmhelpers/canonical_ci/ssh.py 2015-08-19 19:17:42 +0000 | |||
423 | +++ hooks/install.d/charmhelpers/canonical_ci/ssh.py 1970-01-01 00:00:00 +0000 | |||
424 | @@ -1,37 +0,0 @@ | |||
425 | 1 | import os | ||
426 | 2 | import pwd | ||
427 | 3 | |||
428 | 4 | from subprocess import check_output | ||
429 | 5 | from charmhelpers.core.hookenv import log | ||
430 | 6 | |||
431 | 7 | |||
432 | 8 | def public_ssh_key(user='root', ssh_dir=None): | ||
433 | 9 | _ssh_dir = ssh_dir or os.path.join(pwd.getpwnam(user).pw_dir, '.ssh') | ||
434 | 10 | try: | ||
435 | 11 | with open(os.path.join(_ssh_dir, 'id_rsa.pub')) as key: | ||
436 | 12 | return key.read().strip() | ||
437 | 13 | except: | ||
438 | 14 | return None | ||
439 | 15 | |||
440 | 16 | |||
441 | 17 | def initialize_ssh_keys(user='root', ssh_dir=None): | ||
442 | 18 | home_dir = pwd.getpwnam(user).pw_dir | ||
443 | 19 | out_dir = ssh_dir or os.path.join(home_dir, '.ssh') | ||
444 | 20 | if not os.path.isdir(out_dir): | ||
445 | 21 | os.mkdir(out_dir) | ||
446 | 22 | |||
447 | 23 | priv_key = os.path.join(out_dir, 'id_rsa') | ||
448 | 24 | if not os.path.isfile(priv_key): | ||
449 | 25 | log('Generating new ssh key for user %s.' % user) | ||
450 | 26 | cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048', | ||
451 | 27 | '-f', priv_key] | ||
452 | 28 | check_output(cmd) | ||
453 | 29 | |||
454 | 30 | pub_key = '%s.pub' % priv_key | ||
455 | 31 | if not os.path.isfile(pub_key): | ||
456 | 32 | log('Generating missing ssh public key @ %s.' % pub_key) | ||
457 | 33 | cmd = ['ssh-keygen', '-y', '-f', priv_key] | ||
458 | 34 | p = check_output(cmd).strip() | ||
459 | 35 | with open(pub_key, 'wb') as out: | ||
460 | 36 | out.write(p) | ||
461 | 37 | check_output(['chown', '-R', user, out_dir]) | ||
462 | 38 | 0 | ||
463 | === removed file 'hooks/install.d/charmhelpers/canonical_ci/volume.py' | |||
464 | --- hooks/install.d/charmhelpers/canonical_ci/volume.py 2015-08-19 19:17:42 +0000 | |||
465 | +++ hooks/install.d/charmhelpers/canonical_ci/volume.py 1970-01-01 00:00:00 +0000 | |||
466 | @@ -1,223 +0,0 @@ | |||
467 | 1 | # Helpers to facilitate initializing and moving application data | ||
468 | 2 | # to persistent volumes. The bulk of it has been lifted directly | ||
469 | 3 | # from lp:charms/postgresql, with modification to volume_apply() | ||
470 | 4 | # to remove postgres specific bits. | ||
471 | 5 | # | ||
472 | 6 | # - Adam Gandelman <adamg@canonical.com> | ||
473 | 7 | |||
474 | 8 | import subprocess | ||
475 | 9 | import sys | ||
476 | 10 | import os | ||
477 | 11 | import time | ||
478 | 12 | import yaml | ||
479 | 13 | |||
480 | 14 | from charmhelpers.core import hookenv | ||
481 | 15 | |||
482 | 16 | from charmhelpers.core.hookenv import ( | ||
483 | 17 | config, WARNING, INFO, ERROR, CRITICAL, | ||
484 | 18 | log as _log, | ||
485 | 19 | ) | ||
486 | 20 | |||
487 | 21 | |||
488 | 22 | def log(level, msg): | ||
489 | 23 | msg = '[peristent storage] ' + msg | ||
490 | 24 | _log(level=level, message=msg) | ||
491 | 25 | |||
492 | 26 | |||
493 | 27 | def run(command, exit_on_error=True): | ||
494 | 28 | '''Run a command and return the output.''' | ||
495 | 29 | try: | ||
496 | 30 | log(INFO, command) | ||
497 | 31 | return subprocess.check_output( | ||
498 | 32 | command, stderr=subprocess.STDOUT, shell=True) | ||
499 | 33 | except subprocess.CalledProcessError, e: | ||
500 | 34 | log(ERROR, "status=%d, output=%s" % (e.returncode, e.output)) | ||
501 | 35 | if exit_on_error: | ||
502 | 36 | sys.exit(e.returncode) | ||
503 | 37 | else: | ||
504 | 38 | raise | ||
505 | 39 | |||
506 | 40 | |||
507 | 41 | ############################################################################### | ||
508 | 42 | # Volume managment | ||
509 | 43 | ############################################################################### | ||
510 | 44 | #------------------------------ | ||
511 | 45 | # Get volume-id from juju config "volume-map" dictionary as | ||
512 | 46 | # volume-map[JUJU_UNIT_NAME] | ||
513 | 47 | # @return volid | ||
514 | 48 | # | ||
515 | 49 | #------------------------------ | ||
516 | 50 | def volume_get_volid_from_volume_map(): | ||
517 | 51 | volume_map = {} | ||
518 | 52 | try: | ||
519 | 53 | volume_map = yaml.load(config('volume-map').strip()) | ||
520 | 54 | if volume_map: | ||
521 | 55 | return volume_map.get(os.environ['JUJU_UNIT_NAME']) | ||
522 | 56 | except yaml.constructor.ConstructorError as e: | ||
523 | 57 | log(WARNING, "invalid YAML in 'volume-map': {}".format(e)) | ||
524 | 58 | return None | ||
525 | 59 | |||
526 | 60 | |||
527 | 61 | # Is this volume_id permanent ? | ||
528 | 62 | # @returns True if volid set and not --ephemeral, else: | ||
529 | 63 | # False | ||
530 | 64 | def volume_is_permanent(volid): | ||
531 | 65 | if volid and volid != "--ephemeral": | ||
532 | 66 | return True | ||
533 | 67 | return False | ||
534 | 68 | |||
535 | 69 | |||
536 | 70 | # Do we have a valid storage state? | ||
537 | 71 | # @returns volid | ||
538 | 72 | # None config state is invalid - we should not serve | ||
539 | 73 | def volume_get_volume_id(): | ||
540 | 74 | ephemeral_storage = config('volume-ephemeral-storage') | ||
541 | 75 | volid = volume_get_volid_from_volume_map() | ||
542 | 76 | juju_unit_name = hookenv.local_unit() | ||
543 | 77 | if ephemeral_storage in [True, 'yes', 'Yes', 'true', 'True']: | ||
544 | 78 | if volid: | ||
545 | 79 | log(ERROR, | ||
546 | 80 | "volume-ephemeral-storage is True, but " + | ||
547 | 81 | "volume-map[{!r}] -> {}".format(juju_unit_name, volid),) | ||
548 | 82 | return None | ||
549 | 83 | else: | ||
550 | 84 | return "--ephemeral" | ||
551 | 85 | else: | ||
552 | 86 | if not volid: | ||
553 | 87 | log(WARNING, | ||
554 | 88 | "volume-ephemeral-storage is False, but " | ||
555 | 89 | "no volid found for volume-map[{!r}]".format( | ||
556 | 90 | hookenv.local_unit())) | ||
557 | 91 | return None | ||
558 | 92 | return volid | ||
559 | 93 | |||
560 | 94 | |||
561 | 95 | # Initialize and/or mount permanent storage, it straightly calls | ||
562 | 96 | # shell helper | ||
563 | 97 | def volume_init_and_mount(volid): | ||
564 | 98 | command = ("scripts/volume-common.sh call " + | ||
565 | 99 | "volume_init_and_mount %s" % volid) | ||
566 | 100 | output = run(command) | ||
567 | 101 | if output.find("ERROR") >= 0: | ||
568 | 102 | return False | ||
569 | 103 | return True | ||
570 | 104 | |||
571 | 105 | |||
572 | 106 | def volume_mount_point_from_volid(volid): | ||
573 | 107 | if volid and volume_is_permanent(volid): | ||
574 | 108 | return "/srv/juju/%s" % volid | ||
575 | 109 | return None | ||
576 | 110 | |||
577 | 111 | |||
578 | 112 | def volume_apply(data_directory_path, service, user, group): | ||
579 | 113 | # assumes service stopped. | ||
580 | 114 | volid = volume_get_volume_id() | ||
581 | 115 | if volid: | ||
582 | 116 | if volume_is_permanent(volid): | ||
583 | 117 | if not volume_init_and_mount(volid): | ||
584 | 118 | log(ERROR, | ||
585 | 119 | "volume_init_and_mount failed, not applying changes") | ||
586 | 120 | return False | ||
587 | 121 | |||
588 | 122 | if not os.path.exists(data_directory_path): | ||
589 | 123 | log(CRITICAL, | ||
590 | 124 | "postgresql data dir {} not found, " | ||
591 | 125 | "not applying changes.".format(data_directory_path)) | ||
592 | 126 | return False | ||
593 | 127 | |||
594 | 128 | mount_point = volume_mount_point_from_volid(volid) | ||
595 | 129 | # new data path consturcted as if mount_point were chroot, eg | ||
596 | 130 | # /srv/juju/vol-000010/var/lib/mysql | ||
597 | 131 | new_data_path = os.path.join( | ||
598 | 132 | mount_point, *data_directory_path.split('/')) | ||
599 | 133 | |||
600 | 134 | if not mount_point: | ||
601 | 135 | log(ERROR, | ||
602 | 136 | "invalid mount point from volid = {}, " | ||
603 | 137 | "not applying changes.".format(mount_point)) | ||
604 | 138 | return False | ||
605 | 139 | |||
606 | 140 | if ((os.path.islink(data_directory_path) and | ||
607 | 141 | os.readlink(data_directory_path) == new_data_path)): | ||
608 | 142 | log(INFO, | ||
609 | 143 | "%s data dir '%s' already points " | ||
610 | 144 | "to %s skipping storage changes." % ( | ||
611 | 145 | service, data_directory_path, new_data_path)) | ||
612 | 146 | log(INFO, | ||
613 | 147 | "existing-symlink: to fix/avoid UID changes from " | ||
614 | 148 | "previous units, doing: " | ||
615 | 149 | "chown -R %s:%s %s" % (user, group, new_data_path)) | ||
616 | 150 | run("chown -R %s:%s %s" % (user, group, new_data_path)) | ||
617 | 151 | return True | ||
618 | 152 | |||
619 | 153 | # Create new data directory path under mount point if required | ||
620 | 154 | # and set permissions. | ||
621 | 155 | # Create a directory structure below "new" mount_point, as e.g.: | ||
622 | 156 | # /srv/juju/vol-000012345/postgresql/9.1/main , which "mimics": | ||
623 | 157 | # /var/lib/postgresql/9.1/main | ||
624 | 158 | if not os.path.isdir(new_data_path): | ||
625 | 159 | log(INFO, "Creating new data path under mount: %s" % new_data_path) | ||
626 | 160 | os.makedirs(new_data_path) | ||
627 | 161 | |||
628 | 162 | # Ensure directory permissions on every run. | ||
629 | 163 | log(INFO, "Ensuring %s:%s ownership on %s." % | ||
630 | 164 | (user, group, new_data_path)) | ||
631 | 165 | run("chown -R %s:%s %s" % (user, group, new_data_path)) | ||
632 | 166 | |||
633 | 167 | # curr_dir_stat = os.stat(data_directory_path) | ||
634 | 168 | # os.chown(new_data_path, curr_dir_stat.st_uid, curr_dir_stat.st_gi | ||
635 | 169 | # os.chmod(new_data_path, curr_dir_stat.st_mode) | ||
636 | 170 | |||
637 | 171 | # for new_dir in [new_pg_dir, | ||
638 | 172 | # os.path.join(new_pg_dir, config("version")), | ||
639 | 173 | # new_pg_version_cluster_dir]: | ||
640 | 174 | # if not os.path.isdir(new_dir): | ||
641 | 175 | # log("mkdir %s".format(new_dir)) | ||
642 | 176 | # os.mkdir(new_dir) | ||
643 | 177 | # # copy permissions from current data_directory_path | ||
644 | 178 | # os.chown(new_dir, curr_dir_stat.st_uid, curr_dir_stat.st_gid) | ||
645 | 179 | # os.chmod(new_dir, curr_dir_stat.st_mode) | ||
646 | 180 | |||
647 | 181 | # Carefully build this symlink, e.g.: | ||
648 | 182 | # /var/lib/postgresql/9.1/main -> | ||
649 | 183 | # /srv/juju/vol-000012345/postgresql/9.1/main | ||
650 | 184 | # but keep previous "main/" directory, by renaming it to | ||
651 | 185 | # main-$TIMESTAMP | ||
652 | 186 | |||
653 | 187 | log(WARNING, "migrating application data {}/ -> {}/".format( | ||
654 | 188 | data_directory_path, new_data_path)) | ||
655 | 189 | |||
656 | 190 | command = "rsync -a {}/ {}/".format(data_directory_path, new_data_path) | ||
657 | 191 | log(INFO, "run: {}".format(command)) | ||
658 | 192 | run(command) | ||
659 | 193 | |||
660 | 194 | # if not os.path.exists(os.path.join( | ||
661 | 195 | # new_pg_version_cluster_dir, "PG_VERSION")): | ||
662 | 196 | # log("migrating PG data {}/ -> {}/".format( | ||
663 | 197 | # data_directory_path, new_pg_version_cluster_dir), WARNING) | ||
664 | 198 | # # void copying PID file to perm storage (shouldn't be any...) | ||
665 | 199 | # command = "rsync -a --exclude postmaster.pid {}/ {}/".format( | ||
666 | 200 | # data_directory_path, new_pg_version_cluster_dir) | ||
667 | 201 | # log("run: {}".format(command)) | ||
668 | 202 | # run(command) | ||
669 | 203 | |||
670 | 204 | try: | ||
671 | 205 | os.rename(data_directory_path, "{}-{}".format( | ||
672 | 206 | data_directory_path, int(time.time()))) | ||
673 | 207 | log(INFO, "symlinking {} -> {}".format( | ||
674 | 208 | new_data_path, data_directory_path)) | ||
675 | 209 | os.symlink(new_data_path, data_directory_path) | ||
676 | 210 | log(INFO, | ||
677 | 211 | "after-symlink: to fix/avoid UID changes from " | ||
678 | 212 | "previous units, doing: " | ||
679 | 213 | "chown -R %s:%s %s" % (user, group, new_data_path)) | ||
680 | 214 | run("chown -R %s:%s %s" % (user, group, new_data_path)) | ||
681 | 215 | return True | ||
682 | 216 | except OSError: | ||
683 | 217 | log(ERROR, "failed to symlink {} -> {}".format( | ||
684 | 218 | data_directory_path, new_data_path)) | ||
685 | 219 | return False | ||
686 | 220 | else: | ||
687 | 221 | log(ERROR, | ||
688 | 222 | "Invalid volume storage configuration, not applying changes") | ||
689 | 223 | return False | ||
690 | 224 | 0 | ||
691 | === removed directory 'hooks/install.d/charmhelpers/cli' | |||
692 | === removed file 'hooks/install.d/charmhelpers/cli/README.rst' | |||
693 | --- hooks/install.d/charmhelpers/cli/README.rst 2015-08-19 19:17:42 +0000 | |||
694 | +++ hooks/install.d/charmhelpers/cli/README.rst 1970-01-01 00:00:00 +0000 | |||
695 | @@ -1,57 +0,0 @@ | |||
696 | 1 | ========== | ||
697 | 2 | Commandant | ||
698 | 3 | ========== | ||
699 | 4 | |||
700 | 5 | ----------------------------------------------------- | ||
701 | 6 | Automatic command-line interfaces to Python functions | ||
702 | 7 | ----------------------------------------------------- | ||
703 | 8 | |||
704 | 9 | One of the benefits of ``libvirt`` is the uniformity of the interface: the C API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments. If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands. | ||
705 | 10 | |||
706 | 11 | Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment. By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life. | ||
707 | 12 | |||
708 | 13 | Goals | ||
709 | 14 | ===== | ||
710 | 15 | |||
711 | 16 | * Single decorator to expose a function as a command. | ||
712 | 17 | * now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW) | ||
713 | 18 | * Automatic analysis of function signature through ``inspect.getargspec()`` | ||
714 | 19 | * Command argument parser built automatically with ``argparse`` | ||
715 | 20 | * Interactive interpreter loop object made with ``Cmd`` | ||
716 | 21 | * Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps. | ||
717 | 22 | |||
718 | 23 | Other Important Features that need writing | ||
719 | 24 | ------------------------------------------ | ||
720 | 25 | |||
721 | 26 | * Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour | ||
722 | 27 | * The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc. | ||
723 | 28 | - Filename arguments are important, as good practice is for functions to accept file objects as parameters. | ||
724 | 29 | - choices arguments help to limit bad input before the function is called | ||
725 | 30 | * Some automatic behaviour could make for better defaults, once the user can override them. | ||
726 | 31 | - We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True. | ||
727 | 32 | - We could automatically support hyphens as alternates for underscores | ||
728 | 33 | - Arguments defaulting to sequence types could support the ``append`` action. | ||
729 | 34 | |||
730 | 35 | |||
731 | 36 | ----------------------------------------------------- | ||
732 | 37 | Implementing subcommands | ||
733 | 38 | ----------------------------------------------------- | ||
734 | 39 | |||
735 | 40 | (WIP) | ||
736 | 41 | |||
737 | 42 | So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose. | ||
738 | 43 | |||
739 | 44 | Some examples:: | ||
740 | 45 | |||
741 | 46 | from charmhelpers.cli import CommandLine | ||
742 | 47 | from charmhelpers.payload import execd | ||
743 | 48 | from charmhelpers.foo import bar | ||
744 | 49 | |||
745 | 50 | cli = CommandLine() | ||
746 | 51 | |||
747 | 52 | cli.subcommand(execd.execd_run) | ||
748 | 53 | |||
749 | 54 | @cli.subcommand_builder("bar", help="Bar baz qux") | ||
750 | 55 | def barcmd_builder(subparser): | ||
751 | 56 | subparser.add_argument('argument1', help="yackety") | ||
752 | 57 | return bar | ||
753 | 58 | 0 | ||
754 | === removed file 'hooks/install.d/charmhelpers/cli/__init__.py' | |||
755 | --- hooks/install.d/charmhelpers/cli/__init__.py 2015-08-19 19:17:42 +0000 | |||
756 | +++ hooks/install.d/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000 | |||
757 | @@ -1,147 +0,0 @@ | |||
758 | 1 | import inspect | ||
759 | 2 | import itertools | ||
760 | 3 | import argparse | ||
761 | 4 | import sys | ||
762 | 5 | |||
763 | 6 | |||
764 | 7 | class OutputFormatter(object): | ||
765 | 8 | def __init__(self, outfile=sys.stdout): | ||
766 | 9 | self.formats = ( | ||
767 | 10 | "raw", | ||
768 | 11 | "json", | ||
769 | 12 | "py", | ||
770 | 13 | "yaml", | ||
771 | 14 | "csv", | ||
772 | 15 | "tab", | ||
773 | 16 | ) | ||
774 | 17 | self.outfile = outfile | ||
775 | 18 | |||
776 | 19 | def add_arguments(self, argument_parser): | ||
777 | 20 | formatgroup = argument_parser.add_mutually_exclusive_group() | ||
778 | 21 | choices = self.supported_formats | ||
779 | 22 | formatgroup.add_argument("--format", metavar='FMT', | ||
780 | 23 | help="Select output format for returned data, " | ||
781 | 24 | "where FMT is one of: {}".format(choices), | ||
782 | 25 | choices=choices, default='raw') | ||
783 | 26 | for fmt in self.formats: | ||
784 | 27 | fmtfunc = getattr(self, fmt) | ||
785 | 28 | formatgroup.add_argument("-{}".format(fmt[0]), | ||
786 | 29 | "--{}".format(fmt), action='store_const', | ||
787 | 30 | const=fmt, dest='format', | ||
788 | 31 | help=fmtfunc.__doc__) | ||
789 | 32 | |||
790 | 33 | @property | ||
791 | 34 | def supported_formats(self): | ||
792 | 35 | return self.formats | ||
793 | 36 | |||
794 | 37 | def raw(self, output): | ||
795 | 38 | """Output data as raw string (default)""" | ||
796 | 39 | self.outfile.write(str(output)) | ||
797 | 40 | |||
798 | 41 | def py(self, output): | ||
799 | 42 | """Output data as a nicely-formatted python data structure""" | ||
800 | 43 | import pprint | ||
801 | 44 | pprint.pprint(output, stream=self.outfile) | ||
802 | 45 | |||
803 | 46 | def json(self, output): | ||
804 | 47 | """Output data in JSON format""" | ||
805 | 48 | import json | ||
806 | 49 | json.dump(output, self.outfile) | ||
807 | 50 | |||
808 | 51 | def yaml(self, output): | ||
809 | 52 | """Output data in YAML format""" | ||
810 | 53 | import yaml | ||
811 | 54 | yaml.safe_dump(output, self.outfile) | ||
812 | 55 | |||
813 | 56 | def csv(self, output): | ||
814 | 57 | """Output data as excel-compatible CSV""" | ||
815 | 58 | import csv | ||
816 | 59 | csvwriter = csv.writer(self.outfile) | ||
817 | 60 | csvwriter.writerows(output) | ||
818 | 61 | |||
819 | 62 | def tab(self, output): | ||
820 | 63 | """Output data in excel-compatible tab-delimited format""" | ||
821 | 64 | import csv | ||
822 | 65 | csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab) | ||
823 | 66 | csvwriter.writerows(output) | ||
824 | 67 | |||
825 | 68 | def format_output(self, output, fmt='raw'): | ||
826 | 69 | fmtfunc = getattr(self, fmt) | ||
827 | 70 | fmtfunc(output) | ||
828 | 71 | |||
829 | 72 | |||
830 | 73 | class CommandLine(object): | ||
831 | 74 | argument_parser = None | ||
832 | 75 | subparsers = None | ||
833 | 76 | formatter = None | ||
834 | 77 | |||
835 | 78 | def __init__(self): | ||
836 | 79 | if not self.argument_parser: | ||
837 | 80 | self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks') | ||
838 | 81 | if not self.formatter: | ||
839 | 82 | self.formatter = OutputFormatter() | ||
840 | 83 | self.formatter.add_arguments(self.argument_parser) | ||
841 | 84 | if not self.subparsers: | ||
842 | 85 | self.subparsers = self.argument_parser.add_subparsers(help='Commands') | ||
843 | 86 | |||
844 | 87 | def subcommand(self, command_name=None): | ||
845 | 88 | """ | ||
846 | 89 | Decorate a function as a subcommand. Use its arguments as the | ||
847 | 90 | command-line arguments""" | ||
848 | 91 | def wrapper(decorated): | ||
849 | 92 | cmd_name = command_name or decorated.__name__ | ||
850 | 93 | subparser = self.subparsers.add_parser(cmd_name, | ||
851 | 94 | description=decorated.__doc__) | ||
852 | 95 | for args, kwargs in describe_arguments(decorated): | ||
853 | 96 | subparser.add_argument(*args, **kwargs) | ||
854 | 97 | subparser.set_defaults(func=decorated) | ||
855 | 98 | return decorated | ||
856 | 99 | return wrapper | ||
857 | 100 | |||
858 | 101 | def subcommand_builder(self, command_name, description=None): | ||
859 | 102 | """ | ||
860 | 103 | Decorate a function that builds a subcommand. Builders should accept a | ||
861 | 104 | single argument (the subparser instance) and return the function to be | ||
862 | 105 | run as the command.""" | ||
863 | 106 | def wrapper(decorated): | ||
864 | 107 | subparser = self.subparsers.add_parser(command_name) | ||
865 | 108 | func = decorated(subparser) | ||
866 | 109 | subparser.set_defaults(func=func) | ||
867 | 110 | subparser.description = description or func.__doc__ | ||
868 | 111 | return wrapper | ||
869 | 112 | |||
870 | 113 | def run(self): | ||
871 | 114 | "Run cli, processing arguments and executing subcommands." | ||
872 | 115 | arguments = self.argument_parser.parse_args() | ||
873 | 116 | argspec = inspect.getargspec(arguments.func) | ||
874 | 117 | vargs = [] | ||
875 | 118 | kwargs = {} | ||
876 | 119 | if argspec.varargs: | ||
877 | 120 | vargs = getattr(arguments, argspec.varargs) | ||
878 | 121 | for arg in argspec.args: | ||
879 | 122 | kwargs[arg] = getattr(arguments, arg) | ||
880 | 123 | self.formatter.format_output(arguments.func(*vargs, **kwargs), arguments.format) | ||
881 | 124 | |||
882 | 125 | |||
883 | 126 | cmdline = CommandLine() | ||
884 | 127 | |||
885 | 128 | |||
886 | 129 | def describe_arguments(func): | ||
887 | 130 | """ | ||
888 | 131 | Analyze a function's signature and return a data structure suitable for | ||
889 | 132 | passing in as arguments to an argparse parser's add_argument() method.""" | ||
890 | 133 | |||
891 | 134 | argspec = inspect.getargspec(func) | ||
892 | 135 | # we should probably raise an exception somewhere if func includes **kwargs | ||
893 | 136 | if argspec.defaults: | ||
894 | 137 | positional_args = argspec.args[:-len(argspec.defaults)] | ||
895 | 138 | keyword_names = argspec.args[-len(argspec.defaults):] | ||
896 | 139 | for arg, default in itertools.izip(keyword_names, argspec.defaults): | ||
897 | 140 | yield ('--{}'.format(arg),), {'default': default} | ||
898 | 141 | else: | ||
899 | 142 | positional_args = argspec.args | ||
900 | 143 | |||
901 | 144 | for arg in positional_args: | ||
902 | 145 | yield (arg,), {} | ||
903 | 146 | if argspec.varargs: | ||
904 | 147 | yield (argspec.varargs,), {'nargs': '*'} | ||
905 | 148 | 0 | ||
906 | === removed file 'hooks/install.d/charmhelpers/cli/commands.py' | |||
907 | --- hooks/install.d/charmhelpers/cli/commands.py 2015-08-19 19:17:42 +0000 | |||
908 | +++ hooks/install.d/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000 | |||
909 | @@ -1,2 +0,0 @@ | |||
910 | 1 | from . import CommandLine | ||
911 | 2 | import host | ||
912 | 3 | 0 | ||
913 | === removed file 'hooks/install.d/charmhelpers/cli/host.py' | |||
914 | --- hooks/install.d/charmhelpers/cli/host.py 2015-08-19 19:17:42 +0000 | |||
915 | +++ hooks/install.d/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000 | |||
916 | @@ -1,14 +0,0 @@ | |||
917 | 1 | from . import cmdline | ||
918 | 2 | from charmhelpers.core import host | ||
919 | 3 | |||
920 | 4 | |||
921 | 5 | @cmdline.subcommand() | ||
922 | 6 | def mounts(): | ||
923 | 7 | "List mounts" | ||
924 | 8 | return host.mounts() | ||
925 | 9 | |||
926 | 10 | @cmdline.subcommand_builder('service', description="Control system services") | ||
927 | 11 | def service(subparser): | ||
928 | 12 | subparser.add_argument("action", help="The action to perform (start, stop, etc...)") | ||
929 | 13 | subparser.add_argument("service_name", help="Name of the service to control") | ||
930 | 14 | return host.service | ||
931 | 15 | 0 | ||
932 | === removed directory 'hooks/install.d/charmhelpers/contrib' | |||
933 | === removed file 'hooks/install.d/charmhelpers/contrib/__init__.py' | |||
934 | === removed directory 'hooks/install.d/charmhelpers/contrib/ansible' | |||
935 | === removed file 'hooks/install.d/charmhelpers/contrib/ansible/__init__.py' | |||
936 | --- hooks/install.d/charmhelpers/contrib/ansible/__init__.py 2015-08-19 19:17:42 +0000 | |||
937 | +++ hooks/install.d/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000 | |||
938 | @@ -1,101 +0,0 @@ | |||
939 | 1 | # Copyright 2013 Canonical Ltd. | ||
940 | 2 | # | ||
941 | 3 | # Authors: | ||
942 | 4 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
943 | 5 | """Charm Helpers ansible - declare the state of your machines. | ||
944 | 6 | |||
945 | 7 | This helper enables you to declare your machine state, rather than | ||
946 | 8 | program it procedurally (and have to test each change to your procedures). | ||
947 | 9 | Your install hook can be as simple as: | ||
948 | 10 | |||
949 | 11 | {{{ | ||
950 | 12 | import charmhelpers.contrib.ansible | ||
951 | 13 | |||
952 | 14 | |||
953 | 15 | def install(): | ||
954 | 16 | charmhelpers.contrib.ansible.install_ansible_support() | ||
955 | 17 | charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml') | ||
956 | 18 | }}} | ||
957 | 19 | |||
958 | 20 | and won't need to change (nor will its tests) when you change the machine | ||
959 | 21 | state. | ||
960 | 22 | |||
961 | 23 | All of your juju config and relation-data are available as template | ||
962 | 24 | variables within your playbooks and templates. An install playbook looks | ||
963 | 25 | something like: | ||
964 | 26 | |||
965 | 27 | {{{ | ||
966 | 28 | --- | ||
967 | 29 | - hosts: localhost | ||
968 | 30 | user: root | ||
969 | 31 | |||
970 | 32 | tasks: | ||
971 | 33 | - name: Add private repositories. | ||
972 | 34 | template: | ||
973 | 35 | src: ../templates/private-repositories.list.jinja2 | ||
974 | 36 | dest: /etc/apt/sources.list.d/private.list | ||
975 | 37 | |||
976 | 38 | - name: Update the cache. | ||
977 | 39 | apt: update_cache=yes | ||
978 | 40 | |||
979 | 41 | - name: Install dependencies. | ||
980 | 42 | apt: pkg={{ item }} | ||
981 | 43 | with_items: | ||
982 | 44 | - python-mimeparse | ||
983 | 45 | - python-webob | ||
984 | 46 | - sunburnt | ||
985 | 47 | |||
986 | 48 | - name: Setup groups. | ||
987 | 49 | group: name={{ item.name }} gid={{ item.gid }} | ||
988 | 50 | with_items: | ||
989 | 51 | - { name: 'deploy_user', gid: 1800 } | ||
990 | 52 | - { name: 'service_user', gid: 1500 } | ||
991 | 53 | |||
992 | 54 | ... | ||
993 | 55 | }}} | ||
994 | 56 | |||
995 | 57 | Read more online about playbooks[1] and standard ansible modules[2]. | ||
996 | 58 | |||
997 | 59 | [1] http://www.ansibleworks.com/docs/playbooks.html | ||
998 | 60 | [2] http://www.ansibleworks.com/docs/modules.html | ||
999 | 61 | """ | ||
1000 | 62 | import os | ||
1001 | 63 | import subprocess | ||
1002 | 64 | |||
1003 | 65 | import charmhelpers.contrib.saltstack | ||
1004 | 66 | import charmhelpers.core.host | ||
1005 | 67 | import charmhelpers.core.hookenv | ||
1006 | 68 | import charmhelpers.fetch | ||
1007 | 69 | |||
1008 | 70 | |||
1009 | 71 | charm_dir = os.environ.get('CHARM_DIR', '') | ||
1010 | 72 | ansible_hosts_path = '/etc/ansible/hosts' | ||
1011 | 73 | # Ansible will automatically include any vars in the following | ||
1012 | 74 | # file in its inventory when run locally. | ||
1013 | 75 | ansible_vars_path = '/etc/ansible/host_vars/localhost' | ||
1014 | 76 | |||
1015 | 77 | |||
1016 | 78 | def install_ansible_support(from_ppa=True): | ||
1017 | 79 | """Installs the ansible package. | ||
1018 | 80 | |||
1019 | 81 | By default it is installed from the PPA [1] linked from | ||
1020 | 82 | the ansible website [2]. | ||
1021 | 83 | |||
1022 | 84 | [1] https://launchpad.net/~rquillo/+archive/ansible | ||
1023 | 85 | [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian | ||
1024 | 86 | |||
1025 | 87 | If from_ppa is false, you must ensure that the package is available | ||
1026 | 88 | from a configured repository. | ||
1027 | 89 | """ | ||
1028 | 90 | if from_ppa: | ||
1029 | 91 | charmhelpers.fetch.add_source('ppa:rquillo/ansible') | ||
1030 | 92 | charmhelpers.fetch.apt_update(fatal=True) | ||
1031 | 93 | charmhelpers.fetch.apt_install('ansible') | ||
1032 | 94 | with open(ansible_hosts_path, 'w+') as hosts_file: | ||
1033 | 95 | hosts_file.write('localhost ansible_connection=local') | ||
1034 | 96 | |||
1035 | 97 | |||
1036 | 98 | def apply_playbook(playbook): | ||
1037 | 99 | charmhelpers.contrib.saltstack.juju_state_to_yaml( | ||
1038 | 100 | ansible_vars_path, namespace_separator='__') | ||
1039 | 101 | subprocess.check_call(['ansible-playbook', '-c', 'local', playbook]) | ||
1040 | 102 | 0 | ||
1041 | === removed directory 'hooks/install.d/charmhelpers/contrib/charmhelpers' | |||
1042 | === removed file 'hooks/install.d/charmhelpers/contrib/charmhelpers/IMPORT' | |||
1043 | --- hooks/install.d/charmhelpers/contrib/charmhelpers/IMPORT 2015-08-19 19:17:42 +0000 | |||
1044 | +++ hooks/install.d/charmhelpers/contrib/charmhelpers/IMPORT 1970-01-01 00:00:00 +0000 | |||
1045 | @@ -1,4 +0,0 @@ | |||
1046 | 1 | Source lp:charm-tools/trunk | ||
1047 | 2 | |||
1048 | 3 | charm-tools/helpers/python/charmhelpers/__init__.py -> charmhelpers/charmhelpers/contrib/charmhelpers/__init__.py | ||
1049 | 4 | charm-tools/helpers/python/charmhelpers/tests/test_charmhelpers.py -> charmhelpers/tests/contrib/charmhelpers/test_charmhelpers.py | ||
1050 | 5 | 0 | ||
1051 | === removed file 'hooks/install.d/charmhelpers/contrib/charmhelpers/__init__.py' | |||
1052 | --- hooks/install.d/charmhelpers/contrib/charmhelpers/__init__.py 2015-08-19 19:17:42 +0000 | |||
1053 | +++ hooks/install.d/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000 | |||
1054 | @@ -1,184 +0,0 @@ | |||
1055 | 1 | # Copyright 2012 Canonical Ltd. This software is licensed under the | ||
1056 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1057 | 3 | |||
1058 | 4 | import warnings | ||
1059 | 5 | warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) | ||
1060 | 6 | |||
1061 | 7 | """Helper functions for writing Juju charms in Python.""" | ||
1062 | 8 | |||
1063 | 9 | __metaclass__ = type | ||
1064 | 10 | __all__ = [ | ||
1065 | 11 | #'get_config', # core.hookenv.config() | ||
1066 | 12 | #'log', # core.hookenv.log() | ||
1067 | 13 | #'log_entry', # core.hookenv.log() | ||
1068 | 14 | #'log_exit', # core.hookenv.log() | ||
1069 | 15 | #'relation_get', # core.hookenv.relation_get() | ||
1070 | 16 | #'relation_set', # core.hookenv.relation_set() | ||
1071 | 17 | #'relation_ids', # core.hookenv.relation_ids() | ||
1072 | 18 | #'relation_list', # core.hookenv.relation_units() | ||
1073 | 19 | #'config_get', # core.hookenv.config() | ||
1074 | 20 | #'unit_get', # core.hookenv.unit_get() | ||
1075 | 21 | #'open_port', # core.hookenv.open_port() | ||
1076 | 22 | #'close_port', # core.hookenv.close_port() | ||
1077 | 23 | #'service_control', # core.host.service() | ||
1078 | 24 | 'unit_info', # client-side, NOT IMPLEMENTED | ||
1079 | 25 | 'wait_for_machine', # client-side, NOT IMPLEMENTED | ||
1080 | 26 | 'wait_for_page_contents', # client-side, NOT IMPLEMENTED | ||
1081 | 27 | 'wait_for_relation', # client-side, NOT IMPLEMENTED | ||
1082 | 28 | 'wait_for_unit', # client-side, NOT IMPLEMENTED | ||
1083 | 29 | ] | ||
1084 | 30 | |||
1085 | 31 | import operator | ||
1086 | 32 | from shelltoolbox import ( | ||
1087 | 33 | command, | ||
1088 | 34 | ) | ||
1089 | 35 | import tempfile | ||
1090 | 36 | import time | ||
1091 | 37 | import urllib2 | ||
1092 | 38 | import yaml | ||
1093 | 39 | |||
1094 | 40 | SLEEP_AMOUNT = 0.1 | ||
1095 | 41 | # We create a juju_status Command here because it makes testing much, | ||
1096 | 42 | # much easier. | ||
1097 | 43 | juju_status = lambda: command('juju')('status') | ||
1098 | 44 | |||
1099 | 45 | # re-implemented as charmhelpers.fetch.configure_sources() | ||
1100 | 46 | #def configure_source(update=False): | ||
1101 | 47 | # source = config_get('source') | ||
1102 | 48 | # if ((source.startswith('ppa:') or | ||
1103 | 49 | # source.startswith('cloud:') or | ||
1104 | 50 | # source.startswith('http:'))): | ||
1105 | 51 | # run('add-apt-repository', source) | ||
1106 | 52 | # if source.startswith("http:"): | ||
1107 | 53 | # run('apt-key', 'import', config_get('key')) | ||
1108 | 54 | # if update: | ||
1109 | 55 | # run('apt-get', 'update') | ||
1110 | 56 | |||
1111 | 57 | |||
1112 | 58 | # DEPRECATED: client-side only | ||
1113 | 59 | def make_charm_config_file(charm_config): | ||
1114 | 60 | charm_config_file = tempfile.NamedTemporaryFile() | ||
1115 | 61 | charm_config_file.write(yaml.dump(charm_config)) | ||
1116 | 62 | charm_config_file.flush() | ||
1117 | 63 | # The NamedTemporaryFile instance is returned instead of just the name | ||
1118 | 64 | # because we want to take advantage of garbage collection-triggered | ||
1119 | 65 | # deletion of the temp file when it goes out of scope in the caller. | ||
1120 | 66 | return charm_config_file | ||
1121 | 67 | |||
1122 | 68 | |||
1123 | 69 | # DEPRECATED: client-side only | ||
1124 | 70 | def unit_info(service_name, item_name, data=None, unit=None): | ||
1125 | 71 | if data is None: | ||
1126 | 72 | data = yaml.safe_load(juju_status()) | ||
1127 | 73 | service = data['services'].get(service_name) | ||
1128 | 74 | if service is None: | ||
1129 | 75 | # XXX 2012-02-08 gmb: | ||
1130 | 76 | # This allows us to cope with the race condition that we | ||
1131 | 77 | # have between deploying a service and having it come up in | ||
1132 | 78 | # `juju status`. We could probably do with cleaning it up so | ||
1133 | 79 | # that it fails a bit more noisily after a while. | ||
1134 | 80 | return '' | ||
1135 | 81 | units = service['units'] | ||
1136 | 82 | if unit is not None: | ||
1137 | 83 | item = units[unit][item_name] | ||
1138 | 84 | else: | ||
1139 | 85 | # It might seem odd to sort the units here, but we do it to | ||
1140 | 86 | # ensure that when no unit is specified, the first unit for the | ||
1141 | 87 | # service (or at least the one with the lowest number) is the | ||
1142 | 88 | # one whose data gets returned. | ||
1143 | 89 | sorted_unit_names = sorted(units.keys()) | ||
1144 | 90 | item = units[sorted_unit_names[0]][item_name] | ||
1145 | 91 | return item | ||
1146 | 92 | |||
1147 | 93 | |||
1148 | 94 | # DEPRECATED: client-side only | ||
1149 | 95 | def get_machine_data(): | ||
1150 | 96 | return yaml.safe_load(juju_status())['machines'] | ||
1151 | 97 | |||
1152 | 98 | |||
1153 | 99 | # DEPRECATED: client-side only | ||
1154 | 100 | def wait_for_machine(num_machines=1, timeout=300): | ||
1155 | 101 | """Wait `timeout` seconds for `num_machines` machines to come up. | ||
1156 | 102 | |||
1157 | 103 | This wait_for... function can be called by other wait_for functions | ||
1158 | 104 | whose timeouts might be too short in situations where only a bare | ||
1159 | 105 | Juju setup has been bootstrapped. | ||
1160 | 106 | |||
1161 | 107 | :return: A tuple of (num_machines, time_taken). This is used for | ||
1162 | 108 | testing. | ||
1163 | 109 | """ | ||
1164 | 110 | # You may think this is a hack, and you'd be right. The easiest way | ||
1165 | 111 | # to tell what environment we're working in (LXC vs EC2) is to check | ||
1166 | 112 | # the dns-name of the first machine. If it's localhost we're in LXC | ||
1167 | 113 | # and we can just return here. | ||
1168 | 114 | if get_machine_data()[0]['dns-name'] == 'localhost': | ||
1169 | 115 | return 1, 0 | ||
1170 | 116 | start_time = time.time() | ||
1171 | 117 | while True: | ||
1172 | 118 | # Drop the first machine, since it's the Zookeeper and that's | ||
1173 | 119 | # not a machine that we need to wait for. This will only work | ||
1174 | 120 | # for EC2 environments, which is why we return early above if | ||
1175 | 121 | # we're in LXC. | ||
1176 | 122 | machine_data = get_machine_data() | ||
1177 | 123 | non_zookeeper_machines = [ | ||
1178 | 124 | machine_data[key] for key in machine_data.keys()[1:]] | ||
1179 | 125 | if len(non_zookeeper_machines) >= num_machines: | ||
1180 | 126 | all_machines_running = True | ||
1181 | 127 | for machine in non_zookeeper_machines: | ||
1182 | 128 | if machine.get('instance-state') != 'running': | ||
1183 | 129 | all_machines_running = False | ||
1184 | 130 | break | ||
1185 | 131 | if all_machines_running: | ||
1186 | 132 | break | ||
1187 | 133 | if time.time() - start_time >= timeout: | ||
1188 | 134 | raise RuntimeError('timeout waiting for service to start') | ||
1189 | 135 | time.sleep(SLEEP_AMOUNT) | ||
1190 | 136 | return num_machines, time.time() - start_time | ||
1191 | 137 | |||
1192 | 138 | |||
1193 | 139 | # DEPRECATED: client-side only | ||
1194 | 140 | def wait_for_unit(service_name, timeout=480): | ||
1195 | 141 | """Wait `timeout` seconds for a given service name to come up.""" | ||
1196 | 142 | wait_for_machine(num_machines=1) | ||
1197 | 143 | start_time = time.time() | ||
1198 | 144 | while True: | ||
1199 | 145 | state = unit_info(service_name, 'agent-state') | ||
1200 | 146 | if 'error' in state or state == 'started': | ||
1201 | 147 | break | ||
1202 | 148 | if time.time() - start_time >= timeout: | ||
1203 | 149 | raise RuntimeError('timeout waiting for service to start') | ||
1204 | 150 | time.sleep(SLEEP_AMOUNT) | ||
1205 | 151 | if state != 'started': | ||
1206 | 152 | raise RuntimeError('unit did not start, agent-state: ' + state) | ||
1207 | 153 | |||
1208 | 154 | |||
1209 | 155 | # DEPRECATED: client-side only | ||
1210 | 156 | def wait_for_relation(service_name, relation_name, timeout=120): | ||
1211 | 157 | """Wait `timeout` seconds for a given relation to come up.""" | ||
1212 | 158 | start_time = time.time() | ||
1213 | 159 | while True: | ||
1214 | 160 | relation = unit_info(service_name, 'relations').get(relation_name) | ||
1215 | 161 | if relation is not None and relation['state'] == 'up': | ||
1216 | 162 | break | ||
1217 | 163 | if time.time() - start_time >= timeout: | ||
1218 | 164 | raise RuntimeError('timeout waiting for relation to be up') | ||
1219 | 165 | time.sleep(SLEEP_AMOUNT) | ||
1220 | 166 | |||
1221 | 167 | |||
1222 | 168 | # DEPRECATED: client-side only | ||
1223 | 169 | def wait_for_page_contents(url, contents, timeout=120, validate=None): | ||
1224 | 170 | if validate is None: | ||
1225 | 171 | validate = operator.contains | ||
1226 | 172 | start_time = time.time() | ||
1227 | 173 | while True: | ||
1228 | 174 | try: | ||
1229 | 175 | stream = urllib2.urlopen(url) | ||
1230 | 176 | except (urllib2.HTTPError, urllib2.URLError): | ||
1231 | 177 | pass | ||
1232 | 178 | else: | ||
1233 | 179 | page = stream.read() | ||
1234 | 180 | if validate(page, contents): | ||
1235 | 181 | return page | ||
1236 | 182 | if time.time() - start_time >= timeout: | ||
1237 | 183 | raise RuntimeError('timeout waiting for contents of ' + url) | ||
1238 | 184 | time.sleep(SLEEP_AMOUNT) | ||
1239 | 185 | 0 | ||
1240 | === removed directory 'hooks/install.d/charmhelpers/contrib/charmsupport' | |||
1241 | === removed file 'hooks/install.d/charmhelpers/contrib/charmsupport/IMPORT' | |||
1242 | --- hooks/install.d/charmhelpers/contrib/charmsupport/IMPORT 2015-08-19 19:17:42 +0000 | |||
1243 | +++ hooks/install.d/charmhelpers/contrib/charmsupport/IMPORT 1970-01-01 00:00:00 +0000 | |||
1244 | @@ -1,14 +0,0 @@ | |||
1245 | 1 | Source: lp:charmsupport/trunk | ||
1246 | 2 | |||
1247 | 3 | charmsupport/charmsupport/execd.py -> charm-helpers/charmhelpers/contrib/charmsupport/execd.py | ||
1248 | 4 | charmsupport/charmsupport/hookenv.py -> charm-helpers/charmhelpers/contrib/charmsupport/hookenv.py | ||
1249 | 5 | charmsupport/charmsupport/host.py -> charm-helpers/charmhelpers/contrib/charmsupport/host.py | ||
1250 | 6 | charmsupport/charmsupport/nrpe.py -> charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py | ||
1251 | 7 | charmsupport/charmsupport/volumes.py -> charm-helpers/charmhelpers/contrib/charmsupport/volumes.py | ||
1252 | 8 | |||
1253 | 9 | charmsupport/tests/test_execd.py -> charm-helpers/tests/contrib/charmsupport/test_execd.py | ||
1254 | 10 | charmsupport/tests/test_hookenv.py -> charm-helpers/tests/contrib/charmsupport/test_hookenv.py | ||
1255 | 11 | charmsupport/tests/test_host.py -> charm-helpers/tests/contrib/charmsupport/test_host.py | ||
1256 | 12 | charmsupport/tests/test_nrpe.py -> charm-helpers/tests/contrib/charmsupport/test_nrpe.py | ||
1257 | 13 | |||
1258 | 14 | charmsupport/bin/charmsupport -> charm-helpers/bin/contrib/charmsupport/charmsupport | ||
1259 | 15 | 0 | ||
1260 | === removed file 'hooks/install.d/charmhelpers/contrib/charmsupport/__init__.py' | |||
1261 | === removed file 'hooks/install.d/charmhelpers/contrib/charmsupport/nrpe.py' | |||
1262 | --- hooks/install.d/charmhelpers/contrib/charmsupport/nrpe.py 2015-08-19 19:17:42 +0000 | |||
1263 | +++ hooks/install.d/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 | |||
1264 | @@ -1,218 +0,0 @@ | |||
1265 | 1 | """Compatibility with the nrpe-external-master charm""" | ||
1266 | 2 | # Copyright 2012 Canonical Ltd. | ||
1267 | 3 | # | ||
1268 | 4 | # Authors: | ||
1269 | 5 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
1270 | 6 | |||
1271 | 7 | import subprocess | ||
1272 | 8 | import pwd | ||
1273 | 9 | import grp | ||
1274 | 10 | import os | ||
1275 | 11 | import re | ||
1276 | 12 | import shlex | ||
1277 | 13 | import yaml | ||
1278 | 14 | |||
1279 | 15 | from charmhelpers.core.hookenv import ( | ||
1280 | 16 | config, | ||
1281 | 17 | local_unit, | ||
1282 | 18 | log, | ||
1283 | 19 | relation_ids, | ||
1284 | 20 | relation_set, | ||
1285 | 21 | ) | ||
1286 | 22 | |||
1287 | 23 | from charmhelpers.core.host import service | ||
1288 | 24 | |||
1289 | 25 | # This module adds compatibility with the nrpe-external-master and plain nrpe | ||
1290 | 26 | # subordinate charms. To use it in your charm: | ||
1291 | 27 | # | ||
1292 | 28 | # 1. Update metadata.yaml | ||
1293 | 29 | # | ||
1294 | 30 | # provides: | ||
1295 | 31 | # (...) | ||
1296 | 32 | # nrpe-external-master: | ||
1297 | 33 | # interface: nrpe-external-master | ||
1298 | 34 | # scope: container | ||
1299 | 35 | # | ||
1300 | 36 | # and/or | ||
1301 | 37 | # | ||
1302 | 38 | # provides: | ||
1303 | 39 | # (...) | ||
1304 | 40 | # local-monitors: | ||
1305 | 41 | # interface: local-monitors | ||
1306 | 42 | # scope: container | ||
1307 | 43 | |||
1308 | 44 | # | ||
1309 | 45 | # 2. Add the following to config.yaml | ||
1310 | 46 | # | ||
1311 | 47 | # nagios_context: | ||
1312 | 48 | # default: "juju" | ||
1313 | 49 | # type: string | ||
1314 | 50 | # description: | | ||
1315 | 51 | # Used by the nrpe subordinate charms. | ||
1316 | 52 | # A string that will be prepended to instance name to set the host name | ||
1317 | 53 | # in nagios. So for instance the hostname would be something like: | ||
1318 | 54 | # juju-myservice-0 | ||
1319 | 55 | # If you're running multiple environments with the same services in them | ||
1320 | 56 | # this allows you to differentiate between them. | ||
1321 | 57 | # | ||
1322 | 58 | # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master | ||
1323 | 59 | # | ||
1324 | 60 | # 4. Update your hooks.py with something like this: | ||
1325 | 61 | # | ||
1326 | 62 | # from charmsupport.nrpe import NRPE | ||
1327 | 63 | # (...) | ||
1328 | 64 | # def update_nrpe_config(): | ||
1329 | 65 | # nrpe_compat = NRPE() | ||
1330 | 66 | # nrpe_compat.add_check( | ||
1331 | 67 | # shortname = "myservice", | ||
1332 | 68 | # description = "Check MyService", | ||
1333 | 69 | # check_cmd = "check_http -w 2 -c 10 http://localhost" | ||
1334 | 70 | # ) | ||
1335 | 71 | # nrpe_compat.add_check( | ||
1336 | 72 | # "myservice_other", | ||
1337 | 73 | # "Check for widget failures", | ||
1338 | 74 | # check_cmd = "/srv/myapp/scripts/widget_check" | ||
1339 | 75 | # ) | ||
1340 | 76 | # nrpe_compat.write() | ||
1341 | 77 | # | ||
1342 | 78 | # def config_changed(): | ||
1343 | 79 | # (...) | ||
1344 | 80 | # update_nrpe_config() | ||
1345 | 81 | # | ||
1346 | 82 | # def nrpe_external_master_relation_changed(): | ||
1347 | 83 | # update_nrpe_config() | ||
1348 | 84 | # | ||
1349 | 85 | # def local_monitors_relation_changed(): | ||
1350 | 86 | # update_nrpe_config() | ||
1351 | 87 | # | ||
1352 | 88 | # 5. ln -s hooks.py nrpe-external-master-relation-changed | ||
1353 | 89 | # ln -s hooks.py local-monitors-relation-changed | ||
1354 | 90 | |||
1355 | 91 | |||
1356 | 92 | class CheckException(Exception): | ||
1357 | 93 | pass | ||
1358 | 94 | |||
1359 | 95 | |||
1360 | 96 | class Check(object): | ||
1361 | 97 | shortname_re = '[A-Za-z0-9-_]+$' | ||
1362 | 98 | service_template = (""" | ||
1363 | 99 | #--------------------------------------------------- | ||
1364 | 100 | # This file is Juju managed | ||
1365 | 101 | #--------------------------------------------------- | ||
1366 | 102 | define service {{ | ||
1367 | 103 | use active-service | ||
1368 | 104 | host_name {nagios_hostname} | ||
1369 | 105 | service_description {nagios_hostname}[{shortname}] """ | ||
1370 | 106 | """{description} | ||
1371 | 107 | check_command check_nrpe!{command} | ||
1372 | 108 | servicegroups {nagios_servicegroup} | ||
1373 | 109 | }} | ||
1374 | 110 | """) | ||
1375 | 111 | |||
1376 | 112 | def __init__(self, shortname, description, check_cmd): | ||
1377 | 113 | super(Check, self).__init__() | ||
1378 | 114 | # XXX: could be better to calculate this from the service name | ||
1379 | 115 | if not re.match(self.shortname_re, shortname): | ||
1380 | 116 | raise CheckException("shortname must match {}".format( | ||
1381 | 117 | Check.shortname_re)) | ||
1382 | 118 | self.shortname = shortname | ||
1383 | 119 | self.command = "check_{}".format(shortname) | ||
1384 | 120 | # Note: a set of invalid characters is defined by the | ||
1385 | 121 | # Nagios server config | ||
1386 | 122 | # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= | ||
1387 | 123 | self.description = description | ||
1388 | 124 | self.check_cmd = self._locate_cmd(check_cmd) | ||
1389 | 125 | |||
1390 | 126 | def _locate_cmd(self, check_cmd): | ||
1391 | 127 | search_path = ( | ||
1392 | 128 | '/', | ||
1393 | 129 | os.path.join(os.environ['CHARM_DIR'], | ||
1394 | 130 | 'files/nrpe-external-master'), | ||
1395 | 131 | '/usr/lib/nagios/plugins', | ||
1396 | 132 | ) | ||
1397 | 133 | parts = shlex.split(check_cmd) | ||
1398 | 134 | for path in search_path: | ||
1399 | 135 | if os.path.exists(os.path.join(path, parts[0])): | ||
1400 | 136 | command = os.path.join(path, parts[0]) | ||
1401 | 137 | if len(parts) > 1: | ||
1402 | 138 | command += " " + " ".join(parts[1:]) | ||
1403 | 139 | return command | ||
1404 | 140 | log('Check command not found: {}'.format(parts[0])) | ||
1405 | 141 | return '' | ||
1406 | 142 | |||
1407 | 143 | def write(self, nagios_context, hostname): | ||
1408 | 144 | nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format( | ||
1409 | 145 | self.command) | ||
1410 | 146 | with open(nrpe_check_file, 'w') as nrpe_check_config: | ||
1411 | 147 | nrpe_check_config.write("# check {}\n".format(self.shortname)) | ||
1412 | 148 | nrpe_check_config.write("command[{}]={}\n".format( | ||
1413 | 149 | self.command, self.check_cmd)) | ||
1414 | 150 | |||
1415 | 151 | if not os.path.exists(NRPE.nagios_exportdir): | ||
1416 | 152 | log('Not writing service config as {} is not accessible'.format( | ||
1417 | 153 | NRPE.nagios_exportdir)) | ||
1418 | 154 | else: | ||
1419 | 155 | self.write_service_config(nagios_context, hostname) | ||
1420 | 156 | |||
1421 | 157 | def write_service_config(self, nagios_context, hostname): | ||
1422 | 158 | for f in os.listdir(NRPE.nagios_exportdir): | ||
1423 | 159 | if re.search('.*{}.cfg'.format(self.command), f): | ||
1424 | 160 | os.remove(os.path.join(NRPE.nagios_exportdir, f)) | ||
1425 | 161 | |||
1426 | 162 | templ_vars = { | ||
1427 | 163 | 'nagios_hostname': hostname, | ||
1428 | 164 | 'nagios_servicegroup': nagios_context, | ||
1429 | 165 | 'description': self.description, | ||
1430 | 166 | 'shortname': self.shortname, | ||
1431 | 167 | 'command': self.command, | ||
1432 | 168 | } | ||
1433 | 169 | nrpe_service_text = Check.service_template.format(**templ_vars) | ||
1434 | 170 | nrpe_service_file = '{}/service__{}_{}.cfg'.format( | ||
1435 | 171 | NRPE.nagios_exportdir, hostname, self.command) | ||
1436 | 172 | with open(nrpe_service_file, 'w') as nrpe_service_config: | ||
1437 | 173 | nrpe_service_config.write(str(nrpe_service_text)) | ||
1438 | 174 | |||
1439 | 175 | def run(self): | ||
1440 | 176 | subprocess.call(self.check_cmd) | ||
1441 | 177 | |||
1442 | 178 | |||
1443 | 179 | class NRPE(object): | ||
1444 | 180 | nagios_logdir = '/var/log/nagios' | ||
1445 | 181 | nagios_exportdir = '/var/lib/nagios/export' | ||
1446 | 182 | nrpe_confdir = '/etc/nagios/nrpe.d' | ||
1447 | 183 | |||
1448 | 184 | def __init__(self): | ||
1449 | 185 | super(NRPE, self).__init__() | ||
1450 | 186 | self.config = config() | ||
1451 | 187 | self.nagios_context = self.config['nagios_context'] | ||
1452 | 188 | self.unit_name = local_unit().replace('/', '-') | ||
1453 | 189 | self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) | ||
1454 | 190 | self.checks = [] | ||
1455 | 191 | |||
1456 | 192 | def add_check(self, *args, **kwargs): | ||
1457 | 193 | self.checks.append(Check(*args, **kwargs)) | ||
1458 | 194 | |||
1459 | 195 | def write(self): | ||
1460 | 196 | try: | ||
1461 | 197 | nagios_uid = pwd.getpwnam('nagios').pw_uid | ||
1462 | 198 | nagios_gid = grp.getgrnam('nagios').gr_gid | ||
1463 | 199 | except: | ||
1464 | 200 | log("Nagios user not set up, nrpe checks not updated") | ||
1465 | 201 | return | ||
1466 | 202 | |||
1467 | 203 | if not os.path.exists(NRPE.nagios_logdir): | ||
1468 | 204 | os.mkdir(NRPE.nagios_logdir) | ||
1469 | 205 | os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) | ||
1470 | 206 | |||
1471 | 207 | nrpe_monitors = {} | ||
1472 | 208 | monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} | ||
1473 | 209 | for nrpecheck in self.checks: | ||
1474 | 210 | nrpecheck.write(self.nagios_context, self.hostname) | ||
1475 | 211 | nrpe_monitors[nrpecheck.shortname] = { | ||
1476 | 212 | "command": nrpecheck.command, | ||
1477 | 213 | } | ||
1478 | 214 | |||
1479 | 215 | service('restart', 'nagios-nrpe-server') | ||
1480 | 216 | |||
1481 | 217 | for rid in relation_ids("local-monitors"): | ||
1482 | 218 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) | ||
1483 | 219 | 0 | ||
1484 | === removed file 'hooks/install.d/charmhelpers/contrib/charmsupport/volumes.py' | |||
1485 | --- hooks/install.d/charmhelpers/contrib/charmsupport/volumes.py 2015-08-19 19:17:42 +0000 | |||
1486 | +++ hooks/install.d/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000 | |||
1487 | @@ -1,156 +0,0 @@ | |||
1488 | 1 | ''' | ||
1489 | 2 | Functions for managing volumes in juju units. One volume is supported per unit. | ||
1490 | 3 | Subordinates may have their own storage, provided it is on its own partition. | ||
1491 | 4 | |||
1492 | 5 | Configuration stanzas: | ||
1493 | 6 | volume-ephemeral: | ||
1494 | 7 | type: boolean | ||
1495 | 8 | default: true | ||
1496 | 9 | description: > | ||
1497 | 10 | If false, a volume is mounted as sepecified in "volume-map" | ||
1498 | 11 | If true, ephemeral storage will be used, meaning that log data | ||
1499 | 12 | will only exist as long as the machine. YOU HAVE BEEN WARNED. | ||
1500 | 13 | volume-map: | ||
1501 | 14 | type: string | ||
1502 | 15 | default: {} | ||
1503 | 16 | description: > | ||
1504 | 17 | YAML map of units to device names, e.g: | ||
1505 | 18 | "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }" | ||
1506 | 19 | Service units will raise a configure-error if volume-ephemeral | ||
1507 | 20 | is 'true' and no volume-map value is set. Use 'juju set' to set a | ||
1508 | 21 | value and 'juju resolved' to complete configuration. | ||
1509 | 22 | |||
1510 | 23 | Usage: | ||
1511 | 24 | from charmsupport.volumes import configure_volume, VolumeConfigurationError | ||
1512 | 25 | from charmsupport.hookenv import log, ERROR | ||
1513 | 26 | def post_mount_hook(): | ||
1514 | 27 | stop_service('myservice') | ||
1515 | 28 | def post_mount_hook(): | ||
1516 | 29 | start_service('myservice') | ||
1517 | 30 | |||
1518 | 31 | if __name__ == '__main__': | ||
1519 | 32 | try: | ||
1520 | 33 | configure_volume(before_change=pre_mount_hook, | ||
1521 | 34 | after_change=post_mount_hook) | ||
1522 | 35 | except VolumeConfigurationError: | ||
1523 | 36 | log('Storage could not be configured', ERROR) | ||
1524 | 37 | ''' | ||
1525 | 38 | |||
1526 | 39 | # XXX: Known limitations | ||
1527 | 40 | # - fstab is neither consulted nor updated | ||
1528 | 41 | |||
1529 | 42 | import os | ||
1530 | 43 | from charmhelpers.core import hookenv | ||
1531 | 44 | from charmhelpers.core import host | ||
1532 | 45 | import yaml | ||
1533 | 46 | |||
1534 | 47 | |||
1535 | 48 | MOUNT_BASE = '/srv/juju/volumes' | ||
1536 | 49 | |||
1537 | 50 | |||
1538 | 51 | class VolumeConfigurationError(Exception): | ||
1539 | 52 | '''Volume configuration data is missing or invalid''' | ||
1540 | 53 | pass | ||
1541 | 54 | |||
1542 | 55 | |||
1543 | 56 | def get_config(): | ||
1544 | 57 | '''Gather and sanity-check volume configuration data''' | ||
1545 | 58 | volume_config = {} | ||
1546 | 59 | config = hookenv.config() | ||
1547 | 60 | |||
1548 | 61 | errors = False | ||
1549 | 62 | |||
1550 | 63 | if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'): | ||
1551 | 64 | volume_config['ephemeral'] = True | ||
1552 | 65 | else: | ||
1553 | 66 | volume_config['ephemeral'] = False | ||
1554 | 67 | |||
1555 | 68 | try: | ||
1556 | 69 | volume_map = yaml.safe_load(config.get('volume-map', '{}')) | ||
1557 | 70 | except yaml.YAMLError as e: | ||
1558 | 71 | hookenv.log("Error parsing YAML volume-map: {}".format(e), | ||
1559 | 72 | hookenv.ERROR) | ||
1560 | 73 | errors = True | ||
1561 | 74 | if volume_map is None: | ||
1562 | 75 | # probably an empty string | ||
1563 | 76 | volume_map = {} | ||
1564 | 77 | elif not isinstance(volume_map, dict): | ||
1565 | 78 | hookenv.log("Volume-map should be a dictionary, not {}".format( | ||
1566 | 79 | type(volume_map))) | ||
1567 | 80 | errors = True | ||
1568 | 81 | |||
1569 | 82 | volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME']) | ||
1570 | 83 | if volume_config['device'] and volume_config['ephemeral']: | ||
1571 | 84 | # asked for ephemeral storage but also defined a volume ID | ||
1572 | 85 | hookenv.log('A volume is defined for this unit, but ephemeral ' | ||
1573 | 86 | 'storage was requested', hookenv.ERROR) | ||
1574 | 87 | errors = True | ||
1575 | 88 | elif not volume_config['device'] and not volume_config['ephemeral']: | ||
1576 | 89 | # asked for permanent storage but did not define volume ID | ||
1577 | 90 | hookenv.log('Ephemeral storage was requested, but there is no volume ' | ||
1578 | 91 | 'defined for this unit.', hookenv.ERROR) | ||
1579 | 92 | errors = True | ||
1580 | 93 | |||
1581 | 94 | unit_mount_name = hookenv.local_unit().replace('/', '-') | ||
1582 | 95 | volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name) | ||
1583 | 96 | |||
1584 | 97 | if errors: | ||
1585 | 98 | return None | ||
1586 | 99 | return volume_config | ||
1587 | 100 | |||
1588 | 101 | |||
1589 | 102 | def mount_volume(config): | ||
1590 | 103 | if os.path.exists(config['mountpoint']): | ||
1591 | 104 | if not os.path.isdir(config['mountpoint']): | ||
1592 | 105 | hookenv.log('Not a directory: {}'.format(config['mountpoint'])) | ||
1593 | 106 | raise VolumeConfigurationError() | ||
1594 | 107 | else: | ||
1595 | 108 | host.mkdir(config['mountpoint']) | ||
1596 | 109 | if os.path.ismount(config['mountpoint']): | ||
1597 | 110 | unmount_volume(config) | ||
1598 | 111 | if not host.mount(config['device'], config['mountpoint'], persist=True): | ||
1599 | 112 | raise VolumeConfigurationError() | ||
1600 | 113 | |||
1601 | 114 | |||
1602 | 115 | def unmount_volume(config): | ||
1603 | 116 | if os.path.ismount(config['mountpoint']): | ||
1604 | 117 | if not host.umount(config['mountpoint'], persist=True): | ||
1605 | 118 | raise VolumeConfigurationError() | ||
1606 | 119 | |||
1607 | 120 | |||
1608 | 121 | def managed_mounts(): | ||
1609 | 122 | '''List of all mounted managed volumes''' | ||
1610 | 123 | return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts()) | ||
1611 | 124 | |||
1612 | 125 | |||
1613 | 126 | def configure_volume(before_change=lambda: None, after_change=lambda: None): | ||
1614 | 127 | '''Set up storage (or don't) according to the charm's volume configuration. | ||
1615 | 128 | Returns the mount point or "ephemeral". before_change and after_change | ||
1616 | 129 | are optional functions to be called if the volume configuration changes. | ||
1617 | 130 | ''' | ||
1618 | 131 | |||
1619 | 132 | config = get_config() | ||
1620 | 133 | if not config: | ||
1621 | 134 | hookenv.log('Failed to read volume configuration', hookenv.CRITICAL) | ||
1622 | 135 | raise VolumeConfigurationError() | ||
1623 | 136 | |||
1624 | 137 | if config['ephemeral']: | ||
1625 | 138 | if os.path.ismount(config['mountpoint']): | ||
1626 | 139 | before_change() | ||
1627 | 140 | unmount_volume(config) | ||
1628 | 141 | after_change() | ||
1629 | 142 | return 'ephemeral' | ||
1630 | 143 | else: | ||
1631 | 144 | # persistent storage | ||
1632 | 145 | if os.path.ismount(config['mountpoint']): | ||
1633 | 146 | mounts = dict(managed_mounts()) | ||
1634 | 147 | if mounts.get(config['mountpoint']) != config['device']: | ||
1635 | 148 | before_change() | ||
1636 | 149 | unmount_volume(config) | ||
1637 | 150 | mount_volume(config) | ||
1638 | 151 | after_change() | ||
1639 | 152 | else: | ||
1640 | 153 | before_change() | ||
1641 | 154 | mount_volume(config) | ||
1642 | 155 | after_change() | ||
1643 | 156 | return config['mountpoint'] | ||
1644 | 157 | 0 | ||
1645 | === removed directory 'hooks/install.d/charmhelpers/contrib/hahelpers' | |||
1646 | === removed file 'hooks/install.d/charmhelpers/contrib/hahelpers/__init__.py' | |||
1647 | === removed file 'hooks/install.d/charmhelpers/contrib/hahelpers/apache.py' | |||
1648 | --- hooks/install.d/charmhelpers/contrib/hahelpers/apache.py 2015-08-19 19:17:42 +0000 | |||
1649 | +++ hooks/install.d/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000 | |||
1650 | @@ -1,58 +0,0 @@ | |||
1651 | 1 | # | ||
1652 | 2 | # Copyright 2012 Canonical Ltd. | ||
1653 | 3 | # | ||
1654 | 4 | # This file is sourced from lp:openstack-charm-helpers | ||
1655 | 5 | # | ||
1656 | 6 | # Authors: | ||
1657 | 7 | # James Page <james.page@ubuntu.com> | ||
1658 | 8 | # Adam Gandelman <adamg@ubuntu.com> | ||
1659 | 9 | # | ||
1660 | 10 | |||
1661 | 11 | import subprocess | ||
1662 | 12 | |||
1663 | 13 | from charmhelpers.core.hookenv import ( | ||
1664 | 14 | config as config_get, | ||
1665 | 15 | relation_get, | ||
1666 | 16 | relation_ids, | ||
1667 | 17 | related_units as relation_list, | ||
1668 | 18 | log, | ||
1669 | 19 | INFO, | ||
1670 | 20 | ) | ||
1671 | 21 | |||
1672 | 22 | |||
1673 | 23 | def get_cert(): | ||
1674 | 24 | cert = config_get('ssl_cert') | ||
1675 | 25 | key = config_get('ssl_key') | ||
1676 | 26 | if not (cert and key): | ||
1677 | 27 | log("Inspecting identity-service relations for SSL certificate.", | ||
1678 | 28 | level=INFO) | ||
1679 | 29 | cert = key = None | ||
1680 | 30 | for r_id in relation_ids('identity-service'): | ||
1681 | 31 | for unit in relation_list(r_id): | ||
1682 | 32 | if not cert: | ||
1683 | 33 | cert = relation_get('ssl_cert', | ||
1684 | 34 | rid=r_id, unit=unit) | ||
1685 | 35 | if not key: | ||
1686 | 36 | key = relation_get('ssl_key', | ||
1687 | 37 | rid=r_id, unit=unit) | ||
1688 | 38 | return (cert, key) | ||
1689 | 39 | |||
1690 | 40 | |||
1691 | 41 | def get_ca_cert(): | ||
1692 | 42 | ca_cert = None | ||
1693 | 43 | log("Inspecting identity-service relations for CA SSL certificate.", | ||
1694 | 44 | level=INFO) | ||
1695 | 45 | for r_id in relation_ids('identity-service'): | ||
1696 | 46 | for unit in relation_list(r_id): | ||
1697 | 47 | if not ca_cert: | ||
1698 | 48 | ca_cert = relation_get('ca_cert', | ||
1699 | 49 | rid=r_id, unit=unit) | ||
1700 | 50 | return ca_cert | ||
1701 | 51 | |||
1702 | 52 | |||
1703 | 53 | def install_ca_cert(ca_cert): | ||
1704 | 54 | if ca_cert: | ||
1705 | 55 | with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt', | ||
1706 | 56 | 'w') as crt: | ||
1707 | 57 | crt.write(ca_cert) | ||
1708 | 58 | subprocess.check_call(['update-ca-certificates', '--fresh']) | ||
1709 | 59 | 0 | ||
1710 | === removed file 'hooks/install.d/charmhelpers/contrib/hahelpers/ceph.py' | |||
1711 | --- hooks/install.d/charmhelpers/contrib/hahelpers/ceph.py 2015-08-19 19:17:42 +0000 | |||
1712 | +++ hooks/install.d/charmhelpers/contrib/hahelpers/ceph.py 1970-01-01 00:00:00 +0000 | |||
1713 | @@ -1,294 +0,0 @@ | |||
1714 | 1 | # | ||
1715 | 2 | # Copyright 2012 Canonical Ltd. | ||
1716 | 3 | # | ||
1717 | 4 | # This file is sourced from lp:openstack-charm-helpers | ||
1718 | 5 | # | ||
1719 | 6 | # Authors: | ||
1720 | 7 | # James Page <james.page@ubuntu.com> | ||
1721 | 8 | # Adam Gandelman <adamg@ubuntu.com> | ||
1722 | 9 | # | ||
1723 | 10 | |||
1724 | 11 | import commands | ||
1725 | 12 | import os | ||
1726 | 13 | import shutil | ||
1727 | 14 | import time | ||
1728 | 15 | |||
1729 | 16 | from subprocess import ( | ||
1730 | 17 | check_call, | ||
1731 | 18 | check_output, | ||
1732 | 19 | CalledProcessError | ||
1733 | 20 | ) | ||
1734 | 21 | |||
1735 | 22 | from charmhelpers.core.hookenv import ( | ||
1736 | 23 | relation_get, | ||
1737 | 24 | relation_ids, | ||
1738 | 25 | related_units, | ||
1739 | 26 | log, | ||
1740 | 27 | INFO, | ||
1741 | 28 | ERROR | ||
1742 | 29 | ) | ||
1743 | 30 | |||
1744 | 31 | from charmhelpers.fetch import ( | ||
1745 | 32 | apt_install, | ||
1746 | 33 | ) | ||
1747 | 34 | |||
1748 | 35 | from charmhelpers.core.host import ( | ||
1749 | 36 | mount, | ||
1750 | 37 | mounts, | ||
1751 | 38 | service_start, | ||
1752 | 39 | service_stop, | ||
1753 | 40 | umount, | ||
1754 | 41 | ) | ||
1755 | 42 | |||
1756 | 43 | KEYRING = '/etc/ceph/ceph.client.%s.keyring' | ||
1757 | 44 | KEYFILE = '/etc/ceph/ceph.client.%s.key' | ||
1758 | 45 | |||
1759 | 46 | CEPH_CONF = """[global] | ||
1760 | 47 | auth supported = %(auth)s | ||
1761 | 48 | keyring = %(keyring)s | ||
1762 | 49 | mon host = %(mon_hosts)s | ||
1763 | 50 | """ | ||
1764 | 51 | |||
1765 | 52 | |||
1766 | 53 | def running(service): | ||
1767 | 54 | # this local util can be dropped as soon the following branch lands | ||
1768 | 55 | # in lp:charm-helpers | ||
1769 | 56 | # https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/ | ||
1770 | 57 | try: | ||
1771 | 58 | output = check_output(['service', service, 'status']) | ||
1772 | 59 | except CalledProcessError: | ||
1773 | 60 | return False | ||
1774 | 61 | else: | ||
1775 | 62 | if ("start/running" in output or "is running" in output): | ||
1776 | 63 | return True | ||
1777 | 64 | else: | ||
1778 | 65 | return False | ||
1779 | 66 | |||
1780 | 67 | |||
1781 | 68 | def install(): | ||
1782 | 69 | ceph_dir = "/etc/ceph" | ||
1783 | 70 | if not os.path.isdir(ceph_dir): | ||
1784 | 71 | os.mkdir(ceph_dir) | ||
1785 | 72 | apt_install('ceph-common', fatal=True) | ||
1786 | 73 | |||
1787 | 74 | |||
1788 | 75 | def rbd_exists(service, pool, rbd_img): | ||
1789 | 76 | (rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' % | ||
1790 | 77 | (service, pool)) | ||
1791 | 78 | return rbd_img in out | ||
1792 | 79 | |||
1793 | 80 | |||
1794 | 81 | def create_rbd_image(service, pool, image, sizemb): | ||
1795 | 82 | cmd = [ | ||
1796 | 83 | 'rbd', | ||
1797 | 84 | 'create', | ||
1798 | 85 | image, | ||
1799 | 86 | '--size', | ||
1800 | 87 | str(sizemb), | ||
1801 | 88 | '--id', | ||
1802 | 89 | service, | ||
1803 | 90 | '--pool', | ||
1804 | 91 | pool | ||
1805 | 92 | ] | ||
1806 | 93 | check_call(cmd) | ||
1807 | 94 | |||
1808 | 95 | |||
1809 | 96 | def pool_exists(service, name): | ||
1810 | 97 | (rc, out) = commands.getstatusoutput("rados --id %s lspools" % service) | ||
1811 | 98 | return name in out | ||
1812 | 99 | |||
1813 | 100 | |||
1814 | 101 | def create_pool(service, name): | ||
1815 | 102 | cmd = [ | ||
1816 | 103 | 'rados', | ||
1817 | 104 | '--id', | ||
1818 | 105 | service, | ||
1819 | 106 | 'mkpool', | ||
1820 | 107 | name | ||
1821 | 108 | ] | ||
1822 | 109 | check_call(cmd) | ||
1823 | 110 | |||
1824 | 111 | |||
1825 | 112 | def keyfile_path(service): | ||
1826 | 113 | return KEYFILE % service | ||
1827 | 114 | |||
1828 | 115 | |||
1829 | 116 | def keyring_path(service): | ||
1830 | 117 | return KEYRING % service | ||
1831 | 118 | |||
1832 | 119 | |||
1833 | 120 | def create_keyring(service, key): | ||
1834 | 121 | keyring = keyring_path(service) | ||
1835 | 122 | if os.path.exists(keyring): | ||
1836 | 123 | log('ceph: Keyring exists at %s.' % keyring, level=INFO) | ||
1837 | 124 | cmd = [ | ||
1838 | 125 | 'ceph-authtool', | ||
1839 | 126 | keyring, | ||
1840 | 127 | '--create-keyring', | ||
1841 | 128 | '--name=client.%s' % service, | ||
1842 | 129 | '--add-key=%s' % key | ||
1843 | 130 | ] | ||
1844 | 131 | check_call(cmd) | ||
1845 | 132 | log('ceph: Created new ring at %s.' % keyring, level=INFO) | ||
1846 | 133 | |||
1847 | 134 | |||
1848 | 135 | def create_key_file(service, key): | ||
1849 | 136 | # create a file containing the key | ||
1850 | 137 | keyfile = keyfile_path(service) | ||
1851 | 138 | if os.path.exists(keyfile): | ||
1852 | 139 | log('ceph: Keyfile exists at %s.' % keyfile, level=INFO) | ||
1853 | 140 | fd = open(keyfile, 'w') | ||
1854 | 141 | fd.write(key) | ||
1855 | 142 | fd.close() | ||
1856 | 143 | log('ceph: Created new keyfile at %s.' % keyfile, level=INFO) | ||
1857 | 144 | |||
1858 | 145 | |||
1859 | 146 | def get_ceph_nodes(): | ||
1860 | 147 | hosts = [] | ||
1861 | 148 | for r_id in relation_ids('ceph'): | ||
1862 | 149 | for unit in related_units(r_id): | ||
1863 | 150 | hosts.append(relation_get('private-address', unit=unit, rid=r_id)) | ||
1864 | 151 | return hosts | ||
1865 | 152 | |||
1866 | 153 | |||
1867 | 154 | def configure(service, key, auth): | ||
1868 | 155 | create_keyring(service, key) | ||
1869 | 156 | create_key_file(service, key) | ||
1870 | 157 | hosts = get_ceph_nodes() | ||
1871 | 158 | mon_hosts = ",".join(map(str, hosts)) | ||
1872 | 159 | keyring = keyring_path(service) | ||
1873 | 160 | with open('/etc/ceph/ceph.conf', 'w') as ceph_conf: | ||
1874 | 161 | ceph_conf.write(CEPH_CONF % locals()) | ||
1875 | 162 | modprobe_kernel_module('rbd') | ||
1876 | 163 | |||
1877 | 164 | |||
1878 | 165 | def image_mapped(image_name): | ||
1879 | 166 | (rc, out) = commands.getstatusoutput('rbd showmapped') | ||
1880 | 167 | return image_name in out | ||
1881 | 168 | |||
1882 | 169 | |||
1883 | 170 | def map_block_storage(service, pool, image): | ||
1884 | 171 | cmd = [ | ||
1885 | 172 | 'rbd', | ||
1886 | 173 | 'map', | ||
1887 | 174 | '%s/%s' % (pool, image), | ||
1888 | 175 | '--user', | ||
1889 | 176 | service, | ||
1890 | 177 | '--secret', | ||
1891 | 178 | keyfile_path(service), | ||
1892 | 179 | ] | ||
1893 | 180 | check_call(cmd) | ||
1894 | 181 | |||
1895 | 182 | |||
1896 | 183 | def filesystem_mounted(fs): | ||
1897 | 184 | return fs in [f for m, f in mounts()] | ||
1898 | 185 | |||
1899 | 186 | |||
1900 | 187 | def make_filesystem(blk_device, fstype='ext4', timeout=10): | ||
1901 | 188 | count = 0 | ||
1902 | 189 | e_noent = os.errno.ENOENT | ||
1903 | 190 | while not os.path.exists(blk_device): | ||
1904 | 191 | if count >= timeout: | ||
1905 | 192 | log('ceph: gave up waiting on block device %s' % blk_device, | ||
1906 | 193 | level=ERROR) | ||
1907 | 194 | raise IOError(e_noent, os.strerror(e_noent), blk_device) | ||
1908 | 195 | log('ceph: waiting for block device %s to appear' % blk_device, | ||
1909 | 196 | level=INFO) | ||
1910 | 197 | count += 1 | ||
1911 | 198 | time.sleep(1) | ||
1912 | 199 | else: | ||
1913 | 200 | log('ceph: Formatting block device %s as filesystem %s.' % | ||
1914 | 201 | (blk_device, fstype), level=INFO) | ||
1915 | 202 | check_call(['mkfs', '-t', fstype, blk_device]) | ||
1916 | 203 | |||
1917 | 204 | |||
1918 | 205 | def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'): | ||
1919 | 206 | # mount block device into /mnt | ||
1920 | 207 | mount(blk_device, '/mnt') | ||
1921 | 208 | |||
1922 | 209 | # copy data to /mnt | ||
1923 | 210 | try: | ||
1924 | 211 | copy_files(data_src_dst, '/mnt') | ||
1925 | 212 | except: | ||
1926 | 213 | pass | ||
1927 | 214 | |||
1928 | 215 | # umount block device | ||
1929 | 216 | umount('/mnt') | ||
1930 | 217 | |||
1931 | 218 | _dir = os.stat(data_src_dst) | ||
1932 | 219 | uid = _dir.st_uid | ||
1933 | 220 | gid = _dir.st_gid | ||
1934 | 221 | |||
1935 | 222 | # re-mount where the data should originally be | ||
1936 | 223 | mount(blk_device, data_src_dst, persist=True) | ||
1937 | 224 | |||
1938 | 225 | # ensure original ownership of new mount. | ||
1939 | 226 | cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst] | ||
1940 | 227 | check_call(cmd) | ||
1941 | 228 | |||
1942 | 229 | |||
1943 | 230 | # TODO: re-use | ||
1944 | 231 | def modprobe_kernel_module(module): | ||
1945 | 232 | log('ceph: Loading kernel module', level=INFO) | ||
1946 | 233 | cmd = ['modprobe', module] | ||
1947 | 234 | check_call(cmd) | ||
1948 | 235 | cmd = 'echo %s >> /etc/modules' % module | ||
1949 | 236 | check_call(cmd, shell=True) | ||
1950 | 237 | |||
1951 | 238 | |||
1952 | 239 | def copy_files(src, dst, symlinks=False, ignore=None): | ||
1953 | 240 | for item in os.listdir(src): | ||
1954 | 241 | s = os.path.join(src, item) | ||
1955 | 242 | d = os.path.join(dst, item) | ||
1956 | 243 | if os.path.isdir(s): | ||
1957 | 244 | shutil.copytree(s, d, symlinks, ignore) | ||
1958 | 245 | else: | ||
1959 | 246 | shutil.copy2(s, d) | ||
1960 | 247 | |||
1961 | 248 | |||
1962 | 249 | def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point, | ||
1963 | 250 | blk_device, fstype, system_services=[]): | ||
1964 | 251 | """ | ||
1965 | 252 | To be called from the current cluster leader. | ||
1966 | 253 | Ensures given pool and RBD image exists, is mapped to a block device, | ||
1967 | 254 | and the device is formatted and mounted at the given mount_point. | ||
1968 | 255 | |||
1969 | 256 | If formatting a device for the first time, data existing at mount_point | ||
1970 | 257 | will be migrated to the RBD device before being remounted. | ||
1971 | 258 | |||
1972 | 259 | All services listed in system_services will be stopped prior to data | ||
1973 | 260 | migration and restarted when complete. | ||
1974 | 261 | """ | ||
1975 | 262 | # Ensure pool, RBD image, RBD mappings are in place. | ||
1976 | 263 | if not pool_exists(service, pool): | ||
1977 | 264 | log('ceph: Creating new pool %s.' % pool, level=INFO) | ||
1978 | 265 | create_pool(service, pool) | ||
1979 | 266 | |||
1980 | 267 | if not rbd_exists(service, pool, rbd_img): | ||
1981 | 268 | log('ceph: Creating RBD image (%s).' % rbd_img, level=INFO) | ||
1982 | 269 | create_rbd_image(service, pool, rbd_img, sizemb) | ||
1983 | 270 | |||
1984 | 271 | if not image_mapped(rbd_img): | ||
1985 | 272 | log('ceph: Mapping RBD Image as a Block Device.', level=INFO) | ||
1986 | 273 | map_block_storage(service, pool, rbd_img) | ||
1987 | 274 | |||
1988 | 275 | # make file system | ||
1989 | 276 | # TODO: What happens if for whatever reason this is run again and | ||
1990 | 277 | # the data is already in the rbd device and/or is mounted?? | ||
1991 | 278 | # When it is mounted already, it will fail to make the fs | ||
1992 | 279 | # XXX: This is really sketchy! Need to at least add an fstab entry | ||
1993 | 280 | # otherwise this hook will blow away existing data if its executed | ||
1994 | 281 | # after a reboot. | ||
1995 | 282 | if not filesystem_mounted(mount_point): | ||
1996 | 283 | make_filesystem(blk_device, fstype) | ||
1997 | 284 | |||
1998 | 285 | for svc in system_services: | ||
1999 | 286 | if running(svc): | ||
2000 | 287 | log('Stopping services %s prior to migrating data.' % svc, | ||
2001 | 288 | level=INFO) | ||
2002 | 289 | service_stop(svc) | ||
2003 | 290 | |||
2004 | 291 | place_data_on_ceph(service, blk_device, mount_point, fstype) | ||
2005 | 292 | |||
2006 | 293 | for svc in system_services: | ||
2007 | 294 | service_start(svc) | ||
2008 | 295 | 0 | ||
2009 | === removed file 'hooks/install.d/charmhelpers/contrib/hahelpers/cluster.py' | |||
2010 | --- hooks/install.d/charmhelpers/contrib/hahelpers/cluster.py 2015-08-19 19:17:42 +0000 | |||
2011 | +++ hooks/install.d/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000 | |||
2012 | @@ -1,181 +0,0 @@ | |||
2013 | 1 | # | ||
2014 | 2 | # Copyright 2012 Canonical Ltd. | ||
2015 | 3 | # | ||
2016 | 4 | # Authors: | ||
2017 | 5 | # James Page <james.page@ubuntu.com> | ||
2018 | 6 | # Adam Gandelman <adamg@ubuntu.com> | ||
2019 | 7 | # | ||
2020 | 8 | |||
2021 | 9 | import subprocess | ||
2022 | 10 | import os | ||
2023 | 11 | |||
2024 | 12 | from socket import gethostname as get_unit_hostname | ||
2025 | 13 | |||
2026 | 14 | from charmhelpers.core.hookenv import ( | ||
2027 | 15 | log, | ||
2028 | 16 | relation_ids, | ||
2029 | 17 | related_units as relation_list, | ||
2030 | 18 | relation_get, | ||
2031 | 19 | config as config_get, | ||
2032 | 20 | INFO, | ||
2033 | 21 | ERROR, | ||
2034 | 22 | unit_get, | ||
2035 | 23 | ) | ||
2036 | 24 | |||
2037 | 25 | |||
2038 | 26 | class HAIncompleteConfig(Exception): | ||
2039 | 27 | pass | ||
2040 | 28 | |||
2041 | 29 | |||
2042 | 30 | def is_clustered(): | ||
2043 | 31 | for r_id in (relation_ids('ha') or []): | ||
2044 | 32 | for unit in (relation_list(r_id) or []): | ||
2045 | 33 | clustered = relation_get('clustered', | ||
2046 | 34 | rid=r_id, | ||
2047 | 35 | unit=unit) | ||
2048 | 36 | if clustered: | ||
2049 | 37 | return True | ||
2050 | 38 | return False | ||
2051 | 39 | |||
2052 | 40 | |||
2053 | 41 | def is_leader(resource): | ||
2054 | 42 | cmd = [ | ||
2055 | 43 | "crm", "resource", | ||
2056 | 44 | "show", resource | ||
2057 | 45 | ] | ||
2058 | 46 | try: | ||
2059 | 47 | status = subprocess.check_output(cmd) | ||
2060 | 48 | except subprocess.CalledProcessError: | ||
2061 | 49 | return False | ||
2062 | 50 | else: | ||
2063 | 51 | if get_unit_hostname() in status: | ||
2064 | 52 | return True | ||
2065 | 53 | else: | ||
2066 | 54 | return False | ||
2067 | 55 | |||
2068 | 56 | |||
2069 | 57 | def peer_units(): | ||
2070 | 58 | peers = [] | ||
2071 | 59 | for r_id in (relation_ids('cluster') or []): | ||
2072 | 60 | for unit in (relation_list(r_id) or []): | ||
2073 | 61 | peers.append(unit) | ||
2074 | 62 | return peers | ||
2075 | 63 | |||
2076 | 64 | |||
2077 | 65 | def oldest_peer(peers): | ||
2078 | 66 | local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1]) | ||
2079 | 67 | for peer in peers: | ||
2080 | 68 | remote_unit_no = int(peer.split('/')[1]) | ||
2081 | 69 | if remote_unit_no < local_unit_no: | ||
2082 | 70 | return False | ||
2083 | 71 | return True | ||
2084 | 72 | |||
2085 | 73 | |||
2086 | 74 | def eligible_leader(resource): | ||
2087 | 75 | if is_clustered(): | ||
2088 | 76 | if not is_leader(resource): | ||
2089 | 77 | log('Deferring action to CRM leader.', level=INFO) | ||
2090 | 78 | return False | ||
2091 | 79 | else: | ||
2092 | 80 | peers = peer_units() | ||
2093 | 81 | if peers and not oldest_peer(peers): | ||
2094 | 82 | log('Deferring action to oldest service unit.', level=INFO) | ||
2095 | 83 | return False | ||
2096 | 84 | return True | ||
2097 | 85 | |||
2098 | 86 | |||
2099 | 87 | def https(): | ||
2100 | 88 | ''' | ||
2101 | 89 | Determines whether enough data has been provided in configuration | ||
2102 | 90 | or relation data to configure HTTPS | ||
2103 | 91 | . | ||
2104 | 92 | returns: boolean | ||
2105 | 93 | ''' | ||
2106 | 94 | if config_get('use-https') == "yes": | ||
2107 | 95 | return True | ||
2108 | 96 | if config_get('ssl_cert') and config_get('ssl_key'): | ||
2109 | 97 | return True | ||
2110 | 98 | for r_id in relation_ids('identity-service'): | ||
2111 | 99 | for unit in relation_list(r_id): | ||
2112 | 100 | if None not in [ | ||
2113 | 101 | relation_get('https_keystone', rid=r_id, unit=unit), | ||
2114 | 102 | relation_get('ssl_cert', rid=r_id, unit=unit), | ||
2115 | 103 | relation_get('ssl_key', rid=r_id, unit=unit), | ||
2116 | 104 | relation_get('ca_cert', rid=r_id, unit=unit), | ||
2117 | 105 | ]: | ||
2118 | 106 | return True | ||
2119 | 107 | return False | ||
2120 | 108 | |||
2121 | 109 | |||
2122 | 110 | def determine_api_port(public_port): | ||
2123 | 111 | ''' | ||
2124 | 112 | Determine correct API server listening port based on | ||
2125 | 113 | existence of HTTPS reverse proxy and/or haproxy. | ||
2126 | 114 | |||
2127 | 115 | public_port: int: standard public port for given service | ||
2128 | 116 | |||
2129 | 117 | returns: int: the correct listening port for the API service | ||
2130 | 118 | ''' | ||
2131 | 119 | i = 0 | ||
2132 | 120 | if len(peer_units()) > 0 or is_clustered(): | ||
2133 | 121 | i += 1 | ||
2134 | 122 | if https(): | ||
2135 | 123 | i += 1 | ||
2136 | 124 | return public_port - (i * 10) | ||
2137 | 125 | |||
2138 | 126 | |||
2139 | 127 | def determine_haproxy_port(public_port): | ||
2140 | 128 | ''' | ||
2141 | 129 | Description: Determine correct proxy listening port based on public IP + | ||
2142 | 130 | existence of HTTPS reverse proxy. | ||
2143 | 131 | |||
2144 | 132 | public_port: int: standard public port for given service | ||
2145 | 133 | |||
2146 | 134 | returns: int: the correct listening port for the HAProxy service | ||
2147 | 135 | ''' | ||
2148 | 136 | i = 0 | ||
2149 | 137 | if https(): | ||
2150 | 138 | i += 1 | ||
2151 | 139 | return public_port - (i * 10) | ||
2152 | 140 | |||
2153 | 141 | |||
2154 | 142 | def get_hacluster_config(): | ||
2155 | 143 | ''' | ||
2156 | 144 | Obtains all relevant configuration from charm configuration required | ||
2157 | 145 | for initiating a relation to hacluster: | ||
2158 | 146 | |||
2159 | 147 | ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr | ||
2160 | 148 | |||
2161 | 149 | returns: dict: A dict containing settings keyed by setting name. | ||
2162 | 150 | raises: HAIncompleteConfig if settings are missing. | ||
2163 | 151 | ''' | ||
2164 | 152 | settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr'] | ||
2165 | 153 | conf = {} | ||
2166 | 154 | for setting in settings: | ||
2167 | 155 | conf[setting] = config_get(setting) | ||
2168 | 156 | missing = [] | ||
2169 | 157 | [missing.append(s) for s, v in conf.iteritems() if v is None] | ||
2170 | 158 | if missing: | ||
2171 | 159 | log('Insufficient config data to configure hacluster.', level=ERROR) | ||
2172 | 160 | raise HAIncompleteConfig | ||
2173 | 161 | return conf | ||
2174 | 162 | |||
2175 | 163 | |||
2176 | 164 | def canonical_url(configs, vip_setting='vip'): | ||
2177 | 165 | ''' | ||
2178 | 166 | Returns the correct HTTP URL to this host given the state of HTTPS | ||
2179 | 167 | configuration and hacluster. | ||
2180 | 168 | |||
2181 | 169 | :configs : OSTemplateRenderer: A config tempating object to inspect for | ||
2182 | 170 | a complete https context. | ||
2183 | 171 | :vip_setting: str: Setting in charm config that specifies | ||
2184 | 172 | VIP address. | ||
2185 | 173 | ''' | ||
2186 | 174 | scheme = 'http' | ||
2187 | 175 | if 'https' in configs.complete_contexts(): | ||
2188 | 176 | scheme = 'https' | ||
2189 | 177 | if is_clustered(): | ||
2190 | 178 | addr = config_get(vip_setting) | ||
2191 | 179 | else: | ||
2192 | 180 | addr = unit_get('private-address') | ||
2193 | 181 | return '%s://%s' % (scheme, addr) | ||
2194 | 182 | 0 | ||
2195 | === removed directory 'hooks/install.d/charmhelpers/contrib/jujugui' | |||
2196 | === removed file 'hooks/install.d/charmhelpers/contrib/jujugui/IMPORT' | |||
2197 | --- hooks/install.d/charmhelpers/contrib/jujugui/IMPORT 2015-08-19 19:17:42 +0000 | |||
2198 | +++ hooks/install.d/charmhelpers/contrib/jujugui/IMPORT 1970-01-01 00:00:00 +0000 | |||
2199 | @@ -1,4 +0,0 @@ | |||
2200 | 1 | Source: lp:charms/juju-gui | ||
2201 | 2 | |||
2202 | 3 | juju-gui/hooks/utils.py -> charm-helpers/charmhelpers/contrib/jujugui/utils.py | ||
2203 | 4 | juju-gui/tests/test_utils.py -> charm-helpers/tests/contrib/jujugui/test_utils.py | ||
2204 | 5 | 0 | ||
2205 | === removed file 'hooks/install.d/charmhelpers/contrib/jujugui/__init__.py' | |||
2206 | === removed file 'hooks/install.d/charmhelpers/contrib/jujugui/utils.py' | |||
2207 | --- hooks/install.d/charmhelpers/contrib/jujugui/utils.py 2015-08-19 19:17:42 +0000 | |||
2208 | +++ hooks/install.d/charmhelpers/contrib/jujugui/utils.py 1970-01-01 00:00:00 +0000 | |||
2209 | @@ -1,602 +0,0 @@ | |||
2210 | 1 | """Juju GUI charm utilities.""" | ||
2211 | 2 | |||
2212 | 3 | __all__ = [ | ||
2213 | 4 | 'AGENT', | ||
2214 | 5 | 'APACHE', | ||
2215 | 6 | 'API_PORT', | ||
2216 | 7 | 'CURRENT_DIR', | ||
2217 | 8 | 'HAPROXY', | ||
2218 | 9 | 'IMPROV', | ||
2219 | 10 | 'JUJU_DIR', | ||
2220 | 11 | 'JUJU_GUI_DIR', | ||
2221 | 12 | 'JUJU_GUI_SITE', | ||
2222 | 13 | 'JUJU_PEM', | ||
2223 | 14 | 'WEB_PORT', | ||
2224 | 15 | 'bzr_checkout', | ||
2225 | 16 | 'chain', | ||
2226 | 17 | 'cmd_log', | ||
2227 | 18 | 'fetch_api', | ||
2228 | 19 | 'fetch_gui', | ||
2229 | 20 | 'find_missing_packages', | ||
2230 | 21 | 'first_path_in_dir', | ||
2231 | 22 | 'get_api_address', | ||
2232 | 23 | 'get_npm_cache_archive_url', | ||
2233 | 24 | 'get_release_file_url', | ||
2234 | 25 | 'get_staging_dependencies', | ||
2235 | 26 | 'get_zookeeper_address', | ||
2236 | 27 | 'legacy_juju', | ||
2237 | 28 | 'log_hook', | ||
2238 | 29 | 'merge', | ||
2239 | 30 | 'parse_source', | ||
2240 | 31 | 'prime_npm_cache', | ||
2241 | 32 | 'render_to_file', | ||
2242 | 33 | 'save_or_create_certificates', | ||
2243 | 34 | 'setup_apache', | ||
2244 | 35 | 'setup_gui', | ||
2245 | 36 | 'start_agent', | ||
2246 | 37 | 'start_gui', | ||
2247 | 38 | 'start_improv', | ||
2248 | 39 | 'write_apache_config', | ||
2249 | 40 | ] | ||
2250 | 41 | |||
2251 | 42 | from contextlib import contextmanager | ||
2252 | 43 | import errno | ||
2253 | 44 | import json | ||
2254 | 45 | import os | ||
2255 | 46 | import logging | ||
2256 | 47 | import shutil | ||
2257 | 48 | from subprocess import CalledProcessError | ||
2258 | 49 | import tempfile | ||
2259 | 50 | from urlparse import urlparse | ||
2260 | 51 | |||
2261 | 52 | import apt | ||
2262 | 53 | import tempita | ||
2263 | 54 | |||
2264 | 55 | from launchpadlib.launchpad import Launchpad | ||
2265 | 56 | from shelltoolbox import ( | ||
2266 | 57 | Serializer, | ||
2267 | 58 | apt_get_install, | ||
2268 | 59 | command, | ||
2269 | 60 | environ, | ||
2270 | 61 | install_extra_repositories, | ||
2271 | 62 | run, | ||
2272 | 63 | script_name, | ||
2273 | 64 | search_file, | ||
2274 | 65 | su, | ||
2275 | 66 | ) | ||
2276 | 67 | from charmhelpers.core.host import ( | ||
2277 | 68 | service_start, | ||
2278 | 69 | ) | ||
2279 | 70 | from charmhelpers.core.hookenv import ( | ||
2280 | 71 | log, | ||
2281 | 72 | config, | ||
2282 | 73 | unit_get, | ||
2283 | 74 | ) | ||
2284 | 75 | |||
2285 | 76 | |||
2286 | 77 | AGENT = 'juju-api-agent' | ||
2287 | 78 | APACHE = 'apache2' | ||
2288 | 79 | IMPROV = 'juju-api-improv' | ||
2289 | 80 | HAPROXY = 'haproxy' | ||
2290 | 81 | |||
2291 | 82 | API_PORT = 8080 | ||
2292 | 83 | WEB_PORT = 8000 | ||
2293 | 84 | |||
2294 | 85 | CURRENT_DIR = os.getcwd() | ||
2295 | 86 | JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') | ||
2296 | 87 | JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') | ||
2297 | 88 | JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui' | ||
2298 | 89 | JUJU_GUI_PORTS = '/etc/apache2/ports.conf' | ||
2299 | 90 | JUJU_PEM = 'juju.includes-private-key.pem' | ||
2300 | 91 | BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',) | ||
2301 | 92 | DEB_BUILD_DEPENDENCIES = ( | ||
2302 | 93 | 'bzr', 'imagemagick', 'make', 'nodejs', 'npm', | ||
2303 | 94 | ) | ||
2304 | 95 | DEB_STAGE_DEPENDENCIES = ( | ||
2305 | 96 | 'zookeeper', | ||
2306 | 97 | ) | ||
2307 | 98 | |||
2308 | 99 | |||
2309 | 100 | # Store the configuration from on invocation to the next. | ||
2310 | 101 | config_json = Serializer('/tmp/config.json') | ||
2311 | 102 | # Bazaar checkout command. | ||
2312 | 103 | bzr_checkout = command('bzr', 'co', '--lightweight') | ||
2313 | 104 | # Whether or not the charm is deployed using juju-core. | ||
2314 | 105 | # If juju-core has been used to deploy the charm, an agent.conf file must | ||
2315 | 106 | # be present in the charm parent directory. | ||
2316 | 107 | legacy_juju = lambda: not os.path.exists( | ||
2317 | 108 | os.path.join(CURRENT_DIR, '..', 'agent.conf')) | ||
2318 | 109 | |||
2319 | 110 | |||
2320 | 111 | def _get_build_dependencies(): | ||
2321 | 112 | """Install deb dependencies for building.""" | ||
2322 | 113 | log('Installing build dependencies.') | ||
2323 | 114 | cmd_log(install_extra_repositories(*BUILD_REPOSITORIES)) | ||
2324 | 115 | cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES)) | ||
2325 | 116 | |||
2326 | 117 | |||
2327 | 118 | def get_api_address(unit_dir): | ||
2328 | 119 | """Return the Juju API address stored in the uniter agent.conf file.""" | ||
2329 | 120 | import yaml # python-yaml is only installed if juju-core is used. | ||
2330 | 121 | # XXX 2013-03-27 frankban bug=1161443: | ||
2331 | 122 | # currently the uniter agent.conf file does not include the API | ||
2332 | 123 | # address. For now retrieve it from the machine agent file. | ||
2333 | 124 | base_dir = os.path.abspath(os.path.join(unit_dir, '..')) | ||
2334 | 125 | for dirname in os.listdir(base_dir): | ||
2335 | 126 | if dirname.startswith('machine-'): | ||
2336 | 127 | agent_conf = os.path.join(base_dir, dirname, 'agent.conf') | ||
2337 | 128 | break | ||
2338 | 129 | else: | ||
2339 | 130 | raise IOError('Juju agent configuration file not found.') | ||
2340 | 131 | contents = yaml.load(open(agent_conf)) | ||
2341 | 132 | return contents['apiinfo']['addrs'][0] | ||
2342 | 133 | |||
2343 | 134 | |||
2344 | 135 | def get_staging_dependencies(): | ||
2345 | 136 | """Install deb dependencies for the stage (improv) environment.""" | ||
2346 | 137 | log('Installing stage dependencies.') | ||
2347 | 138 | cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES)) | ||
2348 | 139 | |||
2349 | 140 | |||
2350 | 141 | def first_path_in_dir(directory): | ||
2351 | 142 | """Return the full path of the first file/dir in *directory*.""" | ||
2352 | 143 | return os.path.join(directory, os.listdir(directory)[0]) | ||
2353 | 144 | |||
2354 | 145 | |||
2355 | 146 | def _get_by_attr(collection, attr, value): | ||
2356 | 147 | """Return the first item in collection having attr == value. | ||
2357 | 148 | |||
2358 | 149 | Return None if the item is not found. | ||
2359 | 150 | """ | ||
2360 | 151 | for item in collection: | ||
2361 | 152 | if getattr(item, attr) == value: | ||
2362 | 153 | return item | ||
2363 | 154 | |||
2364 | 155 | |||
2365 | 156 | def get_release_file_url(project, series_name, release_version): | ||
2366 | 157 | """Return the URL of the release file hosted in Launchpad. | ||
2367 | 158 | |||
2368 | 159 | The returned URL points to a release file for the given project, series | ||
2369 | 160 | name and release version. | ||
2370 | 161 | The argument *project* is a project object as returned by launchpadlib. | ||
2371 | 162 | The arguments *series_name* and *release_version* are strings. If | ||
2372 | 163 | *release_version* is None, the URL of the latest release will be returned. | ||
2373 | 164 | """ | ||
2374 | 165 | series = _get_by_attr(project.series, 'name', series_name) | ||
2375 | 166 | if series is None: | ||
2376 | 167 | raise ValueError('%r: series not found' % series_name) | ||
2377 | 168 | # Releases are returned by Launchpad in reverse date order. | ||
2378 | 169 | releases = list(series.releases) | ||
2379 | 170 | if not releases: | ||
2380 | 171 | raise ValueError('%r: series does not contain releases' % series_name) | ||
2381 | 172 | if release_version is not None: | ||
2382 | 173 | release = _get_by_attr(releases, 'version', release_version) | ||
2383 | 174 | if release is None: | ||
2384 | 175 | raise ValueError('%r: release not found' % release_version) | ||
2385 | 176 | releases = [release] | ||
2386 | 177 | for release in releases: | ||
2387 | 178 | for file_ in release.files: | ||
2388 | 179 | if str(file_).endswith('.tgz'): | ||
2389 | 180 | return file_.file_link | ||
2390 | 181 | raise ValueError('%r: file not found' % release_version) | ||
2391 | 182 | |||
2392 | 183 | |||
2393 | 184 | def get_zookeeper_address(agent_file_path): | ||
2394 | 185 | """Retrieve the Zookeeper address contained in the given *agent_file_path*. | ||
2395 | 186 | |||
2396 | 187 | The *agent_file_path* is a path to a file containing a line similar to the | ||
2397 | 188 | following:: | ||
2398 | 189 | |||
2399 | 190 | env JUJU_ZOOKEEPER="address" | ||
2400 | 191 | """ | ||
2401 | 192 | line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip() | ||
2402 | 193 | return line.split('=')[1].strip('"') | ||
2403 | 194 | |||
2404 | 195 | |||
2405 | 196 | @contextmanager | ||
2406 | 197 | def log_hook(): | ||
2407 | 198 | """Log when a hook starts and stops its execution. | ||
2408 | 199 | |||
2409 | 200 | Also log to stdout possible CalledProcessError exceptions raised executing | ||
2410 | 201 | the hook. | ||
2411 | 202 | """ | ||
2412 | 203 | script = script_name() | ||
2413 | 204 | log(">>> Entering {}".format(script)) | ||
2414 | 205 | try: | ||
2415 | 206 | yield | ||
2416 | 207 | except CalledProcessError as err: | ||
2417 | 208 | log('Exception caught:') | ||
2418 | 209 | log(err.output) | ||
2419 | 210 | raise | ||
2420 | 211 | finally: | ||
2421 | 212 | log("<<< Exiting {}".format(script)) | ||
2422 | 213 | |||
2423 | 214 | |||
2424 | 215 | def parse_source(source): | ||
2425 | 216 | """Parse the ``juju-gui-source`` option. | ||
2426 | 217 | |||
2427 | 218 | Return a tuple of two elements representing info on how to deploy Juju GUI. | ||
2428 | 219 | Examples: | ||
2429 | 220 | - ('stable', None): latest stable release; | ||
2430 | 221 | - ('stable', '0.1.0'): stable release v0.1.0; | ||
2431 | 222 | - ('trunk', None): latest trunk release; | ||
2432 | 223 | - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1; | ||
2433 | 224 | - ('branch', 'lp:juju-gui'): release is made from a branch; | ||
2434 | 225 | - ('url', 'http://example.com/gui'): release from a downloaded file. | ||
2435 | 226 | """ | ||
2436 | 227 | if source.startswith('url:'): | ||
2437 | 228 | source = source[4:] | ||
2438 | 229 | # Support file paths, including relative paths. | ||
2439 | 230 | if urlparse(source).scheme == '': | ||
2440 | 231 | if not source.startswith('/'): | ||
2441 | 232 | source = os.path.join(os.path.abspath(CURRENT_DIR), source) | ||
2442 | 233 | source = "file://%s" % source | ||
2443 | 234 | return 'url', source | ||
2444 | 235 | if source in ('stable', 'trunk'): | ||
2445 | 236 | return source, None | ||
2446 | 237 | if source.startswith('lp:') or source.startswith('http://'): | ||
2447 | 238 | return 'branch', source | ||
2448 | 239 | if 'build' in source: | ||
2449 | 240 | return 'trunk', source | ||
2450 | 241 | return 'stable', source | ||
2451 | 242 | |||
2452 | 243 | |||
2453 | 244 | def render_to_file(template_name, context, destination): | ||
2454 | 245 | """Render the given *template_name* into *destination* using *context*. | ||
2455 | 246 | |||
2456 | 247 | The tempita template language is used to render contents | ||
2457 | 248 | (see http://pythonpaste.org/tempita/). | ||
2458 | 249 | The argument *template_name* is the name or path of the template file: | ||
2459 | 250 | it may be either a path relative to ``../config`` or an absolute path. | ||
2460 | 251 | The argument *destination* is a file path. | ||
2461 | 252 | The argument *context* is a dict-like object. | ||
2462 | 253 | """ | ||
2463 | 254 | template_path = os.path.abspath(template_name) | ||
2464 | 255 | template = tempita.Template.from_filename(template_path) | ||
2465 | 256 | with open(destination, 'w') as stream: | ||
2466 | 257 | stream.write(template.substitute(context)) | ||
2467 | 258 | |||
2468 | 259 | |||
2469 | 260 | results_log = None | ||
2470 | 261 | |||
2471 | 262 | |||
2472 | 263 | def _setupLogging(): | ||
2473 | 264 | global results_log | ||
2474 | 265 | if results_log is not None: | ||
2475 | 266 | return | ||
2476 | 267 | cfg = config() | ||
2477 | 268 | logging.basicConfig( | ||
2478 | 269 | filename=cfg['command-log-file'], | ||
2479 | 270 | level=logging.INFO, | ||
2480 | 271 | format="%(asctime)s: %(name)s@%(levelname)s %(message)s") | ||
2481 | 272 | results_log = logging.getLogger('juju-gui') | ||
2482 | 273 | |||
2483 | 274 | |||
2484 | 275 | def cmd_log(results): | ||
2485 | 276 | global results_log | ||
2486 | 277 | if not results: | ||
2487 | 278 | return | ||
2488 | 279 | if results_log is None: | ||
2489 | 280 | _setupLogging() | ||
2490 | 281 | # Since 'results' may be multi-line output, start it on a separate line | ||
2491 | 282 | # from the logger timestamp, etc. | ||
2492 | 283 | results_log.info('\n' + results) | ||
2493 | 284 | |||
2494 | 285 | |||
2495 | 286 | def start_improv(staging_env, ssl_cert_path, | ||
2496 | 287 | config_path='/etc/init/juju-api-improv.conf'): | ||
2497 | 288 | """Start a simulated juju environment using ``improv.py``.""" | ||
2498 | 289 | log('Setting up staging start up script.') | ||
2499 | 290 | context = { | ||
2500 | 291 | 'juju_dir': JUJU_DIR, | ||
2501 | 292 | 'keys': ssl_cert_path, | ||
2502 | 293 | 'port': API_PORT, | ||
2503 | 294 | 'staging_env': staging_env, | ||
2504 | 295 | } | ||
2505 | 296 | render_to_file('config/juju-api-improv.conf.template', context, config_path) | ||
2506 | 297 | log('Starting the staging backend.') | ||
2507 | 298 | with su('root'): | ||
2508 | 299 | service_start(IMPROV) | ||
2509 | 300 | |||
2510 | 301 | |||
2511 | 302 | def start_agent( | ||
2512 | 303 | ssl_cert_path, config_path='/etc/init/juju-api-agent.conf', | ||
2513 | 304 | read_only=False): | ||
2514 | 305 | """Start the Juju agent and connect to the current environment.""" | ||
2515 | 306 | # Retrieve the Zookeeper address from the start up script. | ||
2516 | 307 | unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) | ||
2517 | 308 | agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir)) | ||
2518 | 309 | zookeeper = get_zookeeper_address(agent_file) | ||
2519 | 310 | log('Setting up API agent start up script.') | ||
2520 | 311 | context = { | ||
2521 | 312 | 'juju_dir': JUJU_DIR, | ||
2522 | 313 | 'keys': ssl_cert_path, | ||
2523 | 314 | 'port': API_PORT, | ||
2524 | 315 | 'zookeeper': zookeeper, | ||
2525 | 316 | 'read_only': read_only | ||
2526 | 317 | } | ||
2527 | 318 | render_to_file('config/juju-api-agent.conf.template', context, config_path) | ||
2528 | 319 | log('Starting API agent.') | ||
2529 | 320 | with su('root'): | ||
2530 | 321 | service_start(AGENT) | ||
2531 | 322 | |||
2532 | 323 | |||
2533 | 324 | def start_gui( | ||
2534 | 325 | console_enabled, login_help, readonly, in_staging, ssl_cert_path, | ||
2535 | 326 | charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg', | ||
2536 | 327 | config_js_path=None, secure=True, sandbox=False): | ||
2537 | 328 | """Set up and start the Juju GUI server.""" | ||
2538 | 329 | with su('root'): | ||
2539 | 330 | run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR) | ||
2540 | 331 | # XXX 2013-02-05 frankban bug=1116320: | ||
2541 | 332 | # External insecure resources are still loaded when testing in the | ||
2542 | 333 | # debug environment. For now, switch to the production environment if | ||
2543 | 334 | # the charm is configured to serve tests. | ||
2544 | 335 | if in_staging and not serve_tests: | ||
2545 | 336 | build_dirname = 'build-debug' | ||
2546 | 337 | else: | ||
2547 | 338 | build_dirname = 'build-prod' | ||
2548 | 339 | build_dir = os.path.join(JUJU_GUI_DIR, build_dirname) | ||
2549 | 340 | log('Generating the Juju GUI configuration file.') | ||
2550 | 341 | is_legacy_juju = legacy_juju() | ||
2551 | 342 | user, password = None, None | ||
2552 | 343 | if (is_legacy_juju and in_staging) or sandbox: | ||
2553 | 344 | user, password = 'admin', 'admin' | ||
2554 | 345 | else: | ||
2555 | 346 | user, password = None, None | ||
2556 | 347 | |||
2557 | 348 | api_backend = 'python' if is_legacy_juju else 'go' | ||
2558 | 349 | if secure: | ||
2559 | 350 | protocol = 'wss' | ||
2560 | 351 | else: | ||
2561 | 352 | log('Running in insecure mode! Port 80 will serve unencrypted.') | ||
2562 | 353 | protocol = 'ws' | ||
2563 | 354 | |||
2564 | 355 | context = { | ||
2565 | 356 | 'raw_protocol': protocol, | ||
2566 | 357 | 'address': unit_get('public-address'), | ||
2567 | 358 | 'console_enabled': json.dumps(console_enabled), | ||
2568 | 359 | 'login_help': json.dumps(login_help), | ||
2569 | 360 | 'password': json.dumps(password), | ||
2570 | 361 | 'api_backend': json.dumps(api_backend), | ||
2571 | 362 | 'readonly': json.dumps(readonly), | ||
2572 | 363 | 'user': json.dumps(user), | ||
2573 | 364 | 'protocol': json.dumps(protocol), | ||
2574 | 365 | 'sandbox': json.dumps(sandbox), | ||
2575 | 366 | 'charmworld_url': json.dumps(charmworld_url), | ||
2576 | 367 | } | ||
2577 | 368 | if config_js_path is None: | ||
2578 | 369 | config_js_path = os.path.join( | ||
2579 | 370 | build_dir, 'juju-ui', 'assets', 'config.js') | ||
2580 | 371 | render_to_file('config/config.js.template', context, config_js_path) | ||
2581 | 372 | |||
2582 | 373 | write_apache_config(build_dir, serve_tests) | ||
2583 | 374 | |||
2584 | 375 | log('Generating haproxy configuration file.') | ||
2585 | 376 | if is_legacy_juju: | ||
2586 | 377 | # The PyJuju API agent is listening on localhost. | ||
2587 | 378 | api_address = '127.0.0.1:{0}'.format(API_PORT) | ||
2588 | 379 | else: | ||
2589 | 380 | # Retrieve the juju-core API server address. | ||
2590 | 381 | api_address = get_api_address(os.path.join(CURRENT_DIR, '..')) | ||
2591 | 382 | context = { | ||
2592 | 383 | 'api_address': api_address, | ||
2593 | 384 | 'api_pem': JUJU_PEM, | ||
2594 | 385 | 'legacy_juju': is_legacy_juju, | ||
2595 | 386 | 'ssl_cert_path': ssl_cert_path, | ||
2596 | 387 | # In PyJuju environments, use the same certificate for both HTTPS and | ||
2597 | 388 | # WebSocket connections. In juju-core the system already has the proper | ||
2598 | 389 | # certificate installed. | ||
2599 | 390 | 'web_pem': JUJU_PEM, | ||
2600 | 391 | 'web_port': WEB_PORT, | ||
2601 | 392 | 'secure': secure | ||
2602 | 393 | } | ||
2603 | 394 | render_to_file('config/haproxy.cfg.template', context, haproxy_path) | ||
2604 | 395 | log('Starting Juju GUI.') | ||
2605 | 396 | |||
2606 | 397 | |||
2607 | 398 | def write_apache_config(build_dir, serve_tests=False): | ||
2608 | 399 | log('Generating the apache site configuration file.') | ||
2609 | 400 | context = { | ||
2610 | 401 | 'port': WEB_PORT, | ||
2611 | 402 | 'serve_tests': serve_tests, | ||
2612 | 403 | 'server_root': build_dir, | ||
2613 | 404 | 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''), | ||
2614 | 405 | } | ||
2615 | 406 | render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS) | ||
2616 | 407 | render_to_file('config/apache-site.template', context, JUJU_GUI_SITE) | ||
2617 | 408 | |||
2618 | 409 | |||
2619 | 410 | def get_npm_cache_archive_url(Launchpad=Launchpad): | ||
2620 | 411 | """Figure out the URL of the most recent NPM cache archive on Launchpad.""" | ||
2621 | 412 | launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') | ||
2622 | 413 | project = launchpad.projects['juju-gui'] | ||
2623 | 414 | # Find the URL of the most recently created NPM cache archive. | ||
2624 | 415 | npm_cache_url = get_release_file_url(project, 'npm-cache', None) | ||
2625 | 416 | return npm_cache_url | ||
2626 | 417 | |||
2627 | 418 | |||
2628 | 419 | def prime_npm_cache(npm_cache_url): | ||
2629 | 420 | """Download NPM cache archive and prime the NPM cache with it.""" | ||
2630 | 421 | # Download the cache archive and then uncompress it into the NPM cache. | ||
2631 | 422 | npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz') | ||
2632 | 423 | cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url)) | ||
2633 | 424 | npm_cache_dir = os.path.expanduser('~/.npm') | ||
2634 | 425 | # The NPM cache directory probably does not exist, so make it if not. | ||
2635 | 426 | try: | ||
2636 | 427 | os.mkdir(npm_cache_dir) | ||
2637 | 428 | except OSError, e: | ||
2638 | 429 | # If the directory already exists then ignore the error. | ||
2639 | 430 | if e.errno != errno.EEXIST: # File exists. | ||
2640 | 431 | raise | ||
2641 | 432 | uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f') | ||
2642 | 433 | cmd_log(uncompress(npm_cache_archive)) | ||
2643 | 434 | |||
2644 | 435 | |||
2645 | 436 | def fetch_gui(juju_gui_source, logpath): | ||
2646 | 437 | """Retrieve the Juju GUI release/branch.""" | ||
2647 | 438 | # Retrieve a Juju GUI release. | ||
2648 | 439 | origin, version_or_branch = parse_source(juju_gui_source) | ||
2649 | 440 | if origin == 'branch': | ||
2650 | 441 | # Make sure we have the dependencies necessary for us to actually make | ||
2651 | 442 | # a build. | ||
2652 | 443 | _get_build_dependencies() | ||
2653 | 444 | # Create a release starting from a branch. | ||
2654 | 445 | juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') | ||
2655 | 446 | log('Retrieving Juju GUI source checkout from %s.' % version_or_branch) | ||
2656 | 447 | cmd_log(run('rm', '-rf', juju_gui_source_dir)) | ||
2657 | 448 | cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir)) | ||
2658 | 449 | log('Preparing a Juju GUI release.') | ||
2659 | 450 | logdir = os.path.dirname(logpath) | ||
2660 | 451 | fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) | ||
2661 | 452 | log('Output from "make distfile" sent to %s' % name) | ||
2662 | 453 | with environ(NO_BZR='1'): | ||
2663 | 454 | run('make', '-C', juju_gui_source_dir, 'distfile', | ||
2664 | 455 | stdout=fd, stderr=fd) | ||
2665 | 456 | release_tarball = first_path_in_dir( | ||
2666 | 457 | os.path.join(juju_gui_source_dir, 'releases')) | ||
2667 | 458 | else: | ||
2668 | 459 | log('Retrieving Juju GUI release.') | ||
2669 | 460 | if origin == 'url': | ||
2670 | 461 | file_url = version_or_branch | ||
2671 | 462 | else: | ||
2672 | 463 | # Retrieve a release from Launchpad. | ||
2673 | 464 | launchpad = Launchpad.login_anonymously( | ||
2674 | 465 | 'Juju GUI charm', 'production') | ||
2675 | 466 | project = launchpad.projects['juju-gui'] | ||
2676 | 467 | file_url = get_release_file_url(project, origin, version_or_branch) | ||
2677 | 468 | log('Downloading release file from %s.' % file_url) | ||
2678 | 469 | release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') | ||
2679 | 470 | cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) | ||
2680 | 471 | return release_tarball | ||
2681 | 472 | |||
2682 | 473 | |||
2683 | 474 | def fetch_api(juju_api_branch): | ||
2684 | 475 | """Retrieve the Juju branch.""" | ||
2685 | 476 | # Retrieve Juju API source checkout. | ||
2686 | 477 | log('Retrieving Juju API source checkout.') | ||
2687 | 478 | cmd_log(run('rm', '-rf', JUJU_DIR)) | ||
2688 | 479 | cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR)) | ||
2689 | 480 | |||
2690 | 481 | |||
2691 | 482 | def setup_gui(release_tarball): | ||
2692 | 483 | """Set up Juju GUI.""" | ||
2693 | 484 | # Uncompress the release tarball. | ||
2694 | 485 | log('Installing Juju GUI.') | ||
2695 | 486 | release_dir = os.path.join(CURRENT_DIR, 'release') | ||
2696 | 487 | cmd_log(run('rm', '-rf', release_dir)) | ||
2697 | 488 | os.mkdir(release_dir) | ||
2698 | 489 | uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f') | ||
2699 | 490 | cmd_log(uncompress(release_tarball)) | ||
2700 | 491 | # Link the Juju GUI dir to the contents of the release tarball. | ||
2701 | 492 | cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR)) | ||
2702 | 493 | |||
2703 | 494 | |||
2704 | 495 | def setup_apache(): | ||
2705 | 496 | """Set up apache.""" | ||
2706 | 497 | log('Setting up apache.') | ||
2707 | 498 | if not os.path.exists(JUJU_GUI_SITE): | ||
2708 | 499 | cmd_log(run('touch', JUJU_GUI_SITE)) | ||
2709 | 500 | cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE)) | ||
2710 | 501 | cmd_log( | ||
2711 | 502 | run('ln', '-s', JUJU_GUI_SITE, | ||
2712 | 503 | '/etc/apache2/sites-enabled/juju-gui')) | ||
2713 | 504 | |||
2714 | 505 | if not os.path.exists(JUJU_GUI_PORTS): | ||
2715 | 506 | cmd_log(run('touch', JUJU_GUI_PORTS)) | ||
2716 | 507 | cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS)) | ||
2717 | 508 | |||
2718 | 509 | with su('root'): | ||
2719 | 510 | run('a2dissite', 'default') | ||
2720 | 511 | run('a2ensite', 'juju-gui') | ||
2721 | 512 | |||
2722 | 513 | |||
2723 | 514 | def save_or_create_certificates( | ||
2724 | 515 | ssl_cert_path, ssl_cert_contents, ssl_key_contents): | ||
2725 | 516 | """Generate the SSL certificates. | ||
2726 | 517 | |||
2727 | 518 | If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them | ||
2728 | 519 | as certificates; otherwise, generate them. | ||
2729 | 520 | |||
2730 | 521 | Also create a pem file, suitable for use in the haproxy configuration, | ||
2731 | 522 | concatenating the key and the certificate files. | ||
2732 | 523 | """ | ||
2733 | 524 | crt_path = os.path.join(ssl_cert_path, 'juju.crt') | ||
2734 | 525 | key_path = os.path.join(ssl_cert_path, 'juju.key') | ||
2735 | 526 | if not os.path.exists(ssl_cert_path): | ||
2736 | 527 | os.makedirs(ssl_cert_path) | ||
2737 | 528 | if ssl_cert_contents and ssl_key_contents: | ||
2738 | 529 | # Save the provided certificates. | ||
2739 | 530 | with open(crt_path, 'w') as cert_file: | ||
2740 | 531 | cert_file.write(ssl_cert_contents) | ||
2741 | 532 | with open(key_path, 'w') as key_file: | ||
2742 | 533 | key_file.write(ssl_key_contents) | ||
2743 | 534 | else: | ||
2744 | 535 | # Generate certificates. | ||
2745 | 536 | # See http://superuser.com/questions/226192/openssl-without-prompt | ||
2746 | 537 | cmd_log(run( | ||
2747 | 538 | 'openssl', 'req', '-new', '-newkey', 'rsa:4096', | ||
2748 | 539 | '-days', '365', '-nodes', '-x509', '-subj', | ||
2749 | 540 | # These are arbitrary test values for the certificate. | ||
2750 | 541 | '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com', | ||
2751 | 542 | '-keyout', key_path, '-out', crt_path)) | ||
2752 | 543 | # Generate the pem file. | ||
2753 | 544 | pem_path = os.path.join(ssl_cert_path, JUJU_PEM) | ||
2754 | 545 | if os.path.exists(pem_path): | ||
2755 | 546 | os.remove(pem_path) | ||
2756 | 547 | with open(pem_path, 'w') as pem_file: | ||
2757 | 548 | shutil.copyfileobj(open(key_path), pem_file) | ||
2758 | 549 | shutil.copyfileobj(open(crt_path), pem_file) | ||
2759 | 550 | |||
2760 | 551 | |||
2761 | 552 | def find_missing_packages(*packages): | ||
2762 | 553 | """Given a list of packages, return the packages which are not installed. | ||
2763 | 554 | """ | ||
2764 | 555 | cache = apt.Cache() | ||
2765 | 556 | missing = set() | ||
2766 | 557 | for pkg_name in packages: | ||
2767 | 558 | try: | ||
2768 | 559 | pkg = cache[pkg_name] | ||
2769 | 560 | except KeyError: | ||
2770 | 561 | missing.add(pkg_name) | ||
2771 | 562 | continue | ||
2772 | 563 | if pkg.is_installed: | ||
2773 | 564 | continue | ||
2774 | 565 | missing.add(pkg_name) | ||
2775 | 566 | return missing | ||
2776 | 567 | |||
2777 | 568 | |||
2778 | 569 | ## Backend support decorators | ||
2779 | 570 | |||
2780 | 571 | def chain(name): | ||
2781 | 572 | """Helper method to compose a set of mixin objects into a callable. | ||
2782 | 573 | |||
2783 | 574 | Each method is called in the context of its mixin instance, and its | ||
2784 | 575 | argument is the Backend instance. | ||
2785 | 576 | """ | ||
2786 | 577 | # Chain method calls through all implementing mixins. | ||
2787 | 578 | def method(self): | ||
2788 | 579 | for mixin in self.mixins: | ||
2789 | 580 | a_callable = getattr(type(mixin), name, None) | ||
2790 | 581 | if a_callable: | ||
2791 | 582 | a_callable(mixin, self) | ||
2792 | 583 | |||
2793 | 584 | method.__name__ = name | ||
2794 | 585 | return method | ||
2795 | 586 | |||
2796 | 587 | |||
2797 | 588 | def merge(name): | ||
2798 | 589 | """Helper to merge a property from a set of strategy objects | ||
2799 | 590 | into a unified set. | ||
2800 | 591 | """ | ||
2801 | 592 | # Return merged property from every providing mixin as a set. | ||
2802 | 593 | @property | ||
2803 | 594 | def method(self): | ||
2804 | 595 | result = set() | ||
2805 | 596 | for mixin in self.mixins: | ||
2806 | 597 | segment = getattr(type(mixin), name, None) | ||
2807 | 598 | if segment and isinstance(segment, (list, tuple, set)): | ||
2808 | 599 | result |= set(segment) | ||
2809 | 600 | |||
2810 | 601 | return result | ||
2811 | 602 | return method | ||
2812 | 603 | 0 | ||
2813 | === removed directory 'hooks/install.d/charmhelpers/contrib/network' | |||
2814 | === removed file 'hooks/install.d/charmhelpers/contrib/network/__init__.py' | |||
2815 | === removed directory 'hooks/install.d/charmhelpers/contrib/network/ovs' | |||
2816 | === removed file 'hooks/install.d/charmhelpers/contrib/network/ovs/__init__.py' | |||
2817 | --- hooks/install.d/charmhelpers/contrib/network/ovs/__init__.py 2015-08-19 19:17:42 +0000 | |||
2818 | +++ hooks/install.d/charmhelpers/contrib/network/ovs/__init__.py 1970-01-01 00:00:00 +0000 | |||
2819 | @@ -1,72 +0,0 @@ | |||
2820 | 1 | ''' Helpers for interacting with OpenvSwitch ''' | ||
2821 | 2 | import subprocess | ||
2822 | 3 | import os | ||
2823 | 4 | from charmhelpers.core.hookenv import ( | ||
2824 | 5 | log, WARNING | ||
2825 | 6 | ) | ||
2826 | 7 | from charmhelpers.core.host import ( | ||
2827 | 8 | service | ||
2828 | 9 | ) | ||
2829 | 10 | |||
2830 | 11 | |||
2831 | 12 | def add_bridge(name): | ||
2832 | 13 | ''' Add the named bridge to openvswitch ''' | ||
2833 | 14 | log('Creating bridge {}'.format(name)) | ||
2834 | 15 | subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name]) | ||
2835 | 16 | |||
2836 | 17 | |||
2837 | 18 | def del_bridge(name): | ||
2838 | 19 | ''' Delete the named bridge from openvswitch ''' | ||
2839 | 20 | log('Deleting bridge {}'.format(name)) | ||
2840 | 21 | subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name]) | ||
2841 | 22 | |||
2842 | 23 | |||
2843 | 24 | def add_bridge_port(name, port): | ||
2844 | 25 | ''' Add a port to the named openvswitch bridge ''' | ||
2845 | 26 | log('Adding port {} to bridge {}'.format(port, name)) | ||
2846 | 27 | subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port", | ||
2847 | 28 | name, port]) | ||
2848 | 29 | subprocess.check_call(["ip", "link", "set", port, "up"]) | ||
2849 | 30 | |||
2850 | 31 | |||
2851 | 32 | def del_bridge_port(name, port): | ||
2852 | 33 | ''' Delete a port from the named openvswitch bridge ''' | ||
2853 | 34 | log('Deleting port {} from bridge {}'.format(port, name)) | ||
2854 | 35 | subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port", | ||
2855 | 36 | name, port]) | ||
2856 | 37 | subprocess.check_call(["ip", "link", "set", port, "down"]) | ||
2857 | 38 | |||
2858 | 39 | |||
2859 | 40 | def set_manager(manager): | ||
2860 | 41 | ''' Set the controller for the local openvswitch ''' | ||
2861 | 42 | log('Setting manager for local ovs to {}'.format(manager)) | ||
2862 | 43 | subprocess.check_call(['ovs-vsctl', 'set-manager', | ||
2863 | 44 | 'ssl:{}'.format(manager)]) | ||
2864 | 45 | |||
2865 | 46 | |||
2866 | 47 | CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem' | ||
2867 | 48 | |||
2868 | 49 | |||
2869 | 50 | def get_certificate(): | ||
2870 | 51 | ''' Read openvswitch certificate from disk ''' | ||
2871 | 52 | if os.path.exists(CERT_PATH): | ||
2872 | 53 | log('Reading ovs certificate from {}'.format(CERT_PATH)) | ||
2873 | 54 | with open(CERT_PATH, 'r') as cert: | ||
2874 | 55 | full_cert = cert.read() | ||
2875 | 56 | begin_marker = "-----BEGIN CERTIFICATE-----" | ||
2876 | 57 | end_marker = "-----END CERTIFICATE-----" | ||
2877 | 58 | begin_index = full_cert.find(begin_marker) | ||
2878 | 59 | end_index = full_cert.rfind(end_marker) | ||
2879 | 60 | if end_index == -1 or begin_index == -1: | ||
2880 | 61 | raise RuntimeError("Certificate does not contain valid begin" | ||
2881 | 62 | " and end markers.") | ||
2882 | 63 | full_cert = full_cert[begin_index:(end_index + len(end_marker))] | ||
2883 | 64 | return full_cert | ||
2884 | 65 | else: | ||
2885 | 66 | log('Certificate not found', level=WARNING) | ||
2886 | 67 | return None | ||
2887 | 68 | |||
2888 | 69 | |||
2889 | 70 | def full_restart(): | ||
2890 | 71 | ''' Full restart and reload of openvswitch ''' | ||
2891 | 72 | service('force-reload-kmod', 'openvswitch-switch') | ||
2892 | 73 | 0 | ||
2893 | === removed directory 'hooks/install.d/charmhelpers/contrib/openstack' | |||
2894 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/__init__.py' | |||
2895 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/context.py' | |||
2896 | --- hooks/install.d/charmhelpers/contrib/openstack/context.py 2015-08-19 19:17:42 +0000 | |||
2897 | +++ hooks/install.d/charmhelpers/contrib/openstack/context.py 1970-01-01 00:00:00 +0000 | |||
2898 | @@ -1,294 +0,0 @@ | |||
2899 | 1 | import os | ||
2900 | 2 | |||
2901 | 3 | from base64 import b64decode | ||
2902 | 4 | |||
2903 | 5 | from subprocess import ( | ||
2904 | 6 | check_call | ||
2905 | 7 | ) | ||
2906 | 8 | |||
2907 | 9 | from charmhelpers.core.hookenv import ( | ||
2908 | 10 | config, | ||
2909 | 11 | local_unit, | ||
2910 | 12 | log, | ||
2911 | 13 | relation_get, | ||
2912 | 14 | relation_ids, | ||
2913 | 15 | related_units, | ||
2914 | 16 | unit_get, | ||
2915 | 17 | ) | ||
2916 | 18 | |||
2917 | 19 | from charmhelpers.contrib.hahelpers.cluster import ( | ||
2918 | 20 | determine_api_port, | ||
2919 | 21 | determine_haproxy_port, | ||
2920 | 22 | https, | ||
2921 | 23 | is_clustered, | ||
2922 | 24 | peer_units, | ||
2923 | 25 | ) | ||
2924 | 26 | |||
2925 | 27 | from charmhelpers.contrib.hahelpers.apache import ( | ||
2926 | 28 | get_cert, | ||
2927 | 29 | get_ca_cert, | ||
2928 | 30 | ) | ||
2929 | 31 | |||
2930 | 32 | CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' | ||
2931 | 33 | |||
2932 | 34 | |||
2933 | 35 | class OSContextError(Exception): | ||
2934 | 36 | pass | ||
2935 | 37 | |||
2936 | 38 | |||
2937 | 39 | def context_complete(ctxt): | ||
2938 | 40 | _missing = [] | ||
2939 | 41 | for k, v in ctxt.iteritems(): | ||
2940 | 42 | if v is None or v == '': | ||
2941 | 43 | _missing.append(k) | ||
2942 | 44 | if _missing: | ||
2943 | 45 | log('Missing required data: %s' % ' '.join(_missing), level='INFO') | ||
2944 | 46 | return False | ||
2945 | 47 | return True | ||
2946 | 48 | |||
2947 | 49 | |||
2948 | 50 | class OSContextGenerator(object): | ||
2949 | 51 | interfaces = [] | ||
2950 | 52 | |||
2951 | 53 | def __call__(self): | ||
2952 | 54 | raise NotImplementedError | ||
2953 | 55 | |||
2954 | 56 | |||
2955 | 57 | class SharedDBContext(OSContextGenerator): | ||
2956 | 58 | interfaces = ['shared-db'] | ||
2957 | 59 | |||
2958 | 60 | def __call__(self): | ||
2959 | 61 | log('Generating template context for shared-db') | ||
2960 | 62 | conf = config() | ||
2961 | 63 | try: | ||
2962 | 64 | database = conf['database'] | ||
2963 | 65 | username = conf['database-user'] | ||
2964 | 66 | except KeyError as e: | ||
2965 | 67 | log('Could not generate shared_db context. ' | ||
2966 | 68 | 'Missing required charm config options: %s.' % e) | ||
2967 | 69 | raise OSContextError | ||
2968 | 70 | ctxt = {} | ||
2969 | 71 | for rid in relation_ids('shared-db'): | ||
2970 | 72 | for unit in related_units(rid): | ||
2971 | 73 | ctxt = { | ||
2972 | 74 | 'database_host': relation_get('db_host', rid=rid, | ||
2973 | 75 | unit=unit), | ||
2974 | 76 | 'database': database, | ||
2975 | 77 | 'database_user': username, | ||
2976 | 78 | 'database_password': relation_get('password', rid=rid, | ||
2977 | 79 | unit=unit) | ||
2978 | 80 | } | ||
2979 | 81 | if not context_complete(ctxt): | ||
2980 | 82 | return {} | ||
2981 | 83 | return ctxt | ||
2982 | 84 | |||
2983 | 85 | |||
2984 | 86 | class IdentityServiceContext(OSContextGenerator): | ||
2985 | 87 | interfaces = ['identity-service'] | ||
2986 | 88 | |||
2987 | 89 | def __call__(self): | ||
2988 | 90 | log('Generating template context for identity-service') | ||
2989 | 91 | ctxt = {} | ||
2990 | 92 | |||
2991 | 93 | for rid in relation_ids('identity-service'): | ||
2992 | 94 | for unit in related_units(rid): | ||
2993 | 95 | ctxt = { | ||
2994 | 96 | 'service_port': relation_get('service_port', rid=rid, | ||
2995 | 97 | unit=unit), | ||
2996 | 98 | 'service_host': relation_get('service_host', rid=rid, | ||
2997 | 99 | unit=unit), | ||
2998 | 100 | 'auth_host': relation_get('auth_host', rid=rid, unit=unit), | ||
2999 | 101 | 'auth_port': relation_get('auth_port', rid=rid, unit=unit), | ||
3000 | 102 | 'admin_tenant_name': relation_get('service_tenant', | ||
3001 | 103 | rid=rid, unit=unit), | ||
3002 | 104 | 'admin_user': relation_get('service_username', rid=rid, | ||
3003 | 105 | unit=unit), | ||
3004 | 106 | 'admin_password': relation_get('service_password', rid=rid, | ||
3005 | 107 | unit=unit), | ||
3006 | 108 | # XXX: Hard-coded http. | ||
3007 | 109 | 'service_protocol': 'http', | ||
3008 | 110 | 'auth_protocol': 'http', | ||
3009 | 111 | } | ||
3010 | 112 | if not context_complete(ctxt): | ||
3011 | 113 | return {} | ||
3012 | 114 | return ctxt | ||
3013 | 115 | |||
3014 | 116 | |||
3015 | 117 | class AMQPContext(OSContextGenerator): | ||
3016 | 118 | interfaces = ['amqp'] | ||
3017 | 119 | |||
3018 | 120 | def __call__(self): | ||
3019 | 121 | log('Generating template context for amqp') | ||
3020 | 122 | conf = config() | ||
3021 | 123 | try: | ||
3022 | 124 | username = conf['rabbit-user'] | ||
3023 | 125 | vhost = conf['rabbit-vhost'] | ||
3024 | 126 | except KeyError as e: | ||
3025 | 127 | log('Could not generate shared_db context. ' | ||
3026 | 128 | 'Missing required charm config options: %s.' % e) | ||
3027 | 129 | raise OSContextError | ||
3028 | 130 | |||
3029 | 131 | ctxt = {} | ||
3030 | 132 | for rid in relation_ids('amqp'): | ||
3031 | 133 | for unit in related_units(rid): | ||
3032 | 134 | if relation_get('clustered', rid=rid, unit=unit): | ||
3033 | 135 | rabbitmq_host = relation_get('vip', rid=rid, unit=unit) | ||
3034 | 136 | else: | ||
3035 | 137 | rabbitmq_host = relation_get('private-address', | ||
3036 | 138 | rid=rid, unit=unit) | ||
3037 | 139 | ctxt = { | ||
3038 | 140 | 'rabbitmq_host': rabbitmq_host, | ||
3039 | 141 | 'rabbitmq_user': username, | ||
3040 | 142 | 'rabbitmq_password': relation_get('password', rid=rid, | ||
3041 | 143 | unit=unit), | ||
3042 | 144 | 'rabbitmq_virtual_host': vhost, | ||
3043 | 145 | } | ||
3044 | 146 | if not context_complete(ctxt): | ||
3045 | 147 | return {} | ||
3046 | 148 | return ctxt | ||
3047 | 149 | |||
3048 | 150 | |||
3049 | 151 | class CephContext(OSContextGenerator): | ||
3050 | 152 | interfaces = ['ceph'] | ||
3051 | 153 | |||
3052 | 154 | def __call__(self): | ||
3053 | 155 | '''This generates context for /etc/ceph/ceph.conf templates''' | ||
3054 | 156 | log('Generating tmeplate context for ceph') | ||
3055 | 157 | mon_hosts = [] | ||
3056 | 158 | auth = None | ||
3057 | 159 | for rid in relation_ids('ceph'): | ||
3058 | 160 | for unit in related_units(rid): | ||
3059 | 161 | mon_hosts.append(relation_get('private-address', rid=rid, | ||
3060 | 162 | unit=unit)) | ||
3061 | 163 | auth = relation_get('auth', rid=rid, unit=unit) | ||
3062 | 164 | |||
3063 | 165 | ctxt = { | ||
3064 | 166 | 'mon_hosts': ' '.join(mon_hosts), | ||
3065 | 167 | 'auth': auth, | ||
3066 | 168 | } | ||
3067 | 169 | if not context_complete(ctxt): | ||
3068 | 170 | return {} | ||
3069 | 171 | return ctxt | ||
3070 | 172 | |||
3071 | 173 | |||
3072 | 174 | class HAProxyContext(OSContextGenerator): | ||
3073 | 175 | interfaces = ['cluster'] | ||
3074 | 176 | |||
3075 | 177 | def __call__(self): | ||
3076 | 178 | ''' | ||
3077 | 179 | Builds half a context for the haproxy template, which describes | ||
3078 | 180 | all peers to be included in the cluster. Each charm needs to include | ||
3079 | 181 | its own context generator that describes the port mapping. | ||
3080 | 182 | ''' | ||
3081 | 183 | if not relation_ids('cluster'): | ||
3082 | 184 | return {} | ||
3083 | 185 | |||
3084 | 186 | cluster_hosts = {} | ||
3085 | 187 | l_unit = local_unit().replace('/', '-') | ||
3086 | 188 | cluster_hosts[l_unit] = unit_get('private-address') | ||
3087 | 189 | |||
3088 | 190 | for rid in relation_ids('cluster'): | ||
3089 | 191 | for unit in related_units(rid): | ||
3090 | 192 | _unit = unit.replace('/', '-') | ||
3091 | 193 | addr = relation_get('private-address', rid=rid, unit=unit) | ||
3092 | 194 | cluster_hosts[_unit] = addr | ||
3093 | 195 | |||
3094 | 196 | ctxt = { | ||
3095 | 197 | 'units': cluster_hosts, | ||
3096 | 198 | } | ||
3097 | 199 | if len(cluster_hosts.keys()) > 1: | ||
3098 | 200 | # Enable haproxy when we have enough peers. | ||
3099 | 201 | log('Ensuring haproxy enabled in /etc/default/haproxy.') | ||
3100 | 202 | with open('/etc/default/haproxy', 'w') as out: | ||
3101 | 203 | out.write('ENABLED=1\n') | ||
3102 | 204 | return ctxt | ||
3103 | 205 | log('HAProxy context is incomplete, this unit has no peers.') | ||
3104 | 206 | return {} | ||
3105 | 207 | |||
3106 | 208 | |||
3107 | 209 | class ImageServiceContext(OSContextGenerator): | ||
3108 | 210 | interfaces = ['image-servce'] | ||
3109 | 211 | |||
3110 | 212 | def __call__(self): | ||
3111 | 213 | ''' | ||
3112 | 214 | Obtains the glance API server from the image-service relation. Useful | ||
3113 | 215 | in nova and cinder (currently). | ||
3114 | 216 | ''' | ||
3115 | 217 | log('Generating template context for image-service.') | ||
3116 | 218 | rids = relation_ids('image-service') | ||
3117 | 219 | if not rids: | ||
3118 | 220 | return {} | ||
3119 | 221 | for rid in rids: | ||
3120 | 222 | for unit in related_units(rid): | ||
3121 | 223 | api_server = relation_get('glance-api-server', | ||
3122 | 224 | rid=rid, unit=unit) | ||
3123 | 225 | if api_server: | ||
3124 | 226 | return {'glance_api_servers': api_server} | ||
3125 | 227 | log('ImageService context is incomplete. ' | ||
3126 | 228 | 'Missing required relation data.') | ||
3127 | 229 | return {} | ||
3128 | 230 | |||
3129 | 231 | |||
3130 | 232 | class ApacheSSLContext(OSContextGenerator): | ||
3131 | 233 | """ | ||
3132 | 234 | Generates a context for an apache vhost configuration that configures | ||
3133 | 235 | HTTPS reverse proxying for one or many endpoints. Generated context | ||
3134 | 236 | looks something like: | ||
3135 | 237 | { | ||
3136 | 238 | 'namespace': 'cinder', | ||
3137 | 239 | 'private_address': 'iscsi.mycinderhost.com', | ||
3138 | 240 | 'endpoints': [(8776, 8766), (8777, 8767)] | ||
3139 | 241 | } | ||
3140 | 242 | |||
3141 | 243 | The endpoints list consists of a tuples mapping external ports | ||
3142 | 244 | to internal ports. | ||
3143 | 245 | """ | ||
3144 | 246 | interfaces = ['https'] | ||
3145 | 247 | |||
3146 | 248 | # charms should inherit this context and set external ports | ||
3147 | 249 | # and service namespace accordingly. | ||
3148 | 250 | external_ports = [] | ||
3149 | 251 | service_namespace = None | ||
3150 | 252 | |||
3151 | 253 | def enable_modules(self): | ||
3152 | 254 | cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http'] | ||
3153 | 255 | check_call(cmd) | ||
3154 | 256 | |||
3155 | 257 | def configure_cert(self): | ||
3156 | 258 | if not os.path.isdir('/etc/apache2/ssl'): | ||
3157 | 259 | os.mkdir('/etc/apache2/ssl') | ||
3158 | 260 | ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace) | ||
3159 | 261 | if not os.path.isdir(ssl_dir): | ||
3160 | 262 | os.mkdir(ssl_dir) | ||
3161 | 263 | cert, key = get_cert() | ||
3162 | 264 | with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out: | ||
3163 | 265 | cert_out.write(b64decode(cert)) | ||
3164 | 266 | with open(os.path.join(ssl_dir, 'key'), 'w') as key_out: | ||
3165 | 267 | key_out.write(b64decode(key)) | ||
3166 | 268 | ca_cert = get_ca_cert() | ||
3167 | 269 | if ca_cert: | ||
3168 | 270 | with open(CA_CERT_PATH, 'w') as ca_out: | ||
3169 | 271 | ca_out.write(b64decode(ca_cert)) | ||
3170 | 272 | |||
3171 | 273 | def __call__(self): | ||
3172 | 274 | if isinstance(self.external_ports, basestring): | ||
3173 | 275 | self.external_ports = [self.external_ports] | ||
3174 | 276 | if (not self.external_ports or not https()): | ||
3175 | 277 | return {} | ||
3176 | 278 | |||
3177 | 279 | self.configure_cert() | ||
3178 | 280 | self.enable_modules() | ||
3179 | 281 | |||
3180 | 282 | ctxt = { | ||
3181 | 283 | 'namespace': self.service_namespace, | ||
3182 | 284 | 'private_address': unit_get('private-address'), | ||
3183 | 285 | 'endpoints': [] | ||
3184 | 286 | } | ||
3185 | 287 | for ext_port in self.external_ports: | ||
3186 | 288 | if peer_units() or is_clustered(): | ||
3187 | 289 | int_port = determine_haproxy_port(ext_port) | ||
3188 | 290 | else: | ||
3189 | 291 | int_port = determine_api_port(ext_port) | ||
3190 | 292 | portmap = (int(ext_port), int(int_port)) | ||
3191 | 293 | ctxt['endpoints'].append(portmap) | ||
3192 | 294 | return ctxt | ||
3193 | 295 | 0 | ||
3194 | === removed directory 'hooks/install.d/charmhelpers/contrib/openstack/templates' | |||
3195 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/templates/__init__.py' | |||
3196 | --- hooks/install.d/charmhelpers/contrib/openstack/templates/__init__.py 2015-08-19 19:17:42 +0000 | |||
3197 | +++ hooks/install.d/charmhelpers/contrib/openstack/templates/__init__.py 1970-01-01 00:00:00 +0000 | |||
3198 | @@ -1,2 +0,0 @@ | |||
3199 | 1 | # dummy __init__.py to fool syncer into thinking this is a syncable python | ||
3200 | 2 | # module | ||
3201 | 3 | 0 | ||
3202 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/templates/ceph.conf' | |||
3203 | --- hooks/install.d/charmhelpers/contrib/openstack/templates/ceph.conf 2015-08-19 19:17:42 +0000 | |||
3204 | +++ hooks/install.d/charmhelpers/contrib/openstack/templates/ceph.conf 1970-01-01 00:00:00 +0000 | |||
3205 | @@ -1,11 +0,0 @@ | |||
3206 | 1 | ############################################################################### | ||
3207 | 2 | # [ WARNING ] | ||
3208 | 3 | # cinder configuration file maintained by Juju | ||
3209 | 4 | # local changes may be overwritten. | ||
3210 | 5 | ############################################################################### | ||
3211 | 6 | {% if auth -%} | ||
3212 | 7 | [global] | ||
3213 | 8 | auth_supported = {{ auth }} | ||
3214 | 9 | keyring = /etc/ceph/$cluster.$name.keyring | ||
3215 | 10 | mon host = {{ mon_hosts }} | ||
3216 | 11 | {% endif -%} | ||
3217 | 12 | 0 | ||
3218 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/templates/haproxy.cfg' | |||
3219 | --- hooks/install.d/charmhelpers/contrib/openstack/templates/haproxy.cfg 2015-08-19 19:17:42 +0000 | |||
3220 | +++ hooks/install.d/charmhelpers/contrib/openstack/templates/haproxy.cfg 1970-01-01 00:00:00 +0000 | |||
3221 | @@ -1,37 +0,0 @@ | |||
3222 | 1 | global | ||
3223 | 2 | log 127.0.0.1 local0 | ||
3224 | 3 | log 127.0.0.1 local1 notice | ||
3225 | 4 | maxconn 20000 | ||
3226 | 5 | user haproxy | ||
3227 | 6 | group haproxy | ||
3228 | 7 | spread-checks 0 | ||
3229 | 8 | |||
3230 | 9 | defaults | ||
3231 | 10 | log global | ||
3232 | 11 | mode http | ||
3233 | 12 | option httplog | ||
3234 | 13 | option dontlognull | ||
3235 | 14 | retries 3 | ||
3236 | 15 | timeout queue 1000 | ||
3237 | 16 | timeout connect 1000 | ||
3238 | 17 | timeout client 30000 | ||
3239 | 18 | timeout server 30000 | ||
3240 | 19 | |||
3241 | 20 | listen stats :8888 | ||
3242 | 21 | mode http | ||
3243 | 22 | stats enable | ||
3244 | 23 | stats hide-version | ||
3245 | 24 | stats realm Haproxy\ Statistics | ||
3246 | 25 | stats uri / | ||
3247 | 26 | stats auth admin:password | ||
3248 | 27 | |||
3249 | 28 | {% if units -%} | ||
3250 | 29 | {% for service, ports in service_ports.iteritems() -%} | ||
3251 | 30 | listen {{ service }} 0.0.0.0:{{ ports[0] }} | ||
3252 | 31 | balance roundrobin | ||
3253 | 32 | option tcplog | ||
3254 | 33 | {% for unit, address in units.iteritems() -%} | ||
3255 | 34 | server {{ unit }} {{ address }}:{{ ports[1] }} check | ||
3256 | 35 | {% endfor %} | ||
3257 | 36 | {% endfor -%} | ||
3258 | 37 | {% endif -%} | ||
3259 | 38 | 0 | ||
3260 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/templates/openstack_https_frontend' | |||
3261 | --- hooks/install.d/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2015-08-19 19:17:42 +0000 | |||
3262 | +++ hooks/install.d/charmhelpers/contrib/openstack/templates/openstack_https_frontend 1970-01-01 00:00:00 +0000 | |||
3263 | @@ -1,23 +0,0 @@ | |||
3264 | 1 | {% if endpoints -%} | ||
3265 | 2 | {% for ext, int in endpoints -%} | ||
3266 | 3 | Listen {{ ext }} | ||
3267 | 4 | NameVirtualHost *:{{ ext }} | ||
3268 | 5 | <VirtualHost *:{{ ext }}> | ||
3269 | 6 | ServerName {{ private_address }} | ||
3270 | 7 | SSLEngine on | ||
3271 | 8 | SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert | ||
3272 | 9 | SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key | ||
3273 | 10 | ProxyPass / http://localhost:{{ int }}/ | ||
3274 | 11 | ProxyPassReverse / http://localhost:{{ int }}/ | ||
3275 | 12 | ProxyPreserveHost on | ||
3276 | 13 | </VirtualHost> | ||
3277 | 14 | <Proxy *> | ||
3278 | 15 | Order deny,allow | ||
3279 | 16 | Allow from all | ||
3280 | 17 | </Proxy> | ||
3281 | 18 | <Location /> | ||
3282 | 19 | Order allow,deny | ||
3283 | 20 | Allow from all | ||
3284 | 21 | </Location> | ||
3285 | 22 | {% endfor -%} | ||
3286 | 23 | {% endif -%} | ||
3287 | 24 | 0 | ||
3288 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/templating.py' | |||
3289 | --- hooks/install.d/charmhelpers/contrib/openstack/templating.py 2015-08-19 19:17:42 +0000 | |||
3290 | +++ hooks/install.d/charmhelpers/contrib/openstack/templating.py 1970-01-01 00:00:00 +0000 | |||
3291 | @@ -1,261 +0,0 @@ | |||
3292 | 1 | import os | ||
3293 | 2 | |||
3294 | 3 | from charmhelpers.fetch import apt_install | ||
3295 | 4 | |||
3296 | 5 | from charmhelpers.core.hookenv import ( | ||
3297 | 6 | log, | ||
3298 | 7 | ERROR, | ||
3299 | 8 | INFO | ||
3300 | 9 | ) | ||
3301 | 10 | |||
3302 | 11 | from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES | ||
3303 | 12 | |||
3304 | 13 | try: | ||
3305 | 14 | from jinja2 import FileSystemLoader, ChoiceLoader, Environment | ||
3306 | 15 | except ImportError: | ||
3307 | 16 | # python-jinja2 may not be installed yet, or we're running unittests. | ||
3308 | 17 | FileSystemLoader = ChoiceLoader = Environment = None | ||
3309 | 18 | |||
3310 | 19 | |||
3311 | 20 | class OSConfigException(Exception): | ||
3312 | 21 | pass | ||
3313 | 22 | |||
3314 | 23 | |||
3315 | 24 | def get_loader(templates_dir, os_release): | ||
3316 | 25 | """ | ||
3317 | 26 | Create a jinja2.ChoiceLoader containing template dirs up to | ||
3318 | 27 | and including os_release. If directory template directory | ||
3319 | 28 | is missing at templates_dir, it will be omitted from the loader. | ||
3320 | 29 | templates_dir is added to the bottom of the search list as a base | ||
3321 | 30 | loading dir. | ||
3322 | 31 | |||
3323 | 32 | A charm may also ship a templates dir with this module | ||
3324 | 33 | and it will be appended to the bottom of the search list, eg: | ||
3325 | 34 | hooks/charmhelpers/contrib/openstack/templates. | ||
3326 | 35 | |||
3327 | 36 | :param templates_dir: str: Base template directory containing release | ||
3328 | 37 | sub-directories. | ||
3329 | 38 | :param os_release : str: OpenStack release codename to construct template | ||
3330 | 39 | loader. | ||
3331 | 40 | |||
3332 | 41 | :returns : jinja2.ChoiceLoader constructed with a list of | ||
3333 | 42 | jinja2.FilesystemLoaders, ordered in descending | ||
3334 | 43 | order by OpenStack release. | ||
3335 | 44 | """ | ||
3336 | 45 | tmpl_dirs = [(rel, os.path.join(templates_dir, rel)) | ||
3337 | 46 | for rel in OPENSTACK_CODENAMES.itervalues()] | ||
3338 | 47 | |||
3339 | 48 | if not os.path.isdir(templates_dir): | ||
3340 | 49 | log('Templates directory not found @ %s.' % templates_dir, | ||
3341 | 50 | level=ERROR) | ||
3342 | 51 | raise OSConfigException | ||
3343 | 52 | |||
3344 | 53 | # the bottom contains tempaltes_dir and possibly a common templates dir | ||
3345 | 54 | # shipped with the helper. | ||
3346 | 55 | loaders = [FileSystemLoader(templates_dir)] | ||
3347 | 56 | helper_templates = os.path.join(os.path.dirname(__file__), 'templates') | ||
3348 | 57 | if os.path.isdir(helper_templates): | ||
3349 | 58 | loaders.append(FileSystemLoader(helper_templates)) | ||
3350 | 59 | |||
3351 | 60 | for rel, tmpl_dir in tmpl_dirs: | ||
3352 | 61 | if os.path.isdir(tmpl_dir): | ||
3353 | 62 | loaders.insert(0, FileSystemLoader(tmpl_dir)) | ||
3354 | 63 | if rel == os_release: | ||
3355 | 64 | break | ||
3356 | 65 | log('Creating choice loader with dirs: %s' % | ||
3357 | 66 | [l.searchpath for l in loaders], level=INFO) | ||
3358 | 67 | return ChoiceLoader(loaders) | ||
3359 | 68 | |||
3360 | 69 | |||
3361 | 70 | class OSConfigTemplate(object): | ||
3362 | 71 | """ | ||
3363 | 72 | Associates a config file template with a list of context generators. | ||
3364 | 73 | Responsible for constructing a template context based on those generators. | ||
3365 | 74 | """ | ||
3366 | 75 | def __init__(self, config_file, contexts): | ||
3367 | 76 | self.config_file = config_file | ||
3368 | 77 | |||
3369 | 78 | if hasattr(contexts, '__call__'): | ||
3370 | 79 | self.contexts = [contexts] | ||
3371 | 80 | else: | ||
3372 | 81 | self.contexts = contexts | ||
3373 | 82 | |||
3374 | 83 | self._complete_contexts = [] | ||
3375 | 84 | |||
3376 | 85 | def context(self): | ||
3377 | 86 | ctxt = {} | ||
3378 | 87 | for context in self.contexts: | ||
3379 | 88 | _ctxt = context() | ||
3380 | 89 | if _ctxt: | ||
3381 | 90 | ctxt.update(_ctxt) | ||
3382 | 91 | # track interfaces for every complete context. | ||
3383 | 92 | [self._complete_contexts.append(interface) | ||
3384 | 93 | for interface in context.interfaces | ||
3385 | 94 | if interface not in self._complete_contexts] | ||
3386 | 95 | return ctxt | ||
3387 | 96 | |||
3388 | 97 | def complete_contexts(self): | ||
3389 | 98 | ''' | ||
3390 | 99 | Return a list of interfaces that have atisfied contexts. | ||
3391 | 100 | ''' | ||
3392 | 101 | if self._complete_contexts: | ||
3393 | 102 | return self._complete_contexts | ||
3394 | 103 | self.context() | ||
3395 | 104 | return self._complete_contexts | ||
3396 | 105 | |||
3397 | 106 | |||
3398 | 107 | class OSConfigRenderer(object): | ||
3399 | 108 | """ | ||
3400 | 109 | This class provides a common templating system to be used by OpenStack | ||
3401 | 110 | charms. It is intended to help charms share common code and templates, | ||
3402 | 111 | and ease the burden of managing config templates across multiple OpenStack | ||
3403 | 112 | releases. | ||
3404 | 113 | |||
3405 | 114 | Basic usage: | ||
3406 | 115 | # import some common context generates from charmhelpers | ||
3407 | 116 | from charmhelpers.contrib.openstack import context | ||
3408 | 117 | |||
3409 | 118 | # Create a renderer object for a specific OS release. | ||
3410 | 119 | configs = OSConfigRenderer(templates_dir='/tmp/templates', | ||
3411 | 120 | openstack_release='folsom') | ||
3412 | 121 | # register some config files with context generators. | ||
3413 | 122 | configs.register(config_file='/etc/nova/nova.conf', | ||
3414 | 123 | contexts=[context.SharedDBContext(), | ||
3415 | 124 | context.AMQPContext()]) | ||
3416 | 125 | configs.register(config_file='/etc/nova/api-paste.ini', | ||
3417 | 126 | contexts=[context.IdentityServiceContext()]) | ||
3418 | 127 | configs.register(config_file='/etc/haproxy/haproxy.conf', | ||
3419 | 128 | contexts=[context.HAProxyContext()]) | ||
3420 | 129 | # write out a single config | ||
3421 | 130 | configs.write('/etc/nova/nova.conf') | ||
3422 | 131 | # write out all registered configs | ||
3423 | 132 | configs.write_all() | ||
3424 | 133 | |||
3425 | 134 | Details: | ||
3426 | 135 | |||
3427 | 136 | OpenStack Releases and template loading | ||
3428 | 137 | --------------------------------------- | ||
3429 | 138 | When the object is instantiated, it is associated with a specific OS | ||
3430 | 139 | release. This dictates how the template loader will be constructed. | ||
3431 | 140 | |||
3432 | 141 | The constructed loader attempts to load the template from several places | ||
3433 | 142 | in the following order: | ||
3434 | 143 | - from the most recent OS release-specific template dir (if one exists) | ||
3435 | 144 | - the base templates_dir | ||
3436 | 145 | - a template directory shipped in the charm with this helper file. | ||
3437 | 146 | |||
3438 | 147 | |||
3439 | 148 | For the example above, '/tmp/templates' contains the following structure: | ||
3440 | 149 | /tmp/templates/nova.conf | ||
3441 | 150 | /tmp/templates/api-paste.ini | ||
3442 | 151 | /tmp/templates/grizzly/api-paste.ini | ||
3443 | 152 | /tmp/templates/havana/api-paste.ini | ||
3444 | 153 | |||
3445 | 154 | Since it was registered with the grizzly release, it first seraches | ||
3446 | 155 | the grizzly directory for nova.conf, then the templates dir. | ||
3447 | 156 | |||
3448 | 157 | When writing api-paste.ini, it will find the template in the grizzly | ||
3449 | 158 | directory. | ||
3450 | 159 | |||
3451 | 160 | If the object were created with folsom, it would fall back to the | ||
3452 | 161 | base templates dir for its api-paste.ini template. | ||
3453 | 162 | |||
3454 | 163 | This system should help manage changes in config files through | ||
3455 | 164 | openstack releases, allowing charms to fall back to the most recently | ||
3456 | 165 | updated config template for a given release | ||
3457 | 166 | |||
3458 | 167 | The haproxy.conf, since it is not shipped in the templates dir, will | ||
3459 | 168 | be loaded from the module directory's template directory, eg | ||
3460 | 169 | $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows | ||
3461 | 170 | us to ship common templates (haproxy, apache) with the helpers. | ||
3462 | 171 | |||
3463 | 172 | Context generators | ||
3464 | 173 | --------------------------------------- | ||
3465 | 174 | Context generators are used to generate template contexts during hook | ||
3466 | 175 | execution. Doing so may require inspecting service relations, charm | ||
3467 | 176 | config, etc. When registered, a config file is associated with a list | ||
3468 | 177 | of generators. When a template is rendered and written, all context | ||
3469 | 178 | generates are called in a chain to generate the context dictionary | ||
3470 | 179 | passed to the jinja2 template. See context.py for more info. | ||
3471 | 180 | """ | ||
3472 | 181 | def __init__(self, templates_dir, openstack_release): | ||
3473 | 182 | if not os.path.isdir(templates_dir): | ||
3474 | 183 | log('Could not locate templates dir %s' % templates_dir, | ||
3475 | 184 | level=ERROR) | ||
3476 | 185 | raise OSConfigException | ||
3477 | 186 | |||
3478 | 187 | self.templates_dir = templates_dir | ||
3479 | 188 | self.openstack_release = openstack_release | ||
3480 | 189 | self.templates = {} | ||
3481 | 190 | self._tmpl_env = None | ||
3482 | 191 | |||
3483 | 192 | if None in [Environment, ChoiceLoader, FileSystemLoader]: | ||
3484 | 193 | # if this code is running, the object is created pre-install hook. | ||
3485 | 194 | # jinja2 shouldn't get touched until the module is reloaded on next | ||
3486 | 195 | # hook execution, with proper jinja2 bits successfully imported. | ||
3487 | 196 | apt_install('python-jinja2') | ||
3488 | 197 | |||
3489 | 198 | def register(self, config_file, contexts): | ||
3490 | 199 | """ | ||
3491 | 200 | Register a config file with a list of context generators to be called | ||
3492 | 201 | during rendering. | ||
3493 | 202 | """ | ||
3494 | 203 | self.templates[config_file] = OSConfigTemplate(config_file=config_file, | ||
3495 | 204 | contexts=contexts) | ||
3496 | 205 | log('Registered config file: %s' % config_file, level=INFO) | ||
3497 | 206 | |||
3498 | 207 | def _get_tmpl_env(self): | ||
3499 | 208 | if not self._tmpl_env: | ||
3500 | 209 | loader = get_loader(self.templates_dir, self.openstack_release) | ||
3501 | 210 | self._tmpl_env = Environment(loader=loader) | ||
3502 | 211 | |||
3503 | 212 | def _get_template(self, template): | ||
3504 | 213 | self._get_tmpl_env() | ||
3505 | 214 | template = self._tmpl_env.get_template(template) | ||
3506 | 215 | log('Loaded template from %s' % template.filename, level=INFO) | ||
3507 | 216 | return template | ||
3508 | 217 | |||
3509 | 218 | def render(self, config_file): | ||
3510 | 219 | if config_file not in self.templates: | ||
3511 | 220 | log('Config not registered: %s' % config_file, level=ERROR) | ||
3512 | 221 | raise OSConfigException | ||
3513 | 222 | ctxt = self.templates[config_file].context() | ||
3514 | 223 | _tmpl = os.path.basename(config_file) | ||
3515 | 224 | log('Rendering from template: %s' % _tmpl, level=INFO) | ||
3516 | 225 | template = self._get_template(_tmpl) | ||
3517 | 226 | return template.render(ctxt) | ||
3518 | 227 | |||
3519 | 228 | def write(self, config_file): | ||
3520 | 229 | """ | ||
3521 | 230 | Write a single config file, raises if config file is not registered. | ||
3522 | 231 | """ | ||
3523 | 232 | if config_file not in self.templates: | ||
3524 | 233 | log('Config not registered: %s' % config_file, level=ERROR) | ||
3525 | 234 | raise OSConfigException | ||
3526 | 235 | with open(config_file, 'wb') as out: | ||
3527 | 236 | out.write(self.render(config_file)) | ||
3528 | 237 | log('Wrote template %s.' % config_file, level=INFO) | ||
3529 | 238 | |||
3530 | 239 | def write_all(self): | ||
3531 | 240 | """ | ||
3532 | 241 | Write out all registered config files. | ||
3533 | 242 | """ | ||
3534 | 243 | [self.write(k) for k in self.templates.iterkeys()] | ||
3535 | 244 | |||
3536 | 245 | def set_release(self, openstack_release): | ||
3537 | 246 | """ | ||
3538 | 247 | Resets the template environment and generates a new template loader | ||
3539 | 248 | based on a the new openstack release. | ||
3540 | 249 | """ | ||
3541 | 250 | self._tmpl_env = None | ||
3542 | 251 | self.openstack_release = openstack_release | ||
3543 | 252 | self._get_tmpl_env() | ||
3544 | 253 | |||
3545 | 254 | def complete_contexts(self): | ||
3546 | 255 | ''' | ||
3547 | 256 | Returns a list of context interfaces that yield a complete context. | ||
3548 | 257 | ''' | ||
3549 | 258 | interfaces = [] | ||
3550 | 259 | [interfaces.extend(i.complete_contexts()) | ||
3551 | 260 | for i in self.templates.itervalues()] | ||
3552 | 261 | return interfaces | ||
3553 | 262 | 0 | ||
3554 | === removed file 'hooks/install.d/charmhelpers/contrib/openstack/utils.py' | |||
3555 | --- hooks/install.d/charmhelpers/contrib/openstack/utils.py 2015-08-19 19:17:42 +0000 | |||
3556 | +++ hooks/install.d/charmhelpers/contrib/openstack/utils.py 1970-01-01 00:00:00 +0000 | |||
3557 | @@ -1,276 +0,0 @@ | |||
3558 | 1 | #!/usr/bin/python | ||
3559 | 2 | |||
3560 | 3 | # Common python helper functions used for OpenStack charms. | ||
3561 | 4 | |||
3562 | 5 | from collections import OrderedDict | ||
3563 | 6 | |||
3564 | 7 | import apt_pkg as apt | ||
3565 | 8 | import subprocess | ||
3566 | 9 | import os | ||
3567 | 10 | import sys | ||
3568 | 11 | |||
3569 | 12 | from charmhelpers.core.hookenv import ( | ||
3570 | 13 | config, | ||
3571 | 14 | log as juju_log, | ||
3572 | 15 | charm_dir, | ||
3573 | 16 | ) | ||
3574 | 17 | |||
3575 | 18 | from charmhelpers.core.host import ( | ||
3576 | 19 | lsb_release, | ||
3577 | 20 | ) | ||
3578 | 21 | |||
3579 | 22 | from charmhelpers.fetch import ( | ||
3580 | 23 | apt_install, | ||
3581 | 24 | ) | ||
3582 | 25 | |||
3583 | 26 | CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" | ||
3584 | 27 | CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' | ||
3585 | 28 | |||
3586 | 29 | UBUNTU_OPENSTACK_RELEASE = OrderedDict([ | ||
3587 | 30 | ('oneiric', 'diablo'), | ||
3588 | 31 | ('precise', 'essex'), | ||
3589 | 32 | ('quantal', 'folsom'), | ||
3590 | 33 | ('raring', 'grizzly'), | ||
3591 | 34 | ('saucy', 'havana'), | ||
3592 | 35 | ]) | ||
3593 | 36 | |||
3594 | 37 | |||
3595 | 38 | OPENSTACK_CODENAMES = OrderedDict([ | ||
3596 | 39 | ('2011.2', 'diablo'), | ||
3597 | 40 | ('2012.1', 'essex'), | ||
3598 | 41 | ('2012.2', 'folsom'), | ||
3599 | 42 | ('2013.1', 'grizzly'), | ||
3600 | 43 | ('2013.2', 'havana'), | ||
3601 | 44 | ('2014.1', 'icehouse'), | ||
3602 | 45 | ]) | ||
3603 | 46 | |||
3604 | 47 | # The ugly duckling | ||
3605 | 48 | SWIFT_CODENAMES = { | ||
3606 | 49 | '1.4.3': 'diablo', | ||
3607 | 50 | '1.4.8': 'essex', | ||
3608 | 51 | '1.7.4': 'folsom', | ||
3609 | 52 | '1.7.6': 'grizzly', | ||
3610 | 53 | '1.7.7': 'grizzly', | ||
3611 | 54 | '1.8.0': 'grizzly', | ||
3612 | 55 | '1.9.0': 'havana', | ||
3613 | 56 | '1.9.1': 'havana', | ||
3614 | 57 | } | ||
3615 | 58 | |||
3616 | 59 | |||
3617 | 60 | def error_out(msg): | ||
3618 | 61 | juju_log("FATAL ERROR: %s" % msg, level='ERROR') | ||
3619 | 62 | sys.exit(1) | ||
3620 | 63 | |||
3621 | 64 | |||
3622 | 65 | def get_os_codename_install_source(src): | ||
3623 | 66 | '''Derive OpenStack release codename from a given installation source.''' | ||
3624 | 67 | ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] | ||
3625 | 68 | rel = '' | ||
3626 | 69 | if src == 'distro': | ||
3627 | 70 | try: | ||
3628 | 71 | rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel] | ||
3629 | 72 | except KeyError: | ||
3630 | 73 | e = 'Could not derive openstack release for '\ | ||
3631 | 74 | 'this Ubuntu release: %s' % ubuntu_rel | ||
3632 | 75 | error_out(e) | ||
3633 | 76 | return rel | ||
3634 | 77 | |||
3635 | 78 | if src.startswith('cloud:'): | ||
3636 | 79 | ca_rel = src.split(':')[1] | ||
3637 | 80 | ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0] | ||
3638 | 81 | return ca_rel | ||
3639 | 82 | |||
3640 | 83 | # Best guess match based on deb string provided | ||
3641 | 84 | if src.startswith('deb') or src.startswith('ppa'): | ||
3642 | 85 | for k, v in OPENSTACK_CODENAMES.iteritems(): | ||
3643 | 86 | if v in src: | ||
3644 | 87 | return v | ||
3645 | 88 | |||
3646 | 89 | |||
3647 | 90 | def get_os_version_install_source(src): | ||
3648 | 91 | codename = get_os_codename_install_source(src) | ||
3649 | 92 | return get_os_version_codename(codename) | ||
3650 | 93 | |||
3651 | 94 | |||
3652 | 95 | def get_os_codename_version(vers): | ||
3653 | 96 | '''Determine OpenStack codename from version number.''' | ||
3654 | 97 | try: | ||
3655 | 98 | return OPENSTACK_CODENAMES[vers] | ||
3656 | 99 | except KeyError: | ||
3657 | 100 | e = 'Could not determine OpenStack codename for version %s' % vers | ||
3658 | 101 | error_out(e) | ||
3659 | 102 | |||
3660 | 103 | |||
3661 | 104 | def get_os_version_codename(codename): | ||
3662 | 105 | '''Determine OpenStack version number from codename.''' | ||
3663 | 106 | for k, v in OPENSTACK_CODENAMES.iteritems(): | ||
3664 | 107 | if v == codename: | ||
3665 | 108 | return k | ||
3666 | 109 | e = 'Could not derive OpenStack version for '\ | ||
3667 | 110 | 'codename: %s' % codename | ||
3668 | 111 | error_out(e) | ||
3669 | 112 | |||
3670 | 113 | |||
3671 | 114 | def get_os_codename_package(package, fatal=True): | ||
3672 | 115 | '''Derive OpenStack release codename from an installed package.''' | ||
3673 | 116 | apt.init() | ||
3674 | 117 | cache = apt.Cache() | ||
3675 | 118 | |||
3676 | 119 | try: | ||
3677 | 120 | pkg = cache[package] | ||
3678 | 121 | except: | ||
3679 | 122 | if not fatal: | ||
3680 | 123 | return None | ||
3681 | 124 | # the package is unknown to the current apt cache. | ||
3682 | 125 | e = 'Could not determine version of package with no installation '\ | ||
3683 | 126 | 'candidate: %s' % package | ||
3684 | 127 | error_out(e) | ||
3685 | 128 | |||
3686 | 129 | if not pkg.current_ver: | ||
3687 | 130 | if not fatal: | ||
3688 | 131 | return None | ||
3689 | 132 | # package is known, but no version is currently installed. | ||
3690 | 133 | e = 'Could not determine version of uninstalled package: %s' % package | ||
3691 | 134 | error_out(e) | ||
3692 | 135 | |||
3693 | 136 | vers = apt.upstream_version(pkg.current_ver.ver_str) | ||
3694 | 137 | |||
3695 | 138 | try: | ||
3696 | 139 | if 'swift' in pkg.name: | ||
3697 | 140 | vers = vers[:5] | ||
3698 | 141 | return SWIFT_CODENAMES[vers] | ||
3699 | 142 | else: | ||
3700 | 143 | vers = vers[:6] | ||
3701 | 144 | return OPENSTACK_CODENAMES[vers] | ||
3702 | 145 | except KeyError: | ||
3703 | 146 | e = 'Could not determine OpenStack codename for version %s' % vers | ||
3704 | 147 | error_out(e) | ||
3705 | 148 | |||
3706 | 149 | |||
3707 | 150 | def get_os_version_package(pkg, fatal=True): | ||
3708 | 151 | '''Derive OpenStack version number from an installed package.''' | ||
3709 | 152 | codename = get_os_codename_package(pkg, fatal=fatal) | ||
3710 | 153 | |||
3711 | 154 | if not codename: | ||
3712 | 155 | return None | ||
3713 | 156 | |||
3714 | 157 | if 'swift' in pkg: | ||
3715 | 158 | vers_map = SWIFT_CODENAMES | ||
3716 | 159 | else: | ||
3717 | 160 | vers_map = OPENSTACK_CODENAMES | ||
3718 | 161 | |||
3719 | 162 | for version, cname in vers_map.iteritems(): | ||
3720 | 163 | if cname == codename: | ||
3721 | 164 | return version | ||
3722 | 165 | #e = "Could not determine OpenStack version for package: %s" % pkg | ||
3723 | 166 | #error_out(e) | ||
3724 | 167 | |||
3725 | 168 | |||
3726 | 169 | def import_key(keyid): | ||
3727 | 170 | cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \ | ||
3728 | 171 | "--recv-keys %s" % keyid | ||
3729 | 172 | try: | ||
3730 | 173 | subprocess.check_call(cmd.split(' ')) | ||
3731 | 174 | except subprocess.CalledProcessError: | ||
3732 | 175 | error_out("Error importing repo key %s" % keyid) | ||
3733 | 176 | |||
3734 | 177 | |||
3735 | 178 | def configure_installation_source(rel): | ||
3736 | 179 | '''Configure apt installation source.''' | ||
3737 | 180 | if rel == 'distro': | ||
3738 | 181 | return | ||
3739 | 182 | elif rel[:4] == "ppa:": | ||
3740 | 183 | src = rel | ||
3741 | 184 | subprocess.check_call(["add-apt-repository", "-y", src]) | ||
3742 | 185 | elif rel[:3] == "deb": | ||
3743 | 186 | l = len(rel.split('|')) | ||
3744 | 187 | if l == 2: | ||
3745 | 188 | src, key = rel.split('|') | ||
3746 | 189 | juju_log("Importing PPA key from keyserver for %s" % src) | ||
3747 | 190 | import_key(key) | ||
3748 | 191 | elif l == 1: | ||
3749 | 192 | src = rel | ||
3750 | 193 | with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: | ||
3751 | 194 | f.write(src) | ||
3752 | 195 | elif rel[:6] == 'cloud:': | ||
3753 | 196 | ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] | ||
3754 | 197 | rel = rel.split(':')[1] | ||
3755 | 198 | u_rel = rel.split('-')[0] | ||
3756 | 199 | ca_rel = rel.split('-')[1] | ||
3757 | 200 | |||
3758 | 201 | if u_rel != ubuntu_rel: | ||
3759 | 202 | e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\ | ||
3760 | 203 | 'version (%s)' % (ca_rel, ubuntu_rel) | ||
3761 | 204 | error_out(e) | ||
3762 | 205 | |||
3763 | 206 | if 'staging' in ca_rel: | ||
3764 | 207 | # staging is just a regular PPA. | ||
3765 | 208 | os_rel = ca_rel.split('/')[0] | ||
3766 | 209 | ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel | ||
3767 | 210 | cmd = 'add-apt-repository -y %s' % ppa | ||
3768 | 211 | subprocess.check_call(cmd.split(' ')) | ||
3769 | 212 | return | ||
3770 | 213 | |||
3771 | 214 | # map charm config options to actual archive pockets. | ||
3772 | 215 | pockets = { | ||
3773 | 216 | 'folsom': 'precise-updates/folsom', | ||
3774 | 217 | 'folsom/updates': 'precise-updates/folsom', | ||
3775 | 218 | 'folsom/proposed': 'precise-proposed/folsom', | ||
3776 | 219 | 'grizzly': 'precise-updates/grizzly', | ||
3777 | 220 | 'grizzly/updates': 'precise-updates/grizzly', | ||
3778 | 221 | 'grizzly/proposed': 'precise-proposed/grizzly', | ||
3779 | 222 | 'havana': 'precise-updates/havana', | ||
3780 | 223 | 'havana/updates': 'precise-updates/havana', | ||
3781 | 224 | 'havana/proposed': 'precise-proposed/havana', | ||
3782 | 225 | } | ||
3783 | 226 | |||
3784 | 227 | try: | ||
3785 | 228 | pocket = pockets[ca_rel] | ||
3786 | 229 | except KeyError: | ||
3787 | 230 | e = 'Invalid Cloud Archive release specified: %s' % rel | ||
3788 | 231 | error_out(e) | ||
3789 | 232 | |||
3790 | 233 | src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket) | ||
3791 | 234 | apt_install('ubuntu-cloud-keyring', fatal=True) | ||
3792 | 235 | |||
3793 | 236 | with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f: | ||
3794 | 237 | f.write(src) | ||
3795 | 238 | else: | ||
3796 | 239 | error_out("Invalid openstack-release specified: %s" % rel) | ||
3797 | 240 | |||
3798 | 241 | |||
3799 | 242 | def save_script_rc(script_path="scripts/scriptrc", **env_vars): | ||
3800 | 243 | """ | ||
3801 | 244 | Write an rc file in the charm-delivered directory containing | ||
3802 | 245 | exported environment variables provided by env_vars. Any charm scripts run | ||
3803 | 246 | outside the juju hook environment can source this scriptrc to obtain | ||
3804 | 247 | updated config information necessary to perform health checks or | ||
3805 | 248 | service changes. | ||
3806 | 249 | """ | ||
3807 | 250 | juju_rc_path = "%s/%s" % (charm_dir(), script_path) | ||
3808 | 251 | if not os.path.exists(os.path.dirname(juju_rc_path)): | ||
3809 | 252 | os.mkdir(os.path.dirname(juju_rc_path)) | ||
3810 | 253 | with open(juju_rc_path, 'wb') as rc_script: | ||
3811 | 254 | rc_script.write( | ||
3812 | 255 | "#!/bin/bash\n") | ||
3813 | 256 | [rc_script.write('export %s=%s\n' % (u, p)) | ||
3814 | 257 | for u, p in env_vars.iteritems() if u != "script_path"] | ||
3815 | 258 | |||
3816 | 259 | |||
3817 | 260 | def openstack_upgrade_available(package): | ||
3818 | 261 | """ | ||
3819 | 262 | Determines if an OpenStack upgrade is available from installation | ||
3820 | 263 | source, based on version of installed package. | ||
3821 | 264 | |||
3822 | 265 | :param package: str: Name of installed package. | ||
3823 | 266 | |||
3824 | 267 | :returns: bool: : Returns True if configured installation source offers | ||
3825 | 268 | a newer version of package. | ||
3826 | 269 | |||
3827 | 270 | """ | ||
3828 | 271 | |||
3829 | 272 | src = config('openstack-origin') | ||
3830 | 273 | cur_vers = get_os_version_package(package) | ||
3831 | 274 | available_vers = get_os_version_install_source(src) | ||
3832 | 275 | apt.init() | ||
3833 | 276 | return apt.version_compare(available_vers, cur_vers) == 1 | ||
3834 | 277 | 0 | ||
3835 | === removed directory 'hooks/install.d/charmhelpers/contrib/saltstack' | |||
3836 | === removed file 'hooks/install.d/charmhelpers/contrib/saltstack/__init__.py' | |||
3837 | --- hooks/install.d/charmhelpers/contrib/saltstack/__init__.py 2015-08-19 19:17:42 +0000 | |||
3838 | +++ hooks/install.d/charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000 | |||
3839 | @@ -1,149 +0,0 @@ | |||
3840 | 1 | """Charm Helpers saltstack - declare the state of your machines. | ||
3841 | 2 | |||
3842 | 3 | This helper enables you to declare your machine state, rather than | ||
3843 | 4 | program it procedurally (and have to test each change to your procedures). | ||
3844 | 5 | Your install hook can be as simple as: | ||
3845 | 6 | |||
3846 | 7 | {{{ | ||
3847 | 8 | from charmhelpers.contrib.saltstack import ( | ||
3848 | 9 | install_salt_support, | ||
3849 | 10 | update_machine_state, | ||
3850 | 11 | ) | ||
3851 | 12 | |||
3852 | 13 | |||
3853 | 14 | def install(): | ||
3854 | 15 | install_salt_support() | ||
3855 | 16 | update_machine_state('machine_states/dependencies.yaml') | ||
3856 | 17 | update_machine_state('machine_states/installed.yaml') | ||
3857 | 18 | }}} | ||
3858 | 19 | |||
3859 | 20 | and won't need to change (nor will its tests) when you change the machine | ||
3860 | 21 | state. | ||
3861 | 22 | |||
3862 | 23 | It's using a python package called salt-minion which allows various formats for | ||
3863 | 24 | specifying resources, such as: | ||
3864 | 25 | |||
3865 | 26 | {{{ | ||
3866 | 27 | /srv/{{ basedir }}: | ||
3867 | 28 | file.directory: | ||
3868 | 29 | - group: ubunet | ||
3869 | 30 | - user: ubunet | ||
3870 | 31 | - require: | ||
3871 | 32 | - user: ubunet | ||
3872 | 33 | - recurse: | ||
3873 | 34 | - user | ||
3874 | 35 | - group | ||
3875 | 36 | |||
3876 | 37 | ubunet: | ||
3877 | 38 | group.present: | ||
3878 | 39 | - gid: 1500 | ||
3879 | 40 | user.present: | ||
3880 | 41 | - uid: 1500 | ||
3881 | 42 | - gid: 1500 | ||
3882 | 43 | - createhome: False | ||
3883 | 44 | - require: | ||
3884 | 45 | - group: ubunet | ||
3885 | 46 | }}} | ||
3886 | 47 | |||
3887 | 48 | The docs for all the different state definitions are at: | ||
3888 | 49 | http://docs.saltstack.com/ref/states/all/ | ||
3889 | 50 | |||
3890 | 51 | |||
3891 | 52 | TODO: | ||
3892 | 53 | * Add test helpers which will ensure that machine state definitions | ||
3893 | 54 | are functionally (but not necessarily logically) correct (ie. getting | ||
3894 | 55 | salt to parse all state defs. | ||
3895 | 56 | * Add a link to a public bootstrap charm example / blogpost. | ||
3896 | 57 | * Find a way to obviate the need to use the grains['charm_dir'] syntax | ||
3897 | 58 | in templates. | ||
3898 | 59 | """ | ||
3899 | 60 | # Copyright 2013 Canonical Ltd. | ||
3900 | 61 | # | ||
3901 | 62 | # Authors: | ||
3902 | 63 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
3903 | 64 | import os | ||
3904 | 65 | import subprocess | ||
3905 | 66 | import yaml | ||
3906 | 67 | |||
3907 | 68 | import charmhelpers.core.host | ||
3908 | 69 | import charmhelpers.core.hookenv | ||
3909 | 70 | |||
3910 | 71 | |||
3911 | 72 | charm_dir = os.environ.get('CHARM_DIR', '') | ||
3912 | 73 | salt_grains_path = '/etc/salt/grains' | ||
3913 | 74 | |||
3914 | 75 | |||
3915 | 76 | def install_salt_support(from_ppa=True): | ||
3916 | 77 | """Installs the salt-minion helper for machine state. | ||
3917 | 78 | |||
3918 | 79 | By default the salt-minion package is installed from | ||
3919 | 80 | the saltstack PPA. If from_ppa is False you must ensure | ||
3920 | 81 | that the salt-minion package is available in the apt cache. | ||
3921 | 82 | """ | ||
3922 | 83 | if from_ppa: | ||
3923 | 84 | subprocess.check_call([ | ||
3924 | 85 | '/usr/bin/add-apt-repository', | ||
3925 | 86 | '--yes', | ||
3926 | 87 | 'ppa:saltstack/salt', | ||
3927 | 88 | ]) | ||
3928 | 89 | subprocess.check_call(['/usr/bin/apt-get', 'update']) | ||
3929 | 90 | # We install salt-common as salt-minion would run the salt-minion | ||
3930 | 91 | # daemon. | ||
3931 | 92 | charmhelpers.fetch.apt_install('salt-common') | ||
3932 | 93 | |||
3933 | 94 | |||
3934 | 95 | def update_machine_state(state_path): | ||
3935 | 96 | """Update the machine state using the provided state declaration.""" | ||
3936 | 97 | juju_state_to_yaml(salt_grains_path) | ||
3937 | 98 | subprocess.check_call([ | ||
3938 | 99 | 'salt-call', | ||
3939 | 100 | '--local', | ||
3940 | 101 | 'state.template', | ||
3941 | 102 | state_path, | ||
3942 | 103 | ]) | ||
3943 | 104 | |||
3944 | 105 | |||
3945 | 106 | def juju_state_to_yaml(yaml_path, namespace_separator=':'): | ||
3946 | 107 | """Update the juju config and state in a yaml file. | ||
3947 | 108 | |||
3948 | 109 | This includes any current relation-get data, and the charm | ||
3949 | 110 | directory. | ||
3950 | 111 | """ | ||
3951 | 112 | config = charmhelpers.core.hookenv.config() | ||
3952 | 113 | |||
3953 | 114 | # Add the charm_dir which we will need to refer to charm | ||
3954 | 115 | # file resources etc. | ||
3955 | 116 | config['charm_dir'] = charm_dir | ||
3956 | 117 | config['local_unit'] = charmhelpers.core.hookenv.local_unit() | ||
3957 | 118 | |||
3958 | 119 | # Add any relation data prefixed with the relation type. | ||
3959 | 120 | relation_type = charmhelpers.core.hookenv.relation_type() | ||
3960 | 121 | if relation_type is not None: | ||
3961 | 122 | relation_data = charmhelpers.core.hookenv.relation_get() | ||
3962 | 123 | relation_data = dict( | ||
3963 | 124 | ("{relation_type}{namespace_separator}{key}".format( | ||
3964 | 125 | relation_type=relation_type.replace('-', '_'), | ||
3965 | 126 | key=key, | ||
3966 | 127 | namespace_separator=namespace_separator), val) | ||
3967 | 128 | for key, val in relation_data.items()) | ||
3968 | 129 | config.update(relation_data) | ||
3969 | 130 | |||
3970 | 131 | # Don't use non-standard tags for unicode which will not | ||
3971 | 132 | # work when salt uses yaml.load_safe. | ||
3972 | 133 | yaml.add_representer(unicode, lambda dumper, | ||
3973 | 134 | value: dumper.represent_scalar( | ||
3974 | 135 | u'tag:yaml.org,2002:str', value)) | ||
3975 | 136 | |||
3976 | 137 | yaml_dir = os.path.dirname(yaml_path) | ||
3977 | 138 | if not os.path.exists(yaml_dir): | ||
3978 | 139 | os.makedirs(yaml_dir) | ||
3979 | 140 | |||
3980 | 141 | if os.path.exists(yaml_path): | ||
3981 | 142 | with open(yaml_path, "r") as existing_vars_file: | ||
3982 | 143 | existing_vars = yaml.load(existing_vars_file.read()) | ||
3983 | 144 | else: | ||
3984 | 145 | existing_vars = {} | ||
3985 | 146 | |||
3986 | 147 | existing_vars.update(config) | ||
3987 | 148 | with open(yaml_path, "w+") as fp: | ||
3988 | 149 | fp.write(yaml.dump(existing_vars)) | ||
3989 | 150 | 0 | ||
3990 | === removed directory 'hooks/install.d/charmhelpers/contrib/ssl' | |||
3991 | === removed file 'hooks/install.d/charmhelpers/contrib/ssl/__init__.py' | |||
3992 | --- hooks/install.d/charmhelpers/contrib/ssl/__init__.py 2015-08-19 19:17:42 +0000 | |||
3993 | +++ hooks/install.d/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000 | |||
3994 | @@ -1,79 +0,0 @@ | |||
3995 | 1 | import subprocess | ||
3996 | 2 | from charmhelpers.core import hookenv | ||
3997 | 3 | |||
3998 | 4 | |||
3999 | 5 | def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None): | ||
4000 | 6 | """Generate selfsigned SSL keypair | ||
4001 | 7 | |||
4002 | 8 | You must provide one of the 3 optional arguments: | ||
4003 | 9 | config, subject or cn | ||
4004 | 10 | If more than one is provided the leftmost will be used | ||
4005 | 11 | |||
4006 | 12 | Arguments: | ||
4007 | 13 | keyfile -- (required) full path to the keyfile to be created | ||
4008 | 14 | certfile -- (required) full path to the certfile to be created | ||
4009 | 15 | keysize -- (optional) SSL key length | ||
4010 | 16 | config -- (optional) openssl configuration file | ||
4011 | 17 | subject -- (optional) dictionary with SSL subject variables | ||
4012 | 18 | cn -- (optional) cerfificate common name | ||
4013 | 19 | |||
4014 | 20 | Required keys in subject dict: | ||
4015 | 21 | cn -- Common name (eq. FQDN) | ||
4016 | 22 | |||
4017 | 23 | Optional keys in subject dict | ||
4018 | 24 | country -- Country Name (2 letter code) | ||
4019 | 25 | state -- State or Province Name (full name) | ||
4020 | 26 | locality -- Locality Name (eg, city) | ||
4021 | 27 | organization -- Organization Name (eg, company) | ||
4022 | 28 | organizational_unit -- Organizational Unit Name (eg, section) | ||
4023 | 29 | email -- Email Address | ||
4024 | 30 | """ | ||
4025 | 31 | |||
4026 | 32 | cmd = [] | ||
4027 | 33 | if config: | ||
4028 | 34 | cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", | ||
4029 | 35 | "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", | ||
4030 | 36 | "-keyout", keyfile, | ||
4031 | 37 | "-out", certfile, "-config", config] | ||
4032 | 38 | elif subject: | ||
4033 | 39 | ssl_subject = "" | ||
4034 | 40 | if "country" in subject: | ||
4035 | 41 | ssl_subject = ssl_subject + "/C={}".format(subject["country"]) | ||
4036 | 42 | if "state" in subject: | ||
4037 | 43 | ssl_subject = ssl_subject + "/ST={}".format(subject["state"]) | ||
4038 | 44 | if "locality" in subject: | ||
4039 | 45 | ssl_subject = ssl_subject + "/L={}".format(subject["locality"]) | ||
4040 | 46 | if "organization" in subject: | ||
4041 | 47 | ssl_subject = ssl_subject + "/O={}".format(subject["organization"]) | ||
4042 | 48 | if "organizational_unit" in subject: | ||
4043 | 49 | ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"]) | ||
4044 | 50 | if "cn" in subject: | ||
4045 | 51 | ssl_subject = ssl_subject + "/CN={}".format(subject["cn"]) | ||
4046 | 52 | else: | ||
4047 | 53 | hookenv.log("When using \"subject\" argument you must " \ | ||
4048 | 54 | "provide \"cn\" field at very least") | ||
4049 | 55 | return False | ||
4050 | 56 | if "email" in subject: | ||
4051 | 57 | ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"]) | ||
4052 | 58 | |||
4053 | 59 | cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", | ||
4054 | 60 | "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", | ||
4055 | 61 | "-keyout", keyfile, | ||
4056 | 62 | "-out", certfile, "-subj", ssl_subject] | ||
4057 | 63 | elif cn: | ||
4058 | 64 | cmd = ["/usr/bin/openssl", "req", "-new", "-newkey", | ||
4059 | 65 | "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509", | ||
4060 | 66 | "-keyout", keyfile, | ||
4061 | 67 | "-out", certfile, "-subj", "/CN={}".format(cn)] | ||
4062 | 68 | |||
4063 | 69 | if not cmd: | ||
4064 | 70 | hookenv.log("No config, subject or cn provided," \ | ||
4065 | 71 | "unable to generate self signed SSL certificates") | ||
4066 | 72 | return False | ||
4067 | 73 | try: | ||
4068 | 74 | subprocess.check_call(cmd) | ||
4069 | 75 | return True | ||
4070 | 76 | except Exception as e: | ||
4071 | 77 | print "Execution of openssl command failed:\n{}".format(e) | ||
4072 | 78 | return False | ||
4073 | 79 | |||
4074 | 80 | 0 | ||
4075 | === removed directory 'hooks/install.d/charmhelpers/contrib/storage' | |||
4076 | === removed file 'hooks/install.d/charmhelpers/contrib/storage/__init__.py' | |||
4077 | === removed directory 'hooks/install.d/charmhelpers/contrib/storage/linux' | |||
4078 | === removed file 'hooks/install.d/charmhelpers/contrib/storage/linux/__init__.py' | |||
4079 | === removed file 'hooks/install.d/charmhelpers/contrib/storage/linux/loopback.py' | |||
4080 | --- hooks/install.d/charmhelpers/contrib/storage/linux/loopback.py 2015-08-19 19:17:42 +0000 | |||
4081 | +++ hooks/install.d/charmhelpers/contrib/storage/linux/loopback.py 1970-01-01 00:00:00 +0000 | |||
4082 | @@ -1,62 +0,0 @@ | |||
4083 | 1 | |||
4084 | 2 | import os | ||
4085 | 3 | import re | ||
4086 | 4 | |||
4087 | 5 | from subprocess import ( | ||
4088 | 6 | check_call, | ||
4089 | 7 | check_output, | ||
4090 | 8 | ) | ||
4091 | 9 | |||
4092 | 10 | |||
4093 | 11 | ################################################## | ||
4094 | 12 | # loopback device helpers. | ||
4095 | 13 | ################################################## | ||
4096 | 14 | def loopback_devices(): | ||
4097 | 15 | ''' | ||
4098 | 16 | Parse through 'losetup -a' output to determine currently mapped | ||
4099 | 17 | loopback devices. Output is expected to look like: | ||
4100 | 18 | |||
4101 | 19 | /dev/loop0: [0807]:961814 (/tmp/my.img) | ||
4102 | 20 | |||
4103 | 21 | :returns: dict: a dict mapping {loopback_dev: backing_file} | ||
4104 | 22 | ''' | ||
4105 | 23 | loopbacks = {} | ||
4106 | 24 | cmd = ['losetup', '-a'] | ||
4107 | 25 | devs = [d.strip().split(' ') for d in | ||
4108 | 26 | check_output(cmd).splitlines() if d != ''] | ||
4109 | 27 | for dev, _, f in devs: | ||
4110 | 28 | loopbacks[dev.replace(':', '')] = re.search('\((\S+)\)', f).groups()[0] | ||
4111 | 29 | return loopbacks | ||
4112 | 30 | |||
4113 | 31 | |||
4114 | 32 | def create_loopback(file_path): | ||
4115 | 33 | ''' | ||
4116 | 34 | Create a loopback device for a given backing file. | ||
4117 | 35 | |||
4118 | 36 | :returns: str: Full path to new loopback device (eg, /dev/loop0) | ||
4119 | 37 | ''' | ||
4120 | 38 | file_path = os.path.abspath(file_path) | ||
4121 | 39 | check_call(['losetup', '--find', file_path]) | ||
4122 | 40 | for d, f in loopback_devices().iteritems(): | ||
4123 | 41 | if f == file_path: | ||
4124 | 42 | return d | ||
4125 | 43 | |||
4126 | 44 | |||
4127 | 45 | def ensure_loopback_device(path, size): | ||
4128 | 46 | ''' | ||
4129 | 47 | Ensure a loopback device exists for a given backing file path and size. | ||
4130 | 48 | If it a loopback device is not mapped to file, a new one will be created. | ||
4131 | 49 | |||
4132 | 50 | TODO: Confirm size of found loopback device. | ||
4133 | 51 | |||
4134 | 52 | :returns: str: Full path to the ensured loopback device (eg, /dev/loop0) | ||
4135 | 53 | ''' | ||
4136 | 54 | for d, f in loopback_devices().iteritems(): | ||
4137 | 55 | if f == path: | ||
4138 | 56 | return d | ||
4139 | 57 | |||
4140 | 58 | if not os.path.exists(path): | ||
4141 | 59 | cmd = ['truncate', '--size', size, path] | ||
4142 | 60 | check_call(cmd) | ||
4143 | 61 | |||
4144 | 62 | return create_loopback(path) | ||
4145 | 63 | 0 | ||
4146 | === removed file 'hooks/install.d/charmhelpers/contrib/storage/linux/lvm.py' | |||
4147 | --- hooks/install.d/charmhelpers/contrib/storage/linux/lvm.py 2015-08-19 19:17:42 +0000 | |||
4148 | +++ hooks/install.d/charmhelpers/contrib/storage/linux/lvm.py 1970-01-01 00:00:00 +0000 | |||
4149 | @@ -1,88 +0,0 @@ | |||
4150 | 1 | from subprocess import ( | ||
4151 | 2 | CalledProcessError, | ||
4152 | 3 | check_call, | ||
4153 | 4 | check_output, | ||
4154 | 5 | Popen, | ||
4155 | 6 | PIPE, | ||
4156 | 7 | ) | ||
4157 | 8 | |||
4158 | 9 | |||
4159 | 10 | ################################################## | ||
4160 | 11 | # LVM helpers. | ||
4161 | 12 | ################################################## | ||
4162 | 13 | def deactivate_lvm_volume_group(block_device): | ||
4163 | 14 | ''' | ||
4164 | 15 | Deactivate any volume gruop associated with an LVM physical volume. | ||
4165 | 16 | |||
4166 | 17 | :param block_device: str: Full path to LVM physical volume | ||
4167 | 18 | ''' | ||
4168 | 19 | vg = list_lvm_volume_group(block_device) | ||
4169 | 20 | if vg: | ||
4170 | 21 | cmd = ['vgchange', '-an', vg] | ||
4171 | 22 | check_call(cmd) | ||
4172 | 23 | |||
4173 | 24 | |||
4174 | 25 | def is_lvm_physical_volume(block_device): | ||
4175 | 26 | ''' | ||
4176 | 27 | Determine whether a block device is initialized as an LVM PV. | ||
4177 | 28 | |||
4178 | 29 | :param block_device: str: Full path of block device to inspect. | ||
4179 | 30 | |||
4180 | 31 | :returns: boolean: True if block device is a PV, False if not. | ||
4181 | 32 | ''' | ||
4182 | 33 | try: | ||
4183 | 34 | check_output(['pvdisplay', block_device]) | ||
4184 | 35 | return True | ||
4185 | 36 | except CalledProcessError: | ||
4186 | 37 | return False | ||
4187 | 38 | |||
4188 | 39 | |||
4189 | 40 | def remove_lvm_physical_volume(block_device): | ||
4190 | 41 | ''' | ||
4191 | 42 | Remove LVM PV signatures from a given block device. | ||
4192 | 43 | |||
4193 | 44 | :param block_device: str: Full path of block device to scrub. | ||
4194 | 45 | ''' | ||
4195 | 46 | p = Popen(['pvremove', '-ff', block_device], | ||
4196 | 47 | stdin=PIPE) | ||
4197 | 48 | p.communicate(input='y\n') | ||
4198 | 49 | |||
4199 | 50 | |||
4200 | 51 | def list_lvm_volume_group(block_device): | ||
4201 | 52 | ''' | ||
4202 | 53 | List LVM volume group associated with a given block device. | ||
4203 | 54 | |||
4204 | 55 | Assumes block device is a valid LVM PV. | ||
4205 | 56 | |||
4206 | 57 | :param block_device: str: Full path of block device to inspect. | ||
4207 | 58 | |||
4208 | 59 | :returns: str: Name of volume group associated with block device or None | ||
4209 | 60 | ''' | ||
4210 | 61 | vg = None | ||
4211 | 62 | pvd = check_output(['pvdisplay', block_device]).splitlines() | ||
4212 | 63 | for l in pvd: | ||
4213 | 64 | if l.strip().startswith('VG Name'): | ||
4214 | 65 | vg = ' '.join(l.split()).split(' ').pop() | ||
4215 | 66 | return vg | ||
4216 | 67 | |||
4217 | 68 | |||
4218 | 69 | def create_lvm_physical_volume(block_device): | ||
4219 | 70 | ''' | ||
4220 | 71 | Initialize a block device as an LVM physical volume. | ||
4221 | 72 | |||
4222 | 73 | :param block_device: str: Full path of block device to initialize. | ||
4223 | 74 | |||
4224 | 75 | ''' | ||
4225 | 76 | check_call(['pvcreate', block_device]) | ||
4226 | 77 | |||
4227 | 78 | |||
4228 | 79 | def create_lvm_volume_group(volume_group, block_device): | ||
4229 | 80 | ''' | ||
4230 | 81 | Create an LVM volume group backed by a given block device. | ||
4231 | 82 | |||
4232 | 83 | Assumes block device has already been initialized as an LVM PV. | ||
4233 | 84 | |||
4234 | 85 | :param volume_group: str: Name of volume group to create. | ||
4235 | 86 | :block_device: str: Full path of PV-initialized block device. | ||
4236 | 87 | ''' | ||
4237 | 88 | check_call(['vgcreate', volume_group, block_device]) | ||
4238 | 89 | 0 | ||
4239 | === removed file 'hooks/install.d/charmhelpers/contrib/storage/linux/utils.py' | |||
4240 | --- hooks/install.d/charmhelpers/contrib/storage/linux/utils.py 2015-08-19 19:17:42 +0000 | |||
4241 | +++ hooks/install.d/charmhelpers/contrib/storage/linux/utils.py 1970-01-01 00:00:00 +0000 | |||
4242 | @@ -1,25 +0,0 @@ | |||
4243 | 1 | from os import stat | ||
4244 | 2 | from stat import S_ISBLK | ||
4245 | 3 | |||
4246 | 4 | from subprocess import ( | ||
4247 | 5 | check_call | ||
4248 | 6 | ) | ||
4249 | 7 | |||
4250 | 8 | |||
4251 | 9 | def is_block_device(path): | ||
4252 | 10 | ''' | ||
4253 | 11 | Confirm device at path is a valid block device node. | ||
4254 | 12 | |||
4255 | 13 | :returns: boolean: True if path is a block device, False if not. | ||
4256 | 14 | ''' | ||
4257 | 15 | return S_ISBLK(stat(path).st_mode) | ||
4258 | 16 | |||
4259 | 17 | |||
4260 | 18 | def zap_disk(block_device): | ||
4261 | 19 | ''' | ||
4262 | 20 | Clear a block device of partition table. Relies on sgdisk, which is | ||
4263 | 21 | installed as pat of the 'gdisk' package in Ubuntu. | ||
4264 | 22 | |||
4265 | 23 | :param block_device: str: Full path of block device to clean. | ||
4266 | 24 | ''' | ||
4267 | 25 | check_call(['sgdisk', '--zap-all', block_device]) | ||
4268 | 26 | 0 | ||
4269 | === removed directory 'hooks/install.d/charmhelpers/contrib/templating' | |||
4270 | === removed file 'hooks/install.d/charmhelpers/contrib/templating/__init__.py' | |||
4271 | === removed file 'hooks/install.d/charmhelpers/contrib/templating/pyformat.py' | |||
4272 | --- hooks/install.d/charmhelpers/contrib/templating/pyformat.py 2015-08-19 19:17:42 +0000 | |||
4273 | +++ hooks/install.d/charmhelpers/contrib/templating/pyformat.py 1970-01-01 00:00:00 +0000 | |||
4274 | @@ -1,13 +0,0 @@ | |||
4275 | 1 | ''' | ||
4276 | 2 | Templating using standard Python str.format() method. | ||
4277 | 3 | ''' | ||
4278 | 4 | |||
4279 | 5 | from charmhelpers.core import hookenv | ||
4280 | 6 | |||
4281 | 7 | |||
4282 | 8 | def render(template, extra={}, **kwargs): | ||
4283 | 9 | """Return the template rendered using Python's str.format().""" | ||
4284 | 10 | context = hookenv.execution_environment() | ||
4285 | 11 | context.update(extra) | ||
4286 | 12 | context.update(kwargs) | ||
4287 | 13 | return template.format(**context) | ||
4288 | 14 | 0 | ||
4289 | === removed directory 'hooks/install.d/charmhelpers/core' | |||
4290 | === removed file 'hooks/install.d/charmhelpers/core/__init__.py' | |||
4291 | === removed file 'hooks/install.d/charmhelpers/core/hookenv.py' | |||
4292 | --- hooks/install.d/charmhelpers/core/hookenv.py 2015-08-19 19:17:42 +0000 | |||
4293 | +++ hooks/install.d/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000 | |||
4294 | @@ -1,340 +0,0 @@ | |||
4295 | 1 | "Interactions with the Juju environment" | ||
4296 | 2 | # Copyright 2013 Canonical Ltd. | ||
4297 | 3 | # | ||
4298 | 4 | # Authors: | ||
4299 | 5 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
4300 | 6 | |||
4301 | 7 | import os | ||
4302 | 8 | import json | ||
4303 | 9 | import yaml | ||
4304 | 10 | import subprocess | ||
4305 | 11 | import UserDict | ||
4306 | 12 | |||
4307 | 13 | CRITICAL = "CRITICAL" | ||
4308 | 14 | ERROR = "ERROR" | ||
4309 | 15 | WARNING = "WARNING" | ||
4310 | 16 | INFO = "INFO" | ||
4311 | 17 | DEBUG = "DEBUG" | ||
4312 | 18 | MARKER = object() | ||
4313 | 19 | |||
4314 | 20 | cache = {} | ||
4315 | 21 | |||
4316 | 22 | |||
4317 | 23 | def cached(func): | ||
4318 | 24 | ''' Cache return values for multiple executions of func + args | ||
4319 | 25 | |||
4320 | 26 | For example: | ||
4321 | 27 | |||
4322 | 28 | @cached | ||
4323 | 29 | def unit_get(attribute): | ||
4324 | 30 | pass | ||
4325 | 31 | |||
4326 | 32 | unit_get('test') | ||
4327 | 33 | |||
4328 | 34 | will cache the result of unit_get + 'test' for future calls. | ||
4329 | 35 | ''' | ||
4330 | 36 | def wrapper(*args, **kwargs): | ||
4331 | 37 | global cache | ||
4332 | 38 | key = str((func, args, kwargs)) | ||
4333 | 39 | try: | ||
4334 | 40 | return cache[key] | ||
4335 | 41 | except KeyError: | ||
4336 | 42 | res = func(*args, **kwargs) | ||
4337 | 43 | cache[key] = res | ||
4338 | 44 | return res | ||
4339 | 45 | return wrapper | ||
4340 | 46 | |||
4341 | 47 | |||
4342 | 48 | def flush(key): | ||
4343 | 49 | ''' Flushes any entries from function cache where the | ||
4344 | 50 | key is found in the function+args ''' | ||
4345 | 51 | flush_list = [] | ||
4346 | 52 | for item in cache: | ||
4347 | 53 | if key in item: | ||
4348 | 54 | flush_list.append(item) | ||
4349 | 55 | for item in flush_list: | ||
4350 | 56 | del cache[item] | ||
4351 | 57 | |||
4352 | 58 | |||
4353 | 59 | def log(message, level=None): | ||
4354 | 60 | "Write a message to the juju log" | ||
4355 | 61 | command = ['juju-log'] | ||
4356 | 62 | if level: | ||
4357 | 63 | command += ['-l', level] | ||
4358 | 64 | command += [message] | ||
4359 | 65 | subprocess.call(command) | ||
4360 | 66 | |||
4361 | 67 | |||
4362 | 68 | class Serializable(UserDict.IterableUserDict): | ||
4363 | 69 | "Wrapper, an object that can be serialized to yaml or json" | ||
4364 | 70 | |||
4365 | 71 | def __init__(self, obj): | ||
4366 | 72 | # wrap the object | ||
4367 | 73 | UserDict.IterableUserDict.__init__(self) | ||
4368 | 74 | self.data = obj | ||
4369 | 75 | |||
4370 | 76 | def __getattr__(self, attr): | ||
4371 | 77 | # See if this object has attribute. | ||
4372 | 78 | if attr in ("json", "yaml", "data"): | ||
4373 | 79 | return self.__dict__[attr] | ||
4374 | 80 | # Check for attribute in wrapped object. | ||
4375 | 81 | got = getattr(self.data, attr, MARKER) | ||
4376 | 82 | if got is not MARKER: | ||
4377 | 83 | return got | ||
4378 | 84 | # Proxy to the wrapped object via dict interface. | ||
4379 | 85 | try: | ||
4380 | 86 | return self.data[attr] | ||
4381 | 87 | except KeyError: | ||
4382 | 88 | raise AttributeError(attr) | ||
4383 | 89 | |||
4384 | 90 | def __getstate__(self): | ||
4385 | 91 | # Pickle as a standard dictionary. | ||
4386 | 92 | return self.data | ||
4387 | 93 | |||
4388 | 94 | def __setstate__(self, state): | ||
4389 | 95 | # Unpickle into our wrapper. | ||
4390 | 96 | self.data = state | ||
4391 | 97 | |||
4392 | 98 | def json(self): | ||
4393 | 99 | "Serialize the object to json" | ||
4394 | 100 | return json.dumps(self.data) | ||
4395 | 101 | |||
4396 | 102 | def yaml(self): | ||
4397 | 103 | "Serialize the object to yaml" | ||
4398 | 104 | return yaml.dump(self.data) | ||
4399 | 105 | |||
4400 | 106 | |||
4401 | 107 | def execution_environment(): | ||
4402 | 108 | """A convenient bundling of the current execution context""" | ||
4403 | 109 | context = {} | ||
4404 | 110 | context['conf'] = config() | ||
4405 | 111 | if relation_id(): | ||
4406 | 112 | context['reltype'] = relation_type() | ||
4407 | 113 | context['relid'] = relation_id() | ||
4408 | 114 | context['rel'] = relation_get() | ||
4409 | 115 | context['unit'] = local_unit() | ||
4410 | 116 | context['rels'] = relations() | ||
4411 | 117 | context['env'] = os.environ | ||
4412 | 118 | return context | ||
4413 | 119 | |||
4414 | 120 | |||
4415 | 121 | def in_relation_hook(): | ||
4416 | 122 | "Determine whether we're running in a relation hook" | ||
4417 | 123 | return 'JUJU_RELATION' in os.environ | ||
4418 | 124 | |||
4419 | 125 | |||
4420 | 126 | def relation_type(): | ||
4421 | 127 | "The scope for the current relation hook" | ||
4422 | 128 | return os.environ.get('JUJU_RELATION', None) | ||
4423 | 129 | |||
4424 | 130 | |||
4425 | 131 | def relation_id(): | ||
4426 | 132 | "The relation ID for the current relation hook" | ||
4427 | 133 | return os.environ.get('JUJU_RELATION_ID', None) | ||
4428 | 134 | |||
4429 | 135 | |||
4430 | 136 | def local_unit(): | ||
4431 | 137 | "Local unit ID" | ||
4432 | 138 | return os.environ['JUJU_UNIT_NAME'] | ||
4433 | 139 | |||
4434 | 140 | |||
4435 | 141 | def remote_unit(): | ||
4436 | 142 | "The remote unit for the current relation hook" | ||
4437 | 143 | return os.environ['JUJU_REMOTE_UNIT'] | ||
4438 | 144 | |||
4439 | 145 | |||
4440 | 146 | def service_name(): | ||
4441 | 147 | "The name service group this unit belongs to" | ||
4442 | 148 | return local_unit().split('/')[0] | ||
4443 | 149 | |||
4444 | 150 | |||
4445 | 151 | @cached | ||
4446 | 152 | def config(scope=None): | ||
4447 | 153 | "Juju charm configuration" | ||
4448 | 154 | config_cmd_line = ['config-get'] | ||
4449 | 155 | if scope is not None: | ||
4450 | 156 | config_cmd_line.append(scope) | ||
4451 | 157 | config_cmd_line.append('--format=json') | ||
4452 | 158 | try: | ||
4453 | 159 | return json.loads(subprocess.check_output(config_cmd_line)) | ||
4454 | 160 | except ValueError: | ||
4455 | 161 | return None | ||
4456 | 162 | |||
4457 | 163 | |||
4458 | 164 | @cached | ||
4459 | 165 | def relation_get(attribute=None, unit=None, rid=None): | ||
4460 | 166 | _args = ['relation-get', '--format=json'] | ||
4461 | 167 | if rid: | ||
4462 | 168 | _args.append('-r') | ||
4463 | 169 | _args.append(rid) | ||
4464 | 170 | _args.append(attribute or '-') | ||
4465 | 171 | if unit: | ||
4466 | 172 | _args.append(unit) | ||
4467 | 173 | try: | ||
4468 | 174 | return json.loads(subprocess.check_output(_args)) | ||
4469 | 175 | except ValueError: | ||
4470 | 176 | return None | ||
4471 | 177 | |||
4472 | 178 | |||
4473 | 179 | def relation_set(relation_id=None, relation_settings={}, **kwargs): | ||
4474 | 180 | relation_cmd_line = ['relation-set'] | ||
4475 | 181 | if relation_id is not None: | ||
4476 | 182 | relation_cmd_line.extend(('-r', relation_id)) | ||
4477 | 183 | for k, v in (relation_settings.items() + kwargs.items()): | ||
4478 | 184 | if v is None: | ||
4479 | 185 | relation_cmd_line.append('{}='.format(k)) | ||
4480 | 186 | else: | ||
4481 | 187 | relation_cmd_line.append('{}={}'.format(k, v)) | ||
4482 | 188 | subprocess.check_call(relation_cmd_line) | ||
4483 | 189 | # Flush cache of any relation-gets for local unit | ||
4484 | 190 | flush(local_unit()) | ||
4485 | 191 | |||
4486 | 192 | |||
4487 | 193 | @cached | ||
4488 | 194 | def relation_ids(reltype=None): | ||
4489 | 195 | "A list of relation_ids" | ||
4490 | 196 | reltype = reltype or relation_type() | ||
4491 | 197 | relid_cmd_line = ['relation-ids', '--format=json'] | ||
4492 | 198 | if reltype is not None: | ||
4493 | 199 | relid_cmd_line.append(reltype) | ||
4494 | 200 | return json.loads(subprocess.check_output(relid_cmd_line)) or [] | ||
4495 | 201 | return [] | ||
4496 | 202 | |||
4497 | 203 | |||
4498 | 204 | @cached | ||
4499 | 205 | def related_units(relid=None): | ||
4500 | 206 | "A list of related units" | ||
4501 | 207 | relid = relid or relation_id() | ||
4502 | 208 | units_cmd_line = ['relation-list', '--format=json'] | ||
4503 | 209 | if relid is not None: | ||
4504 | 210 | units_cmd_line.extend(('-r', relid)) | ||
4505 | 211 | return json.loads(subprocess.check_output(units_cmd_line)) or [] | ||
4506 | 212 | |||
4507 | 213 | |||
4508 | 214 | @cached | ||
4509 | 215 | def relation_for_unit(unit=None, rid=None): | ||
4510 | 216 | "Get the json represenation of a unit's relation" | ||
4511 | 217 | unit = unit or remote_unit() | ||
4512 | 218 | relation = relation_get(unit=unit, rid=rid) | ||
4513 | 219 | for key in relation: | ||
4514 | 220 | if key.endswith('-list'): | ||
4515 | 221 | relation[key] = relation[key].split() | ||
4516 | 222 | relation['__unit__'] = unit | ||
4517 | 223 | return relation | ||
4518 | 224 | |||
4519 | 225 | |||
4520 | 226 | @cached | ||
4521 | 227 | def relations_for_id(relid=None): | ||
4522 | 228 | "Get relations of a specific relation ID" | ||
4523 | 229 | relation_data = [] | ||
4524 | 230 | relid = relid or relation_ids() | ||
4525 | 231 | for unit in related_units(relid): | ||
4526 | 232 | unit_data = relation_for_unit(unit, relid) | ||
4527 | 233 | unit_data['__relid__'] = relid | ||
4528 | 234 | relation_data.append(unit_data) | ||
4529 | 235 | return relation_data | ||
4530 | 236 | |||
4531 | 237 | |||
4532 | 238 | @cached | ||
4533 | 239 | def relations_of_type(reltype=None): | ||
4534 | 240 | "Get relations of a specific type" | ||
4535 | 241 | relation_data = [] | ||
4536 | 242 | reltype = reltype or relation_type() | ||
4537 | 243 | for relid in relation_ids(reltype): | ||
4538 | 244 | for relation in relations_for_id(relid): | ||
4539 | 245 | relation['__relid__'] = relid | ||
4540 | 246 | relation_data.append(relation) | ||
4541 | 247 | return relation_data | ||
4542 | 248 | |||
4543 | 249 | |||
4544 | 250 | @cached | ||
4545 | 251 | def relation_types(): | ||
4546 | 252 | "Get a list of relation types supported by this charm" | ||
4547 | 253 | charmdir = os.environ.get('CHARM_DIR', '') | ||
4548 | 254 | mdf = open(os.path.join(charmdir, 'metadata.yaml')) | ||
4549 | 255 | md = yaml.safe_load(mdf) | ||
4550 | 256 | rel_types = [] | ||
4551 | 257 | for key in ('provides', 'requires', 'peers'): | ||
4552 | 258 | section = md.get(key) | ||
4553 | 259 | if section: | ||
4554 | 260 | rel_types.extend(section.keys()) | ||
4555 | 261 | mdf.close() | ||
4556 | 262 | return rel_types | ||
4557 | 263 | |||
4558 | 264 | |||
4559 | 265 | @cached | ||
4560 | 266 | def relations(): | ||
4561 | 267 | rels = {} | ||
4562 | 268 | for reltype in relation_types(): | ||
4563 | 269 | relids = {} | ||
4564 | 270 | for relid in relation_ids(reltype): | ||
4565 | 271 | units = {local_unit(): relation_get(unit=local_unit(), rid=relid)} | ||
4566 | 272 | for unit in related_units(relid): | ||
4567 | 273 | reldata = relation_get(unit=unit, rid=relid) | ||
4568 | 274 | units[unit] = reldata | ||
4569 | 275 | relids[relid] = units | ||
4570 | 276 | rels[reltype] = relids | ||
4571 | 277 | return rels | ||
4572 | 278 | |||
4573 | 279 | |||
4574 | 280 | def open_port(port, protocol="TCP"): | ||
4575 | 281 | "Open a service network port" | ||
4576 | 282 | _args = ['open-port'] | ||
4577 | 283 | _args.append('{}/{}'.format(port, protocol)) | ||
4578 | 284 | subprocess.check_call(_args) | ||
4579 | 285 | |||
4580 | 286 | |||
4581 | 287 | def close_port(port, protocol="TCP"): | ||
4582 | 288 | "Close a service network port" | ||
4583 | 289 | _args = ['close-port'] | ||
4584 | 290 | _args.append('{}/{}'.format(port, protocol)) | ||
4585 | 291 | subprocess.check_call(_args) | ||
4586 | 292 | |||
4587 | 293 | |||
4588 | 294 | @cached | ||
4589 | 295 | def unit_get(attribute): | ||
4590 | 296 | _args = ['unit-get', '--format=json', attribute] | ||
4591 | 297 | try: | ||
4592 | 298 | return json.loads(subprocess.check_output(_args)) | ||
4593 | 299 | except ValueError: | ||
4594 | 300 | return None | ||
4595 | 301 | |||
4596 | 302 | |||
4597 | 303 | def unit_private_ip(): | ||
4598 | 304 | return unit_get('private-address') | ||
4599 | 305 | |||
4600 | 306 | |||
4601 | 307 | class UnregisteredHookError(Exception): | ||
4602 | 308 | pass | ||
4603 | 309 | |||
4604 | 310 | |||
4605 | 311 | class Hooks(object): | ||
4606 | 312 | def __init__(self): | ||
4607 | 313 | super(Hooks, self).__init__() | ||
4608 | 314 | self._hooks = {} | ||
4609 | 315 | |||
4610 | 316 | def register(self, name, function): | ||
4611 | 317 | self._hooks[name] = function | ||
4612 | 318 | |||
4613 | 319 | def execute(self, args): | ||
4614 | 320 | hook_name = os.path.basename(args[0]) | ||
4615 | 321 | if hook_name in self._hooks: | ||
4616 | 322 | self._hooks[hook_name]() | ||
4617 | 323 | else: | ||
4618 | 324 | raise UnregisteredHookError(hook_name) | ||
4619 | 325 | |||
4620 | 326 | def hook(self, *hook_names): | ||
4621 | 327 | def wrapper(decorated): | ||
4622 | 328 | for hook_name in hook_names: | ||
4623 | 329 | self.register(hook_name, decorated) | ||
4624 | 330 | else: | ||
4625 | 331 | self.register(decorated.__name__, decorated) | ||
4626 | 332 | if '_' in decorated.__name__: | ||
4627 | 333 | self.register( | ||
4628 | 334 | decorated.__name__.replace('_', '-'), decorated) | ||
4629 | 335 | return decorated | ||
4630 | 336 | return wrapper | ||
4631 | 337 | |||
4632 | 338 | |||
4633 | 339 | def charm_dir(): | ||
4634 | 340 | return os.environ.get('CHARM_DIR') | ||
4635 | 341 | 0 | ||
4636 | === removed file 'hooks/install.d/charmhelpers/core/host.py' | |||
4637 | --- hooks/install.d/charmhelpers/core/host.py 2015-08-19 19:17:42 +0000 | |||
4638 | +++ hooks/install.d/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000 | |||
4639 | @@ -1,241 +0,0 @@ | |||
4640 | 1 | """Tools for working with the host system""" | ||
4641 | 2 | # Copyright 2012 Canonical Ltd. | ||
4642 | 3 | # | ||
4643 | 4 | # Authors: | ||
4644 | 5 | # Nick Moffitt <nick.moffitt@canonical.com> | ||
4645 | 6 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
4646 | 7 | |||
4647 | 8 | import os | ||
4648 | 9 | import pwd | ||
4649 | 10 | import grp | ||
4650 | 11 | import random | ||
4651 | 12 | import string | ||
4652 | 13 | import subprocess | ||
4653 | 14 | import hashlib | ||
4654 | 15 | |||
4655 | 16 | from collections import OrderedDict | ||
4656 | 17 | |||
4657 | 18 | from hookenv import log | ||
4658 | 19 | |||
4659 | 20 | |||
4660 | 21 | def service_start(service_name): | ||
4661 | 22 | return service('start', service_name) | ||
4662 | 23 | |||
4663 | 24 | |||
4664 | 25 | def service_stop(service_name): | ||
4665 | 26 | return service('stop', service_name) | ||
4666 | 27 | |||
4667 | 28 | |||
4668 | 29 | def service_restart(service_name): | ||
4669 | 30 | return service('restart', service_name) | ||
4670 | 31 | |||
4671 | 32 | |||
4672 | 33 | def service_reload(service_name, restart_on_failure=False): | ||
4673 | 34 | service_result = service('reload', service_name) | ||
4674 | 35 | if not service_result and restart_on_failure: | ||
4675 | 36 | service_result = service('restart', service_name) | ||
4676 | 37 | return service_result | ||
4677 | 38 | |||
4678 | 39 | |||
4679 | 40 | def service(action, service_name): | ||
4680 | 41 | cmd = ['service', service_name, action] | ||
4681 | 42 | return subprocess.call(cmd) == 0 | ||
4682 | 43 | |||
4683 | 44 | |||
4684 | 45 | def service_running(service): | ||
4685 | 46 | try: | ||
4686 | 47 | output = subprocess.check_output(['service', service, 'status']) | ||
4687 | 48 | except subprocess.CalledProcessError: | ||
4688 | 49 | return False | ||
4689 | 50 | else: | ||
4690 | 51 | if ("start/running" in output or "is running" in output): | ||
4691 | 52 | return True | ||
4692 | 53 | else: | ||
4693 | 54 | return False | ||
4694 | 55 | |||
4695 | 56 | |||
4696 | 57 | def adduser(username, password=None, shell='/bin/bash', system_user=False): | ||
4697 | 58 | """Add a user""" | ||
4698 | 59 | try: | ||
4699 | 60 | user_info = pwd.getpwnam(username) | ||
4700 | 61 | log('user {0} already exists!'.format(username)) | ||
4701 | 62 | except KeyError: | ||
4702 | 63 | log('creating user {0}'.format(username)) | ||
4703 | 64 | cmd = ['useradd'] | ||
4704 | 65 | if system_user or password is None: | ||
4705 | 66 | cmd.append('--system') | ||
4706 | 67 | else: | ||
4707 | 68 | cmd.extend([ | ||
4708 | 69 | '--create-home', | ||
4709 | 70 | '--shell', shell, | ||
4710 | 71 | '--password', password, | ||
4711 | 72 | ]) | ||
4712 | 73 | cmd.append(username) | ||
4713 | 74 | subprocess.check_call(cmd) | ||
4714 | 75 | user_info = pwd.getpwnam(username) | ||
4715 | 76 | return user_info | ||
4716 | 77 | |||
4717 | 78 | |||
4718 | 79 | def add_user_to_group(username, group): | ||
4719 | 80 | """Add a user to a group""" | ||
4720 | 81 | cmd = [ | ||
4721 | 82 | 'gpasswd', '-a', | ||
4722 | 83 | username, | ||
4723 | 84 | group | ||
4724 | 85 | ] | ||
4725 | 86 | log("Adding user {} to group {}".format(username, group)) | ||
4726 | 87 | subprocess.check_call(cmd) | ||
4727 | 88 | |||
4728 | 89 | |||
4729 | 90 | def rsync(from_path, to_path, flags='-r', options=None): | ||
4730 | 91 | """Replicate the contents of a path""" | ||
4731 | 92 | options = options or ['--delete', '--executability'] | ||
4732 | 93 | cmd = ['/usr/bin/rsync', flags] | ||
4733 | 94 | cmd.extend(options) | ||
4734 | 95 | cmd.append(from_path) | ||
4735 | 96 | cmd.append(to_path) | ||
4736 | 97 | log(" ".join(cmd)) | ||
4737 | 98 | return subprocess.check_output(cmd).strip() | ||
4738 | 99 | |||
4739 | 100 | |||
4740 | 101 | def symlink(source, destination): | ||
4741 | 102 | """Create a symbolic link""" | ||
4742 | 103 | log("Symlinking {} as {}".format(source, destination)) | ||
4743 | 104 | cmd = [ | ||
4744 | 105 | 'ln', | ||
4745 | 106 | '-sf', | ||
4746 | 107 | source, | ||
4747 | 108 | destination, | ||
4748 | 109 | ] | ||
4749 | 110 | subprocess.check_call(cmd) | ||
4750 | 111 | |||
4751 | 112 | |||
4752 | 113 | def mkdir(path, owner='root', group='root', perms=0555, force=False): | ||
4753 | 114 | """Create a directory""" | ||
4754 | 115 | log("Making dir {} {}:{} {:o}".format(path, owner, group, | ||
4755 | 116 | perms)) | ||
4756 | 117 | uid = pwd.getpwnam(owner).pw_uid | ||
4757 | 118 | gid = grp.getgrnam(group).gr_gid | ||
4758 | 119 | realpath = os.path.abspath(path) | ||
4759 | 120 | if os.path.exists(realpath): | ||
4760 | 121 | if force and not os.path.isdir(realpath): | ||
4761 | 122 | log("Removing non-directory file {} prior to mkdir()".format(path)) | ||
4762 | 123 | os.unlink(realpath) | ||
4763 | 124 | else: | ||
4764 | 125 | os.makedirs(realpath, perms) | ||
4765 | 126 | os.chown(realpath, uid, gid) | ||
4766 | 127 | |||
4767 | 128 | |||
4768 | 129 | def write_file(path, content, owner='root', group='root', perms=0444): | ||
4769 | 130 | """Create or overwrite a file with the contents of a string""" | ||
4770 | 131 | log("Writing file {} {}:{} {:o}".format(path, owner, group, perms)) | ||
4771 | 132 | uid = pwd.getpwnam(owner).pw_uid | ||
4772 | 133 | gid = grp.getgrnam(group).gr_gid | ||
4773 | 134 | with open(path, 'w') as target: | ||
4774 | 135 | os.fchown(target.fileno(), uid, gid) | ||
4775 | 136 | os.fchmod(target.fileno(), perms) | ||
4776 | 137 | target.write(content) | ||
4777 | 138 | |||
4778 | 139 | |||
4779 | 140 | def mount(device, mountpoint, options=None, persist=False): | ||
4780 | 141 | '''Mount a filesystem''' | ||
4781 | 142 | cmd_args = ['mount'] | ||
4782 | 143 | if options is not None: | ||
4783 | 144 | cmd_args.extend(['-o', options]) | ||
4784 | 145 | cmd_args.extend([device, mountpoint]) | ||
4785 | 146 | try: | ||
4786 | 147 | subprocess.check_output(cmd_args) | ||
4787 | 148 | except subprocess.CalledProcessError, e: | ||
4788 | 149 | log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) | ||
4789 | 150 | return False | ||
4790 | 151 | if persist: | ||
4791 | 152 | # TODO: update fstab | ||
4792 | 153 | pass | ||
4793 | 154 | return True | ||
4794 | 155 | |||
4795 | 156 | |||
4796 | 157 | def umount(mountpoint, persist=False): | ||
4797 | 158 | '''Unmount a filesystem''' | ||
4798 | 159 | cmd_args = ['umount', mountpoint] | ||
4799 | 160 | try: | ||
4800 | 161 | subprocess.check_output(cmd_args) | ||
4801 | 162 | except subprocess.CalledProcessError, e: | ||
4802 | 163 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) | ||
4803 | 164 | return False | ||
4804 | 165 | if persist: | ||
4805 | 166 | # TODO: update fstab | ||
4806 | 167 | pass | ||
4807 | 168 | return True | ||
4808 | 169 | |||
4809 | 170 | |||
4810 | 171 | def mounts(): | ||
4811 | 172 | '''List of all mounted volumes as [[mountpoint,device],[...]]''' | ||
4812 | 173 | with open('/proc/mounts') as f: | ||
4813 | 174 | # [['/mount/point','/dev/path'],[...]] | ||
4814 | 175 | system_mounts = [m[1::-1] for m in [l.strip().split() | ||
4815 | 176 | for l in f.readlines()]] | ||
4816 | 177 | return system_mounts | ||
4817 | 178 | |||
4818 | 179 | |||
4819 | 180 | def file_hash(path): | ||
4820 | 181 | ''' Generate a md5 hash of the contents of 'path' or None if not found ''' | ||
4821 | 182 | if os.path.exists(path): | ||
4822 | 183 | h = hashlib.md5() | ||
4823 | 184 | with open(path, 'r') as source: | ||
4824 | 185 | h.update(source.read()) # IGNORE:E1101 - it does have update | ||
4825 | 186 | return h.hexdigest() | ||
4826 | 187 | else: | ||
4827 | 188 | return None | ||
4828 | 189 | |||
4829 | 190 | |||
4830 | 191 | def restart_on_change(restart_map): | ||
4831 | 192 | ''' Restart services based on configuration files changing | ||
4832 | 193 | |||
4833 | 194 | This function is used a decorator, for example | ||
4834 | 195 | |||
4835 | 196 | @restart_on_change({ | ||
4836 | 197 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] | ||
4837 | 198 | }) | ||
4838 | 199 | def ceph_client_changed(): | ||
4839 | 200 | ... | ||
4840 | 201 | |||
4841 | 202 | In this example, the cinder-api and cinder-volume services | ||
4842 | 203 | would be restarted if /etc/ceph/ceph.conf is changed by the | ||
4843 | 204 | ceph_client_changed function. | ||
4844 | 205 | ''' | ||
4845 | 206 | def wrap(f): | ||
4846 | 207 | def wrapped_f(*args): | ||
4847 | 208 | checksums = {} | ||
4848 | 209 | for path in restart_map: | ||
4849 | 210 | checksums[path] = file_hash(path) | ||
4850 | 211 | f(*args) | ||
4851 | 212 | restarts = [] | ||
4852 | 213 | for path in restart_map: | ||
4853 | 214 | if checksums[path] != file_hash(path): | ||
4854 | 215 | restarts += restart_map[path] | ||
4855 | 216 | for service_name in list(OrderedDict.fromkeys(restarts)): | ||
4856 | 217 | service('restart', service_name) | ||
4857 | 218 | return wrapped_f | ||
4858 | 219 | return wrap | ||
4859 | 220 | |||
4860 | 221 | |||
4861 | 222 | def lsb_release(): | ||
4862 | 223 | '''Return /etc/lsb-release in a dict''' | ||
4863 | 224 | d = {} | ||
4864 | 225 | with open('/etc/lsb-release', 'r') as lsb: | ||
4865 | 226 | for l in lsb: | ||
4866 | 227 | k, v = l.split('=') | ||
4867 | 228 | d[k.strip()] = v.strip() | ||
4868 | 229 | return d | ||
4869 | 230 | |||
4870 | 231 | |||
4871 | 232 | def pwgen(length=None): | ||
4872 | 233 | '''Generate a random pasword.''' | ||
4873 | 234 | if length is None: | ||
4874 | 235 | length = random.choice(range(35, 45)) | ||
4875 | 236 | alphanumeric_chars = [ | ||
4876 | 237 | l for l in (string.letters + string.digits) | ||
4877 | 238 | if l not in 'l0QD1vAEIOUaeiou'] | ||
4878 | 239 | random_chars = [ | ||
4879 | 240 | random.choice(alphanumeric_chars) for _ in range(length)] | ||
4880 | 241 | return(''.join(random_chars)) | ||
4881 | 242 | 0 | ||
4882 | === removed directory 'hooks/install.d/charmhelpers/fetch' | |||
4883 | === removed file 'hooks/install.d/charmhelpers/fetch/__init__.py' | |||
4884 | --- hooks/install.d/charmhelpers/fetch/__init__.py 2015-08-19 19:17:42 +0000 | |||
4885 | +++ hooks/install.d/charmhelpers/fetch/__init__.py 1970-01-01 00:00:00 +0000 | |||
4886 | @@ -1,209 +0,0 @@ | |||
4887 | 1 | import importlib | ||
4888 | 2 | from yaml import safe_load | ||
4889 | 3 | from charmhelpers.core.host import ( | ||
4890 | 4 | lsb_release | ||
4891 | 5 | ) | ||
4892 | 6 | from urlparse import ( | ||
4893 | 7 | urlparse, | ||
4894 | 8 | urlunparse, | ||
4895 | 9 | ) | ||
4896 | 10 | import subprocess | ||
4897 | 11 | from charmhelpers.core.hookenv import ( | ||
4898 | 12 | config, | ||
4899 | 13 | log, | ||
4900 | 14 | ) | ||
4901 | 15 | import apt_pkg | ||
4902 | 16 | |||
4903 | 17 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive | ||
4904 | 18 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main | ||
4905 | 19 | """ | ||
4906 | 20 | PROPOSED_POCKET = """# Proposed | ||
4907 | 21 | deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted | ||
4908 | 22 | """ | ||
4909 | 23 | |||
4910 | 24 | |||
4911 | 25 | def filter_installed_packages(packages): | ||
4912 | 26 | """Returns a list of packages that require installation""" | ||
4913 | 27 | apt_pkg.init() | ||
4914 | 28 | cache = apt_pkg.Cache() | ||
4915 | 29 | _pkgs = [] | ||
4916 | 30 | for package in packages: | ||
4917 | 31 | try: | ||
4918 | 32 | p = cache[package] | ||
4919 | 33 | p.current_ver or _pkgs.append(package) | ||
4920 | 34 | except KeyError: | ||
4921 | 35 | log('Package {} has no installation candidate.'.format(package), | ||
4922 | 36 | level='WARNING') | ||
4923 | 37 | _pkgs.append(package) | ||
4924 | 38 | return _pkgs | ||
4925 | 39 | |||
4926 | 40 | |||
4927 | 41 | def apt_install(packages, options=None, fatal=False): | ||
4928 | 42 | """Install one or more packages""" | ||
4929 | 43 | options = options or [] | ||
4930 | 44 | cmd = ['apt-get', '-y'] | ||
4931 | 45 | cmd.extend(options) | ||
4932 | 46 | cmd.append('install') | ||
4933 | 47 | if isinstance(packages, basestring): | ||
4934 | 48 | cmd.append(packages) | ||
4935 | 49 | else: | ||
4936 | 50 | cmd.extend(packages) | ||
4937 | 51 | log("Installing {} with options: {}".format(packages, | ||
4938 | 52 | options)) | ||
4939 | 53 | if fatal: | ||
4940 | 54 | subprocess.check_call(cmd) | ||
4941 | 55 | else: | ||
4942 | 56 | subprocess.call(cmd) | ||
4943 | 57 | |||
4944 | 58 | |||
4945 | 59 | def apt_update(fatal=False): | ||
4946 | 60 | """Update local apt cache""" | ||
4947 | 61 | cmd = ['apt-get', 'update'] | ||
4948 | 62 | if fatal: | ||
4949 | 63 | subprocess.check_call(cmd) | ||
4950 | 64 | else: | ||
4951 | 65 | subprocess.call(cmd) | ||
4952 | 66 | |||
4953 | 67 | |||
4954 | 68 | def apt_purge(packages, fatal=False): | ||
4955 | 69 | """Purge one or more packages""" | ||
4956 | 70 | cmd = ['apt-get', '-y', 'purge'] | ||
4957 | 71 | if isinstance(packages, basestring): | ||
4958 | 72 | cmd.append(packages) | ||
4959 | 73 | else: | ||
4960 | 74 | cmd.extend(packages) | ||
4961 | 75 | log("Purging {}".format(packages)) | ||
4962 | 76 | if fatal: | ||
4963 | 77 | subprocess.check_call(cmd) | ||
4964 | 78 | else: | ||
4965 | 79 | subprocess.call(cmd) | ||
4966 | 80 | |||
4967 | 81 | |||
4968 | 82 | def add_source(source, key=None): | ||
4969 | 83 | if ((source.startswith('ppa:') or | ||
4970 | 84 | source.startswith('http:'))): | ||
4971 | 85 | subprocess.check_call(['add-apt-repository', '--yes', source]) | ||
4972 | 86 | elif source.startswith('cloud:'): | ||
4973 | 87 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), | ||
4974 | 88 | fatal=True) | ||
4975 | 89 | pocket = source.split(':')[-1] | ||
4976 | 90 | with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: | ||
4977 | 91 | apt.write(CLOUD_ARCHIVE.format(pocket)) | ||
4978 | 92 | elif source == 'proposed': | ||
4979 | 93 | release = lsb_release()['DISTRIB_CODENAME'] | ||
4980 | 94 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: | ||
4981 | 95 | apt.write(PROPOSED_POCKET.format(release)) | ||
4982 | 96 | if key: | ||
4983 | 97 | subprocess.check_call(['apt-key', 'import', key]) | ||
4984 | 98 | |||
4985 | 99 | |||
4986 | 100 | class SourceConfigError(Exception): | ||
4987 | 101 | pass | ||
4988 | 102 | |||
4989 | 103 | |||
4990 | 104 | def configure_sources(update=False, | ||
4991 | 105 | sources_var='install_sources', | ||
4992 | 106 | keys_var='install_keys'): | ||
4993 | 107 | """ | ||
4994 | 108 | Configure multiple sources from charm configuration | ||
4995 | 109 | |||
4996 | 110 | Example config: | ||
4997 | 111 | install_sources: | ||
4998 | 112 | - "ppa:foo" | ||
4999 | 113 | - "http://example.com/repo precise main" | ||
5000 | 114 | install_keys: |
The diff has been truncated for viewing.
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.