PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/github.com/ptone/jiffylab/webapp/app.py

https://gitlab.com/joostl/docker-stuff
Python | 262 lines | 222 code | 27 blank | 13 comment | 10 complexity | c709071a6a712fe00923836b8442c3aa MD5 | raw file
  1. #!/usr/bin/env python
  2. # coding=utf8
  3. import json
  4. import os
  5. import re
  6. import threading
  7. import time
  8. from unicodedata import normalize
  9. import docker
  10. from flask import Flask, render_template, session, g, redirect, url_for
  11. from flask.ext.bootstrap import Bootstrap
  12. from flask.ext.wtf import Form, TextField
  13. import psutil
  14. import requests
  15. app = Flask(__name__)
  16. app.config['BOOTSTRAP_USE_MINIFIED'] = True
  17. app.config['BOOTSTRAP_USE_CDN'] = True
  18. app.config['BOOTSTRAP_FONTAWESOME'] = True
  19. app.config['SECRET_KEY'] = 'devkey'
  20. CONTAINER_STORAGE = "/usr/local/etc/jiffylab/webapp/containers.json"
  21. SERVICES_HOST = '127.0.0.1'
  22. BASE_IMAGE = 'ptone/jiffylab-base'
  23. initial_memory_budget = psutil.virtual_memory().free # or can use available for vm
  24. # how much memory should each container be limited to
  25. CONTAINER_MEM_LIMIT = 1024 * 1024 * 100
  26. # how much memory must remain in order for a new container to start?
  27. MEM_MIN = CONTAINER_MEM_LIMIT + 1024 * 1024 * 20
  28. app.config.from_object(__name__)
  29. app.config.from_envvar('FLASKAPP_SETTINGS', silent=True)
  30. Bootstrap(app)
  31. docker_client = docker.Client(
  32. base_url='unix://var/run/docker.sock',
  33. version="1.3"
  34. )
  35. lock = threading.Lock()
  36. class ContainerException(Exception):
  37. """
  38. There was some problem generating or launching a docker container
  39. for the user
  40. """
  41. pass
  42. class UserForm(Form):
  43. # TODO use HTML5 email input
  44. email = TextField('Email', description='Please enter your email address.')
  45. @app.before_request
  46. def get_current_user():
  47. g.user = None
  48. email = session.get('email')
  49. if email is not None:
  50. g.user = email
  51. _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
  52. def slugify(text, delim=u'-'):
  53. """Generates a slightly worse ASCII-only slug."""
  54. result = []
  55. for word in _punct_re.split(text.lower()):
  56. word = normalize('NFKD', word).encode('ascii', 'ignore')
  57. if word:
  58. result.append(word)
  59. return unicode(delim.join(result))
  60. def get_image(image_name=BASE_IMAGE):
  61. # TODO catch ConnectionError - requests.exceptions.ConnectionError
  62. for image in docker_client.images():
  63. if image['Repository'] == image_name and image['Tag'] == 'latest':
  64. return image
  65. raise ContainerException("No image found")
  66. return None
  67. def lookup_container(name):
  68. # TODO should this be reset at startup?
  69. container_store = app.config['CONTAINER_STORAGE']
  70. if not os.path.exists(container_store):
  71. with lock:
  72. json.dump({}, open(container_store, 'wb'))
  73. return None
  74. containers = json.load(open(container_store, 'rb'))
  75. try:
  76. return containers[name]
  77. except KeyError:
  78. return None
  79. def check_memory():
  80. """
  81. Check that we have enough memory "budget" to use for this container
  82. Note this is hard because while each container may not be using its full
  83. memory limit amount, you have to consider it like a check written to your
  84. account, you never know when it may be cashed.
  85. """
  86. # the overbook factor says that each container is unlikely to be using its
  87. # full memory limit, and so this is a guestimate of how much you can overbook
  88. # your memory
  89. overbook_factor = .8
  90. remaining_budget = initial_memory_budget - len(docker_client.containers()) * CONTAINER_MEM_LIMIT * overbook_factor
  91. if remaining_budget < MEM_MIN:
  92. raise ContainerException("Sorry, not enough free memory to start your container")
  93. def remember_container(name, containerid):
  94. container_store = app.config['CONTAINER_STORAGE']
  95. with lock:
  96. if not os.path.exists(container_store):
  97. containers = {}
  98. else:
  99. containers = json.load(open(container_store, 'rb'))
  100. containers[name] = containerid
  101. json.dump(containers, open(container_store, 'wb'))
  102. def forget_container(name):
  103. container_store = app.config['CONTAINER_STORAGE']
  104. with lock:
  105. if not os.path.exists(container_store):
  106. return False
  107. else:
  108. containers = json.load(open(container_store, 'rb'))
  109. try:
  110. del(containers[name])
  111. json.dump(containers, open(container_store, 'wb'))
  112. except KeyError:
  113. return False
  114. return True
  115. def add_portmap(cont):
  116. if cont['Ports']:
  117. # a bit of a crazy comprehension to turn:
  118. # Ports': u'49166->8888, 49167->22'
  119. # into a useful dict {8888: 49166, 22: 49167}
  120. cont['portmap'] = {int(k): int(v) for v, k in
  121. [pair.split('->') for
  122. pair in cont['Ports'].split(',')]}
  123. # wait until services are up before returning container
  124. # TODO this could probably be factored better when next
  125. # service added
  126. # this should be done via ajax in the browser
  127. # this will loop and kill the server if it stalls on docker
  128. ipy_wait = shellinabox_wait = True
  129. while ipy_wait or shellinabox_wait:
  130. if ipy_wait:
  131. try:
  132. requests.head("http://{}:{}".format(
  133. app.config['SERVICES_HOST'],
  134. cont['portmap'][8888]))
  135. ipy_wait = False
  136. except requests.exceptions.ConnectionError:
  137. pass
  138. if shellinabox_wait:
  139. try:
  140. requests.head("http://{}:{}".format(
  141. app.config['SERVICES_HOST'],
  142. cont['portmap'][4200]))
  143. shellinabox_wait = False
  144. except requests.exceptions.ConnectionError:
  145. pass
  146. time.sleep(.2)
  147. print 'waiting', app.config['SERVICES_HOST']
  148. return cont
  149. def get_container(cont_id, all=False):
  150. # TODO catch ConnectionError
  151. for cont in docker_client.containers(all=all):
  152. if cont_id in cont['Id']:
  153. return cont
  154. return None
  155. def get_or_make_container(email):
  156. # TODO catch ConnectionError
  157. name = slugify(unicode(email)).lower()
  158. container_id = lookup_container(name)
  159. if not container_id:
  160. image = get_image()
  161. cont = docker_client.create_container(
  162. image['Id'],
  163. None,
  164. hostname="{}box".format(name.split('-')[0]),
  165. )
  166. remember_container(name, cont['Id'])
  167. container_id = cont['Id']
  168. container = get_container(container_id, all=True)
  169. if not container:
  170. # we may have had the container cleared out
  171. forget_container(name)
  172. print 'recurse'
  173. # recurse
  174. # TODO DANGER- could have a over-recursion guard?
  175. return get_or_make_container(email)
  176. if "Up" not in container['Status']:
  177. # if the container is not currently running, restart it
  178. check_memory()
  179. docker_client.start(container_id)
  180. # refresh status
  181. container = get_container(container_id)
  182. container = add_portmap(container)
  183. return container
  184. @app.route('/', methods=['GET', 'POST'])
  185. def index():
  186. try:
  187. container = None
  188. form = UserForm()
  189. print g.user
  190. if g.user:
  191. # show container:
  192. container = get_or_make_container(g.user)
  193. else:
  194. if form.validate_on_submit():
  195. g.user = form.email.data
  196. session['email'] = g.user
  197. container = get_or_make_container(g.user)
  198. return render_template('index.html',
  199. container=container,
  200. form=form,
  201. servicehost=app.config['SERVICES_HOST'],
  202. )
  203. except ContainerException as e:
  204. session.pop('email', None)
  205. return render_template('error.html', error=e)
  206. @app.route('/logout')
  207. def logout():
  208. # remove the username from the session if it's there
  209. session.pop('email', None)
  210. return redirect(url_for('index'))
  211. if '__main__' == __name__:
  212. app.run(debug=True, host='0.0.0.0')