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