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.
- fake/common/djangoapps/student/__init__.py +0 -0
- fake/common/djangoapps/student/models.py +15 -0
- fake/course/__init__.py +0 -0
- requirements/base.txt +25 -0
- requirements/flake8.txt +1 -0
- requirements/setup.txt +3 -0
- requirements/test.txt +19 -0
- samples/hot/sample-template.yaml +427 -0
- src/pip-delete-this-directory.txt +5 -0
- stackamole/__init__.py +8 -0
- stackamole/admin.py +130 -0
- stackamole/common.py +423 -0
- stackamole/jobs.py +211 -0
- stackamole/locale/config.yaml +14 -0
- stackamole/locale/de_DE/LC_MESSAGES/django.po +109 -0
- stackamole/locale/de_DE/LC_MESSAGES/djangojs.po +68 -0
- stackamole/locale/el/LC_MESSAGES/django.po +113 -0
- stackamole/locale/el/LC_MESSAGES/djangojs.po +77 -0
- stackamole/locale/en/LC_MESSAGES/django.po +107 -0
- stackamole/locale/en/LC_MESSAGES/djangojs.po +68 -0
- stackamole/locale/es_419/LC_MESSAGES/django.po +109 -0
- stackamole/locale/es_419/LC_MESSAGES/djangojs.po +68 -0
- stackamole/locale/et_EE/LC_MESSAGES/django.po +110 -0
- stackamole/locale/et_EE/LC_MESSAGES/djangojs.po +68 -0
- stackamole/locale/hi/LC_MESSAGES/django.po +111 -0
- stackamole/locale/hi/LC_MESSAGES/djangojs.po +72 -0
- stackamole/management/__init__.py +0 -0
- stackamole/management/commands/__init__.py +0 -0
- stackamole/management/commands/reaper.py +21 -0
- stackamole/management/commands/suspender.py +20 -0
- stackamole/migrations/0001_initial.py +43 -0
- stackamole/migrations/0002_stacklog.py +47 -0
- stackamole/migrations/0003_blanks.py +84 -0
- stackamole/migrations/0004_auto_20190715_1053.py +24 -0
- stackamole/migrations/0005_auto_20190811_1555.py +56 -0
- stackamole/migrations/0006_auto_20200107_1332.py +44 -0
- stackamole/migrations/0007_add_delete_by_and_delete_age.py +34 -0
- stackamole/migrations/0008_add_database_defaults_for_stack_key_and_password.py +23 -0
- stackamole/migrations/0009_add_null_true_for_key_and_password.py +23 -0
- stackamole/migrations/0010_add_user_foreign_key.py +52 -0
- stackamole/migrations/0011_allow_null_for_learner.py +29 -0
- stackamole/migrations/0012_add_suspend_by.py +21 -0
- stackamole/migrations/0013_migrate_app_label.py +71 -0
- stackamole/migrations/__init__.py +0 -0
- stackamole/models.py +106 -0
- stackamole/openstack.py +132 -0
- stackamole/provider.py +437 -0
- stackamole/public/css/main.css +203 -0
- stackamole/public/js/client.js +269 -0
- stackamole/public/js/guacamole-common-js/0.9.13-incubating-all.min.js +1 -0
- stackamole/public/js/guacamole-common-js/1.4.0-all.min.js +155 -0
- stackamole/public/js/guacamole-common-js/1.5.2-all.min.js +161 -0
- stackamole/public/js/guacamole-common-js/1.5.4-all.min.js +161 -0
- stackamole/public/js/guacamole-common-js/1.5.5-all.min.js +161 -0
- stackamole/public/js/main.js +492 -0
- stackamole/public/js/plugins.js +95 -0
- stackamole/stackamole.py +1226 -0
- stackamole/static/html/lab.html +88 -0
- stackamole/static/html/main.html +93 -0
- stackamole/tasks.py +947 -0
- stackamole_guacamole_client/__init__.py +0 -0
- stackamole_guacamole_client/asgi.py +26 -0
- stackamole_guacamole_client/consumers.py +95 -0
- stackamole_guacamole_client/settings.py +87 -0
- stackamole_xblock-9.4.0.dist-info/METADATA +647 -0
- stackamole_xblock-9.4.0.dist-info/RECORD +89 -0
- stackamole_xblock-9.4.0.dist-info/WHEEL +5 -0
- stackamole_xblock-9.4.0.dist-info/entry_points.txt +3 -0
- stackamole_xblock-9.4.0.dist-info/licenses/LICENSE +661 -0
- stackamole_xblock-9.4.0.dist-info/top_level.txt +7 -0
- tests/__init__.py +0 -0
- tests/resources/course/stackamole/fake_lab_1.xml +9 -0
- tests/resources/course/stackamole/fake_lab_2.xml +9 -0
- tests/resources/course/stackamole/fake_lab_hook_events_1.xml +4 -0
- tests/resources/course/stackamole/fake_lab_hook_events_2.xml +3 -0
- tests/resources/course/stackamole/fake_lab_no_port_name.xml +3 -0
- tests/resources/course/stackamole/fake_lab_no_port_number.xml +3 -0
- tests/resources/course/stackamole/fake_lab_no_provider_capacity.xml +5 -0
- tests/resources/course/stackamole/fake_lab_no_provider_name.xml +3 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_admin.py +170 -0
- tests/unit/test_commands.py +31 -0
- tests/unit/test_common.py +307 -0
- tests/unit/test_jobs.py +649 -0
- tests/unit/test_models.py +72 -0
- tests/unit/test_openstack.py +149 -0
- tests/unit/test_provider.py +847 -0
- tests/unit/test_stackamole.py +1664 -0
- tests/unit/test_tasks.py +2324 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by Django 1.11.15 on 2019-07-15 10:53
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('stackamole', '0003_blanks'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.RemoveField(
|
|
16
|
+
model_name='stacklog',
|
|
17
|
+
name='stack',
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='stacklog',
|
|
21
|
+
name='stack_id',
|
|
22
|
+
field=models.IntegerField(db_index=True, null=True),
|
|
23
|
+
),
|
|
24
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by Django 1.11.18 on 2019-08-11 15:55
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import jsonfield.fields
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('stackamole', '0004_auto_20190715_1053'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name='stack',
|
|
18
|
+
name='hook_events',
|
|
19
|
+
field=jsonfield.fields.JSONField(default="null"),
|
|
20
|
+
),
|
|
21
|
+
migrations.AddField(
|
|
22
|
+
model_name='stack',
|
|
23
|
+
name='hook_script',
|
|
24
|
+
field=models.CharField(max_length=256, null=True),
|
|
25
|
+
),
|
|
26
|
+
migrations.AddField(
|
|
27
|
+
model_name='stack',
|
|
28
|
+
name='providers',
|
|
29
|
+
field=jsonfield.fields.JSONField(default="null"),
|
|
30
|
+
),
|
|
31
|
+
migrations.AddField(
|
|
32
|
+
model_name='stack',
|
|
33
|
+
name='run',
|
|
34
|
+
field=models.CharField(blank=True, max_length=50),
|
|
35
|
+
),
|
|
36
|
+
migrations.AddField(
|
|
37
|
+
model_name='stacklog',
|
|
38
|
+
name='hook_events',
|
|
39
|
+
field=jsonfield.fields.JSONField(default="null"),
|
|
40
|
+
),
|
|
41
|
+
migrations.AddField(
|
|
42
|
+
model_name='stacklog',
|
|
43
|
+
name='hook_script',
|
|
44
|
+
field=models.CharField(max_length=256, null=True),
|
|
45
|
+
),
|
|
46
|
+
migrations.AddField(
|
|
47
|
+
model_name='stacklog',
|
|
48
|
+
name='providers',
|
|
49
|
+
field=jsonfield.fields.JSONField(default="null"),
|
|
50
|
+
),
|
|
51
|
+
migrations.AddField(
|
|
52
|
+
model_name='stacklog',
|
|
53
|
+
name='run',
|
|
54
|
+
field=models.CharField(blank=True, max_length=50),
|
|
55
|
+
),
|
|
56
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by Django 1.11.21 on 2020-01-07 13:32
|
|
3
|
+
from __future__ import unicode_literals
|
|
4
|
+
|
|
5
|
+
from django.db import migrations
|
|
6
|
+
import jsonfield.fields
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('stackamole', '0005_auto_20190811_1555'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AlterField(
|
|
17
|
+
model_name='stack',
|
|
18
|
+
name='hook_events',
|
|
19
|
+
field=jsonfield.fields.JSONField(default=dict),
|
|
20
|
+
),
|
|
21
|
+
migrations.RunSQL(
|
|
22
|
+
"UPDATE stackamole_stack SET hook_events = '{}' WHERE hook_events = '\"null\"';", # noqa: E501
|
|
23
|
+
"UPDATE stackamole_stack SET hook_events = '\"null\"' WHERE hook_events = '{}';", # noqa: E501
|
|
24
|
+
),
|
|
25
|
+
migrations.AlterField(
|
|
26
|
+
model_name='stack',
|
|
27
|
+
name='providers',
|
|
28
|
+
field=jsonfield.fields.JSONField(default=list),
|
|
29
|
+
),
|
|
30
|
+
migrations.RunSQL(
|
|
31
|
+
"UPDATE stackamole_stack SET providers = '[]' WHERE providers = '\"null\"';", # noqa: E501
|
|
32
|
+
"UPDATE stackamole_stack SET providers = '\"null\"' WHERE providers = '[]';", # noqa: E501
|
|
33
|
+
),
|
|
34
|
+
migrations.AlterField(
|
|
35
|
+
model_name='stacklog',
|
|
36
|
+
name='hook_events',
|
|
37
|
+
field=jsonfield.fields.JSONField(default=dict),
|
|
38
|
+
),
|
|
39
|
+
migrations.AlterField(
|
|
40
|
+
model_name='stacklog',
|
|
41
|
+
name='providers',
|
|
42
|
+
field=jsonfield.fields.JSONField(default=list),
|
|
43
|
+
),
|
|
44
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated by Django 2.2.16 on 2020-11-04 13:35
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import stackamole.models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('stackamole', '0006_auto_20200107_1332'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='stack',
|
|
16
|
+
name='delete_age',
|
|
17
|
+
field=models.IntegerField(null=True),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='stack',
|
|
21
|
+
name='delete_by',
|
|
22
|
+
field=models.DateTimeField(db_index=True, default=stackamole.models.default_delete_by_timestamp, null=True), # noqa E501
|
|
23
|
+
),
|
|
24
|
+
migrations.AddField(
|
|
25
|
+
model_name='stacklog',
|
|
26
|
+
name='delete_age',
|
|
27
|
+
field=models.IntegerField(null=True),
|
|
28
|
+
),
|
|
29
|
+
migrations.AddField(
|
|
30
|
+
model_name='stacklog',
|
|
31
|
+
name='delete_by',
|
|
32
|
+
field=models.DateTimeField(db_index=True, default=stackamole.models.default_delete_by_timestamp, null=True), # noqa E501
|
|
33
|
+
),
|
|
34
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 2.2.19 on 2021-03-25 17:08
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('stackamole', '0007_add_delete_by_and_delete_age'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='stack',
|
|
15
|
+
name='key',
|
|
16
|
+
field=models.TextField(blank=True, default=''),
|
|
17
|
+
),
|
|
18
|
+
migrations.AlterField(
|
|
19
|
+
model_name='stack',
|
|
20
|
+
name='password',
|
|
21
|
+
field=models.CharField(blank=True, default='', max_length=128),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 2.2.16 on 2021-03-30 11:28
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('stackamole', '0008_add_database_defaults_for_stack_key_and_password'), # noqa E501
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='stack',
|
|
15
|
+
name='key',
|
|
16
|
+
field=models.TextField(blank=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
migrations.AlterField(
|
|
19
|
+
model_name='stack',
|
|
20
|
+
name='password',
|
|
21
|
+
field=models.CharField(blank=True, max_length=128, null=True),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Migration(migrations.Migration):
|
|
11
|
+
|
|
12
|
+
def backfill_learner(apps, schema_editor):
|
|
13
|
+
"""
|
|
14
|
+
Use the 'student_id' to link stacks to the User model.
|
|
15
|
+
"""
|
|
16
|
+
Stack = apps.get_model("stackamole", "Stack")
|
|
17
|
+
AnonymousUserId = apps.get_model("student", "AnonymousUserId")
|
|
18
|
+
for stack in Stack.objects.all():
|
|
19
|
+
try:
|
|
20
|
+
stack.learner = AnonymousUserId.objects.get(
|
|
21
|
+
anonymous_user_id=stack.student_id).user
|
|
22
|
+
stack.save(update_fields=['learner'])
|
|
23
|
+
except ObjectDoesNotExist:
|
|
24
|
+
logger.warning('Unable to link stack to user: '
|
|
25
|
+
f'{stack.name}')
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
29
|
+
('stackamole', '0009_add_null_true_for_key_and_password'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
operations = [
|
|
33
|
+
migrations.AddField(
|
|
34
|
+
model_name='stack',
|
|
35
|
+
name='learner',
|
|
36
|
+
field=models.ForeignKey(
|
|
37
|
+
db_constraint=False,
|
|
38
|
+
null=True,
|
|
39
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
40
|
+
to=settings.AUTH_USER_MODEL),
|
|
41
|
+
),
|
|
42
|
+
migrations.RunPython(backfill_learner),
|
|
43
|
+
migrations.AlterField(
|
|
44
|
+
model_name='stack',
|
|
45
|
+
name='learner',
|
|
46
|
+
field=models.ForeignKey(
|
|
47
|
+
db_constraint=True,
|
|
48
|
+
null=True,
|
|
49
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
50
|
+
to=settings.AUTH_USER_MODEL),
|
|
51
|
+
),
|
|
52
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.db import migrations, models
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('stackamole', '0010_add_user_foreign_key'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
# Because 0010_add_user_foreign_key was buggy in several releases,
|
|
13
|
+
# and those set the field to be NOT NULL, what we have to do here
|
|
14
|
+
# is undo that. For an installation that undergoes both
|
|
15
|
+
# 0010_add_user_foreign_key and this migration in a fixed release,
|
|
16
|
+
# this AlterField will be a no-op. For one that ran
|
|
17
|
+
# 0010_add_user_foreign_key from a broken release and thus had its
|
|
18
|
+
# field set to NOT NULL, this will allow NULL values.
|
|
19
|
+
operations = [
|
|
20
|
+
migrations.AlterField(
|
|
21
|
+
model_name='stack',
|
|
22
|
+
name='learner',
|
|
23
|
+
field=models.ForeignKey(
|
|
24
|
+
db_constraint=True,
|
|
25
|
+
null=True,
|
|
26
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
27
|
+
to=settings.AUTH_USER_MODEL),
|
|
28
|
+
),
|
|
29
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from django.db import migrations, models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
|
|
6
|
+
dependencies = [
|
|
7
|
+
('stackamole', '0011_allow_null_for_learner'),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
operations = [
|
|
11
|
+
migrations.AddField(
|
|
12
|
+
model_name='stack',
|
|
13
|
+
name='suspend_by',
|
|
14
|
+
field=models.DateTimeField(db_index=True, null=True),
|
|
15
|
+
),
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name='stacklog',
|
|
18
|
+
name='suspend_by',
|
|
19
|
+
field=models.DateTimeField(db_index=True, null=True),
|
|
20
|
+
),
|
|
21
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
# Rename the 'hastexo' tables to 'stackamole' to access labs created
|
|
4
|
+
# prior to the project rename.
|
|
5
|
+
|
|
6
|
+
OLD_APP_LABEL = 'hastexo'
|
|
7
|
+
NEW_APP_LABEL = 'stackamole'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def migrate_tables(apps, schema_editor):
|
|
11
|
+
connection = schema_editor.connection
|
|
12
|
+
existing_tables = connection.introspection.table_names()
|
|
13
|
+
|
|
14
|
+
if 'hastexo_stack' in existing_tables:
|
|
15
|
+
with connection.cursor() as cursor:
|
|
16
|
+
# stackamole_* tables were created just now and contain no data,
|
|
17
|
+
# confirm empty and drop
|
|
18
|
+
cursor.execute("SELECT COUNT(*) FROM stackamole_stack")
|
|
19
|
+
if cursor.fetchone()[0] > 0:
|
|
20
|
+
raise Exception(
|
|
21
|
+
"'hastexo_stack' exists & 'stackamole_stack' contains "
|
|
22
|
+
"data; this scenario is out of scope for automated "
|
|
23
|
+
"migration.")
|
|
24
|
+
cursor.execute("DROP TABLE stackamole_stacklog")
|
|
25
|
+
cursor.execute("DROP TABLE stackamole_stack")
|
|
26
|
+
# rename hastexo_* tables to stackamole_*
|
|
27
|
+
cursor.execute(
|
|
28
|
+
"ALTER TABLE hastexo_stacklog RENAME TO stackamole_stacklog")
|
|
29
|
+
cursor.execute(
|
|
30
|
+
"ALTER TABLE hastexo_stack RENAME TO stackamole_stack")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def migrate_contenttypes(apps, schema_editor):
|
|
34
|
+
ContentType = apps.get_model('contenttypes', 'ContentType')
|
|
35
|
+
db = schema_editor.connection.alias
|
|
36
|
+
if ContentType.objects.using(db).filter(app_label=OLD_APP_LABEL).exists():
|
|
37
|
+
# remove the stackamole content types that were just auto-created
|
|
38
|
+
ContentType.objects.using(db).filter(app_label=NEW_APP_LABEL).delete()
|
|
39
|
+
# rename 'hastexo' content types to 'stackamole'
|
|
40
|
+
ContentType.objects.using(db).filter(app_label=OLD_APP_LABEL).update(
|
|
41
|
+
app_label=NEW_APP_LABEL)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def migrate_migration_records(apps, schema_editor):
|
|
45
|
+
# delete now orphaned 'hastexo' migration records
|
|
46
|
+
schema_editor.connection.cursor().execute(
|
|
47
|
+
"DELETE FROM django_migrations WHERE app = 'hastexo'"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Migration(migrations.Migration):
|
|
52
|
+
|
|
53
|
+
dependencies = [
|
|
54
|
+
('stackamole', '0012_add_suspend_by'),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# One way migration only from 'hastexo' to 'stackamole'
|
|
58
|
+
operations = [
|
|
59
|
+
migrations.RunPython(
|
|
60
|
+
migrate_tables,
|
|
61
|
+
migrations.RunPython.noop,
|
|
62
|
+
),
|
|
63
|
+
migrations.RunPython(
|
|
64
|
+
migrate_contenttypes,
|
|
65
|
+
migrations.RunPython.noop,
|
|
66
|
+
),
|
|
67
|
+
migrations.RunPython(
|
|
68
|
+
migrate_migration_records,
|
|
69
|
+
migrations.RunPython.noop,
|
|
70
|
+
),
|
|
71
|
+
]
|
|
File without changes
|
stackamole/models.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from django.conf import settings as django_settings
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
from jsonfield.fields import JSONField
|
|
5
|
+
|
|
6
|
+
SETTINGS_KEY = 'stackamole'
|
|
7
|
+
DEFAULT_SETTINGS = {"delete_age": 14}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def default_delete_by_timestamp():
|
|
11
|
+
# load the default delete_age value from settings
|
|
12
|
+
try:
|
|
13
|
+
settings = django_settings.XBLOCK_SETTINGS.get(
|
|
14
|
+
SETTINGS_KEY,
|
|
15
|
+
DEFAULT_SETTINGS
|
|
16
|
+
)
|
|
17
|
+
except AttributeError:
|
|
18
|
+
settings = DEFAULT_SETTINGS
|
|
19
|
+
|
|
20
|
+
# create a delete_by timestamp based on the delete_age from settings
|
|
21
|
+
delete_by = timezone.now() + timezone.timedelta(
|
|
22
|
+
days=settings.get("delete_age", 14))
|
|
23
|
+
|
|
24
|
+
return delete_by
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StackCommon(models.Model):
|
|
28
|
+
"""
|
|
29
|
+
Abstract class that defines a stack record's common fields.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
class Meta:
|
|
33
|
+
abstract = True
|
|
34
|
+
|
|
35
|
+
name = models.CharField(max_length=64, db_index=True)
|
|
36
|
+
student_id = models.CharField(max_length=40, db_index=True)
|
|
37
|
+
course_id = models.CharField(max_length=50, db_index=True)
|
|
38
|
+
run = models.CharField(max_length=50, blank=True)
|
|
39
|
+
provider = models.CharField(max_length=32, blank=True)
|
|
40
|
+
providers = JSONField(default=list)
|
|
41
|
+
hook_script = models.CharField(max_length=256, null=True)
|
|
42
|
+
hook_events = JSONField(default=dict)
|
|
43
|
+
protocol = models.CharField(max_length=32, blank=True)
|
|
44
|
+
port = models.IntegerField(null=True)
|
|
45
|
+
status = models.CharField(max_length=32, blank=True, db_index=True)
|
|
46
|
+
error_msg = models.CharField(max_length=256, blank=True)
|
|
47
|
+
ip = models.GenericIPAddressField(null=True)
|
|
48
|
+
user = models.CharField(max_length=32, blank=True)
|
|
49
|
+
launch_task_id = models.CharField(max_length=40, blank=True)
|
|
50
|
+
launch_timestamp = models.DateTimeField(null=True, db_index=True)
|
|
51
|
+
suspend_timestamp = models.DateTimeField(null=True, db_index=True)
|
|
52
|
+
suspend_by = models.DateTimeField(null=True, db_index=True)
|
|
53
|
+
created_on = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
54
|
+
delete_age = models.IntegerField(null=True)
|
|
55
|
+
delete_by = models.DateTimeField(null=True, db_index=True,
|
|
56
|
+
default=default_delete_by_timestamp)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Stack(StackCommon):
|
|
60
|
+
"""
|
|
61
|
+
Stores Heat stack data that needs to be queried across XBlock instances.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
class Meta:
|
|
65
|
+
app_label = 'stackamole'
|
|
66
|
+
unique_together = (('student_id', 'course_id', 'name'),)
|
|
67
|
+
|
|
68
|
+
key = models.TextField(blank=True, null=True)
|
|
69
|
+
password = models.CharField(max_length=128, blank=True, null=True)
|
|
70
|
+
learner = models.ForeignKey(django_settings.AUTH_USER_MODEL,
|
|
71
|
+
null=True,
|
|
72
|
+
db_index=True,
|
|
73
|
+
on_delete=models.PROTECT)
|
|
74
|
+
|
|
75
|
+
def __init__(self, *args, **kwargs):
|
|
76
|
+
super(Stack, self).__init__(*args, **kwargs)
|
|
77
|
+
|
|
78
|
+
# Save previous status
|
|
79
|
+
self.prev_status = self.status
|
|
80
|
+
|
|
81
|
+
def save(self, *args, **kwargs):
|
|
82
|
+
super(Stack, self).save(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
# Populate the log if there was a status change
|
|
85
|
+
if self.status and self.status != self.prev_status:
|
|
86
|
+
self.prev_status = self.status
|
|
87
|
+
|
|
88
|
+
log_fields = {'stack_id': self.id}
|
|
89
|
+
for field in StackCommon._meta.get_fields():
|
|
90
|
+
if field.name == 'created_on':
|
|
91
|
+
continue
|
|
92
|
+
log_fields[field.name] = getattr(self, field.name)
|
|
93
|
+
|
|
94
|
+
log = StackLog(**log_fields)
|
|
95
|
+
log.save()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class StackLog(StackCommon):
|
|
99
|
+
"""
|
|
100
|
+
A log of stack model changes.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
class Meta:
|
|
104
|
+
app_label = 'stackamole'
|
|
105
|
+
|
|
106
|
+
stack_id = models.IntegerField(null=True, db_index=True)
|
stackamole/openstack.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from keystoneauth1.identity import generic
|
|
2
|
+
from keystoneauth1 import session as kssession
|
|
3
|
+
from heatclient import client as heat_client
|
|
4
|
+
from novaclient import client as nova_client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OpenStackWrapper(object):
|
|
8
|
+
"""
|
|
9
|
+
A meta-class for wrapping OpenStack services.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
os_options = (
|
|
13
|
+
'os_auth_url',
|
|
14
|
+
'os_auth_token',
|
|
15
|
+
'os_username',
|
|
16
|
+
'os_password',
|
|
17
|
+
'os_user_id',
|
|
18
|
+
'os_user_domain_id',
|
|
19
|
+
'os_user_domain_name',
|
|
20
|
+
'os_tenant_id',
|
|
21
|
+
'os_tenant_name',
|
|
22
|
+
'os_project_id',
|
|
23
|
+
'os_project_name',
|
|
24
|
+
'os_project_domain_id',
|
|
25
|
+
'os_project_domain_name',
|
|
26
|
+
'os_region_name'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
service_type = None
|
|
30
|
+
endpoint_type = None
|
|
31
|
+
api_version = None
|
|
32
|
+
options = {}
|
|
33
|
+
|
|
34
|
+
def __init__(self, **options):
|
|
35
|
+
# Set OpenStack options
|
|
36
|
+
for os_option in self.os_options:
|
|
37
|
+
self.options[os_option] = options.get(os_option)
|
|
38
|
+
|
|
39
|
+
def get_keystone_auth(self):
|
|
40
|
+
keystone_session = kssession.Session(verify=True)
|
|
41
|
+
if self.options['os_auth_token']:
|
|
42
|
+
kwargs = {
|
|
43
|
+
'token': self.options['os_auth_token'],
|
|
44
|
+
'auth_url': self.options['os_auth_url']
|
|
45
|
+
}
|
|
46
|
+
keystone_auth = generic.Token(**kwargs)
|
|
47
|
+
else:
|
|
48
|
+
project_id = self.options['os_project_id'] or self.options['os_tenant_id'] # noqa: E501
|
|
49
|
+
project_name = self.options['os_project_name'] or self.options['os_tenant_name'] # noqa: E501
|
|
50
|
+
kwargs = {
|
|
51
|
+
'username': self.options['os_username'],
|
|
52
|
+
'user_id': self.options['os_user_id'],
|
|
53
|
+
'user_domain_id': self.options['os_user_domain_id'],
|
|
54
|
+
'user_domain_name': self.options['os_user_domain_name'],
|
|
55
|
+
'password': self.options['os_password'],
|
|
56
|
+
'auth_url': self.options['os_auth_url'],
|
|
57
|
+
'project_id': project_id,
|
|
58
|
+
'project_name': project_name,
|
|
59
|
+
'project_domain_id': self.options['os_project_domain_id'],
|
|
60
|
+
'project_domain_name': self.options['os_project_domain_name'],
|
|
61
|
+
}
|
|
62
|
+
keystone_auth = generic.Password(**kwargs)
|
|
63
|
+
|
|
64
|
+
return (keystone_session, keystone_auth)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HeatWrapper(OpenStackWrapper):
|
|
68
|
+
"""
|
|
69
|
+
A class that wraps the Heat service for the Stackamole XBlock.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self, **options):
|
|
73
|
+
self.service_type = 'orchestration'
|
|
74
|
+
self.endpoint_type = 'publicURL'
|
|
75
|
+
self.api_version = '1'
|
|
76
|
+
|
|
77
|
+
super(HeatWrapper, self).__init__(**options)
|
|
78
|
+
|
|
79
|
+
def get_client(self):
|
|
80
|
+
"""
|
|
81
|
+
This function is based, with minor changes, on the official OpenStack
|
|
82
|
+
Heat client's shell implementation. Specifically,
|
|
83
|
+
python-heatclient/heatclient/shell.py.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
keystone_session, keystone_auth = self.get_keystone_auth()
|
|
87
|
+
|
|
88
|
+
kwargs = {
|
|
89
|
+
'auth_url': self.options['os_auth_url'],
|
|
90
|
+
'session': keystone_session,
|
|
91
|
+
'auth': keystone_auth,
|
|
92
|
+
'service_type': self.service_type,
|
|
93
|
+
'endpoint_type': self.endpoint_type,
|
|
94
|
+
'region_name': self.options['os_region_name'],
|
|
95
|
+
'username': self.options['os_username'],
|
|
96
|
+
'password': self.options['os_password']
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return heat_client.Client(self.api_version, **kwargs)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class NovaWrapper(OpenStackWrapper):
|
|
103
|
+
"""
|
|
104
|
+
A class that wraps the Compute service for the Stackamole XBlock.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
def __init__(self, **options):
|
|
108
|
+
self.service_type = 'compute'
|
|
109
|
+
self.endpoint_type = 'publicURL'
|
|
110
|
+
self.api_version = '2.2'
|
|
111
|
+
|
|
112
|
+
super(NovaWrapper, self).__init__(**options)
|
|
113
|
+
|
|
114
|
+
def get_client(self):
|
|
115
|
+
keystone_session, keystone_auth = self.get_keystone_auth()
|
|
116
|
+
return nova_client.Client(
|
|
117
|
+
self.api_version,
|
|
118
|
+
self.options['os_username'],
|
|
119
|
+
self.options['os_password'],
|
|
120
|
+
project_id=self.options['os_project_id'],
|
|
121
|
+
project_name=self.options['os_project_name'],
|
|
122
|
+
user_id=self.options['os_user_id'],
|
|
123
|
+
auth_url=self.options['os_auth_url'],
|
|
124
|
+
region_name=self.options['os_region_name'],
|
|
125
|
+
endpoint_type=self.endpoint_type,
|
|
126
|
+
service_type=self.service_type,
|
|
127
|
+
session=keystone_session,
|
|
128
|
+
auth=keystone_auth,
|
|
129
|
+
project_domain_id=self.options['os_project_domain_id'],
|
|
130
|
+
project_domain_name=self.options['os_project_domain_name'],
|
|
131
|
+
user_domain_id=self.options['os_user_domain_id'],
|
|
132
|
+
user_domain_name=self.options['os_user_domain_name'])
|