stackamole-xblock 9.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. fake/common/djangoapps/student/__init__.py +0 -0
  2. fake/common/djangoapps/student/models.py +15 -0
  3. fake/course/__init__.py +0 -0
  4. requirements/base.txt +25 -0
  5. requirements/flake8.txt +1 -0
  6. requirements/setup.txt +3 -0
  7. requirements/test.txt +19 -0
  8. samples/hot/sample-template.yaml +427 -0
  9. src/pip-delete-this-directory.txt +5 -0
  10. stackamole/__init__.py +8 -0
  11. stackamole/admin.py +130 -0
  12. stackamole/common.py +423 -0
  13. stackamole/jobs.py +211 -0
  14. stackamole/locale/config.yaml +14 -0
  15. stackamole/locale/de_DE/LC_MESSAGES/django.po +109 -0
  16. stackamole/locale/de_DE/LC_MESSAGES/djangojs.po +68 -0
  17. stackamole/locale/el/LC_MESSAGES/django.po +113 -0
  18. stackamole/locale/el/LC_MESSAGES/djangojs.po +77 -0
  19. stackamole/locale/en/LC_MESSAGES/django.po +107 -0
  20. stackamole/locale/en/LC_MESSAGES/djangojs.po +68 -0
  21. stackamole/locale/es_419/LC_MESSAGES/django.po +109 -0
  22. stackamole/locale/es_419/LC_MESSAGES/djangojs.po +68 -0
  23. stackamole/locale/et_EE/LC_MESSAGES/django.po +110 -0
  24. stackamole/locale/et_EE/LC_MESSAGES/djangojs.po +68 -0
  25. stackamole/locale/hi/LC_MESSAGES/django.po +111 -0
  26. stackamole/locale/hi/LC_MESSAGES/djangojs.po +72 -0
  27. stackamole/management/__init__.py +0 -0
  28. stackamole/management/commands/__init__.py +0 -0
  29. stackamole/management/commands/reaper.py +21 -0
  30. stackamole/management/commands/suspender.py +20 -0
  31. stackamole/migrations/0001_initial.py +43 -0
  32. stackamole/migrations/0002_stacklog.py +47 -0
  33. stackamole/migrations/0003_blanks.py +84 -0
  34. stackamole/migrations/0004_auto_20190715_1053.py +24 -0
  35. stackamole/migrations/0005_auto_20190811_1555.py +56 -0
  36. stackamole/migrations/0006_auto_20200107_1332.py +44 -0
  37. stackamole/migrations/0007_add_delete_by_and_delete_age.py +34 -0
  38. stackamole/migrations/0008_add_database_defaults_for_stack_key_and_password.py +23 -0
  39. stackamole/migrations/0009_add_null_true_for_key_and_password.py +23 -0
  40. stackamole/migrations/0010_add_user_foreign_key.py +52 -0
  41. stackamole/migrations/0011_allow_null_for_learner.py +29 -0
  42. stackamole/migrations/0012_add_suspend_by.py +21 -0
  43. stackamole/migrations/0013_migrate_app_label.py +71 -0
  44. stackamole/migrations/__init__.py +0 -0
  45. stackamole/models.py +106 -0
  46. stackamole/openstack.py +132 -0
  47. stackamole/provider.py +437 -0
  48. stackamole/public/css/main.css +203 -0
  49. stackamole/public/js/client.js +269 -0
  50. stackamole/public/js/guacamole-common-js/0.9.13-incubating-all.min.js +1 -0
  51. stackamole/public/js/guacamole-common-js/1.4.0-all.min.js +155 -0
  52. stackamole/public/js/guacamole-common-js/1.5.2-all.min.js +161 -0
  53. stackamole/public/js/guacamole-common-js/1.5.4-all.min.js +161 -0
  54. stackamole/public/js/guacamole-common-js/1.5.5-all.min.js +161 -0
  55. stackamole/public/js/main.js +492 -0
  56. stackamole/public/js/plugins.js +95 -0
  57. stackamole/stackamole.py +1226 -0
  58. stackamole/static/html/lab.html +88 -0
  59. stackamole/static/html/main.html +93 -0
  60. stackamole/tasks.py +947 -0
  61. stackamole_guacamole_client/__init__.py +0 -0
  62. stackamole_guacamole_client/asgi.py +26 -0
  63. stackamole_guacamole_client/consumers.py +95 -0
  64. stackamole_guacamole_client/settings.py +87 -0
  65. stackamole_xblock-9.4.0.dist-info/METADATA +647 -0
  66. stackamole_xblock-9.4.0.dist-info/RECORD +89 -0
  67. stackamole_xblock-9.4.0.dist-info/WHEEL +5 -0
  68. stackamole_xblock-9.4.0.dist-info/entry_points.txt +3 -0
  69. stackamole_xblock-9.4.0.dist-info/licenses/LICENSE +661 -0
  70. stackamole_xblock-9.4.0.dist-info/top_level.txt +7 -0
  71. tests/__init__.py +0 -0
  72. tests/resources/course/stackamole/fake_lab_1.xml +9 -0
  73. tests/resources/course/stackamole/fake_lab_2.xml +9 -0
  74. tests/resources/course/stackamole/fake_lab_hook_events_1.xml +4 -0
  75. tests/resources/course/stackamole/fake_lab_hook_events_2.xml +3 -0
  76. tests/resources/course/stackamole/fake_lab_no_port_name.xml +3 -0
  77. tests/resources/course/stackamole/fake_lab_no_port_number.xml +3 -0
  78. tests/resources/course/stackamole/fake_lab_no_provider_capacity.xml +5 -0
  79. tests/resources/course/stackamole/fake_lab_no_provider_name.xml +3 -0
  80. tests/unit/__init__.py +0 -0
  81. tests/unit/test_admin.py +170 -0
  82. tests/unit/test_commands.py +31 -0
  83. tests/unit/test_common.py +307 -0
  84. tests/unit/test_jobs.py +649 -0
  85. tests/unit/test_models.py +72 -0
  86. tests/unit/test_openstack.py +149 -0
  87. tests/unit/test_provider.py +847 -0
  88. tests/unit/test_stackamole.py +1664 -0
  89. tests/unit/test_tasks.py +2324 -0
stackamole/common.py ADDED
@@ -0,0 +1,423 @@
1
+ import errno
2
+ import sys
3
+ import time
4
+ import uuid
5
+ import paramiko
6
+ import logging
7
+ import six
8
+
9
+ from io import StringIO
10
+ from socket import timeout as SocketTimeout
11
+ from paramiko import RSAKey, Ed25519Key
12
+ from paramiko.ssh_exception import (AuthenticationException,
13
+ SSHException,
14
+ NoValidConnectionsError)
15
+ from django.conf import settings as django_settings
16
+ from pymongo.errors import PyMongoError
17
+ from tenacity import (
18
+ retry,
19
+ retry_if_exception_type,
20
+ stop_after_attempt,
21
+ wait_exponential,
22
+ before_sleep_log,
23
+ )
24
+
25
+ from .models import Stack
26
+
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ ACTIONS = (
31
+ ADOPT,
32
+ CHECK,
33
+ CREATE,
34
+ DELETE,
35
+ LAUNCH,
36
+ RESTORE,
37
+ RESUME,
38
+ ROLLBACK,
39
+ SNAPSHOT,
40
+ SUSPEND,
41
+ UPDATE
42
+ ) = (
43
+ 'ADOPT',
44
+ 'CHECK',
45
+ 'CREATE',
46
+ 'DELETE',
47
+ 'LAUNCH',
48
+ 'RESTORE',
49
+ 'RESUME',
50
+ 'ROLLBACK',
51
+ 'SNAPSHOT',
52
+ 'SUSPEND',
53
+ 'UPDATE'
54
+ )
55
+
56
+ STATUSES = (
57
+ COMPLETE,
58
+ ERROR,
59
+ FAILED,
60
+ IN_PROGRESS,
61
+ ISSUED,
62
+ PENDING,
63
+ RETRY,
64
+ TIMEOUT
65
+ ) = (
66
+ 'COMPLETE',
67
+ 'ERROR',
68
+ 'FAILED',
69
+ 'IN_PROGRESS',
70
+ 'ISSUED',
71
+ 'PENDING',
72
+ 'RETRY',
73
+ 'TIMEOUT'
74
+ )
75
+
76
+ # Dynamically define all possible states as attributes in this module. To
77
+ # prevent flake8 from complaining about undefined attributes, we add noqa: F821
78
+ # where necessary, below.
79
+ ALL_STATES = tuple(['%s_%s' % (a, s) for a in ACTIONS for s in STATUSES])
80
+ module = sys.modules[__name__]
81
+ for state in ALL_STATES:
82
+ setattr(module, state, state)
83
+
84
+ VALID_STATES = (
85
+ ADOPT_COMPLETE, # noqa: F821
86
+ ADOPT_FAILED, # noqa: F821
87
+ ADOPT_IN_PROGRESS, # noqa: F821
88
+ CHECK_COMPLETE, # noqa: F821
89
+ CHECK_FAILED, # noqa: F821
90
+ CHECK_IN_PROGRESS, # noqa: F821
91
+ CREATE_COMPLETE, # noqa: F821
92
+ CREATE_FAILED, # noqa: F821
93
+ CREATE_IN_PROGRESS, # noqa: F821
94
+ DELETE_COMPLETE, # noqa: F821
95
+ DELETE_FAILED, # noqa: F821
96
+ DELETE_IN_PROGRESS, # noqa: F821
97
+ DELETE_PENDING, # noqa: F821
98
+ LAUNCH_ERROR, # noqa: F821
99
+ LAUNCH_PENDING, # noqa: F821
100
+ LAUNCH_TIMEOUT, # noqa: F821
101
+ RESTORE_COMPLETE, # noqa: F821
102
+ RESTORE_FAILED, # noqa: F821
103
+ RESTORE_IN_PROGRESS, # noqa: F821
104
+ RESUME_COMPLETE, # noqa: F821
105
+ RESUME_FAILED, # noqa: F821
106
+ RESUME_IN_PROGRESS, # noqa: F821
107
+ ROLLBACK_COMPLETE, # noqa: F821
108
+ ROLLBACK_FAILED, # noqa: F821
109
+ ROLLBACK_IN_PROGRESS, # noqa: F821
110
+ SNAPSHOT_COMPLETE, # noqa: F821
111
+ SNAPSHOT_FAILED, # noqa: F821
112
+ SNAPSHOT_IN_PROGRESS, # noqa: F821
113
+ SUSPEND_COMPLETE, # noqa: F821
114
+ SUSPEND_FAILED, # noqa: F821
115
+ SUSPEND_IN_PROGRESS, # noqa: F821
116
+ SUSPEND_ISSUED, # noqa: F821
117
+ SUSPEND_PENDING, # noqa: F821
118
+ SUSPEND_RETRY, # noqa: F821
119
+ UPDATE_COMPLETE, # noqa: F821
120
+ UPDATE_FAILED, # noqa: F821
121
+ UPDATE_IN_PROGRESS # noqa: F821
122
+ )
123
+
124
+ UP_STATES = (
125
+ CREATE_COMPLETE, # noqa: F821
126
+ RESUME_COMPLETE, # noqa: F821
127
+ UPDATE_COMPLETE, # noqa: F821
128
+ ROLLBACK_COMPLETE, # noqa: F821
129
+ SNAPSHOT_COMPLETE # noqa: F821
130
+ )
131
+
132
+ DOWN_STATES = (
133
+ SUSPEND_COMPLETE, # noqa: F821
134
+ DELETE_COMPLETE # noqa: F821
135
+ )
136
+
137
+ PENDING_STATES = (
138
+ LAUNCH_PENDING, # noqa: F821
139
+ SUSPEND_PENDING, # noqa: F821
140
+ DELETE_PENDING # noqa: F821
141
+ )
142
+
143
+ OCCUPANCY_STATES = (
144
+ CREATE_COMPLETE, # noqa: F821
145
+ RESUME_COMPLETE, # noqa: F821
146
+ UPDATE_COMPLETE, # noqa: F821
147
+ ROLLBACK_COMPLETE, # noqa: F821
148
+ SNAPSHOT_COMPLETE, # noqa: F821
149
+ LAUNCH_PENDING, # noqa: F821
150
+ SUSPEND_PENDING, # noqa: F821
151
+ SUSPEND_ISSUED, # noqa: F821
152
+ SUSPEND_RETRY, # noqa: F821
153
+ SUSPEND_COMPLETE, # noqa: F821
154
+ DELETE_PENDING # noqa: F821
155
+ )
156
+
157
+ SETTINGS_KEY = 'stackamole'
158
+
159
+ DEFAULT_SETTINGS = {
160
+ "terminal_url": "/stackamole-xblock/",
161
+ "terminal_color_scheme": "white-black",
162
+ "terminal_font_name": "monospace",
163
+ "terminal_font_size": "10",
164
+ "instructions_layout": "above",
165
+ "enable_fullscreen": False,
166
+ "launch_timeout": 900,
167
+ "remote_exec_timeout": 300,
168
+ "suspend_timeout": 120,
169
+ "suspend_interval": 60,
170
+ "suspend_concurrency": 4,
171
+ "suspend_task_timeout": 900,
172
+ "check_timeout": 120,
173
+ "delete_age": 14,
174
+ "delete_attempts": 3,
175
+ "delete_interval": 86400,
176
+ "delete_task_timeout": 900,
177
+ "sleep_timeout": 10,
178
+ "ssh_connect_timeout": 10,
179
+ "js_timeouts": {
180
+ "status": 15000,
181
+ "keepalive": 30000,
182
+ "idle": 3600000,
183
+ "check": 5000
184
+ },
185
+ "providers": {},
186
+ "guacamole_js_version": '1.5.5',
187
+ "lab_usage_limit": None,
188
+ "lab_usage_limit_breach_policy": None,
189
+ "lab_resize_method": "reconnect"
190
+ }
191
+
192
+ SUPPORTED_LANGUAGES = [
193
+ 'en', # English
194
+ 'de-de', # Deutsch (Deutschland), German (Germany)
195
+ 'el', # Ελληνικά, Greek
196
+ 'es-419', # Español (Latinoamérica), Spanish (Latin America)
197
+ 'et-ee', # Eesti (Eesti), Estonian (Estonia)
198
+ 'hi', # हिन्दी, Hindi
199
+ ]
200
+
201
+
202
+ def _(string):
203
+ """
204
+ Dummy `gettext` replacement to make string extraction tools
205
+ scrape strings marked for translation.
206
+ Translations will actually be loaded via the translation service
207
+ in the XBlock runtime, but we also need to extract strings from
208
+ files that cannot access the service.
209
+ For example, some user-facing error or warning messages.
210
+ """
211
+ return string
212
+
213
+
214
+ class RemoteExecException(Exception):
215
+ pass
216
+
217
+
218
+ class RemoteExecTimeout(RemoteExecException):
219
+ pass
220
+
221
+
222
+ if sys.version_info < (3,):
223
+ def b(x):
224
+ return x
225
+ else:
226
+ import codecs
227
+
228
+ def b(x):
229
+ return codecs.latin_1_encode(x)[0]
230
+
231
+
232
+ def get_xblock_settings():
233
+ try:
234
+ xblock_settings = django_settings.XBLOCK_SETTINGS
235
+ except AttributeError:
236
+ settings = DEFAULT_SETTINGS
237
+ else:
238
+ settings = xblock_settings.get(
239
+ SETTINGS_KEY, DEFAULT_SETTINGS)
240
+
241
+ return settings
242
+
243
+
244
+ def update_stack(name, course_id, student_id, data):
245
+ stack = Stack.objects.select_for_update().get(
246
+ student_id=student_id,
247
+ course_id=course_id,
248
+ name=name
249
+ )
250
+ update_stack_fields(stack, data)
251
+ stack.save(update_fields=list(data.keys()))
252
+
253
+
254
+ def update_stack_fields(stack, data):
255
+ for field, value in data.items():
256
+ if hasattr(stack, field):
257
+ setattr(stack, field, value)
258
+
259
+
260
+ def get_stack(name, course_id, student_id, prop=None):
261
+ stack = Stack.objects.get(
262
+ student_id=student_id,
263
+ course_id=course_id,
264
+ name=name
265
+ )
266
+
267
+ if prop:
268
+ return getattr(stack, prop)
269
+ else:
270
+ return stack
271
+
272
+
273
+ # If PyMongoError is raised here, try again (max attempts = 3)
274
+ # Before every subsequent retry, wait
275
+ # Use before_sleep_log to log attempts
276
+ # Reraise the exception from last attempt
277
+ @retry(retry=retry_if_exception_type(PyMongoError),
278
+ stop=stop_after_attempt(3),
279
+ wait=wait_exponential(),
280
+ before_sleep=before_sleep_log(logger, logging.WARNING),
281
+ reraise=True)
282
+ def read_from_contentstore(course_key, path):
283
+ """
284
+ Loads a file directly from the course's content store.
285
+
286
+ """
287
+ contents = None
288
+ try:
289
+ from xmodule.contentstore.content import StaticContent
290
+ from xmodule.contentstore.django import contentstore
291
+ from opaque_keys.edx.locator import CourseLocator
292
+ except ImportError:
293
+ # We're not running under edx-platform, so ignore.
294
+ pass
295
+ else:
296
+ if isinstance(course_key, six.text_type):
297
+ course_key = CourseLocator.from_string(course_key)
298
+ loc = StaticContent.compute_location(course_key, path)
299
+ asset = contentstore().find(loc)
300
+ contents = asset.data
301
+
302
+ return contents
303
+
304
+
305
+ def ssh_to(user, ip, key):
306
+ ssh = paramiko.SSHClient()
307
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
308
+
309
+ pkey = None
310
+ for key_type in (RSAKey, Ed25519Key):
311
+ try:
312
+ pkey = key_type.from_private_key(StringIO(key))
313
+ except SSHException:
314
+ pass
315
+
316
+ settings = get_xblock_settings()
317
+ sleep_timeout = settings.get("sleep_timeout", 10)
318
+ ssh_connect_timeout = settings.get("ssh_connect_timeout", 10)
319
+
320
+ connected = False
321
+ while not connected:
322
+ try:
323
+ ssh.connect(ip,
324
+ username=user,
325
+ pkey=pkey,
326
+ timeout=ssh_connect_timeout)
327
+ except (EOFError,
328
+ AuthenticationException,
329
+ SSHException,
330
+ NoValidConnectionsError,
331
+ SocketTimeout) as e:
332
+ # Be more persistent than Paramiko normally
333
+ # is, and keep retrying.
334
+ logger.info("Got %s during SSH connection to ip (%s), retrying." %
335
+ (e.__class__.__name__, ip))
336
+ except EnvironmentError as enve:
337
+ if enve.errno in (errno.EAGAIN,
338
+ errno.ENETDOWN,
339
+ errno.ENETUNREACH,
340
+ errno.ENETRESET,
341
+ errno.ECONNABORTED,
342
+ errno.ECONNRESET,
343
+ errno.ENOTCONN,
344
+ errno.EHOSTDOWN):
345
+ # Be more persistent than Paramiko normally
346
+ # is, and keep retrying.
347
+ logger.debug("Got errno %s during SSH connection "
348
+ "to ip (%s), retrying." % (enve.errno, ip))
349
+ elif enve.errno in (errno.ECONNREFUSED,
350
+ errno.EHOSTUNREACH):
351
+ # Paramiko should catch and wrap
352
+ # these. They should never bubble
353
+ # up. Still, continue being more
354
+ # persistent, and retry.
355
+ logger.warning("Got errno %s during SSH connection "
356
+ "to ip (%s). Paramiko bug? Retrying." %
357
+ (enve.errno, ip))
358
+ else:
359
+ # Anything else is an unexpected error.
360
+ raise
361
+ else:
362
+ connected = True
363
+
364
+ if not connected:
365
+ time.sleep(sleep_timeout)
366
+
367
+ return ssh
368
+
369
+
370
+ def remote_exec(ssh, script, params=None, reuse_sftp=None):
371
+ if reuse_sftp:
372
+ sftp = reuse_sftp
373
+ else:
374
+ sftp = ssh.open_sftp()
375
+
376
+ # Generate a temporary filename
377
+ script_file = '/tmp/.%s' % uuid.uuid4()
378
+
379
+ # Open the file remotely and write the script out to it.
380
+ f = sftp.open(script_file, 'w')
381
+ f.write(script)
382
+ f.close()
383
+
384
+ # Make it executable.
385
+ sftp.chmod(script_file, 0o775)
386
+
387
+ # Add command line arguments, if any.
388
+ if params:
389
+ command = "%s %s" % (script_file, params)
390
+ else:
391
+ command = script_file
392
+
393
+ # Run it.
394
+ _, stdout, stderr = ssh.exec_command(command)
395
+
396
+ # Wait for it to complete.
397
+ settings = get_xblock_settings()
398
+ timeout = settings.get("remote_exec_timeout", 300)
399
+ try:
400
+ start = time.time()
401
+ while not stdout.channel.exit_status_ready():
402
+ if timeout and time.time() >= start + timeout:
403
+ error_msg = ("Remote execution timeout after [%d] seconds." %
404
+ timeout)
405
+ raise RemoteExecTimeout(error_msg)
406
+
407
+ time.sleep(1)
408
+ finally:
409
+ # Remove the file
410
+ sftp.remove(script_file)
411
+
412
+ # Check for errors
413
+ retval = stdout.channel.recv_exit_status()
414
+ error_msg = None
415
+ if retval != 0:
416
+ error_msg = stderr.read()
417
+ raise RemoteExecException(error_msg)
418
+
419
+ # Close the sftp session
420
+ if not reuse_sftp:
421
+ sftp.close()
422
+
423
+ return retval
stackamole/jobs.py ADDED
@@ -0,0 +1,211 @@
1
+ from __future__ import print_function
2
+
3
+ import sys
4
+
5
+ from django.db import transaction, close_old_connections
6
+ from django.utils import timezone
7
+
8
+ from .models import Stack
9
+ from .provider import Provider
10
+ from .tasks import DeleteStackTask, SuspendStackTask
11
+ from .common import (
12
+ UP_STATES,
13
+ SUSPEND_PENDING,
14
+ SUSPEND_RETRY,
15
+ SUSPEND_FAILED,
16
+ DELETE_COMPLETE,
17
+ DELETE_IN_PROGRESS,
18
+ DELETE_PENDING,
19
+ LAUNCH_PENDING,
20
+ CREATE_COMPLETE
21
+ )
22
+
23
+
24
+ class AbstractJob(object):
25
+ """
26
+ Parent job class.
27
+
28
+ """
29
+ settings = {}
30
+
31
+ def __init__(self, settings):
32
+ self.settings = settings
33
+
34
+ def log(self, msg):
35
+ """
36
+ Log message to stdout.
37
+
38
+ """
39
+ print(msg, file=sys.stderr)
40
+
41
+ def refresh_db(self):
42
+ """
43
+ Destroy old or unusable database connections. Django will reconnect as
44
+ needed.
45
+
46
+ """
47
+ close_old_connections()
48
+
49
+
50
+ class SuspenderJob(AbstractJob):
51
+ """
52
+ Suspends stacks.
53
+
54
+ """
55
+ def run(self):
56
+ timeout = self.settings.get("suspend_timeout", 120)
57
+ concurrency = self.settings.get("suspend_concurrency", 4)
58
+ timedelta = timezone.timedelta(seconds=timeout)
59
+ cutoff = timezone.now() - timedelta
60
+
61
+ # SUSPEND_RETRY is no longer needed: we always retry on a suspend
62
+ # failure. It is, thus, deprecated.
63
+ states = list(UP_STATES) + [SUSPEND_RETRY, SUSPEND_FAILED]
64
+
65
+ self.refresh_db()
66
+
67
+ # Get stacks to suspend
68
+ with transaction.atomic():
69
+ stacks_without_suspend_by = Stack.objects.select_for_update().filter( # noqa: E501
70
+ suspend_timestamp__isnull=False
71
+ ).filter(
72
+ suspend_by__isnull=True
73
+ ).filter(
74
+ suspend_timestamp__lt=cutoff
75
+ ).filter(
76
+ status__in=states
77
+ ).exclude(
78
+ provider__exact=''
79
+ )
80
+
81
+ stacks_with_suspend_by = Stack.objects.select_for_update().filter(
82
+ suspend_timestamp__isnull=False
83
+ ).filter(
84
+ suspend_by__isnull=False
85
+ ).filter(
86
+ suspend_by__lt=timezone.now()
87
+ ).filter(
88
+ status__in=states
89
+ ).exclude(
90
+ provider__exact=''
91
+ )
92
+
93
+ stacks = stacks_without_suspend_by.union(
94
+ stacks_with_suspend_by).order_by(
95
+ 'suspend_timestamp')[:concurrency]
96
+
97
+ for stack in stacks:
98
+ stack.status = SUSPEND_PENDING
99
+ stack.save(update_fields=["status"])
100
+
101
+ # Suspend them
102
+ for stack in stacks:
103
+ self.suspend_stack(stack)
104
+
105
+ def suspend_stack(self, stack):
106
+ """
107
+ Start an asynchronous suspend task with no expiration and ignoring
108
+ results.
109
+
110
+ """
111
+ soft_time_limit = self.settings.get("suspend_task_timeout", 900)
112
+ hard_time_limit = soft_time_limit + 30
113
+ kwargs = {
114
+ "stack_id": stack.id
115
+ }
116
+ SuspendStackTask.apply_async(
117
+ kwargs=kwargs,
118
+ soft_time_limit=soft_time_limit,
119
+ time_limit=hard_time_limit,
120
+ ignore_result=True
121
+ )
122
+
123
+
124
+ class ReaperJob(AbstractJob):
125
+ """
126
+ Deletes old stacks.
127
+
128
+ """
129
+ def run(self):
130
+ dont_delete = [DELETE_PENDING,
131
+ DELETE_COMPLETE,
132
+ DELETE_IN_PROGRESS,
133
+ LAUNCH_PENDING,
134
+ CREATE_COMPLETE]
135
+
136
+ self.refresh_db()
137
+
138
+ # Get stacks to delete
139
+ with transaction.atomic():
140
+ stacks = Stack.objects.select_for_update().filter(
141
+ suspend_timestamp__isnull=False
142
+ ).filter(
143
+ delete_by__isnull=False
144
+ ).filter(
145
+ delete_by__lt=timezone.now()
146
+ ).exclude(
147
+ status__in=dont_delete
148
+ ).exclude(
149
+ provider__exact=''
150
+ ).order_by('suspend_timestamp')
151
+
152
+ for stack in stacks:
153
+ stack.status = DELETE_PENDING
154
+ stack.save(update_fields=["status"])
155
+
156
+ # Delete them
157
+ for stack in stacks:
158
+ self.delete_stack(stack)
159
+
160
+ # Apocalypse pass: kill all zombie stacks
161
+ providers = self.settings.get("providers", {})
162
+ for provider_name in providers:
163
+ provider = Provider.init(provider_name)
164
+ try:
165
+ provider_stacks = provider.get_stacks()
166
+ except Exception as e:
167
+ error_msg = "Error listing stacks for provider [%s]: %s" % (
168
+ provider_name, str(e))
169
+ self.log(error_msg)
170
+ continue
171
+
172
+ for provider_stack in provider_stacks:
173
+ stack_name = provider_stack["name"]
174
+
175
+ try:
176
+ stack = Stack.objects.get(name=stack_name)
177
+ except Stack.DoesNotExist:
178
+ continue
179
+
180
+ if stack.status == DELETE_COMPLETE:
181
+ error_msg = ("Zombie stack [%s] detected at provider [%s]"
182
+ % (stack_name, provider_name))
183
+ self.log(error_msg)
184
+ stack.provider = provider_name
185
+ stack.status = DELETE_PENDING
186
+ stack.error_msg = error_msg
187
+ stack.save(update_fields=[
188
+ "provider",
189
+ "status",
190
+ "error_msg"
191
+ ])
192
+
193
+ self.delete_stack(stack)
194
+
195
+ def delete_stack(self, stack):
196
+ """
197
+ Start an asynchronous delete task with no expiration and ignoring
198
+ results.
199
+
200
+ """
201
+ soft_time_limit = self.settings.get("delete_task_timeout", 900)
202
+ hard_time_limit = soft_time_limit + 30
203
+ kwargs = {
204
+ "stack_id": stack.id
205
+ }
206
+ DeleteStackTask.apply_async(
207
+ kwargs=kwargs,
208
+ soft_time_limit=soft_time_limit,
209
+ time_limit=hard_time_limit,
210
+ ignore_result=True
211
+ )
@@ -0,0 +1,14 @@
1
+ # Configuration for i18n workflow.
2
+
3
+ locales:
4
+ - en # English - Source Language
5
+ - de_DE # German
6
+ - el # Greek
7
+ - es_419 # Spanish (Latin America)
8
+ - et_EE # Estonian (Estonia)
9
+ - hi # Hindi
10
+
11
+ # Directories we don't search for strings.
12
+ ignore_dirs:
13
+ - 'management'
14
+ - 'migrations'