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