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