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/provider.py ADDED
@@ -0,0 +1,437 @@
1
+ import time
2
+ import base64
3
+ import logging
4
+ import paramiko
5
+ import random
6
+ import string
7
+ from cryptography.hazmat.primitives import serialization, asymmetric
8
+
9
+ try:
10
+ from io import StringIO
11
+ except ImportError:
12
+ from StringIO import StringIO
13
+
14
+ from heatclient.exc import HTTPException, HTTPNotFound
15
+ from keystoneauth1.exceptions.http import HttpError
16
+ from novaclient.exceptions import ClientException, NotFound
17
+
18
+ from .common import (
19
+ b,
20
+ get_xblock_settings,
21
+ IN_PROGRESS,
22
+ FAILED,
23
+ DELETE_COMPLETE,
24
+ DELETE_IN_PROGRESS,
25
+ RESUME_COMPLETE,
26
+ RESUME_IN_PROGRESS,
27
+ SUSPEND_COMPLETE,
28
+ SUSPEND_IN_PROGRESS
29
+ )
30
+ from .openstack import HeatWrapper, NovaWrapper
31
+
32
+
33
+ class ProviderException(Exception):
34
+ pass
35
+
36
+
37
+ class Provider(object):
38
+ """
39
+ Base class for provider drivers.
40
+
41
+ """
42
+ default_credentials = None
43
+ credentials = None
44
+ name = None
45
+ capacity = None
46
+ template = None
47
+ environment = None
48
+ sleep_seconds = None
49
+
50
+ @staticmethod
51
+ def init(name):
52
+ settings = get_xblock_settings()
53
+ sleep_seconds = settings.get("sleep_timeout", 10)
54
+ providers = settings.get("providers")
55
+ config = providers.get(name)
56
+ if config and isinstance(config, dict):
57
+ provider_type = config.get("type")
58
+ if provider_type == "openstack" or not provider_type:
59
+ return OpenstackProvider(name, config, sleep_seconds)
60
+ else:
61
+ raise ProviderException(
62
+ f"Unsupported provider type: {provider_type} "
63
+ f"for provider: {name}.")
64
+ else:
65
+ raise ProviderException(
66
+ f"Configuration missing for provider: {name}")
67
+
68
+ def __init__(self, name, config, sleep):
69
+ self.name = name
70
+ self.sleep_seconds = sleep
71
+ self.reset_logger()
72
+
73
+ # Get credentials
74
+ if config and isinstance(config, dict):
75
+ credentials = {}
76
+ for key, default in self.default_credentials.items():
77
+ credentials[key] = config.get(key, default)
78
+ self.credentials = credentials
79
+ else:
80
+ error_msg = ("No configuration provided for provider %s" %
81
+ self.name)
82
+ raise ProviderException(error_msg)
83
+
84
+ def set_logger(self, logger):
85
+ """Set a logger other than the standard one.
86
+
87
+ This is meant to be used from Celery tasks, which usually
88
+ would want to use their task logger for logging.
89
+ """
90
+ self.logger = logger
91
+
92
+ def reset_logger(self):
93
+ """Reset the logger back to the standard one."""
94
+ self.logger = logging.getLogger(__name__)
95
+
96
+ def set_capacity(self, capacity):
97
+ if capacity in (None, "None"):
98
+ capacity = -1
99
+ else:
100
+ try:
101
+ capacity = int(capacity)
102
+ except (TypeError, ValueError):
103
+ # Invalid capacity: disable the provider
104
+ capacity = 0
105
+
106
+ self.capacity = capacity
107
+
108
+ def set_template(self, template):
109
+ if not template:
110
+ error_msg = ("No template provided for provider %s" % self.name)
111
+ raise ProviderException(error_msg)
112
+
113
+ self.template = template
114
+
115
+ def set_environment(self, environment):
116
+ if not environment:
117
+ error_msg = ("No environment provided for provider %s" % self.name)
118
+ raise ProviderException(error_msg)
119
+
120
+ self.environment = environment
121
+
122
+ def sleep(self):
123
+ time.sleep(self.sleep_seconds)
124
+
125
+ def generate_key_pair(self, encodeb64=False, key_type="rsa"):
126
+ keypair = {}
127
+
128
+ if key_type == "ed25519":
129
+ # use cryptography to generate Ed25519Key until paramiko adds
130
+ # support for the key generation as well.
131
+ ed25519key = asymmetric.ed25519.Ed25519PrivateKey.generate()
132
+
133
+ public_key = ed25519key.public_key().public_bytes(
134
+ encoding=serialization.Encoding.OpenSSH,
135
+ format=serialization.PublicFormat.OpenSSH).decode()
136
+ keypair["public_key"] = public_key
137
+
138
+ private_key = ed25519key.private_bytes(
139
+ encoding=serialization.Encoding.PEM,
140
+ format=serialization.PrivateFormat.OpenSSH,
141
+ encryption_algorithm=serialization.NoEncryption()).decode()
142
+
143
+ else:
144
+ pkey = paramiko.RSAKey.generate(4096)
145
+ keypair["public_key"] = f'{pkey.get_name()} {pkey.get_base64()}'
146
+ s = StringIO()
147
+ pkey.write_private_key(s)
148
+ private_key = s.getvalue()
149
+ s.close()
150
+
151
+ if encodeb64:
152
+ private_key = base64.b64encode(b(private_key))
153
+
154
+ keypair["private_key"] = private_key
155
+
156
+ return keypair
157
+
158
+ def generate_random_password(self, length):
159
+ abc = string.ascii_lowercase
160
+ return "".join(random.choice(abc) for i in range(length))
161
+
162
+ def get_stacks(self):
163
+ raise NotImplementedError()
164
+
165
+ def get_stack(self):
166
+ raise NotImplementedError()
167
+
168
+ def create_stack(self):
169
+ raise NotImplementedError()
170
+
171
+ def delete_stack(self):
172
+ raise NotImplementedError()
173
+
174
+ def suspend_stack(self):
175
+ raise NotImplementedError()
176
+
177
+ def resume_stack(self):
178
+ raise NotImplementedError()
179
+
180
+
181
+ class OpenstackProvider(Provider):
182
+ """
183
+ OpenStack provider driver.
184
+
185
+ """
186
+ default_credentials = {
187
+ "os_auth_url": "",
188
+ "os_auth_token": "",
189
+ "os_username": "",
190
+ "os_password": "",
191
+ "os_user_id": "",
192
+ "os_user_domain_id": "",
193
+ "os_user_domain_name": "",
194
+ "os_project_id": "",
195
+ "os_project_name": "",
196
+ "os_project_domain_id": "",
197
+ "os_project_domain_name": "",
198
+ "os_region_name": ""
199
+ }
200
+ heat_c = None
201
+ nova_c = None
202
+
203
+ def __init__(self, provider, config, sleep):
204
+ super(OpenstackProvider, self).__init__(provider, config, sleep)
205
+
206
+ self.heat_c = self._get_heat_client()
207
+ self.nova_c = self._get_nova_client()
208
+
209
+ def _get_heat_client(self):
210
+ return HeatWrapper(**self.credentials).get_client()
211
+
212
+ def _get_nova_client(self):
213
+ return NovaWrapper(**self.credentials).get_client()
214
+
215
+ def _get_stack_outputs(self, heat_stack):
216
+ outputs = {}
217
+ for o in getattr(heat_stack, 'outputs', []):
218
+ output_key = o["output_key"]
219
+ output_value = o["output_value"]
220
+ outputs[output_key] = output_value
221
+
222
+ return outputs
223
+
224
+ def get_stacks(self):
225
+ stacks = []
226
+ try:
227
+ heat_stacks = self.heat_c.stacks.list()
228
+ except HTTPNotFound:
229
+ return stacks
230
+ except (HTTPException, HttpError) as e:
231
+ raise ProviderException(e)
232
+
233
+ if heat_stacks:
234
+ for heat_stack in heat_stacks:
235
+ stack = {
236
+ "name": heat_stack.stack_name,
237
+ "status": heat_stack.stack_status
238
+ }
239
+ stacks.append(stack)
240
+
241
+ return stacks
242
+
243
+ def get_stack(self, name):
244
+ try:
245
+ self.logger.debug('Fetching information on '
246
+ 'OpenStack Heat stack [%s]' % name)
247
+ heat_stack = self.heat_c.stacks.get(stack_id=name)
248
+ except HTTPNotFound:
249
+ status = DELETE_COMPLETE
250
+ outputs = {}
251
+ except (HTTPException, HttpError) as e:
252
+ raise ProviderException(e)
253
+ else:
254
+ status = heat_stack.stack_status
255
+ outputs = self._get_stack_outputs(heat_stack)
256
+
257
+ return {"status": status,
258
+ "outputs": outputs}
259
+
260
+ def create_stack(self, name, run, key_type=""):
261
+ if not self.template:
262
+ raise ProviderException("Template not set for provider %s." %
263
+ self.name)
264
+ keypair = {}
265
+ if key_type:
266
+ keypair = self.generate_key_pair(key_type=key_type)
267
+ try:
268
+ self.nova_c.keypairs.create(
269
+ name=name,
270
+ public_key=keypair["public_key"],
271
+ key_type='ssh'
272
+ )
273
+ self.logger.info("Created a key with type [%s]" % key_type)
274
+ except ClientException as e:
275
+ raise ProviderException(e)
276
+
277
+ try:
278
+ self.logger.info('Creating OpenStack Heat stack [%s]' % name)
279
+ res = self.heat_c.stacks.create(
280
+ stack_name=name,
281
+ template=self.template,
282
+ environment=self.environment,
283
+ parameters={'run': run}
284
+ )
285
+ except (HTTPException, HttpError) as e:
286
+ raise ProviderException(e)
287
+
288
+ stack_id = res['stack']['id']
289
+
290
+ # Sleep to avoid throttling.
291
+ self.sleep()
292
+
293
+ try:
294
+ heat_stack = self.heat_c.stacks.get(stack_id=stack_id)
295
+ except (HTTPException, HttpError) as e:
296
+ raise ProviderException(e)
297
+
298
+ status = heat_stack.stack_status
299
+
300
+ # Wait for stack creation
301
+ while IN_PROGRESS in status:
302
+ self.sleep()
303
+
304
+ try:
305
+ heat_stack = self.heat_c.stacks.get(stack_id=heat_stack.id)
306
+ except HTTPNotFound:
307
+ raise ProviderException("OpenStack Heat stack "
308
+ "disappeared during creation.")
309
+ except (HTTPException, HttpError) as e:
310
+ raise ProviderException(e)
311
+
312
+ status = heat_stack.stack_status
313
+
314
+ if FAILED in status:
315
+ raise ProviderException("Failure creating OpenStack Heat stack.")
316
+
317
+ res = {"status": status,
318
+ "outputs": self._get_stack_outputs(heat_stack)}
319
+ if keypair:
320
+ res["private_key"] = keypair["private_key"]
321
+
322
+ return res
323
+
324
+ def resume_stack(self, name):
325
+ try:
326
+ self.logger.info('Resuming OpenStack Heat stack [%s]' % name)
327
+ self.heat_c.actions.resume(stack_id=name)
328
+ except (HTTPException, HttpError) as e:
329
+ raise ProviderException(e)
330
+
331
+ status = RESUME_IN_PROGRESS
332
+
333
+ # Wait until resume finishes.
334
+ while (FAILED not in status and
335
+ status != RESUME_COMPLETE):
336
+ self.sleep()
337
+
338
+ try:
339
+ heat_stack = self.heat_c.stacks.get(
340
+ stack_id=name)
341
+ except HTTPNotFound:
342
+ raise ProviderException("OpenStack Heat stack "
343
+ "disappeared during resume.")
344
+ except (HTTPException, HttpError) as e:
345
+ raise ProviderException(e)
346
+ else:
347
+ status = heat_stack.stack_status
348
+
349
+ if FAILED in status:
350
+ raise ProviderException("Failure resuming OpenStack Heat stack")
351
+
352
+ outputs = self._get_stack_outputs(heat_stack)
353
+
354
+ # Reboot servers, if requested
355
+ reboot_on_resume = outputs.get("reboot_on_resume")
356
+ if (reboot_on_resume is not None and
357
+ isinstance(reboot_on_resume, list)):
358
+ for server in reboot_on_resume:
359
+ try:
360
+ self.logger.info("Rebooting OpenStack Nova "
361
+ "instance %s" % server)
362
+ self.nova_c.servers.reboot(server, 'HARD')
363
+ except ClientException as e:
364
+ raise ProviderException(e)
365
+
366
+ return {"status": status,
367
+ "outputs": outputs}
368
+
369
+ def suspend_stack(self, name, wait=True):
370
+ try:
371
+ self.logger.info("Suspending OpenStack Heat stack [%s]" % name)
372
+ self.heat_c.actions.suspend(stack_id=name)
373
+ except (HTTPException, HttpError) as e:
374
+ raise ProviderException(e)
375
+
376
+ status = SUSPEND_IN_PROGRESS
377
+
378
+ # Wait until suspend finishes.
379
+ if wait:
380
+ while (FAILED not in status and
381
+ status != DELETE_COMPLETE and
382
+ status != SUSPEND_COMPLETE):
383
+ self.sleep()
384
+
385
+ try:
386
+ heat_stack = self.heat_c.stacks.get(
387
+ stack_id=name)
388
+ except HTTPNotFound:
389
+ status = DELETE_COMPLETE
390
+ except (HTTPException, HttpError) as e:
391
+ raise ProviderException(e)
392
+ else:
393
+ status = heat_stack.stack_status
394
+
395
+ if FAILED in status:
396
+ raise ProviderException("Failure suspending "
397
+ "OpenStack Heat stack.")
398
+
399
+ return {"status": status}
400
+
401
+ def delete_stack(self, name, wait=True):
402
+ try:
403
+ self.logger.info("Deleting Nova Keypair [%s]" % name)
404
+ self.nova_c.keypairs.delete(name)
405
+ except NotFound:
406
+ self.logger.info(
407
+ "Keypair not found for deletion for stack [%s]" % name)
408
+
409
+ try:
410
+ self.logger.info("Deleting OpenStack Heat stack [%s]" % name)
411
+ self.heat_c.stacks.delete(stack_id=name)
412
+ except (HTTPException, HttpError) as e:
413
+ raise ProviderException(e)
414
+
415
+ status = DELETE_IN_PROGRESS
416
+
417
+ # Wait until delete finishes.
418
+ if wait:
419
+ while (FAILED not in status and
420
+ status != DELETE_COMPLETE):
421
+ self.sleep()
422
+
423
+ try:
424
+ heat_stack = self.heat_c.stacks.get(
425
+ stack_id=name)
426
+ except HTTPNotFound:
427
+ status = DELETE_COMPLETE
428
+ except (HTTPException, HttpError) as e:
429
+ raise ProviderException(e)
430
+ else:
431
+ status = heat_stack.stack_status
432
+
433
+ if FAILED in status:
434
+ raise ProviderException("Failure deleting "
435
+ "OpenStack Heat stack.")
436
+
437
+ return {"status": status}
@@ -0,0 +1,203 @@
1
+ .buttons.bar {
2
+ display: flex;
3
+ }
4
+
5
+ .buttons .check {
6
+ color: blue;
7
+ display: none;
8
+ }
9
+
10
+ .buttons.bar .reset {
11
+ color: red;
12
+ margin-right: auto;
13
+ }
14
+
15
+ .buttons .reset {
16
+ color: red;
17
+ }
18
+
19
+ .buttons .port {
20
+ display: none;
21
+ border: 1px solid #d2c9c9;
22
+ border-radius: 3px;
23
+ box-shadow: inset 0 1px 0 0 #fff;
24
+ color: #333;
25
+ font-size: inherit;
26
+ font-weight: bold;
27
+ background-color: #f1f1f1;
28
+ background-image: -webkit-linear-gradient(#f1f1f1,#d9d1d1);
29
+ background-image: linear-gradient(#f1f1f1,#d9d1d1);
30
+ padding: 7px 18px;
31
+ text-decoration: none;
32
+ text-shadow: 0 1px 0 #fcfbfb;
33
+ background-clip: padding-box;
34
+ font-size: 0.8125em;
35
+ text-transform: none;
36
+ letter-spacing: 0;
37
+ cursor: pointer;
38
+ -webkit-appearance: button;
39
+ vertical-align: top;
40
+ -webkit-font-smoothing: antialiased;
41
+ margin: 0;
42
+ }
43
+
44
+ #container {
45
+ position: relative;
46
+ width: 100%;
47
+ height: 20em;
48
+ }
49
+
50
+ #container.graphical {
51
+ height: 40em;
52
+ }
53
+
54
+ #container.graphical canvas:first-child {
55
+ z-index: 0 !important;
56
+ }
57
+
58
+ #terminal {
59
+ height: 100%;
60
+ width: 100%
61
+ }
62
+
63
+ .dialog-overlay {
64
+ position: absolute;
65
+ top: -0.5em;
66
+ right: -1em;
67
+ bottom: -1px;
68
+ left: -0.5em;
69
+ padding: 20px;
70
+ box-sizing: border-box;
71
+ background: rgba(0,0,0,0.1);
72
+ box-shadow: 0 5px 20px rgba(0,0,0,0.2);
73
+ border-radius: 10px;
74
+ text-align: center;
75
+ z-index: 1001;
76
+ margin: 0 20px 0 10px;
77
+ }
78
+
79
+ .dialog-overlay:before {
80
+ content: "";
81
+ display: inline-block;
82
+ height: 100%;
83
+ vertical-align: middle;
84
+ margin-right: -0.05em;
85
+ }
86
+
87
+ .dialog,
88
+ .error {
89
+ max-width: 480px;
90
+ background: #fff;
91
+ padding: 20px;
92
+ border-radius: 8px;
93
+ box-shadow: 0 0 10px #000;
94
+ text-align: left;
95
+ }
96
+
97
+ .dialog {
98
+ position: absolute;
99
+ top: 50%;
100
+ left: 50%;
101
+ transform: translate(-50%, -50%);
102
+ z-index: 1002;
103
+ }
104
+
105
+ .error {
106
+ margin: 0 auto;
107
+ }
108
+
109
+ .inner {
110
+ background: rgb(245, 245, 245);
111
+ border-radius: 0;
112
+ border: 1px solid rgba(0, 0, 0, 0.9);
113
+ box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.7);
114
+ overflow: hidden;
115
+ padding: 10px;
116
+ position: relative;
117
+ }
118
+
119
+ .inner h1,
120
+ .inner h2,
121
+ .inner p {
122
+ text-align: center !important;
123
+ margin: 0 0 0.5em 0!important;
124
+ }
125
+
126
+ .inner .exam-warning {
127
+ display: none;
128
+ }
129
+
130
+ .inner #reset_code_input {
131
+ margin: 0.5em;
132
+ }
133
+
134
+ .hints_title {
135
+ color: red;
136
+ padding-top: 0.5em;
137
+ }
138
+
139
+ .hints {
140
+ font-size: 0.8em;
141
+ }
142
+
143
+ .dialog {
144
+ display: none;
145
+ }
146
+
147
+ .dialog .loader:before,
148
+ .dialog .loader:after,
149
+ .dialog .loader {
150
+ display: inline-block;
151
+ border-radius: 50%;
152
+ width: 2.5em;
153
+ height: 2.5em;
154
+ animation-fill-mode: both;
155
+ animation: ellipsis 1.8s infinite ease-in-out;
156
+ }
157
+
158
+ .dialog .loader {
159
+ font-size: 2px;
160
+ margin: 10px 0 4px 16px;
161
+ position: relative;
162
+ animation-delay: -0.16s;
163
+ }
164
+
165
+ .dialog .loader:before {
166
+ left: -6em;
167
+ animation-delay: -0.32s;
168
+ }
169
+
170
+ .dialog .loader:after {
171
+ left: 6em;
172
+ }
173
+
174
+ .dialog .loader:before,
175
+ .dialog .loader:after {
176
+ content: '';
177
+ position: absolute;
178
+ top: 0;
179
+ }
180
+
181
+ @keyframes ellipsis {
182
+ 0%,
183
+ 80%,
184
+ 100% {
185
+ box-shadow: 0 2.5em 0 -1.3em #000;
186
+ }
187
+ 40% {
188
+ box-shadow: 0 2.5em 0 0 #000;
189
+ }
190
+ }
191
+
192
+ .content-side-by-side {
193
+ max-width: 1800px !important;
194
+ }
195
+
196
+ .instructions-side-view {
197
+ height: 40em !important;
198
+ padding-left: 1%;
199
+ }
200
+
201
+ .terminal-side-view {
202
+ height: 40em !important;
203
+ }