change database credentials checking from mysqladmin ping to mysqladmin status
[osm/openvim.git] / httpserver.py
1 # -*- coding: utf-8 -*-
2
3 ##
4 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
5 # This file is part of openvim
6 # All Rights Reserved.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: nfvlabs@tid.es
22 ##
23
24 '''
25 This is the thread for the http server North API.
26 Two thread will be launched, with normal and administrative permissions.
27 '''
28
29 __author__="Alfonso Tierno"
30 __date__ ="$10-jul-2014 12:07:15$"
31
32 import bottle
33 import yaml
34 import json
35 import threading
36 import datetime
37 import hashlib
38 import os
39 import RADclass
40 from jsonschema import validate as js_v, exceptions as js_e
41 import host_thread as ht
42 from vim_schema import host_new_schema, host_edit_schema, tenant_new_schema, \
43 tenant_edit_schema, \
44 flavor_new_schema, flavor_update_schema, \
45 image_new_schema, image_update_schema, \
46 server_new_schema, server_action_schema, network_new_schema, network_update_schema, \
47 port_new_schema, port_update_schema
48
49 global my
50 global url_base
51 global config_dic
52
53 url_base="/openvim"
54
55 HTTP_Bad_Request = 400
56 HTTP_Unauthorized = 401
57 HTTP_Not_Found = 404
58 HTTP_Forbidden = 403
59 HTTP_Method_Not_Allowed = 405
60 HTTP_Not_Acceptable = 406
61 HTTP_Request_Timeout = 408
62 HTTP_Conflict = 409
63 HTTP_Service_Unavailable = 503
64 HTTP_Internal_Server_Error= 500
65
66 def md5(fname):
67 hash_md5 = hashlib.md5()
68 with open(fname, "rb") as f:
69 for chunk in iter(lambda: f.read(4096), b""):
70 hash_md5.update(chunk)
71 return hash_md5.hexdigest()
72
73 def check_extended(extended, allow_net_attach=False):
74 '''Makes and extra checking of extended input that cannot be done using jsonschema
75 Attributes:
76 allow_net_attach: for allowing or not the uuid field at interfaces
77 that are allowed for instance, but not for flavors
78 Return: (<0, error_text) if error; (0,None) if not error '''
79 if "numas" not in extended: return 0, None
80 id_s=[]
81 numaid=0
82 for numa in extended["numas"]:
83 nb_formats = 0
84 if "cores" in numa:
85 nb_formats += 1
86 if "cores-id" in numa:
87 if len(numa["cores-id"]) != numa["cores"]:
88 return -HTTP_Bad_Request, "different number of cores-id (%d) than cores (%d) at numa %d" % (len(numa["cores-id"]), numa["cores"],numaid)
89 id_s.extend(numa["cores-id"])
90 if "threads" in numa:
91 nb_formats += 1
92 if "threads-id" in numa:
93 if len(numa["threads-id"]) != numa["threads"]:
94 return -HTTP_Bad_Request, "different number of threads-id (%d) than threads (%d) at numa %d" % (len(numa["threads-id"]), numa["threads"],numaid)
95 id_s.extend(numa["threads-id"])
96 if "paired-threads" in numa:
97 nb_formats += 1
98 if "paired-threads-id" in numa:
99 if len(numa["paired-threads-id"]) != numa["paired-threads"]:
100 return -HTTP_Bad_Request, "different number of paired-threads-id (%d) than paired-threads (%d) at numa %d" % (len(numa["paired-threads-id"]), numa["paired-threads"],numaid)
101 for pair in numa["paired-threads-id"]:
102 if len(pair) != 2:
103 return -HTTP_Bad_Request, "paired-threads-id must contain a list of two elements list at numa %d" % (numaid)
104 id_s.extend(pair)
105 if nb_formats > 1:
106 return -HTTP_Service_Unavailable, "only one of cores, threads, paired-threads are allowed in this version at numa %d" % numaid
107 #check interfaces
108 if "interfaces" in numa:
109 ifaceid=0
110 names=[]
111 vpcis=[]
112 for interface in numa["interfaces"]:
113 if "uuid" in interface and not allow_net_attach:
114 return -HTTP_Bad_Request, "uuid field is not allowed at numa %d interface %s position %d" % (numaid, interface.get("name",""), ifaceid )
115 if "mac_address" in interface and interface["dedicated"]=="yes":
116 return -HTTP_Bad_Request, "mac_address can not be set for dedicated (passthrough) at numa %d, interface %s position %d" % (numaid, interface.get("name",""), ifaceid )
117 if "name" in interface:
118 if interface["name"] in names:
119 return -HTTP_Bad_Request, "name repeated at numa %d, interface %s position %d" % (numaid, interface.get("name",""), ifaceid )
120 names.append(interface["name"])
121 if "vpci" in interface:
122 if interface["vpci"] in vpcis:
123 return -HTTP_Bad_Request, "vpci %s repeated at numa %d, interface %s position %d" % (interface["vpci"], numaid, interface.get("name",""), ifaceid )
124 vpcis.append(interface["vpci"])
125 ifaceid+=1
126 numaid+=1
127 if numaid > 1:
128 return -HTTP_Service_Unavailable, "only one numa can be defined in this version "
129 for a in range(0,len(id_s)):
130 if a not in id_s:
131 return -HTTP_Bad_Request, "core/thread identifiers must start at 0 and gaps are not alloed. Missing id number %d" % a
132
133 return 0, None
134
135 #
136 # dictionaries that change from HTTP API to database naming
137 #
138 http2db_host={'id':'uuid'}
139 http2db_tenant={'id':'uuid'}
140 http2db_flavor={'id':'uuid','imageRef':'image_id'}
141 http2db_image={'id':'uuid', 'created':'created_at', 'updated':'modified_at', 'public': 'public'}
142 http2db_server={'id':'uuid','hostId':'host_id','flavorRef':'flavor_id','imageRef':'image_id','created':'created_at'}
143 http2db_network={'id':'uuid','provider:vlan':'vlan', 'provider:physical': 'provider'}
144 http2db_port={'id':'uuid', 'network_id':'net_id', 'mac_address':'mac', 'device_owner':'type','device_id':'instance_id','binding:switch_port':'switch_port','binding:vlan':'vlan', 'bandwidth':'Mbps'}
145
146 def remove_extra_items(data, schema):
147 deleted=[]
148 if type(data) is tuple or type(data) is list:
149 for d in data:
150 a= remove_extra_items(d, schema['items'])
151 if a is not None: deleted.append(a)
152 elif type(data) is dict:
153 for k in data.keys():
154 if 'properties' not in schema or k not in schema['properties'].keys():
155 del data[k]
156 deleted.append(k)
157 else:
158 a = remove_extra_items(data[k], schema['properties'][k])
159 if a is not None: deleted.append({k:a})
160 if len(deleted) == 0: return None
161 elif len(deleted) == 1: return deleted[0]
162 else: return deleted
163
164 def delete_nulls(var):
165 if type(var) is dict:
166 for k in var.keys():
167 if var[k] is None: del var[k]
168 elif type(var[k]) is dict or type(var[k]) is list or type(var[k]) is tuple:
169 if delete_nulls(var[k]): del var[k]
170 if len(var) == 0: return True
171 elif type(var) is list or type(var) is tuple:
172 for k in var:
173 if type(k) is dict: delete_nulls(k)
174 if len(var) == 0: return True
175 return False
176
177
178 class httpserver(threading.Thread):
179 def __init__(self, db_conn, name="http", host='localhost', port=8080, admin=False, config_=None):
180 '''
181 Creates a new thread to attend the http connections
182 Attributes:
183 db_conn: database connection
184 name: name of this thread
185 host: ip or name where to listen
186 port: port where to listen
187 admin: if this has privileges of administrator or not
188 config_: unless the first thread must be provided. It is a global dictionary where to allocate the self variable
189 '''
190 global url_base
191 global config_dic
192
193 #initialization
194 if config_ is not None:
195 config_dic = config_
196 if 'http_threads' not in config_dic:
197 config_dic['http_threads'] = {}
198 threading.Thread.__init__(self)
199 self.host = host
200 self.port = port
201 self.db = db_conn
202 self.admin = admin
203 if name in config_dic:
204 print "httpserver Warning!!! Onether thread with the same name", name
205 n=0
206 while name+str(n) in config_dic:
207 n +=1
208 name +=str(n)
209 self.name = name
210 self.url_preffix = 'http://' + self.host + ':' + str(self.port) + url_base
211 config_dic['http_threads'][name] = self
212
213 #Ensure that when the main program exits the thread will also exit
214 self.daemon = True
215 self.setDaemon(True)
216
217 def run(self):
218 bottle.run(host=self.host, port=self.port, debug=True) #quiet=True
219
220 def gethost(self, host_id):
221 result, content = self.db.get_host(host_id)
222 if result < 0:
223 print "httpserver.gethost error %d %s" % (result, content)
224 bottle.abort(-result, content)
225 elif result==0:
226 print "httpserver.gethost host '%s' not found" % host_id
227 bottle.abort(HTTP_Not_Found, content)
228 else:
229 data={'host' : content}
230 convert_boolean(content, ('admin_state_up',) )
231 change_keys_http2db(content, http2db_host, reverse=True)
232 print data['host']
233 return format_out(data)
234
235 @bottle.route(url_base + '/', method='GET')
236 def http_get():
237 print
238 return 'works' #TODO: put links or redirection to /openvim???
239
240 #
241 # Util funcions
242 #
243
244 def change_keys_http2db(data, http_db, reverse=False):
245 '''Change keys of dictionary data according to the key_dict values
246 This allow change from http interface names to database names.
247 When reverse is True, the change is otherwise
248 Attributes:
249 data: can be a dictionary or a list
250 http_db: is a dictionary with hhtp names as keys and database names as value
251 reverse: by default change is done from http API to database. If True change is done otherwise
252 Return: None, but data is modified'''
253 if type(data) is tuple or type(data) is list:
254 for d in data:
255 change_keys_http2db(d, http_db, reverse)
256 elif type(data) is dict or type(data) is bottle.FormsDict:
257 if reverse:
258 for k,v in http_db.items():
259 if v in data: data[k]=data.pop(v)
260 else:
261 for k,v in http_db.items():
262 if k in data: data[v]=data.pop(k)
263
264
265
266 def format_out(data):
267 '''return string of dictionary data according to requested json, yaml, xml. By default json'''
268 if 'application/yaml' in bottle.request.headers.get('Accept'):
269 bottle.response.content_type='application/yaml'
270 return yaml.safe_dump(data, explicit_start=True, indent=4, default_flow_style=False, tags=False, encoding='utf-8', allow_unicode=True) #, canonical=True, default_style='"'
271 else: #by default json
272 bottle.response.content_type='application/json'
273 #return data #json no style
274 return json.dumps(data, indent=4) + "\n"
275
276 def format_in(schema):
277 try:
278 error_text = "Invalid header format "
279 format_type = bottle.request.headers.get('Content-Type', 'application/json')
280 if 'application/json' in format_type:
281 error_text = "Invalid json format "
282 #Use the json decoder instead of bottle decoder because it informs about the location of error formats with a ValueError exception
283 client_data = json.load(bottle.request.body)
284 #client_data = bottle.request.json()
285 elif 'application/yaml' in format_type:
286 error_text = "Invalid yaml format "
287 client_data = yaml.load(bottle.request.body)
288 elif format_type == 'application/xml':
289 bottle.abort(501, "Content-Type: application/xml not supported yet.")
290 else:
291 print "HTTP HEADERS: " + str(bottle.request.headers.items())
292 bottle.abort(HTTP_Not_Acceptable, 'Content-Type ' + str(format_type) + ' not supported.')
293 return
294 #if client_data == None:
295 # bottle.abort(HTTP_Bad_Request, "Content error, empty")
296 # return
297 #check needed_items
298
299 #print "HTTP input data: ", str(client_data)
300 error_text = "Invalid content "
301 js_v(client_data, schema)
302
303 return client_data
304 except (ValueError, yaml.YAMLError) as exc:
305 error_text += str(exc)
306 print error_text
307 bottle.abort(HTTP_Bad_Request, error_text)
308 except js_e.ValidationError as exc:
309 print "HTTP validate_in error, jsonschema exception ", exc.message, "at", exc.path
310 print " CONTENT: " + str(bottle.request.body.readlines())
311 error_pos = ""
312 if len(exc.path)>0: error_pos=" at '" + ":".join(map(str, exc.path)) + "'"
313 bottle.abort(HTTP_Bad_Request, error_text + error_pos+": "+exc.message)
314 #except:
315 # bottle.abort(HTTP_Bad_Request, "Content error: Failed to parse Content-Type", error_pos)
316 # raise
317
318 def filter_query_string(qs, http2db, allowed):
319 '''Process query string (qs) checking that contains only valid tokens for avoiding SQL injection
320 Attributes:
321 'qs': bottle.FormsDict variable to be processed. None or empty is considered valid
322 'allowed': list of allowed string tokens (API http naming). All the keys of 'qs' must be one of 'allowed'
323 'http2db': dictionary with change from http API naming (dictionary key) to database naming(dictionary value)
324 Return: A tuple with the (select,where,limit) to be use in a database query. All of then transformed to the database naming
325 select: list of items to retrieve, filtered by query string 'field=token'. If no 'field' is present, allowed list is returned
326 where: dictionary with key, value, taken from the query string token=value. Empty if nothing is provided
327 limit: limit dictated by user with the query string 'limit'. 100 by default
328 abort if not permitted, using bottel.abort
329 '''
330 where={}
331 limit=100
332 select=[]
333 if type(qs) is not bottle.FormsDict:
334 print '!!!!!!!!!!!!!!invalid query string not a dictionary'
335 #bottle.abort(HTTP_Internal_Server_Error, "call programmer")
336 else:
337 for k in qs:
338 if k=='field':
339 select += qs.getall(k)
340 for v in select:
341 if v not in allowed:
342 bottle.abort(HTTP_Bad_Request, "Invalid query string at 'field="+v+"'")
343 elif k=='limit':
344 try:
345 limit=int(qs[k])
346 except:
347 bottle.abort(HTTP_Bad_Request, "Invalid query string at 'limit="+qs[k]+"'")
348 else:
349 if k not in allowed:
350 bottle.abort(HTTP_Bad_Request, "Invalid query string at '"+k+"="+qs[k]+"'")
351 if qs[k]!="null": where[k]=qs[k]
352 else: where[k]=None
353 if len(select)==0: select += allowed
354 #change from http api to database naming
355 for i in range(0,len(select)):
356 k=select[i]
357 if k in http2db:
358 select[i] = http2db[k]
359 change_keys_http2db(where, http2db)
360 #print "filter_query_string", select,where,limit
361
362 return select,where,limit
363
364
365 def convert_bandwidth(data, reverse=False):
366 '''Check the field bandwidth recursively and when found, it removes units and convert to number
367 It assumes that bandwidth is well formed
368 Attributes:
369 'data': dictionary bottle.FormsDict variable to be checked. None or empty is considered valid
370 'reverse': by default convert form str to int (Mbps), if True it convert from number to units
371 Return:
372 None
373 '''
374 if type(data) is dict:
375 for k in data.keys():
376 if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
377 convert_bandwidth(data[k], reverse)
378 if "bandwidth" in data:
379 try:
380 value=str(data["bandwidth"])
381 if not reverse:
382 pos = value.find("bps")
383 if pos>0:
384 if value[pos-1]=="G": data["bandwidth"] = int(data["bandwidth"][:pos-1]) * 1000
385 elif value[pos-1]=="k": data["bandwidth"]= int(data["bandwidth"][:pos-1]) / 1000
386 else: data["bandwidth"]= int(data["bandwidth"][:pos-1])
387 else:
388 value = int(data["bandwidth"])
389 if value % 1000 == 0: data["bandwidth"]=str(value/1000) + " Gbps"
390 else: data["bandwidth"]=str(value) + " Mbps"
391 except:
392 print "convert_bandwidth exception for type", type(data["bandwidth"]), " data", data["bandwidth"]
393 return
394 if type(data) is tuple or type(data) is list:
395 for k in data:
396 if type(k) is dict or type(k) is tuple or type(k) is list:
397 convert_bandwidth(k, reverse)
398
399 def convert_boolean(data, items):
400 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
401 It assumes that bandwidth is well formed
402 Attributes:
403 'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
404 'items': tuple of keys to convert
405 Return:
406 None
407 '''
408 if type(data) is dict:
409 for k in data.keys():
410 if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
411 convert_boolean(data[k], items)
412 if k in items:
413 if type(data[k]) is str:
414 if data[k]=="false": data[k]=False
415 elif data[k]=="true": data[k]=True
416 if type(data) is tuple or type(data) is list:
417 for k in data:
418 if type(k) is dict or type(k) is tuple or type(k) is list:
419 convert_boolean(k, items)
420
421 def convert_datetime2str(var):
422 '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s'
423 It enters recursively in the dict var finding this kind of variables
424 '''
425 if type(var) is dict:
426 for k,v in var.items():
427 if type(v) is datetime.datetime:
428 var[k]= v.strftime('%Y-%m-%dT%H:%M:%S')
429 elif type(v) is dict or type(v) is list or type(v) is tuple:
430 convert_datetime2str(v)
431 if len(var) == 0: return True
432 elif type(var) is list or type(var) is tuple:
433 for v in var:
434 convert_datetime2str(v)
435
436 def check_valid_tenant(my, tenant_id):
437 if tenant_id=='any':
438 if not my.admin:
439 return HTTP_Unauthorized, "Needed admin privileges"
440 else:
441 result, _ = my.db.get_table(FROM='tenants', SELECT=('uuid',), WHERE={'uuid': tenant_id})
442 if result<=0:
443 return HTTP_Not_Found, "tenant '%s' not found" % tenant_id
444 return 0, None
445
446 def check_valid_uuid(uuid):
447 id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
448 try:
449 js_v(uuid, id_schema)
450 return True
451 except js_e.ValidationError:
452 return False
453
454 @bottle.error(400)
455 @bottle.error(401)
456 @bottle.error(404)
457 @bottle.error(403)
458 @bottle.error(405)
459 @bottle.error(406)
460 @bottle.error(408)
461 @bottle.error(409)
462 @bottle.error(503)
463 @bottle.error(500)
464 def error400(error):
465 e={"error":{"code":error.status_code, "type":error.status, "description":error.body}}
466 return format_out(e)
467
468 @bottle.hook('after_request')
469 def enable_cors():
470 #TODO: Alf: Is it needed??
471 bottle.response.headers['Access-Control-Allow-Origin'] = '*'
472
473 #
474 # HOSTS
475 #
476
477 @bottle.route(url_base + '/hosts', method='GET')
478 def http_get_hosts():
479 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_host,
480 ('id','name','description','status','admin_state_up') )
481
482 myself = config_dic['http_threads'][ threading.current_thread().name ]
483 result, content = myself.db.get_table(FROM='hosts', SELECT=select_, WHERE=where_, LIMIT=limit_)
484 if result < 0:
485 print "http_get_hosts Error", content
486 bottle.abort(-result, content)
487 else:
488 convert_boolean(content, ('admin_state_up',) )
489 change_keys_http2db(content, http2db_host, reverse=True)
490 for row in content:
491 row['links'] = ( {'href': myself.url_preffix + '/hosts/' + str(row['id']), 'rel': 'bookmark'}, )
492 data={'hosts' : content}
493 return format_out(data)
494
495 @bottle.route(url_base + '/hosts/<host_id>', method='GET')
496 def http_get_host_id(host_id):
497 my = config_dic['http_threads'][ threading.current_thread().name ]
498 return my.gethost(host_id)
499
500 @bottle.route(url_base + '/hosts', method='POST')
501 def http_post_hosts():
502 '''insert a host into the database. All resources are got and inserted'''
503 my = config_dic['http_threads'][ threading.current_thread().name ]
504 #check permissions
505 if not my.admin:
506 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
507
508 #parse input data
509 http_content = format_in( host_new_schema )
510 r = remove_extra_items(http_content, host_new_schema)
511 if r is not None: print "http_post_host_id: Warning: remove extra items ", r
512 change_keys_http2db(http_content['host'], http2db_host)
513
514 host = http_content['host']
515 warning_text=""
516 if 'host-data' in http_content:
517 host.update(http_content['host-data'])
518 ip_name=http_content['host-data']['ip_name']
519 user=http_content['host-data']['user']
520 password=http_content['host-data'].get('password', None)
521 else:
522 ip_name=host['ip_name']
523 user=host['user']
524 password=host.get('password', None)
525
526 #fill rad info
527 rad = RADclass.RADclass()
528 (return_status, code) = rad.obtain_RAD(user, password, ip_name)
529
530 #return
531 if not return_status:
532 print 'http_post_hosts ERROR obtaining RAD', code
533 bottle.abort(HTTP_Bad_Request, code)
534 return
535 warning_text=code
536 rad_structure = yaml.load(rad.to_text())
537 print 'rad_structure\n---------------------'
538 print json.dumps(rad_structure, indent=4)
539 print '---------------------'
540 #return
541 WHERE_={"family":rad_structure['processor']['family'], 'manufacturer':rad_structure['processor']['manufacturer'], 'version':rad_structure['processor']['version']}
542 result, content = my.db.get_table(FROM='host_ranking',
543 SELECT=('ranking',),
544 WHERE=WHERE_)
545 if result > 0:
546 host['ranking'] = content[0]['ranking']
547 else:
548 #error_text= "Host " + str(WHERE_)+ " not found in ranking table. Not valid for VIM management"
549 #bottle.abort(HTTP_Bad_Request, error_text)
550 #return
551 warning_text += "Host " + str(WHERE_)+ " not found in ranking table. Assuming lowest value 100\n"
552 host['ranking'] = 100 #TODO: as not used in this version, set the lowest value
553
554 features = rad_structure['processor'].get('features', ())
555 host['features'] = ",".join(features)
556 host['numas'] = []
557
558 for node in (rad_structure['resource topology']['nodes'] or {}).itervalues():
559 interfaces= []
560 cores = []
561 eligible_cores=[]
562 count = 0
563 for core in node['cpu']['eligible_cores']:
564 eligible_cores.extend(core)
565 for core in node['cpu']['cores']:
566 for thread_id in core:
567 c={'core_id': count, 'thread_id': thread_id}
568 if thread_id not in eligible_cores: c['status'] = 'noteligible'
569 cores.append(c)
570 count = count+1
571
572 if 'nics' in node:
573 for port_k, port_v in node['nics']['nic 0']['ports'].iteritems():
574 if port_v['virtual']:
575 continue
576 else:
577 sriovs = []
578 for port_k2, port_v2 in node['nics']['nic 0']['ports'].iteritems():
579 if port_v2['virtual'] and port_v2['PF_pci_id']==port_k:
580 sriovs.append({'pci':port_k2, 'mac':port_v2['mac'], 'source_name':port_v2['source_name']})
581 if len(sriovs)>0:
582 #sort sriov according to pci and rename them to the vf number
583 new_sriovs = sorted(sriovs, key=lambda k: k['pci'])
584 index=0
585 for sriov in new_sriovs:
586 sriov['source_name'] = index
587 index += 1
588 interfaces.append ({'pci':str(port_k), 'Mbps': port_v['speed']/1000000, 'sriovs': new_sriovs, 'mac':port_v['mac'], 'source_name':port_v['source_name']})
589 #@TODO LA memoria devuelta por el RAD es incorrecta, almenos para IVY1, NFV100
590 memory=node['memory']['node_size'] / (1024*1024*1024)
591 #memory=get_next_2pow(node['memory']['hugepage_nr'])
592 host['numas'].append( {'numa_socket': node['id'], 'hugepages': node['memory']['hugepage_nr'], 'memory':memory, 'interfaces': interfaces, 'cores': cores } )
593 print json.dumps(host, indent=4)
594 #return
595 #
596 #insert in data base
597 result, content = my.db.new_host(host)
598 if result >= 0:
599 if content['admin_state_up']:
600 #create thread
601 host_test_mode = True if config_dic['mode']=='test' or config_dic['mode']=="OF only" else False
602 host_develop_mode = True if config_dic['mode']=='development' else False
603 host_develop_bridge_iface = config_dic.get('development_bridge', None)
604 thread = ht.host_thread(name=host.get('name',ip_name), user=user, host=ip_name, db=config_dic['db'], db_lock=config_dic['db_lock'],
605 test=host_test_mode, image_path=config_dic['image_path'],
606 version=config_dic['version'], host_id=content['uuid'],
607 develop_mode=host_develop_mode, develop_bridge_iface=host_develop_bridge_iface )
608 thread.start()
609 config_dic['host_threads'][ content['uuid'] ] = thread
610
611 #return host data
612 change_keys_http2db(content, http2db_host, reverse=True)
613 if len(warning_text)>0:
614 content["warning"]= warning_text
615 data={'host' : content}
616 return format_out(data)
617 else:
618 bottle.abort(HTTP_Bad_Request, content)
619 return
620
621 @bottle.route(url_base + '/hosts/<host_id>', method='PUT')
622 def http_put_host_id(host_id):
623 '''modify a host into the database. All resources are got and inserted'''
624 my = config_dic['http_threads'][ threading.current_thread().name ]
625 #check permissions
626 if not my.admin:
627 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
628
629 #parse input data
630 http_content = format_in( host_edit_schema )
631 r = remove_extra_items(http_content, host_edit_schema)
632 if r is not None: print "http_post_host_id: Warning: remove extra items ", r
633 change_keys_http2db(http_content['host'], http2db_host)
634
635 #insert in data base
636 result, content = my.db.edit_host(host_id, http_content['host'])
637 if result >= 0:
638 convert_boolean(content, ('admin_state_up',) )
639 change_keys_http2db(content, http2db_host, reverse=True)
640 data={'host' : content}
641
642 #reload thread
643 config_dic['host_threads'][host_id].name = content.get('name',content['ip_name'])
644 config_dic['host_threads'][host_id].user = content['user']
645 config_dic['host_threads'][host_id].host = content['ip_name']
646 config_dic['host_threads'][host_id].insert_task("reload")
647
648 #print data
649 return format_out(data)
650 else:
651 bottle.abort(HTTP_Bad_Request, content)
652 return
653
654
655
656 @bottle.route(url_base + '/hosts/<host_id>', method='DELETE')
657 def http_delete_host_id(host_id):
658 my = config_dic['http_threads'][ threading.current_thread().name ]
659 #check permissions
660 if not my.admin:
661 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
662 result, content = my.db.delete_row('hosts', host_id)
663 if result == 0:
664 bottle.abort(HTTP_Not_Found, content)
665 elif result >0:
666 #terminate thread
667 if host_id in config_dic['host_threads']:
668 config_dic['host_threads'][host_id].insert_task("exit")
669 #return data
670 data={'result' : content}
671 return format_out(data)
672 else:
673 print "http_delete_host_id error",result, content
674 bottle.abort(-result, content)
675 return
676
677
678
679 #
680 # TENANTS
681 #
682
683 @bottle.route(url_base + '/tenants', method='GET')
684 def http_get_tenants():
685 my = config_dic['http_threads'][ threading.current_thread().name ]
686 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_tenant,
687 ('id','name','description','enabled') )
688 result, content = my.db.get_table(FROM='tenants', SELECT=select_,WHERE=where_,LIMIT=limit_)
689 if result < 0:
690 print "http_get_tenants Error", content
691 bottle.abort(-result, content)
692 else:
693 change_keys_http2db(content, http2db_tenant, reverse=True)
694 convert_boolean(content, ('enabled',))
695 data={'tenants' : content}
696 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
697 return format_out(data)
698
699 @bottle.route(url_base + '/tenants/<tenant_id>', method='GET')
700 def http_get_tenant_id(tenant_id):
701 my = config_dic['http_threads'][ threading.current_thread().name ]
702 result, content = my.db.get_table(FROM='tenants', SELECT=('uuid','name','description', 'enabled'),WHERE={'uuid': tenant_id} )
703 if result < 0:
704 print "http_get_tenant_id error %d %s" % (result, content)
705 bottle.abort(-result, content)
706 elif result==0:
707 print "http_get_tenant_id tenant '%s' not found" % tenant_id
708 bottle.abort(HTTP_Not_Found, "tenant %s not found" % tenant_id)
709 else:
710 change_keys_http2db(content, http2db_tenant, reverse=True)
711 convert_boolean(content, ('enabled',))
712 data={'tenant' : content[0]}
713 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
714 return format_out(data)
715
716
717 @bottle.route(url_base + '/tenants', method='POST')
718 def http_post_tenants():
719 '''insert a tenant into the database.'''
720 my = config_dic['http_threads'][ threading.current_thread().name ]
721 #parse input data
722 http_content = format_in( tenant_new_schema )
723 r = remove_extra_items(http_content, tenant_new_schema)
724 if r is not None: print "http_post_tenants: Warning: remove extra items ", r
725 change_keys_http2db(http_content['tenant'], http2db_tenant)
726
727 #insert in data base
728 result, content = my.db.new_tenant(http_content['tenant'])
729
730 if result >= 0:
731 return http_get_tenant_id(content)
732 else:
733 bottle.abort(-result, content)
734 return
735
736 @bottle.route(url_base + '/tenants/<tenant_id>', method='PUT')
737 def http_put_tenant_id(tenant_id):
738 '''update a tenant into the database.'''
739 my = config_dic['http_threads'][ threading.current_thread().name ]
740 #parse input data
741 http_content = format_in( tenant_edit_schema )
742 r = remove_extra_items(http_content, tenant_edit_schema)
743 if r is not None: print "http_put_tenant_id: Warning: remove extra items ", r
744 change_keys_http2db(http_content['tenant'], http2db_tenant)
745
746 #insert in data base
747 result, content = my.db.update_rows('tenants', http_content['tenant'], WHERE={'uuid': tenant_id}, log=True )
748 if result >= 0:
749 return http_get_tenant_id(tenant_id)
750 else:
751 bottle.abort(-result, content)
752 return
753
754 @bottle.route(url_base + '/tenants/<tenant_id>', method='DELETE')
755 def http_delete_tenant_id(tenant_id):
756 my = config_dic['http_threads'][ threading.current_thread().name ]
757 #check permissions
758 r, tenants_flavors = my.db.get_table(FROM='tenants_flavors', SELECT=('flavor_id','tenant_id'), WHERE={'tenant_id': tenant_id})
759 if r<=0:
760 tenants_flavors=()
761 r, tenants_images = my.db.get_table(FROM='tenants_images', SELECT=('image_id','tenant_id'), WHERE={'tenant_id': tenant_id})
762 if r<=0:
763 tenants_images=()
764 result, content = my.db.delete_row('tenants', tenant_id)
765 if result == 0:
766 bottle.abort(HTTP_Not_Found, content)
767 elif result >0:
768 print "alf", tenants_flavors, tenants_images
769 for flavor in tenants_flavors:
770 my.db.delete_row_by_key("flavors", "uuid", flavor['flavor_id'])
771 for image in tenants_images:
772 my.db.delete_row_by_key("images", "uuid", image['image_id'])
773 data={'result' : content}
774 return format_out(data)
775 else:
776 print "http_delete_tenant_id error",result, content
777 bottle.abort(-result, content)
778 return
779
780 #
781 # FLAVORS
782 #
783
784 @bottle.route(url_base + '/<tenant_id>/flavors', method='GET')
785 def http_get_flavors(tenant_id):
786 my = config_dic['http_threads'][ threading.current_thread().name ]
787 #check valid tenant_id
788 result,content = check_valid_tenant(my, tenant_id)
789 if result != 0:
790 bottle.abort(result, content)
791 #obtain data
792 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_flavor,
793 ('id','name','description','public') )
794 if tenant_id=='any':
795 from_ ='flavors'
796 else:
797 from_ ='tenants_flavors inner join flavors on tenants_flavors.flavor_id=flavors.uuid'
798 where_['tenant_id'] = tenant_id
799 result, content = my.db.get_table(FROM=from_, SELECT=select_, WHERE=where_, LIMIT=limit_)
800 if result < 0:
801 print "http_get_flavors Error", content
802 bottle.abort(-result, content)
803 else:
804 change_keys_http2db(content, http2db_flavor, reverse=True)
805 for row in content:
806 row['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'flavors', str(row['id']) ) ), 'rel':'bookmark' } ]
807 data={'flavors' : content}
808 return format_out(data)
809
810 @bottle.route(url_base + '/<tenant_id>/flavors/<flavor_id>', method='GET')
811 def http_get_flavor_id(tenant_id, flavor_id):
812 my = config_dic['http_threads'][ threading.current_thread().name ]
813 #check valid tenant_id
814 result,content = check_valid_tenant(my, tenant_id)
815 if result != 0:
816 bottle.abort(result, content)
817 #obtain data
818 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_flavor,
819 ('id','name','description','ram', 'vcpus', 'extended', 'disk', 'public') )
820 if tenant_id=='any':
821 from_ ='flavors'
822 else:
823 from_ ='tenants_flavors as tf inner join flavors as f on tf.flavor_id=f.uuid'
824 where_['tenant_id'] = tenant_id
825 where_['uuid'] = flavor_id
826 result, content = my.db.get_table(SELECT=select_, FROM=from_, WHERE=where_, LIMIT=limit_)
827
828 if result < 0:
829 print "http_get_flavor_id error %d %s" % (result, content)
830 bottle.abort(-result, content)
831 elif result==0:
832 print "http_get_flavors_id flavor '%s' not found" % str(flavor_id)
833 bottle.abort(HTTP_Not_Found, 'flavor %s not found' % flavor_id)
834 else:
835 change_keys_http2db(content, http2db_flavor, reverse=True)
836 if 'extended' in content[0] and content[0]['extended'] is not None:
837 extended = json.loads(content[0]['extended'])
838 if 'devices' in extended:
839 change_keys_http2db(extended['devices'], http2db_flavor, reverse=True)
840 content[0]['extended']=extended
841 convert_bandwidth(content[0], reverse=True)
842 content[0]['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'flavors', str(content[0]['id']) ) ), 'rel':'bookmark' } ]
843 data={'flavor' : content[0]}
844 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
845 return format_out(data)
846
847
848 @bottle.route(url_base + '/<tenant_id>/flavors', method='POST')
849 def http_post_flavors(tenant_id):
850 '''insert a flavor into the database, and attach to tenant.'''
851 my = config_dic['http_threads'][ threading.current_thread().name ]
852 #check valid tenant_id
853 result,content = check_valid_tenant(my, tenant_id)
854 if result != 0:
855 bottle.abort(result, content)
856 http_content = format_in( flavor_new_schema )
857 r = remove_extra_items(http_content, flavor_new_schema)
858 if r is not None: print "http_post_flavors: Warning: remove extra items ", r
859 change_keys_http2db(http_content['flavor'], http2db_flavor)
860 extended_dict = http_content['flavor'].pop('extended', None)
861 if extended_dict is not None:
862 result, content = check_extended(extended_dict)
863 if result<0:
864 print "http_post_flavors wrong input extended error %d %s" % (result, content)
865 bottle.abort(-result, content)
866 return
867 convert_bandwidth(extended_dict)
868 if 'devices' in extended_dict: change_keys_http2db(extended_dict['devices'], http2db_flavor)
869 http_content['flavor']['extended'] = json.dumps(extended_dict)
870 #insert in data base
871 result, content = my.db.new_flavor(http_content['flavor'], tenant_id)
872 if result >= 0:
873 return http_get_flavor_id(tenant_id, content)
874 else:
875 print "http_psot_flavors error %d %s" % (result, content)
876 bottle.abort(-result, content)
877 return
878
879 @bottle.route(url_base + '/<tenant_id>/flavors/<flavor_id>', method='DELETE')
880 def http_delete_flavor_id(tenant_id, flavor_id):
881 '''Deletes the flavor_id of a tenant. IT removes from tenants_flavors table.'''
882 my = config_dic['http_threads'][ threading.current_thread().name ]
883 #check valid tenant_id
884 result,content = check_valid_tenant(my, tenant_id)
885 if result != 0:
886 bottle.abort(result, content)
887 return
888 result, content = my.db.delete_image_flavor('flavor', flavor_id, tenant_id)
889 if result == 0:
890 bottle.abort(HTTP_Not_Found, content)
891 elif result >0:
892 data={'result' : content}
893 return format_out(data)
894 else:
895 print "http_delete_flavor_id error",result, content
896 bottle.abort(-result, content)
897 return
898
899 @bottle.route(url_base + '/<tenant_id>/flavors/<flavor_id>/<action>', method='POST')
900 def http_attach_detach_flavors(tenant_id, flavor_id, action):
901 '''attach/detach an existing flavor in this tenant. That is insert/remove at tenants_flavors table.'''
902 #TODO alf: not tested at all!!!
903 my = config_dic['http_threads'][ threading.current_thread().name ]
904 #check valid tenant_id
905 result,content = check_valid_tenant(my, tenant_id)
906 if result != 0:
907 bottle.abort(result, content)
908 if tenant_id=='any':
909 bottle.abort(HTTP_Bad_Request, "Invalid tenant 'any' with this command")
910 #check valid action
911 if action!='attach' and action != 'detach':
912 bottle.abort(HTTP_Method_Not_Allowed, "actions can be attach or detach")
913 return
914
915 #Ensure that flavor exist
916 from_ ='tenants_flavors as tf right join flavors as f on tf.flavor_id=f.uuid'
917 where_={'uuid': flavor_id}
918 result, content = my.db.get_table(SELECT=('public','tenant_id'), FROM=from_, WHERE=where_)
919 if result==0:
920 if action=='attach':
921 text_error="Flavor '%s' not found" % flavor_id
922 else:
923 text_error="Flavor '%s' not found for tenant '%s'" % (flavor_id, tenant_id)
924 bottle.abort(HTTP_Not_Found, text_error)
925 return
926 elif result>0:
927 flavor=content[0]
928 if action=='attach':
929 if flavor['tenant_id']!=None:
930 bottle.abort(HTTP_Conflict, "Flavor '%s' already attached to tenant '%s'" % (flavor_id, tenant_id))
931 if flavor['public']=='no' and not my.admin:
932 #allow only attaching public flavors
933 bottle.abort(HTTP_Unauthorized, "Needed admin rights to attach a private flavor")
934 return
935 #insert in data base
936 result, content = my.db.new_row('tenants_flavors', {'flavor_id':flavor_id, 'tenant_id': tenant_id})
937 if result >= 0:
938 return http_get_flavor_id(tenant_id, flavor_id)
939 else: #detach
940 if flavor['tenant_id']==None:
941 bottle.abort(HTTP_Not_Found, "Flavor '%s' not attached to tenant '%s'" % (flavor_id, tenant_id))
942 result, content = my.db.delete_row_by_dict(FROM='tenants_flavors', WHERE={'flavor_id':flavor_id, 'tenant_id':tenant_id})
943 if result>=0:
944 if flavor['public']=='no':
945 #try to delete the flavor completely to avoid orphan flavors, IGNORE error
946 my.db.delete_row_by_dict(FROM='flavors', WHERE={'uuid':flavor_id})
947 data={'result' : "flavor detached"}
948 return format_out(data)
949
950 #if get here is because an error
951 print "http_attach_detach_flavors error %d %s" % (result, content)
952 bottle.abort(-result, content)
953 return
954
955 @bottle.route(url_base + '/<tenant_id>/flavors/<flavor_id>', method='PUT')
956 def http_put_flavor_id(tenant_id, flavor_id):
957 '''update a flavor_id into the database.'''
958 my = config_dic['http_threads'][ threading.current_thread().name ]
959 #check valid tenant_id
960 result,content = check_valid_tenant(my, tenant_id)
961 if result != 0:
962 bottle.abort(result, content)
963 #parse input data
964 http_content = format_in( flavor_update_schema )
965 r = remove_extra_items(http_content, flavor_update_schema)
966 if r is not None: print "http_put_flavor_id: Warning: remove extra items ", r
967 change_keys_http2db(http_content['flavor'], http2db_flavor)
968 extended_dict = http_content['flavor'].pop('extended', None)
969 if extended_dict is not None:
970 result, content = check_extended(extended_dict)
971 if result<0:
972 print "http_put_flavor_id wrong input extended error %d %s" % (result, content)
973 bottle.abort(-result, content)
974 return
975 convert_bandwidth(extended_dict)
976 if 'devices' in extended_dict: change_keys_http2db(extended_dict['devices'], http2db_flavor)
977 http_content['flavor']['extended'] = json.dumps(extended_dict)
978 #Ensure that flavor exist
979 where_={'uuid': flavor_id}
980 if tenant_id=='any':
981 from_ ='flavors'
982 else:
983 from_ ='tenants_flavors as ti inner join flavors as i on ti.flavor_id=i.uuid'
984 where_['tenant_id'] = tenant_id
985 result, content = my.db.get_table(SELECT=('public',), FROM=from_, WHERE=where_)
986 if result==0:
987 text_error="Flavor '%s' not found" % flavor_id
988 if tenant_id!='any':
989 text_error +=" for tenant '%s'" % flavor_id
990 bottle.abort(HTTP_Not_Found, text_error)
991 return
992 elif result>0:
993 if content[0]['public']=='yes' and not my.admin:
994 #allow only modifications over private flavors
995 bottle.abort(HTTP_Unauthorized, "Needed admin rights to edit a public flavor")
996 return
997 #insert in data base
998 result, content = my.db.update_rows('flavors', http_content['flavor'], {'uuid': flavor_id})
999
1000 if result < 0:
1001 print "http_put_flavor_id error %d %s" % (result, content)
1002 bottle.abort(-result, content)
1003 return
1004 else:
1005 return http_get_flavor_id(tenant_id, flavor_id)
1006
1007
1008
1009 #
1010 # IMAGES
1011 #
1012
1013 @bottle.route(url_base + '/<tenant_id>/images', method='GET')
1014 def http_get_images(tenant_id):
1015 my = config_dic['http_threads'][ threading.current_thread().name ]
1016 #check valid tenant_id
1017 result,content = check_valid_tenant(my, tenant_id)
1018 if result != 0:
1019 bottle.abort(result, content)
1020 #obtain data
1021 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_image,
1022 ('id','name','description','path','public') )
1023 if tenant_id=='any':
1024 from_ ='images'
1025 else:
1026 from_ ='tenants_images inner join images on tenants_images.image_id=images.uuid'
1027 where_['tenant_id'] = tenant_id
1028 result, content = my.db.get_table(SELECT=select_, FROM=from_, WHERE=where_, LIMIT=limit_)
1029 if result < 0:
1030 print "http_get_images Error", content
1031 bottle.abort(-result, content)
1032 else:
1033 change_keys_http2db(content, http2db_image, reverse=True)
1034 #for row in content: row['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'images', str(row['id']) ) ), 'rel':'bookmark' } ]
1035 data={'images' : content}
1036 return format_out(data)
1037
1038 @bottle.route(url_base + '/<tenant_id>/images/<image_id>', method='GET')
1039 def http_get_image_id(tenant_id, image_id):
1040 my = config_dic['http_threads'][ threading.current_thread().name ]
1041 #check valid tenant_id
1042 result,content = check_valid_tenant(my, tenant_id)
1043 if result != 0:
1044 bottle.abort(result, content)
1045 #obtain data
1046 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_image,
1047 ('id','name','description','progress', 'status','path', 'created', 'updated','public') )
1048 if tenant_id=='any':
1049 from_ ='images'
1050 else:
1051 from_ ='tenants_images as ti inner join images as i on ti.image_id=i.uuid'
1052 where_['tenant_id'] = tenant_id
1053 where_['uuid'] = image_id
1054 result, content = my.db.get_table(SELECT=select_, FROM=from_, WHERE=where_, LIMIT=limit_)
1055
1056 if result < 0:
1057 print "http_get_images error %d %s" % (result, content)
1058 bottle.abort(-result, content)
1059 elif result==0:
1060 print "http_get_images image '%s' not found" % str(image_id)
1061 bottle.abort(HTTP_Not_Found, 'image %s not found' % image_id)
1062 else:
1063 convert_datetime2str(content)
1064 change_keys_http2db(content, http2db_image, reverse=True)
1065 if 'metadata' in content[0] and content[0]['metadata'] is not None:
1066 metadata = json.loads(content[0]['metadata'])
1067 content[0]['metadata']=metadata
1068 content[0]['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'images', str(content[0]['id']) ) ), 'rel':'bookmark' } ]
1069 data={'image' : content[0]}
1070 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
1071 return format_out(data)
1072
1073 @bottle.route(url_base + '/<tenant_id>/images', method='POST')
1074 def http_post_images(tenant_id):
1075 '''insert a image into the database, and attach to tenant.'''
1076 my = config_dic['http_threads'][ threading.current_thread().name ]
1077 #check valid tenant_id
1078 result,content = check_valid_tenant(my, tenant_id)
1079 if result != 0:
1080 bottle.abort(result, content)
1081 http_content = format_in(image_new_schema)
1082 r = remove_extra_items(http_content, image_new_schema)
1083 if r is not None: print "http_post_images: Warning: remove extra items ", r
1084 change_keys_http2db(http_content['image'], http2db_image)
1085 metadata_dict = http_content['image'].pop('metadata', None)
1086 if metadata_dict is not None:
1087 http_content['image']['metadata'] = json.dumps(metadata_dict)
1088 #calculate checksum
1089 host_test_mode = True if config_dic['mode']=='test' or config_dic['mode']=="OF only" else False
1090 try:
1091 image_file = http_content['image'].get('path',None)
1092 if os.path.exists(image_file):
1093 http_content['image']['checksum'] = md5(image_file)
1094 else:
1095 if not host_test_mode:
1096 content = "Image file not found"
1097 print "http_post_images error: %d %s" % (HTTP_Bad_Request, content)
1098 bottle.abort(HTTP_Bad_Request, content)
1099 except Exception as e:
1100 print "ERROR. Unexpected exception: %s" % (str(e))
1101 bottle.abort(HTTP_Internal_Server_Error, type(e).__name__ + ": " + str(e))
1102 #insert in data base
1103 result, content = my.db.new_image(http_content['image'], tenant_id)
1104 if result >= 0:
1105 return http_get_image_id(tenant_id, content)
1106 else:
1107 print "http_post_images error %d %s" % (result, content)
1108 bottle.abort(-result, content)
1109 return
1110
1111 @bottle.route(url_base + '/<tenant_id>/images/<image_id>', method='DELETE')
1112 def http_delete_image_id(tenant_id, image_id):
1113 '''Deletes the image_id of a tenant. IT removes from tenants_images table.'''
1114 my = config_dic['http_threads'][ threading.current_thread().name ]
1115 #check valid tenant_id
1116 result,content = check_valid_tenant(my, tenant_id)
1117 if result != 0:
1118 bottle.abort(result, content)
1119 result, content = my.db.delete_image_flavor('image', image_id, tenant_id)
1120 if result == 0:
1121 bottle.abort(HTTP_Not_Found, content)
1122 elif result >0:
1123 data={'result' : content}
1124 return format_out(data)
1125 else:
1126 print "http_delete_image_id error",result, content
1127 bottle.abort(-result, content)
1128 return
1129
1130 @bottle.route(url_base + '/<tenant_id>/images/<image_id>/<action>', method='POST')
1131 def http_attach_detach_images(tenant_id, image_id, action):
1132 '''attach/detach an existing image in this tenant. That is insert/remove at tenants_images table.'''
1133 #TODO alf: not tested at all!!!
1134 my = config_dic['http_threads'][ threading.current_thread().name ]
1135 #check valid tenant_id
1136 result,content = check_valid_tenant(my, tenant_id)
1137 if result != 0:
1138 bottle.abort(result, content)
1139 if tenant_id=='any':
1140 bottle.abort(HTTP_Bad_Request, "Invalid tenant 'any' with this command")
1141 #check valid action
1142 if action!='attach' and action != 'detach':
1143 bottle.abort(HTTP_Method_Not_Allowed, "actions can be attach or detach")
1144 return
1145
1146 #Ensure that image exist
1147 from_ ='tenants_images as ti right join images as i on ti.image_id=i.uuid'
1148 where_={'uuid': image_id}
1149 result, content = my.db.get_table(SELECT=('public','tenant_id'), FROM=from_, WHERE=where_)
1150 if result==0:
1151 if action=='attach':
1152 text_error="Image '%s' not found" % image_id
1153 else:
1154 text_error="Image '%s' not found for tenant '%s'" % (image_id, tenant_id)
1155 bottle.abort(HTTP_Not_Found, text_error)
1156 return
1157 elif result>0:
1158 image=content[0]
1159 if action=='attach':
1160 if image['tenant_id']!=None:
1161 bottle.abort(HTTP_Conflict, "Image '%s' already attached to tenant '%s'" % (image_id, tenant_id))
1162 if image['public']=='no' and not my.admin:
1163 #allow only attaching public images
1164 bottle.abort(HTTP_Unauthorized, "Needed admin rights to attach a private image")
1165 return
1166 #insert in data base
1167 result, content = my.db.new_row('tenants_images', {'image_id':image_id, 'tenant_id': tenant_id})
1168 if result >= 0:
1169 return http_get_image_id(tenant_id, image_id)
1170 else: #detach
1171 if image['tenant_id']==None:
1172 bottle.abort(HTTP_Not_Found, "Image '%s' not attached to tenant '%s'" % (image_id, tenant_id))
1173 result, content = my.db.delete_row_by_dict(FROM='tenants_images', WHERE={'image_id':image_id, 'tenant_id':tenant_id})
1174 if result>=0:
1175 if image['public']=='no':
1176 #try to delete the image completely to avoid orphan images, IGNORE error
1177 my.db.delete_row_by_dict(FROM='images', WHERE={'uuid':image_id})
1178 data={'result' : "image detached"}
1179 return format_out(data)
1180
1181 #if get here is because an error
1182 print "http_attach_detach_images error %d %s" % (result, content)
1183 bottle.abort(-result, content)
1184 return
1185
1186 @bottle.route(url_base + '/<tenant_id>/images/<image_id>', method='PUT')
1187 def http_put_image_id(tenant_id, image_id):
1188 '''update a image_id into the database.'''
1189 my = config_dic['http_threads'][ threading.current_thread().name ]
1190 #check valid tenant_id
1191 result,content = check_valid_tenant(my, tenant_id)
1192 if result != 0:
1193 bottle.abort(result, content)
1194 #parse input data
1195 http_content = format_in( image_update_schema )
1196 r = remove_extra_items(http_content, image_update_schema)
1197 if r is not None: print "http_put_image_id: Warning: remove extra items ", r
1198 change_keys_http2db(http_content['image'], http2db_image)
1199 metadata_dict = http_content['image'].pop('metadata', None)
1200 if metadata_dict is not None:
1201 http_content['image']['metadata'] = json.dumps(metadata_dict)
1202 #Ensure that image exist
1203 where_={'uuid': image_id}
1204 if tenant_id=='any':
1205 from_ ='images'
1206 else:
1207 from_ ='tenants_images as ti inner join images as i on ti.image_id=i.uuid'
1208 where_['tenant_id'] = tenant_id
1209 result, content = my.db.get_table(SELECT=('public',), FROM=from_, WHERE=where_)
1210 if result==0:
1211 text_error="Image '%s' not found" % image_id
1212 if tenant_id!='any':
1213 text_error +=" for tenant '%s'" % image_id
1214 bottle.abort(HTTP_Not_Found, text_error)
1215 return
1216 elif result>0:
1217 if content[0]['public']=='yes' and not my.admin:
1218 #allow only modifications over private images
1219 bottle.abort(HTTP_Unauthorized, "Needed admin rights to edit a public image")
1220 return
1221 #insert in data base
1222 result, content = my.db.update_rows('images', http_content['image'], {'uuid': image_id})
1223
1224 if result < 0:
1225 print "http_put_image_id error %d %s" % (result, content)
1226 bottle.abort(-result, content)
1227 return
1228 else:
1229 return http_get_image_id(tenant_id, image_id)
1230
1231
1232 #
1233 # SERVERS
1234 #
1235
1236 @bottle.route(url_base + '/<tenant_id>/servers', method='GET')
1237 def http_get_servers(tenant_id):
1238 my = config_dic['http_threads'][ threading.current_thread().name ]
1239 result,content = check_valid_tenant(my, tenant_id)
1240 if result != 0:
1241 bottle.abort(result, content)
1242 return
1243 #obtain data
1244 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_server,
1245 ('id','name','description','hostId','imageRef','flavorRef','status', 'tenant_id') )
1246 if tenant_id!='any':
1247 where_['tenant_id'] = tenant_id
1248 result, content = my.db.get_table(SELECT=select_, FROM='instances', WHERE=where_, LIMIT=limit_)
1249 if result < 0:
1250 print "http_get_servers Error", content
1251 bottle.abort(-result, content)
1252 else:
1253 change_keys_http2db(content, http2db_server, reverse=True)
1254 for row in content:
1255 tenant_id = row.pop('tenant_id')
1256 row['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'servers', str(row['id']) ) ), 'rel':'bookmark' } ]
1257 data={'servers' : content}
1258 return format_out(data)
1259
1260 @bottle.route(url_base + '/<tenant_id>/servers/<server_id>', method='GET')
1261 def http_get_server_id(tenant_id, server_id):
1262 my = config_dic['http_threads'][ threading.current_thread().name ]
1263 #check valid tenant_id
1264 result,content = check_valid_tenant(my, tenant_id)
1265 if result != 0:
1266 bottle.abort(result, content)
1267 return
1268 #obtain data
1269 result, content = my.db.get_instance(server_id)
1270 if result == 0:
1271 bottle.abort(HTTP_Not_Found, content)
1272 elif result >0:
1273 #change image/flavor-id to id and link
1274 convert_bandwidth(content, reverse=True)
1275 convert_datetime2str(content)
1276 if content["ram"]==0 : del content["ram"]
1277 if content["vcpus"]==0 : del content["vcpus"]
1278 if 'flavor_id' in content:
1279 if content['flavor_id'] is not None:
1280 content['flavor'] = {'id':content['flavor_id'],
1281 'links':[{'href': "/".join( (my.url_preffix, content['tenant_id'], 'flavors', str(content['flavor_id']) ) ), 'rel':'bookmark'}]
1282 }
1283 del content['flavor_id']
1284 if 'image_id' in content:
1285 if content['image_id'] is not None:
1286 content['image'] = {'id':content['image_id'],
1287 'links':[{'href': "/".join( (my.url_preffix, content['tenant_id'], 'images', str(content['image_id']) ) ), 'rel':'bookmark'}]
1288 }
1289 del content['image_id']
1290 change_keys_http2db(content, http2db_server, reverse=True)
1291 if 'extended' in content:
1292 if 'devices' in content['extended']: change_keys_http2db(content['extended']['devices'], http2db_server, reverse=True)
1293
1294 data={'server' : content}
1295 return format_out(data)
1296 else:
1297 bottle.abort(-result, content)
1298 return
1299
1300 @bottle.route(url_base + '/<tenant_id>/servers', method='POST')
1301 def http_post_server_id(tenant_id):
1302 '''deploys a new server'''
1303 my = config_dic['http_threads'][ threading.current_thread().name ]
1304 #check valid tenant_id
1305 result,content = check_valid_tenant(my, tenant_id)
1306 if result != 0:
1307 bottle.abort(result, content)
1308 return
1309 if tenant_id=='any':
1310 bottle.abort(HTTP_Bad_Request, "Invalid tenant 'any' with this command")
1311 #chek input
1312 http_content = format_in( server_new_schema )
1313 r = remove_extra_items(http_content, server_new_schema)
1314 if r is not None: print "http_post_serves: Warning: remove extra items ", r
1315 change_keys_http2db(http_content['server'], http2db_server)
1316 extended_dict = http_content['server'].get('extended', None)
1317 if extended_dict is not None:
1318 result, content = check_extended(extended_dict, True)
1319 if result<0:
1320 print "http_post_servers wrong input extended error %d %s" % (result, content)
1321 bottle.abort(-result, content)
1322 return
1323 convert_bandwidth(extended_dict)
1324 if 'devices' in extended_dict: change_keys_http2db(extended_dict['devices'], http2db_server)
1325
1326 server = http_content['server']
1327 server_start = server.get('start', 'yes')
1328 server['tenant_id'] = tenant_id
1329 #check flavor valid and take info
1330 result, content = my.db.get_table(FROM='tenants_flavors as tf join flavors as f on tf.flavor_id=f.uuid',
1331 SELECT=('ram','vcpus','extended'), WHERE={'uuid':server['flavor_id'], 'tenant_id':tenant_id})
1332 if result<=0:
1333 bottle.abort(HTTP_Not_Found, 'flavor_id %s not found' % server['flavor_id'])
1334 return
1335 server['flavor']=content[0]
1336 #check image valid and take info
1337 result, content = my.db.get_table(FROM='tenants_images as ti join images as i on ti.image_id=i.uuid',
1338 SELECT=('path','metadata'), WHERE={'uuid':server['image_id'], 'tenant_id':tenant_id, "status":"ACTIVE"})
1339 if result<=0:
1340 bottle.abort(HTTP_Not_Found, 'image_id %s not found or not ACTIVE' % server['image_id'])
1341 return
1342 server['image']=content[0]
1343 if "hosts_id" in server:
1344 result, content = my.db.get_table(FROM='hosts', SELECT=('uuid',), WHERE={'uuid': server['host_id']})
1345 if result<=0:
1346 bottle.abort(HTTP_Not_Found, 'hostId %s not found' % server['host_id'])
1347 return
1348 #print json.dumps(server, indent=4)
1349
1350 result, content = ht.create_server(server, config_dic['db'], config_dic['db_lock'], config_dic['mode']=='normal')
1351
1352 if result >= 0:
1353 #Insert instance to database
1354 nets=[]
1355 print
1356 print "inserting at DB"
1357 print
1358 if server_start == 'no':
1359 content['status'] = 'INACTIVE'
1360 ports_to_free=[]
1361 new_instance_result, new_instance = my.db.new_instance(content, nets, ports_to_free)
1362 if new_instance_result < 0:
1363 print "Error http_post_servers() :", new_instance_result, new_instance
1364 bottle.abort(-new_instance_result, new_instance)
1365 return
1366 print
1367 print "inserted at DB"
1368 print
1369 for port in ports_to_free:
1370 r,c = config_dic['host_threads'][ server['host_id'] ].insert_task( 'restore-iface',*port )
1371 if r < 0:
1372 print ' http_post_servers ERROR RESTORE IFACE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1373 #updata nets
1374 for net in nets:
1375 r,c = config_dic['of_thread'].insert_task("update-net", net)
1376 if r < 0:
1377 print ':http_post_servers ERROR UPDATING NETS !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1378
1379
1380
1381 #look for dhcp ip address
1382 r2, c2 = my.db.get_table(FROM="ports", SELECT=["mac", "net_id"], WHERE={"instance_id": new_instance})
1383 if r2 >0 and config_dic.get("dhcp_server"):
1384 for iface in c2:
1385 if iface["net_id"] in config_dic["dhcp_nets"]:
1386 #print "dhcp insert add task"
1387 r,c = config_dic['dhcp_thread'].insert_task("add", iface["mac"])
1388 if r < 0:
1389 print ':http_post_servers ERROR UPDATING dhcp_server !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1390
1391 #Start server
1392
1393 server['uuid'] = new_instance
1394 #server_start = server.get('start', 'yes')
1395 if server_start != 'no':
1396 server['paused'] = True if server_start == 'paused' else False
1397 server['action'] = {"start":None}
1398 server['status'] = "CREATING"
1399 #Program task
1400 r,c = config_dic['host_threads'][ server['host_id'] ].insert_task( 'instance',server )
1401 if r<0:
1402 my.db.update_rows('instances', {'status':"ERROR"}, {'uuid':server['uuid'], 'last_error':c}, log=True)
1403
1404 return http_get_server_id(tenant_id, new_instance)
1405 else:
1406 bottle.abort(HTTP_Bad_Request, content)
1407 return
1408
1409 def http_server_action(server_id, tenant_id, action):
1410 '''Perform actions over a server as resume, reboot, terminate, ...'''
1411 my = config_dic['http_threads'][ threading.current_thread().name ]
1412 server={"uuid": server_id, "action":action}
1413 where={'uuid': server_id}
1414 if tenant_id!='any':
1415 where['tenant_id']= tenant_id
1416 result, content = my.db.get_table(FROM='instances', WHERE=where)
1417 if result == 0:
1418 bottle.abort(HTTP_Not_Found, "server %s not found" % server_id)
1419 return
1420 if result < 0:
1421 print "http_post_server_action error getting data %d %s" % (result, content)
1422 bottle.abort(HTTP_Internal_Server_Error, content)
1423 return
1424 server.update(content[0])
1425 tenant_id = server["tenant_id"]
1426
1427 #TODO check a right content
1428 new_status = None
1429 if 'terminate' in action:
1430 new_status='DELETING'
1431 elif server['status'] == 'ERROR': #or server['status'] == 'CREATING':
1432 if 'terminate' not in action and 'rebuild' not in action:
1433 bottle.abort(HTTP_Method_Not_Allowed, "Server is in ERROR status, must be rebuit or deleted ")
1434 return
1435 # elif server['status'] == 'INACTIVE':
1436 # if 'start' not in action and 'createImage' not in action:
1437 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'INACTIVE' status is 'start'")
1438 # return
1439 # if 'start' in action:
1440 # new_status='CREATING'
1441 # server['paused']='no'
1442 # elif server['status'] == 'PAUSED':
1443 # if 'resume' not in action:
1444 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'PAUSED' status is 'resume'")
1445 # return
1446 # elif server['status'] == 'ACTIVE':
1447 # if 'pause' not in action and 'reboot'not in action and 'shutoff'not in action:
1448 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'ACTIVE' status is 'pause','reboot' or 'shutoff'")
1449 # return
1450
1451 if 'start' in action or 'createImage' in action or 'rebuild' in action:
1452 #check image valid and take info
1453 image_id = server['image_id']
1454 if 'createImage' in action:
1455 if 'imageRef' in action['createImage']:
1456 image_id = action['createImage']['imageRef']
1457 elif 'disk' in action['createImage']:
1458 result, content = my.db.get_table(FROM='instance_devices',
1459 SELECT=('image_id','dev'), WHERE={'instance_id':server['uuid'],"type":"disk"})
1460 if result<=0:
1461 bottle.abort(HTTP_Not_Found, 'disk not found for server')
1462 return
1463 elif result>1:
1464 disk_id=None
1465 if action['createImage']['imageRef']['disk'] != None:
1466 for disk in content:
1467 if disk['dev'] == action['createImage']['imageRef']['disk']:
1468 disk_id = disk['image_id']
1469 break
1470 if disk_id == None:
1471 bottle.abort(HTTP_Not_Found, 'disk %s not found for server' % action['createImage']['imageRef']['disk'])
1472 return
1473 else:
1474 bottle.abort(HTTP_Not_Found, 'more than one disk found for server' )
1475 return
1476 image_id = disk_id
1477 else: #result==1
1478 image_id = content[0]['image_id']
1479
1480 result, content = my.db.get_table(FROM='tenants_images as ti join images as i on ti.image_id=i.uuid',
1481 SELECT=('path','metadata'), WHERE={'uuid':image_id, 'tenant_id':tenant_id, "status":"ACTIVE"})
1482 if result<=0:
1483 bottle.abort(HTTP_Not_Found, 'image_id %s not found or not ACTIVE' % image_id)
1484 return
1485 if content[0]['metadata'] is not None:
1486 try:
1487 metadata = json.loads(content[0]['metadata'])
1488 except:
1489 return -HTTP_Internal_Server_Error, "Can not decode image metadata"
1490 content[0]['metadata']=metadata
1491 else:
1492 content[0]['metadata'] = {}
1493 server['image']=content[0]
1494 if 'createImage' in action:
1495 action['createImage']['source'] = {'image_id': image_id, 'path': content[0]['path']}
1496 if 'createImage' in action:
1497 #Create an entry in Database for the new image
1498 new_image={'status':'BUILD', 'progress': 0 }
1499 new_image_metadata=content[0]
1500 if 'metadata' in server['image'] and server['image']['metadata'] != None:
1501 new_image_metadata.update(server['image']['metadata'])
1502 new_image_metadata = {"use_incremental":"no"}
1503 if 'metadata' in action['createImage']:
1504 new_image_metadata.update(action['createImage']['metadata'])
1505 new_image['metadata'] = json.dumps(new_image_metadata)
1506 new_image['name'] = action['createImage'].get('name', None)
1507 new_image['description'] = action['createImage'].get('description', None)
1508 new_image['uuid']=my.db.new_uuid()
1509 if 'path' in action['createImage']:
1510 new_image['path'] = action['createImage']['path']
1511 else:
1512 new_image['path']="/provisional/path/" + new_image['uuid']
1513 result, image_uuid = my.db.new_image(new_image, tenant_id)
1514 if result<=0:
1515 bottle.abort(HTTP_Bad_Request, 'Error: ' + image_uuid)
1516 return
1517 server['new_image'] = new_image
1518
1519
1520 #Program task
1521 r,c = config_dic['host_threads'][ server['host_id'] ].insert_task( 'instance',server )
1522 if r<0:
1523 print "Task queue full at host ", server['host_id']
1524 bottle.abort(HTTP_Request_Timeout, c)
1525 if 'createImage' in action and result >= 0:
1526 return http_get_image_id(tenant_id, image_uuid)
1527
1528 #Update DB only for CREATING or DELETING status
1529 data={'result' : 'in process'}
1530 if new_status != None and new_status == 'DELETING':
1531 nets=[]
1532 ports_to_free=[]
1533 #look for dhcp ip address
1534 r2, c2 = my.db.get_table(FROM="ports", SELECT=["mac", "net_id"], WHERE={"instance_id": server_id})
1535 r,c = my.db.delete_instance(server_id, tenant_id, nets, ports_to_free, "requested by http")
1536 for port in ports_to_free:
1537 r1,c1 = config_dic['host_threads'][ server['host_id'] ].insert_task( 'restore-iface',*port )
1538 if r1 < 0:
1539 print ' http_post_server_action error at server deletion ERROR resore-iface !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c1
1540 data={'result' : 'deleting in process, but ifaces cannot be restored!!!!!'}
1541 for net in nets:
1542 r1,c1 = config_dic['of_thread'].insert_task("update-net", net)
1543 if r1 < 0:
1544 print ' http_post_server_action error at server deletion ERROR UPDATING NETS !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c1
1545 data={'result' : 'deleting in process, but openflow rules cannot be deleted!!!!!'}
1546 #look for dhcp ip address
1547 if r2 >0 and config_dic.get("dhcp_server"):
1548 for iface in c2:
1549 if iface["net_id"] in config_dic["dhcp_nets"]:
1550 r,c = config_dic['dhcp_thread'].insert_task("del", iface["mac"])
1551 #print "dhcp insert del task"
1552 if r < 0:
1553 print ':http_post_servers ERROR UPDATING dhcp_server !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1554
1555 return format_out(data)
1556
1557
1558
1559 @bottle.route(url_base + '/<tenant_id>/servers/<server_id>', method='DELETE')
1560 def http_delete_server_id(tenant_id, server_id):
1561 '''delete a server'''
1562 my = config_dic['http_threads'][ threading.current_thread().name ]
1563 #check valid tenant_id
1564 result,content = check_valid_tenant(my, tenant_id)
1565 if result != 0:
1566 bottle.abort(result, content)
1567 return
1568
1569 return http_server_action(server_id, tenant_id, {"terminate":None} )
1570
1571
1572 @bottle.route(url_base + '/<tenant_id>/servers/<server_id>/action', method='POST')
1573 def http_post_server_action(tenant_id, server_id):
1574 '''take an action over a server'''
1575 my = config_dic['http_threads'][ threading.current_thread().name ]
1576 #check valid tenant_id
1577 result,content = check_valid_tenant(my, tenant_id)
1578 if result != 0:
1579 bottle.abort(result, content)
1580 return
1581 http_content = format_in( server_action_schema )
1582 #r = remove_extra_items(http_content, server_action_schema)
1583 #if r is not None: print "http_post_server_action: Warning: remove extra items ", r
1584
1585 return http_server_action(server_id, tenant_id, http_content)
1586
1587 #
1588 # NETWORKS
1589 #
1590
1591
1592 @bottle.route(url_base + '/networks', method='GET')
1593 def http_get_networks():
1594 my = config_dic['http_threads'][ threading.current_thread().name ]
1595 #obtain data
1596 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_network,
1597 ('id','name','tenant_id','type',
1598 'shared','provider:vlan','status','last_error','admin_state_up','provider:physical') )
1599 #TODO temporally remove tenant_id
1600 if "tenant_id" in where_:
1601 del where_["tenant_id"]
1602 result, content = my.db.get_table(SELECT=select_, FROM='nets', WHERE=where_, LIMIT=limit_)
1603 if result < 0:
1604 print "http_get_networks error %d %s" % (result, content)
1605 bottle.abort(-result, content)
1606 else:
1607 convert_boolean(content, ('shared', 'admin_state_up', 'enable_dhcp') )
1608 delete_nulls(content)
1609 change_keys_http2db(content, http2db_network, reverse=True)
1610 data={'networks' : content}
1611 return format_out(data)
1612
1613 @bottle.route(url_base + '/networks/<network_id>', method='GET')
1614 def http_get_network_id(network_id):
1615 my = config_dic['http_threads'][ threading.current_thread().name ]
1616 #obtain data
1617 where_ = bottle.request.query
1618 where_['uuid'] = network_id
1619 result, content = my.db.get_table(FROM='nets', WHERE=where_, LIMIT=100)
1620
1621 if result < 0:
1622 print "http_get_networks_id error %d %s" % (result, content)
1623 bottle.abort(-result, content)
1624 elif result==0:
1625 print "http_get_networks_id network '%s' not found" % network_id
1626 bottle.abort(HTTP_Not_Found, 'network %s not found' % network_id)
1627 else:
1628 convert_boolean(content, ('shared', 'admin_state_up', 'enale_dhcp') )
1629 change_keys_http2db(content, http2db_network, reverse=True)
1630 #get ports
1631 result, ports = my.db.get_table(FROM='ports', SELECT=('uuid as port_id',),
1632 WHERE={'net_id': network_id}, LIMIT=100)
1633 if len(ports) > 0:
1634 content[0]['ports'] = ports
1635 delete_nulls(content[0])
1636 data={'network' : content[0]}
1637 return format_out(data)
1638
1639 @bottle.route(url_base + '/networks', method='POST')
1640 def http_post_networks():
1641 '''insert a network into the database.'''
1642 my = config_dic['http_threads'][ threading.current_thread().name ]
1643 #parse input data
1644 http_content = format_in( network_new_schema )
1645 r = remove_extra_items(http_content, network_new_schema)
1646 if r is not None: print "http_post_networks: Warning: remove extra items ", r
1647 change_keys_http2db(http_content['network'], http2db_network)
1648 network=http_content['network']
1649 #check valid tenant_id
1650 tenant_id= network.get('tenant_id')
1651 if tenant_id!=None:
1652 result, _ = my.db.get_table(FROM='tenants', SELECT=('uuid',), WHERE={'uuid': tenant_id,"enabled":True})
1653 if result<=0:
1654 bottle.abort(HTTP_Not_Found, 'tenant %s not found or not enabled' % tenant_id)
1655 return
1656 bridge_net = None
1657 #check valid params
1658 net_provider = network.get('provider')
1659 net_type = network.get('type')
1660 net_vlan = network.get("vlan")
1661 net_bind_net = network.get("bind_net")
1662 net_bind_type= network.get("bind_type")
1663 name = network["name"]
1664
1665 #check if network name ends with :<vlan_tag> and network exist in order to make and automated bindning
1666 vlan_index =name.rfind(":")
1667 if net_bind_net==None and net_bind_type==None and vlan_index > 1:
1668 try:
1669 vlan_tag = int(name[vlan_index+1:])
1670 if vlan_tag >0 and vlan_tag < 4096:
1671 net_bind_net = name[:vlan_index]
1672 net_bind_type = "vlan:" + name[vlan_index+1:]
1673 except:
1674 pass
1675
1676 if net_bind_net != None:
1677 #look for a valid net
1678 if check_valid_uuid(net_bind_net):
1679 net_bind_key = "uuid"
1680 else:
1681 net_bind_key = "name"
1682 result, content = my.db.get_table(FROM='nets', WHERE={net_bind_key: net_bind_net} )
1683 if result<0:
1684 bottle.abort(HTTP_Internal_Server_Error, 'getting nets from db ' + content)
1685 return
1686 elif result==0:
1687 bottle.abort(HTTP_Bad_Request, "bind_net %s '%s'not found" % (net_bind_key, net_bind_net) )
1688 return
1689 elif result>1:
1690 bottle.abort(HTTP_Bad_Request, "more than one bind_net %s '%s' found, use uuid" % (net_bind_key, net_bind_net) )
1691 return
1692 network["bind_net"] = content[0]["uuid"]
1693 if net_bind_type != None:
1694 if net_bind_type[0:5] != "vlan:":
1695 bottle.abort(HTTP_Bad_Request, "bad format for 'bind_type', must be 'vlan:<tag>'")
1696 return
1697 if int(net_bind_type[5:]) > 4095 or int(net_bind_type[5:])<=0 :
1698 bottle.abort(HTTP_Bad_Request, "bad format for 'bind_type', must be 'vlan:<tag>' with a tag between 1 and 4095")
1699 return
1700 network["bind_type"] = net_bind_type
1701
1702 if net_provider!=None:
1703 if net_provider[:9]=="openflow:":
1704 if net_type!=None:
1705 if net_type!="ptp" and net_type!="data":
1706 bottle.abort(HTTP_Bad_Request, "Only 'ptp' or 'data' net types can be bound to 'openflow'")
1707 else:
1708 net_type='data'
1709 else:
1710 if net_type!=None:
1711 if net_type!="bridge_man" and net_type!="bridge_data":
1712 bottle.abort(HTTP_Bad_Request, "Only 'bridge_man' or 'bridge_data' net types can be bound to 'bridge', 'macvtap' or 'default")
1713 else:
1714 net_type='bridge_man'
1715
1716 if net_type==None:
1717 net_type='bridge_man'
1718
1719 if net_provider != None:
1720 if net_provider[:7]=='bridge:':
1721 #check it is one of the pre-provisioned bridges
1722 bridge_net_name = net_provider[7:]
1723 for brnet in config_dic['bridge_nets']:
1724 if brnet[0]==bridge_net_name: # free
1725 if brnet[3] != None:
1726 bottle.abort(HTTP_Conflict, "invalid 'provider:physical', bridge '%s' is already used" % bridge_net_name)
1727 return
1728 bridge_net=brnet
1729 net_vlan = brnet[1]
1730 break
1731 # if bridge_net==None:
1732 # bottle.abort(HTTP_Bad_Request, "invalid 'provider:physical', bridge '%s' is not one of the provisioned 'bridge_ifaces' in the configuration file" % bridge_net_name)
1733 # return
1734 elif net_type=='bridge_data' or net_type=='bridge_man':
1735 #look for a free precreated nets
1736 for brnet in config_dic['bridge_nets']:
1737 if brnet[3]==None: # free
1738 if bridge_net != None:
1739 if net_type=='bridge_man': #look for the smaller speed
1740 if brnet[2] < bridge_net[2]: bridge_net = brnet
1741 else: #look for the larger speed
1742 if brnet[2] > bridge_net[2]: bridge_net = brnet
1743 else:
1744 bridge_net = brnet
1745 net_vlan = brnet[1]
1746 if bridge_net==None:
1747 bottle.abort(HTTP_Bad_Request, "Max limits of bridge networks reached. Future versions of VIM will overcome this limit")
1748 return
1749 else:
1750 print "using net", bridge_net
1751 net_provider = "bridge:"+bridge_net[0]
1752 net_vlan = bridge_net[1]
1753 if net_vlan==None and (net_type=="data" or net_type=="ptp"):
1754 net_vlan = my.db.get_free_net_vlan()
1755 if net_vlan < 0:
1756 bottle.abort(HTTP_Internal_Server_Error, "Error getting an available vlan")
1757 return
1758
1759 network['provider'] = net_provider
1760 network['type'] = net_type
1761 network['vlan'] = net_vlan
1762 result, content = my.db.new_row('nets', network, True, True)
1763
1764 if result >= 0:
1765 if bridge_net!=None:
1766 bridge_net[3] = content
1767
1768 if config_dic.get("dhcp_server"):
1769 if network["name"] in config_dic["dhcp_server"].get("nets", () ):
1770 config_dic["dhcp_nets"].append(content)
1771 print "dhcp_server: add new net", content
1772 elif bridge_net != None and bridge_net[0] in config_dic["dhcp_server"].get("bridge_ifaces", () ):
1773 config_dic["dhcp_nets"].append(content)
1774 print "dhcp_server: add new net", content
1775 return http_get_network_id(content)
1776 else:
1777 print "http_post_networks error %d %s" % (result, content)
1778 bottle.abort(-result, content)
1779 return
1780
1781
1782 @bottle.route(url_base + '/networks/<network_id>', method='PUT')
1783 def http_put_network_id(network_id):
1784 '''update a network_id into the database.'''
1785 my = config_dic['http_threads'][ threading.current_thread().name ]
1786 #parse input data
1787 http_content = format_in( network_update_schema )
1788 r = remove_extra_items(http_content, network_update_schema)
1789 change_keys_http2db(http_content['network'], http2db_network)
1790 network=http_content['network']
1791
1792 #Look for the previous data
1793 where_ = {'uuid': network_id}
1794 result, network_old = my.db.get_table(FROM='nets', WHERE=where_)
1795 if result < 0:
1796 print "http_put_network_id error %d %s" % (result, network_old)
1797 bottle.abort(-result, network_old)
1798 return
1799 elif result==0:
1800 print "http_put_network_id network '%s' not found" % network_id
1801 bottle.abort(HTTP_Not_Found, 'network %s not found' % network_id)
1802 return
1803 #get ports
1804 nbports, content = my.db.get_table(FROM='ports', SELECT=('uuid as port_id',),
1805 WHERE={'net_id': network_id}, LIMIT=100)
1806 if result < 0:
1807 print "http_put_network_id error %d %s" % (result, network_old)
1808 bottle.abort(-result, content)
1809 return
1810 if nbports>0:
1811 if 'type' in network and network['type'] != network_old[0]['type']:
1812 bottle.abort(HTTP_Method_Not_Allowed, "Can not change type of network while having ports attached")
1813 if 'vlan' in network and network['vlan'] != network_old[0]['vlan']:
1814 bottle.abort(HTTP_Method_Not_Allowed, "Can not change vlan of network while having ports attached")
1815
1816 #check valid params
1817 net_provider = network.get('provider', network_old[0]['provider'])
1818 net_type = network.get('type', network_old[0]['type'])
1819 net_bind_net = network.get("bind_net")
1820 net_bind_type= network.get("bind_type")
1821 if net_bind_net != None:
1822 #look for a valid net
1823 if check_valid_uuid(net_bind_net):
1824 net_bind_key = "uuid"
1825 else:
1826 net_bind_key = "name"
1827 result, content = my.db.get_table(FROM='nets', WHERE={net_bind_key: net_bind_net} )
1828 if result<0:
1829 bottle.abort(HTTP_Internal_Server_Error, 'getting nets from db ' + content)
1830 return
1831 elif result==0:
1832 bottle.abort(HTTP_Bad_Request, "bind_net %s '%s'not found" % (net_bind_key, net_bind_net) )
1833 return
1834 elif result>1:
1835 bottle.abort(HTTP_Bad_Request, "more than one bind_net %s '%s' found, use uuid" % (net_bind_key, net_bind_net) )
1836 return
1837 network["bind_net"] = content[0]["uuid"]
1838 if net_bind_type != None:
1839 if net_bind_type[0:5] != "vlan:":
1840 bottle.abort(HTTP_Bad_Request, "bad format for 'bind_type', must be 'vlan:<tag>'")
1841 return
1842 if int(net_bind_type[5:]) > 4095 or int(net_bind_type[5:])<=0 :
1843 bottle.abort(HTTP_Bad_Request, "bad format for 'bind_type', must be 'vlan:<tag>' with a tag between 1 and 4095")
1844 return
1845 if net_provider!=None:
1846 if net_provider[:9]=="openflow:":
1847 if net_type!="ptp" and net_type!="data":
1848 bottle.abort(HTTP_Bad_Request, "Only 'ptp' or 'data' net types can be bound to 'openflow'")
1849 else:
1850 if net_type!="bridge_man" and net_type!="bridge_data":
1851 bottle.abort(HTTP_Bad_Request, "Only 'bridge_man' or 'bridge_data' net types can be bound to 'bridge', 'macvtap' or 'default")
1852
1853 #insert in data base
1854 result, content = my.db.update_rows('nets', network, WHERE={'uuid': network_id}, log=True )
1855 if result >= 0:
1856 if result>0: # and nbports>0 and 'admin_state_up' in network and network['admin_state_up'] != network_old[0]['admin_state_up']:
1857 r,c = config_dic['of_thread'].insert_task("update-net", network_id)
1858 if r < 0:
1859 print "http_put_network_id error while launching openflow rules"
1860 bottle.abort(HTTP_Internal_Server_Error, c)
1861 if config_dic.get("dhcp_server"):
1862 if network_id in config_dic["dhcp_nets"]:
1863 config_dic["dhcp_nets"].remove(network_id)
1864 print "dhcp_server: delete net", network_id
1865 if network.get("name", network_old["name"]) in config_dic["dhcp_server"].get("nets", () ):
1866 config_dic["dhcp_nets"].append(network_id)
1867 print "dhcp_server: add new net", network_id
1868 else:
1869 net_bind = network.get("bind", network_old["bind"] )
1870 if net_bind and net_bind[:7]=="bridge:" and net_bind[7:] in config_dic["dhcp_server"].get("bridge_ifaces", () ):
1871 config_dic["dhcp_nets"].append(network_id)
1872 print "dhcp_server: add new net", network_id
1873 return http_get_network_id(network_id)
1874 else:
1875 bottle.abort(-result, content)
1876 return
1877
1878
1879 @bottle.route(url_base + '/networks/<network_id>', method='DELETE')
1880 def http_delete_network_id(network_id):
1881 '''delete a network_id from the database.'''
1882 my = config_dic['http_threads'][ threading.current_thread().name ]
1883
1884 #delete from the data base
1885 result, content = my.db.delete_row('nets', network_id )
1886
1887 if result == 0:
1888 bottle.abort(HTTP_Not_Found, content)
1889 elif result >0:
1890 for brnet in config_dic['bridge_nets']:
1891 if brnet[3]==network_id:
1892 brnet[3]=None
1893 break
1894 if config_dic.get("dhcp_server") and network_id in config_dic["dhcp_nets"]:
1895 config_dic["dhcp_nets"].remove(network_id)
1896 print "dhcp_server: delete net", network_id
1897 data={'result' : content}
1898 return format_out(data)
1899 else:
1900 print "http_delete_network_id error",result, content
1901 bottle.abort(-result, content)
1902 return
1903 #
1904 # OPENFLOW
1905 #
1906 @bottle.route(url_base + '/networks/<network_id>/openflow', method='GET')
1907 def http_get_openflow_id(network_id):
1908 '''To obtain the list of openflow rules of a network
1909 '''
1910 my = config_dic['http_threads'][ threading.current_thread().name ]
1911 #ignore input data
1912 if network_id=='all':
1913 where_={}
1914 else:
1915 where_={"net_id": network_id}
1916 result, content = my.db.get_table(SELECT=("name","net_id","priority","vlan_id","ingress_port","src_mac","dst_mac","actions"),
1917 WHERE=where_, FROM='of_flows')
1918 if result < 0:
1919 bottle.abort(-result, content)
1920 return
1921 data={'openflow-rules' : content}
1922 return format_out(data)
1923
1924 @bottle.route(url_base + '/networks/<network_id>/openflow', method='PUT')
1925 def http_put_openflow_id(network_id):
1926 '''To make actions over the net. The action is to reinstall the openflow rules
1927 network_id can be 'all'
1928 '''
1929 my = config_dic['http_threads'][ threading.current_thread().name ]
1930 if not my.admin:
1931 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
1932 return
1933 #ignore input data
1934 if network_id=='all':
1935 where_={}
1936 else:
1937 where_={"uuid": network_id}
1938 result, content = my.db.get_table(SELECT=("uuid","type"), WHERE=where_, FROM='nets')
1939 if result < 0:
1940 bottle.abort(-result, content)
1941 return
1942
1943 for net in content:
1944 if net["type"]!="ptp" and net["type"]!="data":
1945 result-=1
1946 continue
1947 r,c = config_dic['of_thread'].insert_task("update-net", net['uuid'])
1948 if r < 0:
1949 print "http_put_openflow_id error while launching openflow rules"
1950 bottle.abort(HTTP_Internal_Server_Error, c)
1951 data={'result' : str(result)+" nets updates"}
1952 return format_out(data)
1953
1954 @bottle.route(url_base + '/networks/openflow/clear', method='DELETE')
1955 @bottle.route(url_base + '/networks/clear/openflow', method='DELETE')
1956 def http_clear_openflow_rules():
1957 '''To make actions over the net. The action is to delete ALL openflow rules
1958 '''
1959 my = config_dic['http_threads'][ threading.current_thread().name ]
1960 if not my.admin:
1961 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
1962 return
1963 #ignore input data
1964 r,c = config_dic['of_thread'].insert_task("clear-all")
1965 if r < 0:
1966 print "http_delete_openflow_id error while launching openflow rules"
1967 bottle.abort(HTTP_Internal_Server_Error, c)
1968 return
1969
1970 data={'result' : " Clearing openflow rules in process"}
1971 return format_out(data)
1972
1973 @bottle.route(url_base + '/networks/openflow/ports', method='GET')
1974 def http_get_openflow_ports():
1975 '''Obtain switch ports names of openflow controller
1976 '''
1977 data={'ports' : config_dic['of_thread'].OF_connector.pp2ofi}
1978 return format_out(data)
1979
1980
1981 #
1982 # PORTS
1983 #
1984
1985 @bottle.route(url_base + '/ports', method='GET')
1986 def http_get_ports():
1987 #obtain data
1988 my = config_dic['http_threads'][ threading.current_thread().name ]
1989 select_,where_,limit_ = filter_query_string(bottle.request.query, http2db_port,
1990 ('id','name','tenant_id','network_id','vpci','mac_address','device_owner','device_id',
1991 'binding:switch_port','binding:vlan','bandwidth','status','admin_state_up','ip_address') )
1992 #result, content = my.db.get_ports(where_)
1993 result, content = my.db.get_table(SELECT=select_, WHERE=where_, FROM='ports',LIMIT=limit_)
1994 if result < 0:
1995 print "http_get_ports Error", result, content
1996 bottle.abort(-result, content)
1997 return
1998 else:
1999 convert_boolean(content, ('admin_state_up',) )
2000 delete_nulls(content)
2001 change_keys_http2db(content, http2db_port, reverse=True)
2002 data={'ports' : content}
2003 return format_out(data)
2004
2005 @bottle.route(url_base + '/ports/<port_id>', method='GET')
2006 def http_get_port_id(port_id):
2007 my = config_dic['http_threads'][ threading.current_thread().name ]
2008 #obtain data
2009 result, content = my.db.get_table(WHERE={'uuid': port_id}, FROM='ports')
2010 if result < 0:
2011 print "http_get_ports error", result, content
2012 bottle.abort(-result, content)
2013 elif result==0:
2014 print "http_get_ports port '%s' not found" % str(port_id)
2015 bottle.abort(HTTP_Not_Found, 'port %s not found' % port_id)
2016 else:
2017 convert_boolean(content, ('admin_state_up',) )
2018 delete_nulls(content)
2019 change_keys_http2db(content, http2db_port, reverse=True)
2020 data={'port' : content[0]}
2021 return format_out(data)
2022
2023
2024 @bottle.route(url_base + '/ports', method='POST')
2025 def http_post_ports():
2026 '''insert an external port into the database.'''
2027 my = config_dic['http_threads'][ threading.current_thread().name ]
2028 if not my.admin:
2029 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
2030 #parse input data
2031 http_content = format_in( port_new_schema )
2032 r = remove_extra_items(http_content, port_new_schema)
2033 if r is not None: print "http_post_ports: Warning: remove extra items ", r
2034 change_keys_http2db(http_content['port'], http2db_port)
2035 port=http_content['port']
2036
2037 port['type'] = 'external'
2038 if 'net_id' in port and port['net_id'] == None:
2039 del port['net_id']
2040
2041 if 'net_id' in port:
2042 #check that new net has the correct type
2043 result, new_net = my.db.check_target_net(port['net_id'], None, 'external' )
2044 if result < 0:
2045 bottle.abort(HTTP_Bad_Request, new_net)
2046 return
2047 #insert in data base
2048 result, uuid = my.db.new_row('ports', port, True, True)
2049 if result > 0:
2050 if 'net_id' in port:
2051 r,c = config_dic['of_thread'].insert_task("update-net", port['net_id'])
2052 if r < 0:
2053 print "http_post_ports error while launching openflow rules"
2054 bottle.abort(HTTP_Internal_Server_Error, c)
2055 return http_get_port_id(uuid)
2056 else:
2057 bottle.abort(-result, uuid)
2058 return
2059
2060 @bottle.route(url_base + '/ports/<port_id>', method='PUT')
2061 def http_put_port_id(port_id):
2062 '''update a port_id into the database.'''
2063
2064 my = config_dic['http_threads'][ threading.current_thread().name ]
2065 #parse input data
2066 http_content = format_in( port_update_schema )
2067 change_keys_http2db(http_content['port'], http2db_port)
2068 port_dict=http_content['port']
2069
2070 #Look for the previous port data
2071 where_ = {'uuid': port_id}
2072 result, content = my.db.get_table(FROM="ports",WHERE=where_)
2073 if result < 0:
2074 print "http_put_port_id error", result, content
2075 bottle.abort(-result, content)
2076 return
2077 elif result==0:
2078 print "http_put_port_id port '%s' not found" % port_id
2079 bottle.abort(HTTP_Not_Found, 'port %s not found' % port_id)
2080 return
2081 print port_dict
2082 for k in ('vlan','switch_port','mac_address', 'tenant_id'):
2083 if k in port_dict and not my.admin:
2084 bottle.abort(HTTP_Unauthorized, "Needed admin privileges for changing " + k)
2085 return
2086
2087 port=content[0]
2088 #change_keys_http2db(port, http2db_port, reverse=True)
2089 nets = []
2090 host_id = None
2091 result=1
2092 if 'net_id' in port_dict:
2093 #change of net.
2094 old_net = port.get('net_id', None)
2095 new_net = port_dict['net_id']
2096 if old_net != new_net:
2097
2098 if new_net is not None: nets.append(new_net) #put first the new net, so that new openflow rules are created before removing the old ones
2099 if old_net is not None: nets.append(old_net)
2100 if port['type'] == 'instance:bridge':
2101 bottle.abort(HTTP_Forbidden, "bridge interfaces cannot be attached to a different net")
2102 return
2103 elif port['type'] == 'external':
2104 if not my.admin:
2105 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
2106 return
2107 else:
2108 if new_net != None:
2109 #check that new net has the correct type
2110 result, new_net_dict = my.db.check_target_net(new_net, None, port['type'] )
2111
2112 #change VLAN for SR-IOV ports
2113 if result>=0 and port["type"]=="instance:data" and port["model"]=="VF": #TODO consider also VFnotShared
2114 if new_net == None:
2115 port_dict["vlan"] = None
2116 else:
2117 port_dict["vlan"] = new_net_dict["vlan"]
2118 #get host where this VM is allocated
2119 result, content = my.db.get_table(FROM="instances",WHERE={"uuid":port["instance_id"]})
2120 if result<0:
2121 print "http_put_port_id database error", content
2122 elif result>0:
2123 host_id = content[0]["host_id"]
2124
2125 #insert in data base
2126 if result >= 0:
2127 result, content = my.db.update_rows('ports', port_dict, WHERE={'uuid': port_id}, log=False )
2128
2129 #Insert task to complete actions
2130 if result > 0:
2131 for net_id in nets:
2132 r,v = config_dic['of_thread'].insert_task("update-net", net_id)
2133 if r<0: print "Error ********* http_put_port_id update_of_flows: ", v
2134 #TODO Do something if fails
2135 if host_id != None:
2136 config_dic['host_threads'][host_id].insert_task("edit-iface", port_id, old_net, new_net)
2137
2138 if result >= 0:
2139 return http_get_port_id(port_id)
2140 else:
2141 bottle.abort(HTTP_Bad_Request, content)
2142 return
2143
2144
2145 @bottle.route(url_base + '/ports/<port_id>', method='DELETE')
2146 def http_delete_port_id(port_id):
2147 '''delete a port_id from the database.'''
2148 my = config_dic['http_threads'][ threading.current_thread().name ]
2149 if not my.admin:
2150 bottle.abort(HTTP_Unauthorized, "Needed admin privileges")
2151 return
2152
2153 #Look for the previous port data
2154 where_ = {'uuid': port_id, "type": "external"}
2155 result, ports = my.db.get_table(WHERE=where_, FROM='ports',LIMIT=100)
2156
2157 if result<=0:
2158 print "http_delete_port_id port '%s' not found" % port_id
2159 bottle.abort(HTTP_Not_Found, 'port %s not found or device_owner is not external' % port_id)
2160 return
2161 #delete from the data base
2162 result, content = my.db.delete_row('ports', port_id )
2163
2164 if result == 0:
2165 bottle.abort(HTTP_Not_Found, content)
2166 elif result >0:
2167 network = ports[0].get('net_id', None)
2168 if network is not None:
2169 #change of net.
2170 r,c = config_dic['of_thread'].insert_task("update-net", network)
2171 if r<0: print "!!!!!! http_delete_port_id update_of_flows error", r, c
2172 data={'result' : content}
2173 return format_out(data)
2174 else:
2175 print "http_delete_port_id error",result, content
2176 bottle.abort(-result, content)
2177 return
2178