2
# vim: et ai ts=4 sw=4:
10
from pwd import getpwnam
11
from grp import getgrnam
13
CHARM_PACKAGES = ["gunicorn"]
15
INJECTED_WARNING = """
16
#------------------------------------------------------------------------------
17
# The following is the import code for the settings directory injected by Juju
18
#------------------------------------------------------------------------------
22
###############################################################################
23
# Supporting functions
24
###############################################################################
25
MSG_CRITICAL = "CRITICAL"
29
MSG_WARNING = "WARNING"
32
def juju_log(level, msg):
33
subprocess.call(['juju-log', '-l', level, msg])
35
#------------------------------------------------------------------------------
36
# run: Run a command, return the output
37
#------------------------------------------------------------------------------
38
def run(command, exit_on_error=True, cwd=None):
40
juju_log(MSG_DEBUG, command)
41
return subprocess.check_output(
42
command, stderr=subprocess.STDOUT, shell=True, cwd=cwd)
43
except subprocess.CalledProcessError, e:
44
juju_log(MSG_ERROR, "status=%d, output=%s" % (e.returncode, e.output))
46
sys.exit(e.returncode)
51
#------------------------------------------------------------------------------
52
# install_file: install a file resource. overwites existing files.
53
#------------------------------------------------------------------------------
54
def install_file(contents, dest, owner="root", group="root", mode=0600):
55
uid = getpwnam(owner)[2]
56
gid = getgrnam(group)[2]
57
dest_fd = os.open(dest, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, mode)
58
os.fchown(dest_fd, uid, gid)
59
with os.fdopen(dest_fd, 'w') as destfile:
60
destfile.write(str(contents))
63
#------------------------------------------------------------------------------
64
# install_dir: create a directory
65
#------------------------------------------------------------------------------
66
def install_dir(dirname, owner="root", group="root", mode=0700):
68
'/usr/bin/install -o {} -g {} -m {} -d {}'.format(owner, group, oct(mode),
72
#------------------------------------------------------------------------------
73
# config_get: Returns a dictionary containing all of the config information
74
# Optional parameter: scope
75
# scope: limits the scope of the returned configuration to the
76
# desired config item.
77
#------------------------------------------------------------------------------
78
def config_get(scope=None):
80
config_cmd_line = ['config-get']
82
config_cmd_line.append(scope)
83
config_cmd_line.append('--format=json')
84
config_data = json.loads(subprocess.check_output(config_cmd_line))
91
#------------------------------------------------------------------------------
92
# relation_json: Returns json-formatted relation data
93
# Optional parameters: scope, relation_id
94
# scope: limits the scope of the returned data to the
96
# unit_name: limits the data ( and optionally the scope )
97
# to the specified unit
98
# relation_id: specify relation id for out of context usage.
99
#------------------------------------------------------------------------------
100
def relation_json(scope=None, unit_name=None, relation_id=None):
101
command = ['relation-get', '--format=json']
102
if relation_id is not None:
103
command.extend(('-r', relation_id))
104
if scope is not None:
105
command.append(scope)
108
if unit_name is not None:
109
command.append(unit_name)
110
output = subprocess.check_output(command, stderr=subprocess.STDOUT)
111
return output or None
114
#------------------------------------------------------------------------------
115
# relation_get: Returns a dictionary containing the relation information
116
# Optional parameters: scope, relation_id
117
# scope: limits the scope of the returned data to the
119
# unit_name: limits the data ( and optionally the scope )
120
# to the specified unit
121
#------------------------------------------------------------------------------
122
def relation_get(scope=None, unit_name=None, relation_id=None):
123
j = relation_json(scope, unit_name, relation_id)
130
def relation_set(keyvalues, relation_id=None):
133
args.extend(['-r', relation_id])
134
args.extend(["{}='{}'".format(k, v or '') for k, v in keyvalues.items()])
135
run("relation-set {}".format(' '.join(args)))
137
## Posting json to relation-set doesn't seem to work as documented?
140
## cmd = ['relation-set']
142
## cmd.extend(['-r', relation_id])
144
## cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
145
## stderr=subprocess.PIPE)
146
## (out, err) = p.communicate(json.dumps(keyvalues))
148
## juju_log(MSG_ERROR, err)
150
## juju_log(MSG_DEBUG, "relation-set {}".format(repr(keyvalues)))
153
def relation_list(relation_id=None):
154
"""Return the list of units participating in the relation."""
155
if relation_id is None:
156
relation_id = os.environ['JUJU_RELATION_ID']
157
cmd = ['relation-list', '--format=json', '-r', relation_id]
158
json_units = subprocess.check_output(cmd).strip()
160
return json.loads(subprocess.check_output(cmd))
164
#------------------------------------------------------------------------------
165
# relation_ids: Returns a list of relation ids
166
# optional parameters: relation_type
167
# relation_type: return relations only of this type
168
#------------------------------------------------------------------------------
169
def relation_ids(relation_types=('db',)):
170
# accept strings or iterators
171
if isinstance(relation_types, basestring):
172
reltypes = [relation_types, ]
174
reltypes = relation_types
176
for reltype in reltypes:
177
relid_cmd_line = ['relation-ids', '--format=json', reltype]
178
json_relids = subprocess.check_output(relid_cmd_line).strip()
180
relids.extend(json.loads(json_relids))
184
#------------------------------------------------------------------------------
185
# relation_get_all: Returns a dictionary containing the relation information
186
# optional parameters: relation_type
187
# relation_type: limits the scope of the returned data to the
189
#------------------------------------------------------------------------------
190
def relation_get_all(*args, **kwargs):
192
relids = relation_ids(*args, **kwargs)
194
units_cmd_line = ['relation-list', '--format=json', '-r', relid]
195
json_units = subprocess.check_output(units_cmd_line).strip()
197
for unit in json.loads(json_units):
199
json.loads(relation_json(relation_id=relid,
201
for key in unit_data:
202
if key.endswith('-list'):
203
unit_data[key] = unit_data[key].split()
204
unit_data['relation-id'] = relid
205
unit_data['unit'] = unit
206
relation_data.append(unit_data)
209
def apt_get_update():
210
cmd_line = ['apt-get', 'update']
211
return(subprocess.call(cmd_line))
214
#------------------------------------------------------------------------------
215
# apt_get_install( packages ): Installs package(s)
216
#------------------------------------------------------------------------------
217
def apt_get_install(packages=None):
220
cmd_line = ['apt-get', '-y', 'install', '-qq']
221
if isinstance(packages, list):
222
cmd_line.extend(packages)
224
cmd_line.append(packages)
225
return(subprocess.call(cmd_line))
228
#------------------------------------------------------------------------------
229
# pip_install( package ): Installs a python package
230
#------------------------------------------------------------------------------
231
def pip_install(packages=None, upgrade=False):
232
cmd_line = ['pip', 'install']
236
cmd_line.append('-u')
237
if packages.startswith('svn+') or packages.startswith('git+') or \
238
packages.startswith('hg+') or packages.startswith('bzr+'):
239
cmd_line.append('-e')
240
cmd_line.append(packages)
243
#------------------------------------------------------------------------------
244
# pip_install_req( path ): Installs a requirements file
245
#------------------------------------------------------------------------------
246
def pip_install_req(path=None, upgrade=False):
247
cmd_line = ['pip', 'install']
251
cmd_line.append('-u')
252
cmd_line.append('-r')
253
cmd_line.append(path)
254
cwd = os.path.dirname(path)
257
#------------------------------------------------------------------------------
258
# open_port: Convenience function to open a port in juju to
260
#------------------------------------------------------------------------------
261
def open_port(port=None, protocol="TCP"):
264
return(subprocess.call(['open-port', "%d/%s" %
265
(int(port), protocol)]))
268
#------------------------------------------------------------------------------
269
# close_port: Convenience function to close a port in juju to
271
#------------------------------------------------------------------------------
272
def close_port(port=None, protocol="TCP"):
275
return(subprocess.call(['close-port', "%d/%s" %
276
(int(port), protocol)]))
279
#------------------------------------------------------------------------------
280
# update_service_ports: Convenience function that evaluate the old and new
281
# service ports to decide which ports need to be
282
# opened and which to close
283
#------------------------------------------------------------------------------
284
def update_service_port(old_service_port=None, new_service_port=None):
285
if old_service_port is None or new_service_port is None:
287
if new_service_port != old_service_port:
288
close_port(old_service_port)
289
open_port(new_service_port)
295
def install_or_append(contents, dest, owner="root", group="root", mode=0600):
296
if os.path.exists(dest):
297
uid = getpwnam(owner)[2]
298
gid = getgrnam(group)[2]
299
dest_fd = os.open(dest, os.O_APPEND, mode)
300
os.fchown(dest_fd, uid, gid)
301
with os.fdopen(dest_fd, 'a') as destfile:
302
destfile.write(str(contents))
304
install_file(contents, dest, owner, group, mode)
306
def token_sql_safe(value):
307
# Only allow alphanumeric + underscore in database identifiers
308
if re.search('[^A-Za-z0-9_]', value):
313
s = s.replace(':', '_')
314
s = s.replace('-', '_')
315
s = s.replace('/', '_')
316
s = s.replace('"', '_')
317
s = s.replace("'", '_')
320
def user_name(relid, remote_unit, admin=False, schema=False):
321
components = [sanitize(relid), sanitize(remote_unit)]
323
components.append("admin")
325
components.append("schema")
326
return "_".join(components)
328
def get_relation_host():
329
remote_host = run("relation-get ip")
331
# remote unit $JUJU_REMOTE_UNIT uses deprecated 'ip=' component of
333
remote_host = run("relation-get private-address")
338
this_host = run("unit-get private-address")
339
return this_host.strip()
341
def process_template(template_name, template_vars, destination):
342
# --- exported service configuration file
343
from jinja2 import Environment, FileSystemLoader
344
template_env = Environment(
345
loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
349
template_env.get_template(template_name).render(template_vars)
351
with open(destination, 'w') as inject_file:
352
inject_file.write(str(template))
354
def append_template(template_name, template_vars, path, try_append=False):
356
# --- exported service configuration file
357
from jinja2 import Environment, FileSystemLoader
358
template_env = Environment(
359
loader=FileSystemLoader(os.path.join(os.environ['CHARM_DIR'],
363
template_env.get_template(template_name).render(template_vars)
366
if os.path.exists(path):
367
with open(path, 'r') as inject_file:
368
if not str(template) in inject_file:
374
with open(path, 'a') as inject_file:
375
inject_file.write(INJECTED_WARNING)
376
inject_file.write(str(template))
380
###############################################################################
382
###############################################################################
385
for retry in xrange(0,24):
386
if apt_get_install(CHARM_PACKAGES):
394
for retry in xrange(0,24):
395
if apt_get_install(CHARM_PACKAGES):
400
def wsgi_file_relation_joined_changed():
401
wsgi_config = config_data
402
relation_id = os.environ['JUJU_RELATION_ID']
403
juju_log(MSG_INFO, "JUJU_RELATION_ID: %s".format(relation_id))
405
remote_unit_name = sanitize(os.environ['JUJU_REMOTE_UNIT'].split('/')[0])
406
juju_log(MSG_INFO, "JUJU_REMOTE_UNIT: %s".format(remote_unit_name))
407
wsgi_config['unit_name'] = remote_unit_name
409
project_conf = '/etc/init/%s.conf' % remote_unit_name
411
working_dir = relation_get('working_dir')
415
wsgi_config['working_dir'] = working_dir
416
wsgi_config['project_name'] = remote_unit_name
418
for v in wsgi_config.keys():
419
if v.startswith('wsgi_') or v in ['python_path', 'listen_ip', 'port']:
420
upstream_value = relation_get(v)
422
wsgi_config[v] = upstream_value
424
if wsgi_config['wsgi_worker_class'] == 'eventlet':
425
apt_get_install('python-eventlet')
426
elif wsgi_config['wsgi_worker_class'] == 'gevent':
427
apt_get_install('python-gevent')
428
elif wsgi_config['wsgi_worker_class'] == 'tornado':
429
apt_get_install('python-tornado')
431
if wsgi_config['wsgi_workers'] == 0:
432
res = run('python -c "import multiprocessing ; print(multiprocessing.cpu_count())"')
433
wsgi_config['wsgi_workers'] = int(res) + 1
435
if wsgi_config['wsgi_access_logfile']:
436
wsgi_config['wsgi_extra'] = " ".join([
437
wsgi_config['wsgi_extra'],
438
'--access-logformat=%s' % wsgi_config['wsgi_access_logfile'],
439
'--access-logformat="%s"' % wsgi_config['wsgi_access_logformat']
442
wsgi_config['wsgi_wsgi_file'] = wsgi_config['wsgi_wsgi_file']
444
process_template('upstart.tmpl', wsgi_config, project_conf)
447
# We need this because when the contained charm configuration or code changed
448
# Gunicorn needs to restart to run the new code.
449
run("service %s restart || service %s start" % (remote_unit_name, remote_unit_name))
452
def wsgi_file_relation_broken():
453
remote_unit_name = sanitize(os.environ['JUJU_REMOTE_UNIT'].split('/')[0])
455
run('service %s stop' % remote_unit_name)
456
run('rm /etc/init/%s.conf' % remote_unit_name)
459
###############################################################################
461
###############################################################################
462
config_data = config_get()
463
juju_log(MSG_DEBUG, "got config: %s" % str(config_data))
465
unit_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
467
hook_name = os.path.basename(sys.argv[0])
469
###############################################################################
471
###############################################################################
473
juju_log(MSG_INFO, "Running {} hook".format(hook_name))
474
if hook_name == "install":
477
elif hook_name == "upgrade-charm":
480
elif hook_name in ["wsgi-file-relation-joined", "wsgi-file-relation-changed"]:
481
wsgi_file_relation_joined_changed()
483
elif hook_name == "wsgi-file-relation-broken":
484
wsgi_file_relation_broken()
487
print "Unknown hook {}".format(hook_name)
490
if __name__ == '__main__':
491
raise SystemExit(main())