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
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
|
+
}
|