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