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