Revision 68b952f9
b/snf-cyclades-app/synnefo/db/aes_encrypt.py | ||
---|---|---|
29 | 29 |
|
30 | 30 |
|
31 | 31 |
def encrypt_db_charfield(plaintext): |
32 |
if plaintext == None:
|
|
32 |
if not plaintext:
|
|
33 | 33 |
return plaintext |
34 | 34 |
salt = "".join([choice(letters + digits) for i in xrange(SALT_LEN)]) |
35 | 35 |
|
... | ... | |
42 | 42 |
|
43 | 43 |
|
44 | 44 |
def decrypt_db_charfield(ciphertext): |
45 |
if ciphertext == None:
|
|
45 |
if not ciphertext:
|
|
46 | 46 |
return ciphertext |
47 | 47 |
has_prefix = ciphertext.startswith(DB_ENCRYPTED_FIELD_PREFIX + ':') |
48 | 48 |
if not has_prefix: # Non-encoded value |
b/snf-cyclades-app/synnefo/db/managers.py | ||
---|---|---|
71 | 71 |
if num == 1: |
72 | 72 |
return query[0] |
73 | 73 |
if not num: |
74 |
raise self.model.DoesNotExist( |
|
75 |
"%s matching query does not exist. "
|
|
76 |
"Lookup parameters were %s" %
|
|
77 |
(self.model._meta.object_name, kwargs))
|
|
74 |
raise self.model.DoesNotExist("%s matching query does not exist. "
|
|
75 |
"Lookup parameters were %s" %
|
|
76 |
(self.model._meta.object_name,
|
|
77 |
kwargs))
|
|
78 | 78 |
raise self.model.MultipleObjectsReturned( |
79 | 79 |
"get() returned more than one %s -- it returned %s! " |
80 | 80 |
"Lookup parameters were %s" % |
b/snf-cyclades-app/synnefo/db/models.py | ||
---|---|---|
55 | 55 |
ram = models.IntegerField('RAM size in MiB', default=0) |
56 | 56 |
disk = models.IntegerField('Disk size in GiB', default=0) |
57 | 57 |
disk_template = models.CharField('Disk template', max_length=32, |
58 |
default=settings.DEFAULT_GANETI_DISK_TEMPLATE) |
|
58 |
default=settings.DEFAULT_GANETI_DISK_TEMPLATE)
|
|
59 | 59 |
deleted = models.BooleanField('Deleted', default=False) |
60 | 60 |
|
61 | 61 |
class Meta: |
... | ... | |
78 | 78 |
username = models.CharField('Username', max_length=64, blank=True, |
79 | 79 |
null=True) |
80 | 80 |
password_hash = models.CharField('Password', max_length=128, blank=True, |
81 |
null=True) |
|
81 |
null=True)
|
|
82 | 82 |
# Sha1 is up to 40 characters long |
83 | 83 |
hash = models.CharField('Hash', max_length=40, editable=False, null=False) |
84 | 84 |
# Unique index of the Backend, used for the mac-prefixes of the |
... | ... | |
128 | 128 |
|
129 | 129 |
def create_hash(self): |
130 | 130 |
"""Create a hash for this backend. """ |
131 |
return sha1('%s%s%s%s' % \
|
|
132 |
(self.clustername, self.port, self.username, self.password)) \
|
|
133 |
.hexdigest()
|
|
131 |
sha = sha1('%s%s%s%s' %
|
|
132 |
(self.clustername, self.port, self.username, self.password))
|
|
133 |
return sha.hexdigest()
|
|
134 | 134 |
|
135 | 135 |
@property |
136 | 136 |
def password(self): |
... | ... | |
147 | 147 |
super(Backend, self).save(*args, **kwargs) |
148 | 148 |
if self.hash != old_hash: |
149 | 149 |
# Populate the new hash to the new instances |
150 |
self.virtual_machines.filter(deleted=False).update(backend_hash=self.hash) |
|
150 |
self.virtual_machines.filter(deleted=False)\ |
|
151 |
.update(backend_hash=self.hash) |
|
151 | 152 |
|
152 | 153 |
def delete(self, *args, **kwargs): |
153 | 154 |
# Integrity Error if non-deleted VMs are associated with Backend |
... | ... | |
192 | 193 |
|
193 | 194 |
|
194 | 195 |
class QuotaHolderSerial(models.Model): |
195 |
serial = models.BigIntegerField(null=False, primary_key=True, db_index=True) |
|
196 |
serial = models.BigIntegerField(null=False, primary_key=True, |
|
197 |
db_index=True) |
|
196 | 198 |
pending = models.BooleanField(default=True, db_index=True) |
197 | 199 |
accepted = models.BooleanField(default=False) |
198 | 200 |
rejected = models.BooleanField(default=False) |
... | ... | |
209 | 211 |
class VirtualMachine(models.Model): |
210 | 212 |
# The list of possible actions for a VM |
211 | 213 |
ACTIONS = ( |
212 |
('CREATE', 'Create VM'), |
|
213 |
('START', 'Start VM'), |
|
214 |
('STOP', 'Shutdown VM'), |
|
215 |
('SUSPEND', 'Admin Suspend VM'), |
|
216 |
('REBOOT', 'Reboot VM'), |
|
217 |
('DESTROY', 'Destroy VM') |
|
214 |
('CREATE', 'Create VM'),
|
|
215 |
('START', 'Start VM'),
|
|
216 |
('STOP', 'Shutdown VM'),
|
|
217 |
('SUSPEND', 'Admin Suspend VM'),
|
|
218 |
('REBOOT', 'Reboot VM'),
|
|
219 |
('DESTROY', 'Destroy VM')
|
|
218 | 220 |
) |
219 | 221 |
|
220 | 222 |
# The internal operating state of a VM |
... | ... | |
296 | 298 |
suspended = models.BooleanField('Administratively Suspended', |
297 | 299 |
default=False) |
298 | 300 |
serial = models.ForeignKey(QuotaHolderSerial, |
299 |
related_name='virtual_machine', null=True) |
|
301 |
related_name='virtual_machine', null=True)
|
|
300 | 302 |
|
301 | 303 |
# VM State |
302 | 304 |
# The following fields are volatile data, in the sense |
... | ... | |
422 | 424 |
) |
423 | 425 |
|
424 | 426 |
ACTIONS = ( |
425 |
('CREATE', 'Create Network'), |
|
426 |
('DESTROY', 'Destroy Network'), |
|
427 |
('CREATE', 'Create Network'),
|
|
428 |
('DESTROY', 'Destroy Network'),
|
|
427 | 429 |
) |
428 | 430 |
|
429 | 431 |
RSAPI_STATE_FROM_OPER_STATE = { |
... | ... | |
435 | 437 |
|
436 | 438 |
FLAVORS = { |
437 | 439 |
'CUSTOM': { |
438 |
'mode': 'bridged',
|
|
439 |
'link': settings.DEFAULT_BRIDGE,
|
|
440 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX,
|
|
441 |
'tags': None,
|
|
442 |
'desc': "Basic flavor used for a bridged network",
|
|
440 |
'mode': 'bridged', |
|
441 |
'link': settings.DEFAULT_BRIDGE, |
|
442 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX, |
|
443 |
'tags': None, |
|
444 |
'desc': "Basic flavor used for a bridged network", |
|
443 | 445 |
}, |
444 | 446 |
'IP_LESS_ROUTED': { |
445 |
'mode': 'routed',
|
|
446 |
'link': settings.DEFAULT_ROUTING_TABLE,
|
|
447 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX,
|
|
448 |
'tags': 'ip-less-routed',
|
|
449 |
'desc': "Flavor used for an IP-less routed network using"
|
|
450 |
" Proxy ARP",
|
|
447 |
'mode': 'routed', |
|
448 |
'link': settings.DEFAULT_ROUTING_TABLE, |
|
449 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX, |
|
450 |
'tags': 'ip-less-routed', |
|
451 |
'desc': "Flavor used for an IP-less routed network using" |
|
452 |
" Proxy ARP", |
|
451 | 453 |
}, |
452 | 454 |
'MAC_FILTERED': { |
453 |
'mode': 'bridged',
|
|
454 |
'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
|
|
455 |
'mac_prefix': 'pool',
|
|
456 |
'tags': 'private-filtered',
|
|
457 |
'desc': "Flavor used for bridged networks that offer isolation"
|
|
458 |
" via filtering packets based on their src "
|
|
459 |
" MAC (ebtables)",
|
|
455 |
'mode': 'bridged', |
|
456 |
'link': settings.DEFAULT_MAC_FILTERED_BRIDGE, |
|
457 |
'mac_prefix': 'pool', |
|
458 |
'tags': 'private-filtered', |
|
459 |
'desc': "Flavor used for bridged networks that offer isolation" |
|
460 |
" via filtering packets based on their src " |
|
461 |
" MAC (ebtables)", |
|
460 | 462 |
}, |
461 | 463 |
'PHYSICAL_VLAN': { |
462 |
'mode': 'bridged',
|
|
463 |
'link': 'pool',
|
|
464 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX,
|
|
465 |
'tags': 'physical-vlan',
|
|
466 |
'desc': "Flavor used for bridged network that offer isolation"
|
|
467 |
" via dedicated physical vlan",
|
|
464 |
'mode': 'bridged', |
|
465 |
'link': 'pool', |
|
466 |
'mac_prefix': settings.DEFAULT_MAC_PREFIX, |
|
467 |
'tags': 'physical-vlan', |
|
468 |
'desc': "Flavor used for bridged network that offer isolation" |
|
469 |
" via dedicated physical vlan", |
|
468 | 470 |
}, |
469 | 471 |
} |
470 | 472 |
|
... | ... | |
528 | 530 |
backends = [backend] if backend\ |
529 | 531 |
else Backend.objects.filter(offline=False) |
530 | 532 |
for backend in backends: |
531 |
if not BackendNetwork.objects.filter(backend=backend, network=self)\ |
|
532 |
.exists(): |
|
533 |
backend_exists =\ |
|
534 |
BackendNetwork.objects.filter(backend=backend, network=self)\ |
|
535 |
.exists() |
|
536 |
if not backend_exists: |
|
533 | 537 |
BackendNetwork.objects.create(backend=backend, network=self) |
534 | 538 |
|
535 | 539 |
def get_pool(self): |
... | ... | |
564 | 568 |
self.status = status |
565 | 569 |
|
566 | 570 |
def __str__(self): |
567 |
return repr('<opcode: %s, status: %s>' % (self.opcode,
|
|
568 |
self.status)) |
|
571 |
return repr('<opcode: %s, status: %s>' |
|
572 |
% (self.opcode, self.status))
|
|
569 | 573 |
|
570 | 574 |
class InvalidActionError(Exception): |
571 | 575 |
def __init__(self, action): |
... | ... | |
647 | 651 |
try: |
648 | 652 |
utils.validate_mac(mac_prefix + ":00:00:00") |
649 | 653 |
except utils.InvalidMacAddress: |
650 |
raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % \
|
|
651 |
mac_prefix)
|
|
654 |
raise utils.InvalidMacAddress("Invalid MAC prefix '%s'" % |
|
655 |
mac_prefix) |
|
652 | 656 |
self.mac_prefix = mac_prefix |
653 | 657 |
|
654 | 658 |
|
... | ... | |
754 | 758 |
|
755 | 759 |
def since(self, vm, created_since, **kwargs): |
756 | 760 |
return self.get_query_set().filter(vm=vm, created__gt=created_since, |
757 |
**kwargs) |
|
761 |
**kwargs)
|
|
758 | 762 |
|
759 | 763 |
|
760 | 764 |
class VirtualMachineDiagnostic(models.Model): |
b/snf-cyclades-app/synnefo/db/models_factory.py | ||
---|---|---|
173 | 173 |
network = factory.SubFactory(NetworkFactory) |
174 | 174 |
index = factory.Sequence(lambda x: x, type=int) |
175 | 175 |
mac = factory.Sequence(lambda n: |
176 |
'aa:{0}{0}:{0}{0}:aa:{0}{0}:{0}{0}'.format(hex(int(n) % 15)[2:3]))
|
|
177 |
ipv4 = factory.LazyAttributeSequence(lambda a, n: a.network.subnet[:-4] + \
|
|
176 |
'aa:{0}{0}:{0}{0}:aa:{0}{0}:{0}{0}'.format(hex(int(n) % 15)[2:3])) |
|
177 |
ipv4 = factory.LazyAttributeSequence(lambda a, n: a.network.subnet[:-4] + |
|
178 | 178 |
'{0}'.format(int(n) + 2)) |
179 | 179 |
firewall_profile =\ |
180 | 180 |
factory.Sequence(round_seq_first(FACTORY_FOR.FIREWALL_PROFILES)) |
b/snf-cyclades-app/synnefo/logic/utils.py | ||
---|---|---|
33 | 33 |
from django.conf import settings |
34 | 34 |
from copy import deepcopy |
35 | 35 |
|
36 |
|
|
36 | 37 |
def id_from_instance_name(name): |
37 | 38 |
"""Returns VirtualMachine's Django id, given a ganeti machine name. |
38 | 39 |
|
... | ... | |
82 | 83 |
(vm.operstate) through the RSAPI_STATE_FROM_OPER_STATE dictionary. |
83 | 84 |
|
84 | 85 |
The last state reported by Ganeti is set whenever Ganeti reports |
85 |
successful completion of an operation. If Ganeti says an OP_INSTANCE_STARTUP |
|
86 |
operation succeeded, vm.operstate is set to "STARTED". |
|
87 |
|
|
88 |
* To support any transitional states defined by the API (only REBOOT for the time |
|
89 |
being) this mapping is amended with information reported by Ganeti regarding |
|
90 |
any outstanding operation. If an OP_INSTANCE_STARTUP had succeeded previously |
|
91 |
and an OP_INSTANCE_REBOOT has been reported as in progress, the API state is |
|
92 |
"REBOOT". |
|
86 |
successful completion of an operation. If Ganeti says an |
|
87 |
OP_INSTANCE_STARTUP operation succeeded, vm.operstate is set to |
|
88 |
"STARTED". |
|
89 |
|
|
90 |
* To support any transitional states defined by the API (only REBOOT for |
|
91 |
the time being) this mapping is amended with information reported by Ganeti |
|
92 |
regarding any outstanding operation. If an OP_INSTANCE_STARTUP had |
|
93 |
succeeded previously and an OP_INSTANCE_REBOOT has been reported as in |
|
94 |
progress, the API state is "REBOOT". |
|
93 | 95 |
|
94 | 96 |
""" |
95 | 97 |
try: |
b/snf-cyclades-app/synnefo/quotas/__init__.py | ||
---|---|---|
77 | 77 |
if resource == "cyclades.network.private": |
78 | 78 |
user_networks = Network.objects.filter(userid=userid, |
79 | 79 |
deleted=False).count() |
80 |
user_network_limit = NETWORKS_USER_QUOTA.get(userid,
|
|
81 |
MAX_NETWORKS_PER_USER)
|
|
80 |
user_network_limit =\
|
|
81 |
NETWORKS_USER_QUOTA.get(userid, MAX_NETWORKS_PER_USER)
|
|
82 | 82 |
if user_networks + size >= user_network_limit: |
83 | 83 |
raise NoQuantityError() |
84 | 84 |
|
... | ... | |
244 | 244 |
resources = invert_resources(resources) |
245 | 245 |
provisions = [('cyclades', 'cyclades.' + r, s) |
246 | 246 |
for r, s in resources.items()] |
247 |
return {"context": {},
|
|
248 |
"target": user,
|
|
249 |
"key": "1",
|
|
250 |
"clientkey": "cyclades",
|
|
251 |
#"owner": "",
|
|
252 |
#"ownerkey": "1",
|
|
253 |
"name": "",
|
|
254 |
"provisions": provisions}
|
|
247 |
return {"context": {},
|
|
248 |
"target": user,
|
|
249 |
"key": "1",
|
|
250 |
"clientkey": "cyclades",
|
|
251 |
#"owner": "", |
|
252 |
#"ownerkey": "1", |
|
253 |
"name": "",
|
|
254 |
"provisions": provisions} |
|
255 | 255 |
|
256 | 256 |
## |
257 | 257 |
## Reconcile pending commissions |
b/snf-cyclades-app/synnefo/quotas/management/commands/quotas-init.py | ||
---|---|---|
69 | 69 |
continue |
70 | 70 |
reset_holding = [] |
71 | 71 |
for res, val in resources.items(): |
72 |
reset_holding.append((user, "cyclades." + res, "1", val, 0, 0, 0)) |
|
72 |
reset_holding.append((user, "cyclades." + res, "1", val, 0, |
|
73 |
0, 0)) |
|
73 | 74 |
if not options['dry_run']: |
74 | 75 |
try: |
75 |
qh.reset_holding(context={}, reset_holding=reset_holding) |
|
76 |
qh.reset_holding(context={}, |
|
77 |
reset_holding=reset_holding) |
|
76 | 78 |
except Exception as e: |
77 | 79 |
self.stderr.write("Can not set up holding:%s" % e) |
78 | 80 |
else: |
b/snf-cyclades-app/synnefo/quotas/management/commands/quotas-verify.py | ||
---|---|---|
86 | 86 |
db_extra = db_res - qh_res |
87 | 87 |
if db_extra: |
88 | 88 |
for res in db_extra: |
89 |
write("Resource %s exists in DB for %s but not in QH\n"\
|
|
89 |
write("Resource %s exists in DB for %s but not in QH\n" |
|
90 | 90 |
% (res, user)) |
91 | 91 |
qh_extra = qh_res - db_res |
92 | 92 |
if qh_extra: |
93 | 93 |
for res in qh_extra: |
94 |
write("Resource %s exists in QH for %s but not in DB\n"\
|
|
94 |
write("Resource %s exists in QH for %s but not in DB\n" |
|
95 | 95 |
% (res, user)) |
96 | 96 |
return False |
b/snf-cyclades-app/synnefo/quotas/management/commands/reconcile-quotas.py | ||
---|---|---|
54 | 54 |
accepted, rejected = quotas.resolve_pending_commissions() |
55 | 55 |
|
56 | 56 |
if accepted: |
57 |
self.stdout.write("Pending accepted commissions:\n %s\n" \
|
|
57 |
self.stdout.write("Pending accepted commissions:\n %s\n" |
|
58 | 58 |
% list_to_string(accepted)) |
59 | 59 |
|
60 | 60 |
if rejected: |
61 |
self.stdout.write("Pending rejected commissions:\n %s\n" \
|
|
61 |
self.stdout.write("Pending rejected commissions:\n %s\n" |
|
62 | 62 |
% list_to_string(rejected)) |
63 | 63 |
|
64 | 64 |
if fix and (accepted or rejected): |
b/snf-cyclades-app/synnefo/quotas/util.py | ||
---|---|---|
65 | 65 |
|
66 | 66 |
# Get resources related with networks |
67 | 67 |
net_resources = networks.values("userid")\ |
68 |
.annotate(num=Count("id"))
|
|
68 |
.annotate(num=Count("id")) |
|
69 | 69 |
for net_res in net_resources: |
70 | 70 |
user = net_res['userid'] |
71 | 71 |
if user not in holdings: |
... | ... | |
99 | 99 |
|
100 | 100 |
|
101 | 101 |
def decode_holding(holding): |
102 |
entity, resource, imported, exported, returned, released = \ |
|
103 |
holding |
|
102 |
entity, resource, imported, exported, returned, released = holding |
|
104 | 103 |
res = resource.replace("cyclades.", "") |
105 | 104 |
return (res, imported - exported + returned - released) |
b/snf-cyclades-app/synnefo/vmapi/__init__.py | ||
---|---|---|
38 | 38 |
|
39 | 39 |
from synnefo.vmapi.settings import CACHE_KEY_PREFIX, CACHE_BACKEND |
40 | 40 |
|
41 |
|
|
41 | 42 |
def get_uuid(): |
42 | 43 |
return str(uuid4()) |
43 | 44 |
|
45 |
|
|
44 | 46 |
def get_key(*args): |
45 | 47 |
args = map(str, filter(bool, list(args))) |
46 | 48 |
args.insert(0, CACHE_KEY_PREFIX) |
... | ... | |
54 | 56 |
# here. |
55 | 57 |
if hasattr(backend, 'close'): |
56 | 58 |
signals.request_finished.connect(backend.close) |
57 |
|
b/snf-cyclades-app/synnefo/vmapi/models.py | ||
---|---|---|
41 | 41 |
|
42 | 42 |
log = getLogger('synnefo.vmapi') |
43 | 43 |
|
44 |
|
|
44 | 45 |
def create_server_params(sender, created_vm_params, **kwargs): |
45 | 46 |
json_value = json.dumps(created_vm_params) |
46 | 47 |
uuid = get_uuid() |
... | ... | |
55 | 56 |
return uuid |
56 | 57 |
|
57 | 58 |
server_created.connect(create_server_params) |
58 |
|
b/snf-cyclades-app/synnefo/vmapi/settings.py | ||
---|---|---|
34 | 34 |
from django.conf import settings |
35 | 35 |
|
36 | 36 |
CACHE_BACKEND = getattr(settings, 'VMAPI_CACHE_BACKEND', |
37 |
settings.CACHE_BACKEND) |
|
37 |
settings.CACHE_BACKEND)
|
|
38 | 38 |
CACHE_KEY_PREFIX = getattr(settings, 'VMAPI_CACHE_KEY_PREFIX', |
39 |
'vmapi') |
|
39 |
'vmapi')
|
|
40 | 40 |
RESET_PARAMS = getattr(settings, 'VMAPI_RESET_PARAMS', True) |
41 | 41 |
BASE_URL = getattr(settings, 'VMAPI_BASE_URL', |
42 |
'https://cyclades.okeanos.grnet.gr/') |
|
42 |
'https://cyclades.okeanos.grnet.gr/') |
b/snf-cyclades-app/synnefo/vmapi/tests.py | ||
---|---|---|
37 | 37 |
|
38 | 38 |
from synnefo.vmapi import settings |
39 | 39 |
|
40 |
|
|
40 | 41 |
class TestServerParams(TestCase): |
41 | 42 |
|
42 | 43 |
def test_cache_backend(self): |
... | ... | |
55 | 56 |
from synnefo.vmapi.models import create_server_params |
56 | 57 |
from synnefo.vmapi import backend |
57 | 58 |
try: |
58 |
from synnefo.api.servers import server_created |
|
59 | 59 |
from synnefo.db.models import VirtualMachine |
60 | 60 |
except ImportError: |
61 | 61 |
print "Skipping test_params_create" |
... | ... | |
66 | 66 |
params = {'password': 'X^942Jjfdsa', 'personality': {}} |
67 | 67 |
uuid = create_server_params(sender=vm, created_vm_params=params) |
68 | 68 |
|
69 |
self.assertEqual(vm.config_url, settings.BASE_URL + '/vmapi/server-params/%s' % uuid) |
|
69 |
self.assertEqual(vm.config_url, settings.BASE_URL + |
|
70 |
'/vmapi/server-params/%s' % uuid) |
|
70 | 71 |
key = "vmapi_%s" % uuid |
71 | 72 |
self.assertEqual(type(backend.get(key)), str) |
72 | 73 |
data = json.loads(backend.get(key)) |
... | ... | |
78 | 79 |
self.assertEqual(response.status_code, 200) |
79 | 80 |
response = self.client.get('/vmapi/server-params/%s' % uuid) |
80 | 81 |
self.assertEqual(response.status_code, 404) |
81 |
|
|
82 |
|
|
83 |
def test_params_view(self): |
|
84 |
pass |
|
85 |
|
b/snf-cyclades-app/synnefo/vmapi/urls.py | ||
---|---|---|
31 | 31 |
# interpreted as representing official policies, either expressed |
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 |
from django.conf.urls.defaults import include, patterns, url
|
|
34 |
from django.conf.urls.defaults import patterns, url |
|
35 | 35 |
|
36 | 36 |
urlpatterns = patterns('synnefo.vmapi.views', |
37 |
url(r'^server-params/(?P<uuid>.*)$', 'server_params', name="vmapi_server_params"), |
|
38 |
) |
|
37 |
url(r'^server-params/(?P<uuid>.*)$', 'server_params', |
|
38 |
name="vmapi_server_params"),) |
b/snf-cyclades-app/synnefo/vmapi/views.py | ||
---|---|---|
40 | 40 |
|
41 | 41 |
log = getLogger('synnefo.vmapi') |
42 | 42 |
|
43 |
|
|
43 | 44 |
def server_params(request, uuid): |
44 | 45 |
if not uuid: |
45 | 46 |
raise Http404 |
... | ... | |
54 | 55 |
backend.set(cache_key, None) |
55 | 56 |
|
56 | 57 |
return HttpResponse(params, content_type="application/json") |
57 |
|
Also available in: Unified diff