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