1 # -*- coding: utf-8 -*-
4 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
5 # This file is part of openvim
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
12 # http://www.apache.org/licenses/LICENSE-2.0
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
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: nfvlabs@tid.es
25 This is the thread for the http server North API.
26 Two thread will be launched, with normal and administrative permissions.
29 __author__
="Alfonso Tierno, Gerardo Garcia"
30 __date__
="$10-jul-2014 12:07:15$"
41 #import only if needed because not needed in test mode. To allow an easier installation import RADclass
42 from jsonschema
import validate
as js_v
, exceptions
as js_e
43 import host_thread
as ht
44 from vim_schema
import host_new_schema
, host_edit_schema
, tenant_new_schema
, \
46 flavor_new_schema
, flavor_update_schema
, \
47 image_new_schema
, image_update_schema
, \
48 server_new_schema
, server_action_schema
, network_new_schema
, network_update_schema
, \
49 port_new_schema
, port_update_schema
54 global RADclass_module
55 RADclass
=None #RADclass module is charged only if not in test mode
59 HTTP_Bad_Request
= 400
60 HTTP_Unauthorized
= 401
63 HTTP_Method_Not_Allowed
= 405
64 HTTP_Not_Acceptable
= 406
65 HTTP_Request_Timeout
= 408
67 HTTP_Service_Unavailable
= 503
68 HTTP_Internal_Server_Error
= 500
71 hash_md5
= hashlib
.md5()
72 with
open(fname
, "rb") as f
:
73 for chunk
in iter(lambda: f
.read(4096), b
""):
74 hash_md5
.update(chunk
)
75 return hash_md5
.hexdigest()
77 def md5_string(fname
):
78 hash_md5
= hashlib
.md5()
79 hash_md5
.update(fname
)
80 return hash_md5
.hexdigest()
82 def check_extended(extended
, allow_net_attach
=False):
83 '''Makes and extra checking of extended input that cannot be done using jsonschema
85 allow_net_attach: for allowing or not the uuid field at interfaces
86 that are allowed for instance, but not for flavors
87 Return: (<0, error_text) if error; (0,None) if not error '''
88 if "numas" not in extended
: return 0, None
91 for numa
in extended
["numas"]:
95 if "cores-id" in numa
:
96 if len(numa
["cores-id"]) != numa
["cores"]:
97 return -HTTP_Bad_Request
, "different number of cores-id (%d) than cores (%d) at numa %d" % (len(numa
["cores-id"]), numa
["cores"],numaid
)
98 id_s
.extend(numa
["cores-id"])
101 if "threads-id" in numa
:
102 if len(numa
["threads-id"]) != numa
["threads"]:
103 return -HTTP_Bad_Request
, "different number of threads-id (%d) than threads (%d) at numa %d" % (len(numa
["threads-id"]), numa
["threads"],numaid
)
104 id_s
.extend(numa
["threads-id"])
105 if "paired-threads" in numa
:
107 if "paired-threads-id" in numa
:
108 if len(numa
["paired-threads-id"]) != numa
["paired-threads"]:
109 return -HTTP_Bad_Request
, "different number of paired-threads-id (%d) than paired-threads (%d) at numa %d" % (len(numa
["paired-threads-id"]), numa
["paired-threads"],numaid
)
110 for pair
in numa
["paired-threads-id"]:
112 return -HTTP_Bad_Request
, "paired-threads-id must contain a list of two elements list at numa %d" % (numaid
)
115 return -HTTP_Service_Unavailable
, "only one of cores, threads, paired-threads are allowed in this version at numa %d" % numaid
117 if "interfaces" in numa
:
121 for interface
in numa
["interfaces"]:
122 if "uuid" in interface
and not allow_net_attach
:
123 return -HTTP_Bad_Request
, "uuid field is not allowed at numa %d interface %s position %d" % (numaid
, interface
.get("name",""), ifaceid
)
124 if "mac_address" in interface
and interface
["dedicated"]=="yes":
125 return -HTTP_Bad_Request
, "mac_address can not be set for dedicated (passthrough) at numa %d, interface %s position %d" % (numaid
, interface
.get("name",""), ifaceid
)
126 if "name" in interface
:
127 if interface
["name"] in names
:
128 return -HTTP_Bad_Request
, "name repeated at numa %d, interface %s position %d" % (numaid
, interface
.get("name",""), ifaceid
)
129 names
.append(interface
["name"])
130 if "vpci" in interface
:
131 if interface
["vpci"] in vpcis
:
132 return -HTTP_Bad_Request
, "vpci %s repeated at numa %d, interface %s position %d" % (interface
["vpci"], numaid
, interface
.get("name",""), ifaceid
)
133 vpcis
.append(interface
["vpci"])
137 return -HTTP_Service_Unavailable
, "only one numa can be defined in this version "
138 for a
in range(0,len(id_s
)):
140 return -HTTP_Bad_Request
, "core/thread identifiers must start at 0 and gaps are not alloed. Missing id number %d" % a
145 # dictionaries that change from HTTP API to database naming
147 http2db_host
={'id':'uuid'}
148 http2db_tenant
={'id':'uuid'}
149 http2db_flavor
={'id':'uuid','imageRef':'image_id'}
150 http2db_image
={'id':'uuid', 'created':'created_at', 'updated':'modified_at', 'public': 'public'}
151 http2db_server
={'id':'uuid','hostId':'host_id','flavorRef':'flavor_id','imageRef':'image_id','created':'created_at'}
152 http2db_network
={'id':'uuid','provider:vlan':'vlan', 'provider:physical': 'provider'}
153 http2db_port
={'id':'uuid', 'network_id':'net_id', 'mac_address':'mac', 'device_owner':'type','device_id':'instance_id','binding:switch_port':'switch_port','binding:vlan':'vlan', 'bandwidth':'Mbps'}
155 def remove_extra_items(data
, schema
):
157 if type(data
) is tuple or type(data
) is list:
159 a
= remove_extra_items(d
, schema
['items'])
160 if a
is not None: deleted
.append(a
)
161 elif type(data
) is dict:
162 for k
in data
.keys():
163 if 'properties' not in schema
or k
not in schema
['properties'].keys():
167 a
= remove_extra_items(data
[k
], schema
['properties'][k
])
168 if a
is not None: deleted
.append({k
:a
})
169 if len(deleted
) == 0: return None
170 elif len(deleted
) == 1: return deleted
[0]
173 def delete_nulls(var
):
174 if type(var
) is dict:
176 if var
[k
] is None: del var
[k
]
177 elif type(var
[k
]) is dict or type(var
[k
]) is list or type(var
[k
]) is tuple:
178 if delete_nulls(var
[k
]): del var
[k
]
179 if len(var
) == 0: return True
180 elif type(var
) is list or type(var
) is tuple:
182 if type(k
) is dict: delete_nulls(k
)
183 if len(var
) == 0: return True
187 class httpserver(threading
.Thread
):
188 def __init__(self
, db_conn
, name
="http", host
='localhost', port
=8080, admin
=False, config_
=None):
190 Creates a new thread to attend the http connections
192 db_conn: database connection
193 name: name of this thread
194 host: ip or name where to listen
195 port: port where to listen
196 admin: if this has privileges of administrator or not
197 config_: unless the first thread must be provided. It is a global dictionary where to allocate the self variable
203 if config_
is not None:
205 if 'http_threads' not in config_dic
:
206 config_dic
['http_threads'] = {}
207 threading
.Thread
.__init
__(self
)
212 if name
in config_dic
:
213 print "httpserver Warning!!! Onether thread with the same name", name
215 while name
+str(n
) in config_dic
:
219 self
.url_preffix
= 'http://' + self
.host
+ ':' + str(self
.port
) + url_base
220 config_dic
['http_threads'][name
] = self
222 #Ensure that when the main program exits the thread will also exit
227 bottle
.run(host
=self
.host
, port
=self
.port
, debug
=True) #quiet=True
229 def gethost(self
, host_id
):
230 result
, content
= self
.db
.get_host(host_id
)
232 print "httpserver.gethost error %d %s" % (result
, content
)
233 bottle
.abort(-result
, content
)
235 print "httpserver.gethost host '%s' not found" % host_id
236 bottle
.abort(HTTP_Not_Found
, content
)
238 data
={'host' : content
}
239 convert_boolean(content
, ('admin_state_up',) )
240 change_keys_http2db(content
, http2db_host
, reverse
=True)
242 return format_out(data
)
244 @bottle.route(url_base
+ '/', method
='GET')
247 return 'works' #TODO: put links or redirection to /openvim???
253 def change_keys_http2db(data
, http_db
, reverse
=False):
254 '''Change keys of dictionary data according to the key_dict values
255 This allow change from http interface names to database names.
256 When reverse is True, the change is otherwise
258 data: can be a dictionary or a list
259 http_db: is a dictionary with hhtp names as keys and database names as value
260 reverse: by default change is done from http API to database. If True change is done otherwise
261 Return: None, but data is modified'''
262 if type(data
) is tuple or type(data
) is list:
264 change_keys_http2db(d
, http_db
, reverse
)
265 elif type(data
) is dict or type(data
) is bottle
.FormsDict
:
267 for k
,v
in http_db
.items():
268 if v
in data
: data
[k
]=data
.pop(v
)
270 for k
,v
in http_db
.items():
271 if k
in data
: data
[v
]=data
.pop(k
)
275 def format_out(data
):
276 '''return string of dictionary data according to requested json, yaml, xml. By default json'''
277 if 'application/yaml' in bottle
.request
.headers
.get('Accept'):
278 bottle
.response
.content_type
='application/yaml'
279 return yaml
.safe_dump(data
, explicit_start
=True, indent
=4, default_flow_style
=False, tags
=False, encoding
='utf-8', allow_unicode
=True) #, canonical=True, default_style='"'
280 else: #by default json
281 bottle
.response
.content_type
='application/json'
282 #return data #json no style
283 return json
.dumps(data
, indent
=4) + "\n"
285 def format_in(schema
):
287 error_text
= "Invalid header format "
288 format_type
= bottle
.request
.headers
.get('Content-Type', 'application/json')
289 if 'application/json' in format_type
:
290 error_text
= "Invalid json format "
291 #Use the json decoder instead of bottle decoder because it informs about the location of error formats with a ValueError exception
292 client_data
= json
.load(bottle
.request
.body
)
293 #client_data = bottle.request.json()
294 elif 'application/yaml' in format_type
:
295 error_text
= "Invalid yaml format "
296 client_data
= yaml
.load(bottle
.request
.body
)
297 elif format_type
== 'application/xml':
298 bottle
.abort(501, "Content-Type: application/xml not supported yet.")
300 print "HTTP HEADERS: " + str(bottle
.request
.headers
.items())
301 bottle
.abort(HTTP_Not_Acceptable
, 'Content-Type ' + str(format_type
) + ' not supported.')
303 #if client_data == None:
304 # bottle.abort(HTTP_Bad_Request, "Content error, empty")
308 #print "HTTP input data: ", str(client_data)
309 error_text
= "Invalid content "
310 js_v(client_data
, schema
)
313 except (ValueError, yaml
.YAMLError
) as exc
:
314 error_text
+= str(exc
)
316 bottle
.abort(HTTP_Bad_Request
, error_text
)
317 except js_e
.ValidationError
as exc
:
318 print "HTTP validate_in error, jsonschema exception ", exc
.message
, "at", exc
.path
319 print " CONTENT: " + str(bottle
.request
.body
.readlines())
321 if len(exc
.path
)>0: error_pos
=" at '" + ":".join(map(str, exc
.path
)) + "'"
322 bottle
.abort(HTTP_Bad_Request
, error_text
+ error_pos
+": "+exc
.message
)
324 # bottle.abort(HTTP_Bad_Request, "Content error: Failed to parse Content-Type", error_pos)
327 def filter_query_string(qs
, http2db
, allowed
):
328 '''Process query string (qs) checking that contains only valid tokens for avoiding SQL injection
330 'qs': bottle.FormsDict variable to be processed. None or empty is considered valid
331 'allowed': list of allowed string tokens (API http naming). All the keys of 'qs' must be one of 'allowed'
332 'http2db': dictionary with change from http API naming (dictionary key) to database naming(dictionary value)
333 Return: A tuple with the (select,where,limit) to be use in a database query. All of then transformed to the database naming
334 select: list of items to retrieve, filtered by query string 'field=token'. If no 'field' is present, allowed list is returned
335 where: dictionary with key, value, taken from the query string token=value. Empty if nothing is provided
336 limit: limit dictated by user with the query string 'limit'. 100 by default
337 abort if not permitted, using bottel.abort
342 if type(qs
) is not bottle
.FormsDict
:
343 print '!!!!!!!!!!!!!!invalid query string not a dictionary'
344 #bottle.abort(HTTP_Internal_Server_Error, "call programmer")
348 select
+= qs
.getall(k
)
351 bottle
.abort(HTTP_Bad_Request
, "Invalid query string at 'field="+v
+"'")
356 bottle
.abort(HTTP_Bad_Request
, "Invalid query string at 'limit="+qs
[k
]+"'")
359 bottle
.abort(HTTP_Bad_Request
, "Invalid query string at '"+k
+"="+qs
[k
]+"'")
360 if qs
[k
]!="null": where
[k
]=qs
[k
]
362 if len(select
)==0: select
+= allowed
363 #change from http api to database naming
364 for i
in range(0,len(select
)):
367 select
[i
] = http2db
[k
]
368 change_keys_http2db(where
, http2db
)
369 #print "filter_query_string", select,where,limit
371 return select
,where
,limit
374 def convert_bandwidth(data
, reverse
=False):
375 '''Check the field bandwidth recursively and when found, it removes units and convert to number
376 It assumes that bandwidth is well formed
378 'data': dictionary bottle.FormsDict variable to be checked. None or empty is considered valid
379 'reverse': by default convert form str to int (Mbps), if True it convert from number to units
383 if type(data
) is dict:
384 for k
in data
.keys():
385 if type(data
[k
]) is dict or type(data
[k
]) is tuple or type(data
[k
]) is list:
386 convert_bandwidth(data
[k
], reverse
)
387 if "bandwidth" in data
:
389 value
=str(data
["bandwidth"])
391 pos
= value
.find("bps")
393 if value
[pos
-1]=="G": data
["bandwidth"] = int(data
["bandwidth"][:pos
-1]) * 1000
394 elif value
[pos
-1]=="k": data
["bandwidth"]= int(data
["bandwidth"][:pos
-1]) / 1000
395 else: data
["bandwidth"]= int(data
["bandwidth"][:pos
-1])
397 value
= int(data
["bandwidth"])
398 if value
% 1000 == 0: data
["bandwidth"]=str(value
/1000) + " Gbps"
399 else: data
["bandwidth"]=str(value
) + " Mbps"
401 print "convert_bandwidth exception for type", type(data
["bandwidth"]), " data", data
["bandwidth"]
403 if type(data
) is tuple or type(data
) is list:
405 if type(k
) is dict or type(k
) is tuple or type(k
) is list:
406 convert_bandwidth(k
, reverse
)
408 def convert_boolean(data
, items
):
409 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
410 It assumes that bandwidth is well formed
412 'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
413 'items': tuple of keys to convert
417 if type(data
) is dict:
418 for k
in data
.keys():
419 if type(data
[k
]) is dict or type(data
[k
]) is tuple or type(data
[k
]) is list:
420 convert_boolean(data
[k
], items
)
422 if type(data
[k
]) is str:
423 if data
[k
]=="false": data
[k
]=False
424 elif data
[k
]=="true": data
[k
]=True
425 if type(data
) is tuple or type(data
) is list:
427 if type(k
) is dict or type(k
) is tuple or type(k
) is list:
428 convert_boolean(k
, items
)
430 def convert_datetime2str(var
):
431 '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s'
432 It enters recursively in the dict var finding this kind of variables
434 if type(var
) is dict:
435 for k
,v
in var
.items():
436 if type(v
) is datetime
.datetime
:
437 var
[k
]= v
.strftime('%Y-%m-%dT%H:%M:%S')
438 elif type(v
) is dict or type(v
) is list or type(v
) is tuple:
439 convert_datetime2str(v
)
440 if len(var
) == 0: return True
441 elif type(var
) is list or type(var
) is tuple:
443 convert_datetime2str(v
)
445 def check_valid_tenant(my
, tenant_id
):
448 return HTTP_Unauthorized
, "Needed admin privileges"
450 result
, _
= my
.db
.get_table(FROM
='tenants', SELECT
=('uuid',), WHERE
={'uuid': tenant_id
})
452 return HTTP_Not_Found
, "tenant '%s' not found" % tenant_id
455 def check_valid_uuid(uuid
):
456 id_schema
= {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
458 js_v(uuid
, id_schema
)
460 except js_e
.ValidationError
:
466 Check if string value is a well-wormed url
467 :param url: string url
468 :return: True if is a valid url, False if is not well-formed
471 parsed_url
= urlparse
.urlparse(url
)
486 e
={"error":{"code":error
.status_code
, "type":error
.status
, "description":error
.body
}}
489 @bottle.hook('after_request')
491 #TODO: Alf: Is it needed??
492 bottle
.response
.headers
['Access-Control-Allow-Origin'] = '*'
498 @bottle.route(url_base
+ '/hosts', method
='GET')
499 def http_get_hosts():
500 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_host
,
501 ('id','name','description','status','admin_state_up') )
503 myself
= config_dic
['http_threads'][ threading
.current_thread().name
]
504 result
, content
= myself
.db
.get_table(FROM
='hosts', SELECT
=select_
, WHERE
=where_
, LIMIT
=limit_
)
506 print "http_get_hosts Error", content
507 bottle
.abort(-result
, content
)
509 convert_boolean(content
, ('admin_state_up',) )
510 change_keys_http2db(content
, http2db_host
, reverse
=True)
512 row
['links'] = ( {'href': myself
.url_preffix
+ '/hosts/' + str(row
['id']), 'rel': 'bookmark'}, )
513 data
={'hosts' : content
}
514 return format_out(data
)
516 @bottle.route(url_base
+ '/hosts/<host_id>', method
='GET')
517 def http_get_host_id(host_id
):
518 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
519 return my
.gethost(host_id
)
521 @bottle.route(url_base
+ '/hosts', method
='POST')
522 def http_post_hosts():
523 '''insert a host into the database. All resources are got and inserted'''
524 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
527 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
530 http_content
= format_in( host_new_schema
)
531 r
= remove_extra_items(http_content
, host_new_schema
)
532 if r
is not None: print "http_post_host_id: Warning: remove extra items ", r
533 change_keys_http2db(http_content
['host'], http2db_host
)
535 host
= http_content
['host']
537 if 'host-data' in http_content
:
538 host
.update(http_content
['host-data'])
539 ip_name
=http_content
['host-data']['ip_name']
540 user
=http_content
['host-data']['user']
541 password
=http_content
['host-data'].get('password', None)
543 ip_name
=host
['ip_name']
545 password
=host
.get('password', None)
546 if not RADclass_module
:
548 RADclass_module
= imp
.find_module("RADclass")
549 except (IOError, ImportError) as e
:
550 raise ImportError("Cannot import RADclass.py Openvim not properly installed" +str(e
))
553 rad
= RADclass_module
.RADclass()
554 (return_status
, code
) = rad
.obtain_RAD(user
, password
, ip_name
)
557 if not return_status
:
558 print 'http_post_hosts ERROR obtaining RAD', code
559 bottle
.abort(HTTP_Bad_Request
, code
)
562 rad_structure
= yaml
.load(rad
.to_text())
563 print 'rad_structure\n---------------------'
564 print json
.dumps(rad_structure
, indent
=4)
565 print '---------------------'
567 WHERE_
={"family":rad_structure
['processor']['family'], 'manufacturer':rad_structure
['processor']['manufacturer'], 'version':rad_structure
['processor']['version']}
568 result
, content
= my
.db
.get_table(FROM
='host_ranking',
572 host
['ranking'] = content
[0]['ranking']
574 #error_text= "Host " + str(WHERE_)+ " not found in ranking table. Not valid for VIM management"
575 #bottle.abort(HTTP_Bad_Request, error_text)
577 warning_text
+= "Host " + str(WHERE_
)+ " not found in ranking table. Assuming lowest value 100\n"
578 host
['ranking'] = 100 #TODO: as not used in this version, set the lowest value
580 features
= rad_structure
['processor'].get('features', ())
581 host
['features'] = ",".join(features
)
584 for node
in (rad_structure
['resource topology']['nodes'] or {}).itervalues():
589 for core
in node
['cpu']['eligible_cores']:
590 eligible_cores
.extend(core
)
591 for core
in node
['cpu']['cores']:
592 for thread_id
in core
:
593 c
={'core_id': count
, 'thread_id': thread_id
}
594 if thread_id
not in eligible_cores
: c
['status'] = 'noteligible'
599 for port_k
, port_v
in node
['nics']['nic 0']['ports'].iteritems():
600 if port_v
['virtual']:
604 for port_k2
, port_v2
in node
['nics']['nic 0']['ports'].iteritems():
605 if port_v2
['virtual'] and port_v2
['PF_pci_id']==port_k
:
606 sriovs
.append({'pci':port_k2
, 'mac':port_v2
['mac'], 'source_name':port_v2
['source_name']})
608 #sort sriov according to pci and rename them to the vf number
609 new_sriovs
= sorted(sriovs
, key
=lambda k
: k
['pci'])
611 for sriov
in new_sriovs
:
612 sriov
['source_name'] = index
614 interfaces
.append ({'pci':str(port_k
), 'Mbps': port_v
['speed']/1000000, 'sriovs': new_sriovs
, 'mac':port_v
['mac'], 'source_name':port_v
['source_name']})
615 memory
=node
['memory']['node_size'] / (1024*1024*1024)
616 #memory=get_next_2pow(node['memory']['hugepage_nr'])
617 host
['numas'].append( {'numa_socket': node
['id'], 'hugepages': node
['memory']['hugepage_nr'], 'memory':memory
, 'interfaces': interfaces
, 'cores': cores
} )
618 print json
.dumps(host
, indent
=4)
622 result
, content
= my
.db
.new_host(host
)
624 if content
['admin_state_up']:
626 host_test_mode
= True if config_dic
['mode']=='test' or config_dic
['mode']=="OF only" else False
627 host_develop_mode
= True if config_dic
['mode']=='development' else False
628 host_develop_bridge_iface
= config_dic
.get('development_bridge', None)
629 thread
= ht
.host_thread(name
=host
.get('name',ip_name
), user
=user
, host
=ip_name
, db
=config_dic
['db'], db_lock
=config_dic
['db_lock'],
630 test
=host_test_mode
, image_path
=config_dic
['image_path'],
631 version
=config_dic
['version'], host_id
=content
['uuid'],
632 develop_mode
=host_develop_mode
, develop_bridge_iface
=host_develop_bridge_iface
)
634 config_dic
['host_threads'][ content
['uuid'] ] = thread
637 change_keys_http2db(content
, http2db_host
, reverse
=True)
638 if len(warning_text
)>0:
639 content
["warning"]= warning_text
640 data
={'host' : content
}
641 return format_out(data
)
643 bottle
.abort(HTTP_Bad_Request
, content
)
646 @bottle.route(url_base
+ '/hosts/<host_id>', method
='PUT')
647 def http_put_host_id(host_id
):
648 '''modify a host into the database. All resources are got and inserted'''
649 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
652 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
655 http_content
= format_in( host_edit_schema
)
656 r
= remove_extra_items(http_content
, host_edit_schema
)
657 if r
is not None: print "http_post_host_id: Warning: remove extra items ", r
658 change_keys_http2db(http_content
['host'], http2db_host
)
661 result
, content
= my
.db
.edit_host(host_id
, http_content
['host'])
663 convert_boolean(content
, ('admin_state_up',) )
664 change_keys_http2db(content
, http2db_host
, reverse
=True)
665 data
={'host' : content
}
668 config_dic
['host_threads'][host_id
].name
= content
.get('name',content
['ip_name'])
669 config_dic
['host_threads'][host_id
].user
= content
['user']
670 config_dic
['host_threads'][host_id
].host
= content
['ip_name']
671 config_dic
['host_threads'][host_id
].insert_task("reload")
674 return format_out(data
)
676 bottle
.abort(HTTP_Bad_Request
, content
)
681 @bottle.route(url_base
+ '/hosts/<host_id>', method
='DELETE')
682 def http_delete_host_id(host_id
):
683 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
686 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
687 result
, content
= my
.db
.delete_row('hosts', host_id
)
689 bottle
.abort(HTTP_Not_Found
, content
)
692 if host_id
in config_dic
['host_threads']:
693 config_dic
['host_threads'][host_id
].insert_task("exit")
695 data
={'result' : content
}
696 return format_out(data
)
698 print "http_delete_host_id error",result
, content
699 bottle
.abort(-result
, content
)
708 @bottle.route(url_base
+ '/tenants', method
='GET')
709 def http_get_tenants():
710 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
711 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_tenant
,
712 ('id','name','description','enabled') )
713 result
, content
= my
.db
.get_table(FROM
='tenants', SELECT
=select_
,WHERE
=where_
,LIMIT
=limit_
)
715 print "http_get_tenants Error", content
716 bottle
.abort(-result
, content
)
718 change_keys_http2db(content
, http2db_tenant
, reverse
=True)
719 convert_boolean(content
, ('enabled',))
720 data
={'tenants' : content
}
721 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
722 return format_out(data
)
724 @bottle.route(url_base
+ '/tenants/<tenant_id>', method
='GET')
725 def http_get_tenant_id(tenant_id
):
726 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
727 result
, content
= my
.db
.get_table(FROM
='tenants', SELECT
=('uuid','name','description', 'enabled'),WHERE
={'uuid': tenant_id
} )
729 print "http_get_tenant_id error %d %s" % (result
, content
)
730 bottle
.abort(-result
, content
)
732 print "http_get_tenant_id tenant '%s' not found" % tenant_id
733 bottle
.abort(HTTP_Not_Found
, "tenant %s not found" % tenant_id
)
735 change_keys_http2db(content
, http2db_tenant
, reverse
=True)
736 convert_boolean(content
, ('enabled',))
737 data
={'tenant' : content
[0]}
738 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
739 return format_out(data
)
742 @bottle.route(url_base
+ '/tenants', method
='POST')
743 def http_post_tenants():
744 '''insert a tenant into the database.'''
745 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
747 http_content
= format_in( tenant_new_schema
)
748 r
= remove_extra_items(http_content
, tenant_new_schema
)
749 if r
is not None: print "http_post_tenants: Warning: remove extra items ", r
750 change_keys_http2db(http_content
['tenant'], http2db_tenant
)
753 result
, content
= my
.db
.new_tenant(http_content
['tenant'])
756 return http_get_tenant_id(content
)
758 bottle
.abort(-result
, content
)
761 @bottle.route(url_base
+ '/tenants/<tenant_id>', method
='PUT')
762 def http_put_tenant_id(tenant_id
):
763 '''update a tenant into the database.'''
764 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
766 http_content
= format_in( tenant_edit_schema
)
767 r
= remove_extra_items(http_content
, tenant_edit_schema
)
768 if r
is not None: print "http_put_tenant_id: Warning: remove extra items ", r
769 change_keys_http2db(http_content
['tenant'], http2db_tenant
)
772 result
, content
= my
.db
.update_rows('tenants', http_content
['tenant'], WHERE
={'uuid': tenant_id
}, log
=True )
774 return http_get_tenant_id(tenant_id
)
776 bottle
.abort(-result
, content
)
779 @bottle.route(url_base
+ '/tenants/<tenant_id>', method
='DELETE')
780 def http_delete_tenant_id(tenant_id
):
781 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
783 r
, tenants_flavors
= my
.db
.get_table(FROM
='tenants_flavors', SELECT
=('flavor_id','tenant_id'), WHERE
={'tenant_id': tenant_id
})
786 r
, tenants_images
= my
.db
.get_table(FROM
='tenants_images', SELECT
=('image_id','tenant_id'), WHERE
={'tenant_id': tenant_id
})
789 result
, content
= my
.db
.delete_row('tenants', tenant_id
)
791 bottle
.abort(HTTP_Not_Found
, content
)
793 print "alf", tenants_flavors
, tenants_images
794 for flavor
in tenants_flavors
:
795 my
.db
.delete_row_by_key("flavors", "uuid", flavor
['flavor_id'])
796 for image
in tenants_images
:
797 my
.db
.delete_row_by_key("images", "uuid", image
['image_id'])
798 data
={'result' : content
}
799 return format_out(data
)
801 print "http_delete_tenant_id error",result
, content
802 bottle
.abort(-result
, content
)
809 @bottle.route(url_base
+ '/<tenant_id>/flavors', method
='GET')
810 def http_get_flavors(tenant_id
):
811 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
812 #check valid tenant_id
813 result
,content
= check_valid_tenant(my
, tenant_id
)
815 bottle
.abort(result
, content
)
817 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_flavor
,
818 ('id','name','description','public') )
822 from_
='tenants_flavors inner join flavors on tenants_flavors.flavor_id=flavors.uuid'
823 where_
['tenant_id'] = tenant_id
824 result
, content
= my
.db
.get_table(FROM
=from_
, SELECT
=select_
, WHERE
=where_
, LIMIT
=limit_
)
826 print "http_get_flavors Error", content
827 bottle
.abort(-result
, content
)
829 change_keys_http2db(content
, http2db_flavor
, reverse
=True)
831 row
['links']=[ {'href': "/".join( (my
.url_preffix
, tenant_id
, 'flavors', str(row
['id']) ) ), 'rel':'bookmark' } ]
832 data
={'flavors' : content
}
833 return format_out(data
)
835 @bottle.route(url_base
+ '/<tenant_id>/flavors/<flavor_id>', method
='GET')
836 def http_get_flavor_id(tenant_id
, flavor_id
):
837 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
838 #check valid tenant_id
839 result
,content
= check_valid_tenant(my
, tenant_id
)
841 bottle
.abort(result
, content
)
843 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_flavor
,
844 ('id','name','description','ram', 'vcpus', 'extended', 'disk', 'public') )
848 from_
='tenants_flavors as tf inner join flavors as f on tf.flavor_id=f.uuid'
849 where_
['tenant_id'] = tenant_id
850 where_
['uuid'] = flavor_id
851 result
, content
= my
.db
.get_table(SELECT
=select_
, FROM
=from_
, WHERE
=where_
, LIMIT
=limit_
)
854 print "http_get_flavor_id error %d %s" % (result
, content
)
855 bottle
.abort(-result
, content
)
857 print "http_get_flavors_id flavor '%s' not found" % str(flavor_id
)
858 bottle
.abort(HTTP_Not_Found
, 'flavor %s not found' % flavor_id
)
860 change_keys_http2db(content
, http2db_flavor
, reverse
=True)
861 if 'extended' in content
[0] and content
[0]['extended'] is not None:
862 extended
= json
.loads(content
[0]['extended'])
863 if 'devices' in extended
:
864 change_keys_http2db(extended
['devices'], http2db_flavor
, reverse
=True)
865 content
[0]['extended']=extended
866 convert_bandwidth(content
[0], reverse
=True)
867 content
[0]['links']=[ {'href': "/".join( (my
.url_preffix
, tenant_id
, 'flavors', str(content
[0]['id']) ) ), 'rel':'bookmark' } ]
868 data
={'flavor' : content
[0]}
869 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
870 return format_out(data
)
873 @bottle.route(url_base
+ '/<tenant_id>/flavors', method
='POST')
874 def http_post_flavors(tenant_id
):
875 '''insert a flavor into the database, and attach to tenant.'''
876 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
877 #check valid tenant_id
878 result
,content
= check_valid_tenant(my
, tenant_id
)
880 bottle
.abort(result
, content
)
881 http_content
= format_in( flavor_new_schema
)
882 r
= remove_extra_items(http_content
, flavor_new_schema
)
883 if r
is not None: print "http_post_flavors: Warning: remove extra items ", r
884 change_keys_http2db(http_content
['flavor'], http2db_flavor
)
885 extended_dict
= http_content
['flavor'].pop('extended', None)
886 if extended_dict
is not None:
887 result
, content
= check_extended(extended_dict
)
889 print "http_post_flavors wrong input extended error %d %s" % (result
, content
)
890 bottle
.abort(-result
, content
)
892 convert_bandwidth(extended_dict
)
893 if 'devices' in extended_dict
: change_keys_http2db(extended_dict
['devices'], http2db_flavor
)
894 http_content
['flavor']['extended'] = json
.dumps(extended_dict
)
896 result
, content
= my
.db
.new_flavor(http_content
['flavor'], tenant_id
)
898 return http_get_flavor_id(tenant_id
, content
)
900 print "http_psot_flavors error %d %s" % (result
, content
)
901 bottle
.abort(-result
, content
)
904 @bottle.route(url_base
+ '/<tenant_id>/flavors/<flavor_id>', method
='DELETE')
905 def http_delete_flavor_id(tenant_id
, flavor_id
):
906 '''Deletes the flavor_id of a tenant. IT removes from tenants_flavors table.'''
907 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
908 #check valid tenant_id
909 result
,content
= check_valid_tenant(my
, tenant_id
)
911 bottle
.abort(result
, content
)
913 result
, content
= my
.db
.delete_image_flavor('flavor', flavor_id
, tenant_id
)
915 bottle
.abort(HTTP_Not_Found
, content
)
917 data
={'result' : content
}
918 return format_out(data
)
920 print "http_delete_flavor_id error",result
, content
921 bottle
.abort(-result
, content
)
924 @bottle.route(url_base
+ '/<tenant_id>/flavors/<flavor_id>/<action>', method
='POST')
925 def http_attach_detach_flavors(tenant_id
, flavor_id
, action
):
926 '''attach/detach an existing flavor in this tenant. That is insert/remove at tenants_flavors table.'''
927 #TODO alf: not tested at all!!!
928 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
929 #check valid tenant_id
930 result
,content
= check_valid_tenant(my
, tenant_id
)
932 bottle
.abort(result
, content
)
934 bottle
.abort(HTTP_Bad_Request
, "Invalid tenant 'any' with this command")
936 if action
!='attach' and action
!= 'detach':
937 bottle
.abort(HTTP_Method_Not_Allowed
, "actions can be attach or detach")
940 #Ensure that flavor exist
941 from_
='tenants_flavors as tf right join flavors as f on tf.flavor_id=f.uuid'
942 where_
={'uuid': flavor_id
}
943 result
, content
= my
.db
.get_table(SELECT
=('public','tenant_id'), FROM
=from_
, WHERE
=where_
)
946 text_error
="Flavor '%s' not found" % flavor_id
948 text_error
="Flavor '%s' not found for tenant '%s'" % (flavor_id
, tenant_id
)
949 bottle
.abort(HTTP_Not_Found
, text_error
)
954 if flavor
['tenant_id']!=None:
955 bottle
.abort(HTTP_Conflict
, "Flavor '%s' already attached to tenant '%s'" % (flavor_id
, tenant_id
))
956 if flavor
['public']=='no' and not my
.admin
:
957 #allow only attaching public flavors
958 bottle
.abort(HTTP_Unauthorized
, "Needed admin rights to attach a private flavor")
961 result
, content
= my
.db
.new_row('tenants_flavors', {'flavor_id':flavor_id
, 'tenant_id': tenant_id
})
963 return http_get_flavor_id(tenant_id
, flavor_id
)
965 if flavor
['tenant_id']==None:
966 bottle
.abort(HTTP_Not_Found
, "Flavor '%s' not attached to tenant '%s'" % (flavor_id
, tenant_id
))
967 result
, content
= my
.db
.delete_row_by_dict(FROM
='tenants_flavors', WHERE
={'flavor_id':flavor_id
, 'tenant_id':tenant_id
})
969 if flavor
['public']=='no':
970 #try to delete the flavor completely to avoid orphan flavors, IGNORE error
971 my
.db
.delete_row_by_dict(FROM
='flavors', WHERE
={'uuid':flavor_id
})
972 data
={'result' : "flavor detached"}
973 return format_out(data
)
975 #if get here is because an error
976 print "http_attach_detach_flavors error %d %s" % (result
, content
)
977 bottle
.abort(-result
, content
)
980 @bottle.route(url_base
+ '/<tenant_id>/flavors/<flavor_id>', method
='PUT')
981 def http_put_flavor_id(tenant_id
, flavor_id
):
982 '''update a flavor_id into the database.'''
983 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
984 #check valid tenant_id
985 result
,content
= check_valid_tenant(my
, tenant_id
)
987 bottle
.abort(result
, content
)
989 http_content
= format_in( flavor_update_schema
)
990 r
= remove_extra_items(http_content
, flavor_update_schema
)
991 if r
is not None: print "http_put_flavor_id: Warning: remove extra items ", r
992 change_keys_http2db(http_content
['flavor'], http2db_flavor
)
993 extended_dict
= http_content
['flavor'].pop('extended', None)
994 if extended_dict
is not None:
995 result
, content
= check_extended(extended_dict
)
997 print "http_put_flavor_id wrong input extended error %d %s" % (result
, content
)
998 bottle
.abort(-result
, content
)
1000 convert_bandwidth(extended_dict
)
1001 if 'devices' in extended_dict
: change_keys_http2db(extended_dict
['devices'], http2db_flavor
)
1002 http_content
['flavor']['extended'] = json
.dumps(extended_dict
)
1003 #Ensure that flavor exist
1004 where_
={'uuid': flavor_id
}
1005 if tenant_id
=='any':
1008 from_
='tenants_flavors as ti inner join flavors as i on ti.flavor_id=i.uuid'
1009 where_
['tenant_id'] = tenant_id
1010 result
, content
= my
.db
.get_table(SELECT
=('public',), FROM
=from_
, WHERE
=where_
)
1012 text_error
="Flavor '%s' not found" % flavor_id
1013 if tenant_id
!='any':
1014 text_error
+=" for tenant '%s'" % flavor_id
1015 bottle
.abort(HTTP_Not_Found
, text_error
)
1018 if content
[0]['public']=='yes' and not my
.admin
:
1019 #allow only modifications over private flavors
1020 bottle
.abort(HTTP_Unauthorized
, "Needed admin rights to edit a public flavor")
1022 #insert in data base
1023 result
, content
= my
.db
.update_rows('flavors', http_content
['flavor'], {'uuid': flavor_id
})
1026 print "http_put_flavor_id error %d %s" % (result
, content
)
1027 bottle
.abort(-result
, content
)
1030 return http_get_flavor_id(tenant_id
, flavor_id
)
1038 @bottle.route(url_base
+ '/<tenant_id>/images', method
='GET')
1039 def http_get_images(tenant_id
):
1040 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1041 #check valid tenant_id
1042 result
,content
= check_valid_tenant(my
, tenant_id
)
1044 bottle
.abort(result
, content
)
1046 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_image
,
1047 ('id','name','checksum','description','path','public') )
1048 if tenant_id
=='any':
1052 from_
='tenants_images right join images on tenants_images.image_id=images.uuid'
1053 where_or_
= {'tenant_id': tenant_id
, 'public': 'yes'}
1054 result
, content
= my
.db
.get_table(SELECT
=select_
, DISTINCT
=True, FROM
=from_
, WHERE
=where_
, WHERE_OR
=where_or_
, WHERE_AND_OR
="AND", LIMIT
=limit_
)
1056 print "http_get_images Error", content
1057 bottle
.abort(-result
, content
)
1059 change_keys_http2db(content
, http2db_image
, reverse
=True)
1060 #for row in content: row['links']=[ {'href': "/".join( (my.url_preffix, tenant_id, 'images', str(row['id']) ) ), 'rel':'bookmark' } ]
1061 data
={'images' : content
}
1062 return format_out(data
)
1064 @bottle.route(url_base
+ '/<tenant_id>/images/<image_id>', method
='GET')
1065 def http_get_image_id(tenant_id
, image_id
):
1066 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1067 #check valid tenant_id
1068 result
,content
= check_valid_tenant(my
, tenant_id
)
1070 bottle
.abort(result
, content
)
1072 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_image
,
1073 ('id','name','checksum','description','progress', 'status','path', 'created', 'updated','public') )
1074 if tenant_id
=='any':
1078 from_
='tenants_images as ti right join images as i on ti.image_id=i.uuid'
1079 where_or_
= {'tenant_id': tenant_id
, 'public': "yes"}
1080 where_
['uuid'] = image_id
1081 result
, content
= my
.db
.get_table(SELECT
=select_
, DISTINCT
=True, FROM
=from_
, WHERE
=where_
, WHERE_OR
=where_or_
, WHERE_AND_OR
="AND", LIMIT
=limit_
)
1084 print "http_get_images error %d %s" % (result
, content
)
1085 bottle
.abort(-result
, content
)
1087 print "http_get_images image '%s' not found" % str(image_id
)
1088 bottle
.abort(HTTP_Not_Found
, 'image %s not found' % image_id
)
1090 convert_datetime2str(content
)
1091 change_keys_http2db(content
, http2db_image
, reverse
=True)
1092 if 'metadata' in content
[0] and content
[0]['metadata'] is not None:
1093 metadata
= json
.loads(content
[0]['metadata'])
1094 content
[0]['metadata']=metadata
1095 content
[0]['links']=[ {'href': "/".join( (my
.url_preffix
, tenant_id
, 'images', str(content
[0]['id']) ) ), 'rel':'bookmark' } ]
1096 data
={'image' : content
[0]}
1097 #data['tenants_links'] = dict([('tenant', row['id']) for row in content])
1098 return format_out(data
)
1100 @bottle.route(url_base
+ '/<tenant_id>/images', method
='POST')
1101 def http_post_images(tenant_id
):
1102 '''insert a image into the database, and attach to tenant.'''
1103 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1104 #check valid tenant_id
1105 result
,content
= check_valid_tenant(my
, tenant_id
)
1107 bottle
.abort(result
, content
)
1108 http_content
= format_in(image_new_schema
)
1109 r
= remove_extra_items(http_content
, image_new_schema
)
1110 if r
is not None: print "http_post_images: Warning: remove extra items ", r
1111 change_keys_http2db(http_content
['image'], http2db_image
)
1112 metadata_dict
= http_content
['image'].pop('metadata', None)
1113 if metadata_dict
is not None:
1114 http_content
['image']['metadata'] = json
.dumps(metadata_dict
)
1117 image_file
= http_content
['image'].get('path',None)
1118 parsed_url
= urlparse
.urlparse(image_file
)
1119 if parsed_url
.scheme
== "" and parsed_url
.netloc
== "":
1120 # The path is a local file
1121 if os
.path
.exists(image_file
):
1122 http_content
['image']['checksum'] = md5(image_file
)
1124 # The path is a URL. Code should be added to download the image and calculate the checksum
1125 #http_content['image']['checksum'] = md5(downloaded_image)
1127 # Finally, only if we are in test mode and checksum has not been calculated, we calculate it from the path
1128 host_test_mode
= True if config_dic
['mode']=='test' or config_dic
['mode']=="OF only" else False
1130 if 'checksum' not in http_content
['image']:
1131 http_content
['image']['checksum'] = md5_string(image_file
)
1133 # At this point, if the path is a local file and no chechsum has been obtained yet, an error is sent back.
1134 # If it is a URL, no error is sent. Checksum will be an empty string
1135 if parsed_url
.scheme
== "" and parsed_url
.netloc
== "" and 'checksum' not in http_content
['image']:
1136 content
= "Image file not found"
1137 print "http_post_images error: %d %s" % (HTTP_Bad_Request
, content
)
1138 bottle
.abort(HTTP_Bad_Request
, content
)
1139 except Exception as e
:
1140 print "ERROR. Unexpected exception: %s" % (str(e
))
1141 bottle
.abort(HTTP_Internal_Server_Error
, type(e
).__name
__ + ": " + str(e
))
1142 #insert in data base
1143 result
, content
= my
.db
.new_image(http_content
['image'], tenant_id
)
1145 return http_get_image_id(tenant_id
, content
)
1147 print "http_post_images error %d %s" % (result
, content
)
1148 bottle
.abort(-result
, content
)
1151 @bottle.route(url_base
+ '/<tenant_id>/images/<image_id>', method
='DELETE')
1152 def http_delete_image_id(tenant_id
, image_id
):
1153 '''Deletes the image_id of a tenant. IT removes from tenants_images table.'''
1154 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1155 #check valid tenant_id
1156 result
,content
= check_valid_tenant(my
, tenant_id
)
1158 bottle
.abort(result
, content
)
1159 result
, content
= my
.db
.delete_image_flavor('image', image_id
, tenant_id
)
1161 bottle
.abort(HTTP_Not_Found
, content
)
1163 data
={'result' : content
}
1164 return format_out(data
)
1166 print "http_delete_image_id error",result
, content
1167 bottle
.abort(-result
, content
)
1170 @bottle.route(url_base
+ '/<tenant_id>/images/<image_id>/<action>', method
='POST')
1171 def http_attach_detach_images(tenant_id
, image_id
, action
):
1172 '''attach/detach an existing image in this tenant. That is insert/remove at tenants_images table.'''
1173 #TODO alf: not tested at all!!!
1174 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1175 #check valid tenant_id
1176 result
,content
= check_valid_tenant(my
, tenant_id
)
1178 bottle
.abort(result
, content
)
1179 if tenant_id
=='any':
1180 bottle
.abort(HTTP_Bad_Request
, "Invalid tenant 'any' with this command")
1182 if action
!='attach' and action
!= 'detach':
1183 bottle
.abort(HTTP_Method_Not_Allowed
, "actions can be attach or detach")
1186 #Ensure that image exist
1187 from_
='tenants_images as ti right join images as i on ti.image_id=i.uuid'
1188 where_
={'uuid': image_id
}
1189 result
, content
= my
.db
.get_table(SELECT
=('public','tenant_id'), FROM
=from_
, WHERE
=where_
)
1191 if action
=='attach':
1192 text_error
="Image '%s' not found" % image_id
1194 text_error
="Image '%s' not found for tenant '%s'" % (image_id
, tenant_id
)
1195 bottle
.abort(HTTP_Not_Found
, text_error
)
1199 if action
=='attach':
1200 if image
['tenant_id']!=None:
1201 bottle
.abort(HTTP_Conflict
, "Image '%s' already attached to tenant '%s'" % (image_id
, tenant_id
))
1202 if image
['public']=='no' and not my
.admin
:
1203 #allow only attaching public images
1204 bottle
.abort(HTTP_Unauthorized
, "Needed admin rights to attach a private image")
1206 #insert in data base
1207 result
, content
= my
.db
.new_row('tenants_images', {'image_id':image_id
, 'tenant_id': tenant_id
})
1209 return http_get_image_id(tenant_id
, image_id
)
1211 if image
['tenant_id']==None:
1212 bottle
.abort(HTTP_Not_Found
, "Image '%s' not attached to tenant '%s'" % (image_id
, tenant_id
))
1213 result
, content
= my
.db
.delete_row_by_dict(FROM
='tenants_images', WHERE
={'image_id':image_id
, 'tenant_id':tenant_id
})
1215 if image
['public']=='no':
1216 #try to delete the image completely to avoid orphan images, IGNORE error
1217 my
.db
.delete_row_by_dict(FROM
='images', WHERE
={'uuid':image_id
})
1218 data
={'result' : "image detached"}
1219 return format_out(data
)
1221 #if get here is because an error
1222 print "http_attach_detach_images error %d %s" % (result
, content
)
1223 bottle
.abort(-result
, content
)
1226 @bottle.route(url_base
+ '/<tenant_id>/images/<image_id>', method
='PUT')
1227 def http_put_image_id(tenant_id
, image_id
):
1228 '''update a image_id into the database.'''
1229 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1230 #check valid tenant_id
1231 result
,content
= check_valid_tenant(my
, tenant_id
)
1233 bottle
.abort(result
, content
)
1235 http_content
= format_in( image_update_schema
)
1236 r
= remove_extra_items(http_content
, image_update_schema
)
1237 if r
is not None: print "http_put_image_id: Warning: remove extra items ", r
1238 change_keys_http2db(http_content
['image'], http2db_image
)
1239 metadata_dict
= http_content
['image'].pop('metadata', None)
1240 if metadata_dict
is not None:
1241 http_content
['image']['metadata'] = json
.dumps(metadata_dict
)
1242 #Ensure that image exist
1243 where_
={'uuid': image_id
}
1244 if tenant_id
=='any':
1248 from_
='tenants_images as ti right join images as i on ti.image_id=i.uuid'
1249 where_or_
= {'tenant_id': tenant_id
, 'public': 'yes'}
1250 result
, content
= my
.db
.get_table(SELECT
=('public',), DISTINCT
=True, FROM
=from_
, WHERE
=where_
, WHERE_OR
=where_or_
, WHERE_AND_OR
="AND")
1252 text_error
="Image '%s' not found" % image_id
1253 if tenant_id
!='any':
1254 text_error
+=" for tenant '%s'" % image_id
1255 bottle
.abort(HTTP_Not_Found
, text_error
)
1258 if content
[0]['public']=='yes' and not my
.admin
:
1259 #allow only modifications over private images
1260 bottle
.abort(HTTP_Unauthorized
, "Needed admin rights to edit a public image")
1262 #insert in data base
1263 result
, content
= my
.db
.update_rows('images', http_content
['image'], {'uuid': image_id
})
1266 print "http_put_image_id error %d %s" % (result
, content
)
1267 bottle
.abort(-result
, content
)
1270 return http_get_image_id(tenant_id
, image_id
)
1277 @bottle.route(url_base
+ '/<tenant_id>/servers', method
='GET')
1278 def http_get_servers(tenant_id
):
1279 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1280 result
,content
= check_valid_tenant(my
, tenant_id
)
1282 bottle
.abort(result
, content
)
1285 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_server
,
1286 ('id','name','description','hostId','imageRef','flavorRef','status', 'tenant_id') )
1287 if tenant_id
!='any':
1288 where_
['tenant_id'] = tenant_id
1289 result
, content
= my
.db
.get_table(SELECT
=select_
, FROM
='instances', WHERE
=where_
, LIMIT
=limit_
)
1291 print "http_get_servers Error", content
1292 bottle
.abort(-result
, content
)
1294 change_keys_http2db(content
, http2db_server
, reverse
=True)
1296 tenant_id
= row
.pop('tenant_id')
1297 row
['links']=[ {'href': "/".join( (my
.url_preffix
, tenant_id
, 'servers', str(row
['id']) ) ), 'rel':'bookmark' } ]
1298 data
={'servers' : content
}
1299 return format_out(data
)
1301 @bottle.route(url_base
+ '/<tenant_id>/servers/<server_id>', method
='GET')
1302 def http_get_server_id(tenant_id
, server_id
):
1303 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1304 #check valid tenant_id
1305 result
,content
= check_valid_tenant(my
, tenant_id
)
1307 bottle
.abort(result
, content
)
1310 result
, content
= my
.db
.get_instance(server_id
)
1312 bottle
.abort(HTTP_Not_Found
, content
)
1314 #change image/flavor-id to id and link
1315 convert_bandwidth(content
, reverse
=True)
1316 convert_datetime2str(content
)
1317 if content
["ram"]==0 : del content
["ram"]
1318 if content
["vcpus"]==0 : del content
["vcpus"]
1319 if 'flavor_id' in content
:
1320 if content
['flavor_id'] is not None:
1321 content
['flavor'] = {'id':content
['flavor_id'],
1322 'links':[{'href': "/".join( (my
.url_preffix
, content
['tenant_id'], 'flavors', str(content
['flavor_id']) ) ), 'rel':'bookmark'}]
1324 del content
['flavor_id']
1325 if 'image_id' in content
:
1326 if content
['image_id'] is not None:
1327 content
['image'] = {'id':content
['image_id'],
1328 'links':[{'href': "/".join( (my
.url_preffix
, content
['tenant_id'], 'images', str(content
['image_id']) ) ), 'rel':'bookmark'}]
1330 del content
['image_id']
1331 change_keys_http2db(content
, http2db_server
, reverse
=True)
1332 if 'extended' in content
:
1333 if 'devices' in content
['extended']: change_keys_http2db(content
['extended']['devices'], http2db_server
, reverse
=True)
1335 data
={'server' : content
}
1336 return format_out(data
)
1338 bottle
.abort(-result
, content
)
1341 @bottle.route(url_base
+ '/<tenant_id>/servers', method
='POST')
1342 def http_post_server_id(tenant_id
):
1343 '''deploys a new server'''
1344 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1345 #check valid tenant_id
1346 result
,content
= check_valid_tenant(my
, tenant_id
)
1348 bottle
.abort(result
, content
)
1350 if tenant_id
=='any':
1351 bottle
.abort(HTTP_Bad_Request
, "Invalid tenant 'any' with this command")
1353 http_content
= format_in( server_new_schema
)
1354 r
= remove_extra_items(http_content
, server_new_schema
)
1355 if r
is not None: print "http_post_serves: Warning: remove extra items ", r
1356 change_keys_http2db(http_content
['server'], http2db_server
)
1357 extended_dict
= http_content
['server'].get('extended', None)
1358 if extended_dict
is not None:
1359 result
, content
= check_extended(extended_dict
, True)
1361 print "http_post_servers wrong input extended error %d %s" % (result
, content
)
1362 bottle
.abort(-result
, content
)
1364 convert_bandwidth(extended_dict
)
1365 if 'devices' in extended_dict
: change_keys_http2db(extended_dict
['devices'], http2db_server
)
1367 server
= http_content
['server']
1368 server_start
= server
.get('start', 'yes')
1369 server
['tenant_id'] = tenant_id
1370 #check flavor valid and take info
1371 result
, content
= my
.db
.get_table(FROM
='tenants_flavors as tf join flavors as f on tf.flavor_id=f.uuid',
1372 SELECT
=('ram','vcpus','extended'), WHERE
={'uuid':server
['flavor_id'], 'tenant_id':tenant_id
})
1374 bottle
.abort(HTTP_Not_Found
, 'flavor_id %s not found' % server
['flavor_id'])
1376 server
['flavor']=content
[0]
1377 #check image valid and take info
1378 result
, content
= my
.db
.get_table(FROM
='tenants_images as ti right join images as i on ti.image_id=i.uuid',
1379 SELECT
=('path', 'metadata', 'image_id'),
1380 WHERE
={'uuid':server
['image_id'], "status":"ACTIVE"},
1381 WHERE_OR
={'tenant_id':tenant_id
, 'public': 'yes'},
1385 bottle
.abort(HTTP_Not_Found
, 'image_id %s not found or not ACTIVE' % server
['image_id'])
1387 for image_dict
in content
:
1388 if image_dict
.get("image_id"):
1391 # insert in data base tenants_images
1392 r2
, c2
= my
.db
.new_row('tenants_images', {'image_id': server
['image_id'], 'tenant_id': tenant_id
})
1394 bottle
.abort(HTTP_Not_Found
, 'image_id %s cannot be used. Error %s' % (server
['image_id'], c2
))
1396 server
['image']={"path": content
[0]["path"], "metadata": content
[0]["metadata"]}
1397 if "hosts_id" in server
:
1398 result
, content
= my
.db
.get_table(FROM
='hosts', SELECT
=('uuid',), WHERE
={'uuid': server
['host_id']})
1400 bottle
.abort(HTTP_Not_Found
, 'hostId %s not found' % server
['host_id'])
1402 #print json.dumps(server, indent=4)
1404 result
, content
= ht
.create_server(server
, config_dic
['db'], config_dic
['db_lock'], config_dic
['mode']=='normal')
1407 #Insert instance to database
1410 print "inserting at DB"
1412 if server_start
== 'no':
1413 content
['status'] = 'INACTIVE'
1415 new_instance_result
, new_instance
= my
.db
.new_instance(content
, nets
, ports_to_free
)
1416 if new_instance_result
< 0:
1417 print "Error http_post_servers() :", new_instance_result
, new_instance
1418 bottle
.abort(-new_instance_result
, new_instance
)
1421 print "inserted at DB"
1423 for port
in ports_to_free
:
1424 r
,c
= config_dic
['host_threads'][ server
['host_id'] ].insert_task( 'restore-iface',*port
)
1426 print ' http_post_servers ERROR RESTORE IFACE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1429 r
,c
= config_dic
['of_thread'].insert_task("update-net", net
)
1431 print ':http_post_servers ERROR UPDATING NETS !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1435 #look for dhcp ip address
1436 r2
, c2
= my
.db
.get_table(FROM
="ports", SELECT
=["mac", "net_id"], WHERE
={"instance_id": new_instance
})
1437 if r2
>0 and config_dic
.get("dhcp_server"):
1439 if iface
["net_id"] in config_dic
["dhcp_nets"]:
1440 #print "dhcp insert add task"
1441 r
,c
= config_dic
['dhcp_thread'].insert_task("add", iface
["mac"])
1443 print ':http_post_servers ERROR UPDATING dhcp_server !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1447 server
['uuid'] = new_instance
1448 #server_start = server.get('start', 'yes')
1449 if server_start
!= 'no':
1450 server
['paused'] = True if server_start
== 'paused' else False
1451 server
['action'] = {"start":None}
1452 server
['status'] = "CREATING"
1454 r
,c
= config_dic
['host_threads'][ server
['host_id'] ].insert_task( 'instance',server
)
1456 my
.db
.update_rows('instances', {'status':"ERROR"}, {'uuid':server
['uuid'], 'last_error':c
}, log
=True)
1458 return http_get_server_id(tenant_id
, new_instance
)
1460 bottle
.abort(HTTP_Bad_Request
, content
)
1463 def http_server_action(server_id
, tenant_id
, action
):
1464 '''Perform actions over a server as resume, reboot, terminate, ...'''
1465 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1466 server
={"uuid": server_id
, "action":action
}
1467 where
={'uuid': server_id
}
1468 if tenant_id
!='any':
1469 where
['tenant_id']= tenant_id
1470 result
, content
= my
.db
.get_table(FROM
='instances', WHERE
=where
)
1472 bottle
.abort(HTTP_Not_Found
, "server %s not found" % server_id
)
1475 print "http_post_server_action error getting data %d %s" % (result
, content
)
1476 bottle
.abort(HTTP_Internal_Server_Error
, content
)
1478 server
.update(content
[0])
1479 tenant_id
= server
["tenant_id"]
1481 #TODO check a right content
1483 if 'terminate' in action
:
1484 new_status
='DELETING'
1485 elif server
['status'] == 'ERROR': #or server['status'] == 'CREATING':
1486 if 'terminate' not in action
and 'rebuild' not in action
:
1487 bottle
.abort(HTTP_Method_Not_Allowed
, "Server is in ERROR status, must be rebuit or deleted ")
1489 # elif server['status'] == 'INACTIVE':
1490 # if 'start' not in action and 'createImage' not in action:
1491 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'INACTIVE' status is 'start'")
1493 # if 'start' in action:
1494 # new_status='CREATING'
1495 # server['paused']='no'
1496 # elif server['status'] == 'PAUSED':
1497 # if 'resume' not in action:
1498 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'PAUSED' status is 'resume'")
1500 # elif server['status'] == 'ACTIVE':
1501 # if 'pause' not in action and 'reboot'not in action and 'shutoff'not in action:
1502 # bottle.abort(HTTP_Method_Not_Allowed, "The only possible action over an instance in 'ACTIVE' status is 'pause','reboot' or 'shutoff'")
1505 if 'start' in action
or 'createImage' in action
or 'rebuild' in action
:
1506 #check image valid and take info
1507 image_id
= server
['image_id']
1508 if 'createImage' in action
:
1509 if 'imageRef' in action
['createImage']:
1510 image_id
= action
['createImage']['imageRef']
1511 elif 'disk' in action
['createImage']:
1512 result
, content
= my
.db
.get_table(FROM
='instance_devices',
1513 SELECT
=('image_id','dev'), WHERE
={'instance_id':server
['uuid'],"type":"disk"})
1515 bottle
.abort(HTTP_Not_Found
, 'disk not found for server')
1519 if action
['createImage']['imageRef']['disk'] != None:
1520 for disk
in content
:
1521 if disk
['dev'] == action
['createImage']['imageRef']['disk']:
1522 disk_id
= disk
['image_id']
1525 bottle
.abort(HTTP_Not_Found
, 'disk %s not found for server' % action
['createImage']['imageRef']['disk'])
1528 bottle
.abort(HTTP_Not_Found
, 'more than one disk found for server' )
1532 image_id
= content
[0]['image_id']
1534 result
, content
= my
.db
.get_table(FROM
='tenants_images as ti right join images as i on ti.image_id=i.uuid',
1535 SELECT
=('path','metadata'), WHERE
={'uuid':image_id
, "status":"ACTIVE"},
1536 WHERE_OR
={'tenant_id':tenant_id
, 'public': 'yes'}, WHERE_AND_OR
="AND", DISTINCT
=True)
1538 bottle
.abort(HTTP_Not_Found
, 'image_id %s not found or not ACTIVE' % image_id
)
1540 if content
[0]['metadata'] is not None:
1542 metadata
= json
.loads(content
[0]['metadata'])
1544 return -HTTP_Internal_Server_Error
, "Can not decode image metadata"
1545 content
[0]['metadata']=metadata
1547 content
[0]['metadata'] = {}
1548 server
['image']=content
[0]
1549 if 'createImage' in action
:
1550 action
['createImage']['source'] = {'image_id': image_id
, 'path': content
[0]['path']}
1551 if 'createImage' in action
:
1552 #Create an entry in Database for the new image
1553 new_image
={'status':'BUILD', 'progress': 0 }
1554 new_image_metadata
=content
[0]
1555 if 'metadata' in server
['image'] and server
['image']['metadata'] != None:
1556 new_image_metadata
.update(server
['image']['metadata'])
1557 new_image_metadata
= {"use_incremental":"no"}
1558 if 'metadata' in action
['createImage']:
1559 new_image_metadata
.update(action
['createImage']['metadata'])
1560 new_image
['metadata'] = json
.dumps(new_image_metadata
)
1561 new_image
['name'] = action
['createImage'].get('name', None)
1562 new_image
['description'] = action
['createImage'].get('description', None)
1563 new_image
['uuid']=my
.db
.new_uuid()
1564 if 'path' in action
['createImage']:
1565 new_image
['path'] = action
['createImage']['path']
1567 new_image
['path']="/provisional/path/" + new_image
['uuid']
1568 result
, image_uuid
= my
.db
.new_image(new_image
, tenant_id
)
1570 bottle
.abort(HTTP_Bad_Request
, 'Error: ' + image_uuid
)
1572 server
['new_image'] = new_image
1576 r
,c
= config_dic
['host_threads'][ server
['host_id'] ].insert_task( 'instance',server
)
1578 print "Task queue full at host ", server
['host_id']
1579 bottle
.abort(HTTP_Request_Timeout
, c
)
1580 if 'createImage' in action
and result
>= 0:
1581 return http_get_image_id(tenant_id
, image_uuid
)
1583 #Update DB only for CREATING or DELETING status
1584 data
={'result' : 'in process'}
1585 if new_status
!= None and new_status
== 'DELETING':
1588 #look for dhcp ip address
1589 r2
, c2
= my
.db
.get_table(FROM
="ports", SELECT
=["mac", "net_id"], WHERE
={"instance_id": server_id
})
1590 r
,c
= my
.db
.delete_instance(server_id
, tenant_id
, nets
, ports_to_free
, "requested by http")
1591 for port
in ports_to_free
:
1592 r1
,c1
= config_dic
['host_threads'][ server
['host_id'] ].insert_task( 'restore-iface',*port
)
1594 print ' http_post_server_action error at server deletion ERROR resore-iface !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c1
1595 data
={'result' : 'deleting in process, but ifaces cannot be restored!!!!!'}
1597 r1
,c1
= config_dic
['of_thread'].insert_task("update-net", net
)
1599 print ' http_post_server_action error at server deletion ERROR UPDATING NETS !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c1
1600 data
={'result' : 'deleting in process, but openflow rules cannot be deleted!!!!!'}
1601 #look for dhcp ip address
1602 if r2
>0 and config_dic
.get("dhcp_server"):
1604 if iface
["net_id"] in config_dic
["dhcp_nets"]:
1605 r
,c
= config_dic
['dhcp_thread'].insert_task("del", iface
["mac"])
1606 #print "dhcp insert del task"
1608 print ':http_post_servers ERROR UPDATING dhcp_server !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' + c
1610 return format_out(data
)
1614 @bottle.route(url_base
+ '/<tenant_id>/servers/<server_id>', method
='DELETE')
1615 def http_delete_server_id(tenant_id
, server_id
):
1616 '''delete a server'''
1617 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1618 #check valid tenant_id
1619 result
,content
= check_valid_tenant(my
, tenant_id
)
1621 bottle
.abort(result
, content
)
1624 return http_server_action(server_id
, tenant_id
, {"terminate":None} )
1627 @bottle.route(url_base
+ '/<tenant_id>/servers/<server_id>/action', method
='POST')
1628 def http_post_server_action(tenant_id
, server_id
):
1629 '''take an action over a server'''
1630 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1631 #check valid tenant_id
1632 result
,content
= check_valid_tenant(my
, tenant_id
)
1634 bottle
.abort(result
, content
)
1636 http_content
= format_in( server_action_schema
)
1637 #r = remove_extra_items(http_content, server_action_schema)
1638 #if r is not None: print "http_post_server_action: Warning: remove extra items ", r
1640 return http_server_action(server_id
, tenant_id
, http_content
)
1647 @bottle.route(url_base
+ '/networks', method
='GET')
1648 def http_get_networks():
1649 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1651 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_network
,
1652 ('id','name','tenant_id','type',
1653 'shared','provider:vlan','status','last_error','admin_state_up','provider:physical') )
1654 #TODO temporally remove tenant_id
1655 if "tenant_id" in where_
:
1656 del where_
["tenant_id"]
1657 result
, content
= my
.db
.get_table(SELECT
=select_
, FROM
='nets', WHERE
=where_
, LIMIT
=limit_
)
1659 print "http_get_networks error %d %s" % (result
, content
)
1660 bottle
.abort(-result
, content
)
1662 convert_boolean(content
, ('shared', 'admin_state_up', 'enable_dhcp') )
1663 delete_nulls(content
)
1664 change_keys_http2db(content
, http2db_network
, reverse
=True)
1665 data
={'networks' : content
}
1666 return format_out(data
)
1668 @bottle.route(url_base
+ '/networks/<network_id>', method
='GET')
1669 def http_get_network_id(network_id
):
1670 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1672 where_
= bottle
.request
.query
1673 where_
['uuid'] = network_id
1674 result
, content
= my
.db
.get_table(FROM
='nets', WHERE
=where_
, LIMIT
=100)
1677 print "http_get_networks_id error %d %s" % (result
, content
)
1678 bottle
.abort(-result
, content
)
1680 print "http_get_networks_id network '%s' not found" % network_id
1681 bottle
.abort(HTTP_Not_Found
, 'network %s not found' % network_id
)
1683 convert_boolean(content
, ('shared', 'admin_state_up', 'enale_dhcp') )
1684 change_keys_http2db(content
, http2db_network
, reverse
=True)
1686 result
, ports
= my
.db
.get_table(FROM
='ports', SELECT
=('uuid as port_id',),
1687 WHERE
={'net_id': network_id
}, LIMIT
=100)
1689 content
[0]['ports'] = ports
1690 delete_nulls(content
[0])
1691 data
={'network' : content
[0]}
1692 return format_out(data
)
1694 @bottle.route(url_base
+ '/networks', method
='POST')
1695 def http_post_networks():
1696 '''insert a network into the database.'''
1697 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1699 http_content
= format_in( network_new_schema
)
1700 r
= remove_extra_items(http_content
, network_new_schema
)
1701 if r
is not None: print "http_post_networks: Warning: remove extra items ", r
1702 change_keys_http2db(http_content
['network'], http2db_network
)
1703 network
=http_content
['network']
1704 #check valid tenant_id
1705 tenant_id
= network
.get('tenant_id')
1707 result
, _
= my
.db
.get_table(FROM
='tenants', SELECT
=('uuid',), WHERE
={'uuid': tenant_id
,"enabled":True})
1709 bottle
.abort(HTTP_Not_Found
, 'tenant %s not found or not enabled' % tenant_id
)
1713 net_provider
= network
.get('provider')
1714 net_type
= network
.get('type')
1715 net_vlan
= network
.get("vlan")
1716 net_bind_net
= network
.get("bind_net")
1717 net_bind_type
= network
.get("bind_type")
1718 name
= network
["name"]
1720 #check if network name ends with :<vlan_tag> and network exist in order to make and automated bindning
1721 vlan_index
=name
.rfind(":")
1722 if net_bind_net
==None and net_bind_type
==None and vlan_index
> 1:
1724 vlan_tag
= int(name
[vlan_index
+1:])
1725 if vlan_tag
>0 and vlan_tag
< 4096:
1726 net_bind_net
= name
[:vlan_index
]
1727 net_bind_type
= "vlan:" + name
[vlan_index
+1:]
1731 if net_bind_net
!= None:
1732 #look for a valid net
1733 if check_valid_uuid(net_bind_net
):
1734 net_bind_key
= "uuid"
1736 net_bind_key
= "name"
1737 result
, content
= my
.db
.get_table(FROM
='nets', WHERE
={net_bind_key
: net_bind_net
} )
1739 bottle
.abort(HTTP_Internal_Server_Error
, 'getting nets from db ' + content
)
1742 bottle
.abort(HTTP_Bad_Request
, "bind_net %s '%s'not found" % (net_bind_key
, net_bind_net
) )
1745 bottle
.abort(HTTP_Bad_Request
, "more than one bind_net %s '%s' found, use uuid" % (net_bind_key
, net_bind_net
) )
1747 network
["bind_net"] = content
[0]["uuid"]
1748 if net_bind_type
!= None:
1749 if net_bind_type
[0:5] != "vlan:":
1750 bottle
.abort(HTTP_Bad_Request
, "bad format for 'bind_type', must be 'vlan:<tag>'")
1752 if int(net_bind_type
[5:]) > 4095 or int(net_bind_type
[5:])<=0 :
1753 bottle
.abort(HTTP_Bad_Request
, "bad format for 'bind_type', must be 'vlan:<tag>' with a tag between 1 and 4095")
1755 network
["bind_type"] = net_bind_type
1757 if net_provider
!=None:
1758 if net_provider
[:9]=="openflow:":
1760 if net_type
!="ptp" and net_type
!="data":
1761 bottle
.abort(HTTP_Bad_Request
, "Only 'ptp' or 'data' net types can be bound to 'openflow'")
1766 if net_type
!="bridge_man" and net_type
!="bridge_data":
1767 bottle
.abort(HTTP_Bad_Request
, "Only 'bridge_man' or 'bridge_data' net types can be bound to 'bridge', 'macvtap' or 'default")
1769 net_type
='bridge_man'
1772 net_type
='bridge_man'
1774 if net_provider
!= None:
1775 if net_provider
[:7]=='bridge:':
1776 #check it is one of the pre-provisioned bridges
1777 bridge_net_name
= net_provider
[7:]
1778 for brnet
in config_dic
['bridge_nets']:
1779 if brnet
[0]==bridge_net_name
: # free
1780 if brnet
[3] != None:
1781 bottle
.abort(HTTP_Conflict
, "invalid 'provider:physical', bridge '%s' is already used" % bridge_net_name
)
1786 # if bridge_net==None:
1787 # 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)
1789 elif net_type
=='bridge_data' or net_type
=='bridge_man':
1790 #look for a free precreated nets
1791 for brnet
in config_dic
['bridge_nets']:
1792 if brnet
[3]==None: # free
1793 if bridge_net
!= None:
1794 if net_type
=='bridge_man': #look for the smaller speed
1795 if brnet
[2] < bridge_net
[2]: bridge_net
= brnet
1796 else: #look for the larger speed
1797 if brnet
[2] > bridge_net
[2]: bridge_net
= brnet
1801 if bridge_net
==None:
1802 bottle
.abort(HTTP_Bad_Request
, "Max limits of bridge networks reached. Future versions of VIM will overcome this limit")
1805 print "using net", bridge_net
1806 net_provider
= "bridge:"+bridge_net
[0]
1807 net_vlan
= bridge_net
[1]
1808 if net_vlan
==None and (net_type
=="data" or net_type
=="ptp"):
1809 net_vlan
= my
.db
.get_free_net_vlan()
1811 bottle
.abort(HTTP_Internal_Server_Error
, "Error getting an available vlan")
1814 network
['provider'] = net_provider
1815 network
['type'] = net_type
1816 network
['vlan'] = net_vlan
1817 result
, content
= my
.db
.new_row('nets', network
, True, True)
1820 if bridge_net
!=None:
1821 bridge_net
[3] = content
1823 if config_dic
.get("dhcp_server"):
1824 if network
["name"] in config_dic
["dhcp_server"].get("nets", () ):
1825 config_dic
["dhcp_nets"].append(content
)
1826 print "dhcp_server: add new net", content
1827 elif bridge_net
!= None and bridge_net
[0] in config_dic
["dhcp_server"].get("bridge_ifaces", () ):
1828 config_dic
["dhcp_nets"].append(content
)
1829 print "dhcp_server: add new net", content
1830 return http_get_network_id(content
)
1832 print "http_post_networks error %d %s" % (result
, content
)
1833 bottle
.abort(-result
, content
)
1837 @bottle.route(url_base
+ '/networks/<network_id>', method
='PUT')
1838 def http_put_network_id(network_id
):
1839 '''update a network_id into the database.'''
1840 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1842 http_content
= format_in( network_update_schema
)
1843 r
= remove_extra_items(http_content
, network_update_schema
)
1844 change_keys_http2db(http_content
['network'], http2db_network
)
1845 network
=http_content
['network']
1847 #Look for the previous data
1848 where_
= {'uuid': network_id
}
1849 result
, network_old
= my
.db
.get_table(FROM
='nets', WHERE
=where_
)
1851 print "http_put_network_id error %d %s" % (result
, network_old
)
1852 bottle
.abort(-result
, network_old
)
1855 print "http_put_network_id network '%s' not found" % network_id
1856 bottle
.abort(HTTP_Not_Found
, 'network %s not found' % network_id
)
1859 nbports
, content
= my
.db
.get_table(FROM
='ports', SELECT
=('uuid as port_id',),
1860 WHERE
={'net_id': network_id
}, LIMIT
=100)
1862 print "http_put_network_id error %d %s" % (result
, network_old
)
1863 bottle
.abort(-result
, content
)
1866 if 'type' in network
and network
['type'] != network_old
[0]['type']:
1867 bottle
.abort(HTTP_Method_Not_Allowed
, "Can not change type of network while having ports attached")
1868 if 'vlan' in network
and network
['vlan'] != network_old
[0]['vlan']:
1869 bottle
.abort(HTTP_Method_Not_Allowed
, "Can not change vlan of network while having ports attached")
1872 net_provider
= network
.get('provider', network_old
[0]['provider'])
1873 net_type
= network
.get('type', network_old
[0]['type'])
1874 net_bind_net
= network
.get("bind_net")
1875 net_bind_type
= network
.get("bind_type")
1876 if net_bind_net
!= None:
1877 #look for a valid net
1878 if check_valid_uuid(net_bind_net
):
1879 net_bind_key
= "uuid"
1881 net_bind_key
= "name"
1882 result
, content
= my
.db
.get_table(FROM
='nets', WHERE
={net_bind_key
: net_bind_net
} )
1884 bottle
.abort(HTTP_Internal_Server_Error
, 'getting nets from db ' + content
)
1887 bottle
.abort(HTTP_Bad_Request
, "bind_net %s '%s'not found" % (net_bind_key
, net_bind_net
) )
1890 bottle
.abort(HTTP_Bad_Request
, "more than one bind_net %s '%s' found, use uuid" % (net_bind_key
, net_bind_net
) )
1892 network
["bind_net"] = content
[0]["uuid"]
1893 if net_bind_type
!= None:
1894 if net_bind_type
[0:5] != "vlan:":
1895 bottle
.abort(HTTP_Bad_Request
, "bad format for 'bind_type', must be 'vlan:<tag>'")
1897 if int(net_bind_type
[5:]) > 4095 or int(net_bind_type
[5:])<=0 :
1898 bottle
.abort(HTTP_Bad_Request
, "bad format for 'bind_type', must be 'vlan:<tag>' with a tag between 1 and 4095")
1900 if net_provider
!=None:
1901 if net_provider
[:9]=="openflow:":
1902 if net_type
!="ptp" and net_type
!="data":
1903 bottle
.abort(HTTP_Bad_Request
, "Only 'ptp' or 'data' net types can be bound to 'openflow'")
1905 if net_type
!="bridge_man" and net_type
!="bridge_data":
1906 bottle
.abort(HTTP_Bad_Request
, "Only 'bridge_man' or 'bridge_data' net types can be bound to 'bridge', 'macvtap' or 'default")
1908 #insert in data base
1909 result
, content
= my
.db
.update_rows('nets', network
, WHERE
={'uuid': network_id
}, log
=True )
1911 if result
>0: # and nbports>0 and 'admin_state_up' in network and network['admin_state_up'] != network_old[0]['admin_state_up']:
1912 r
,c
= config_dic
['of_thread'].insert_task("update-net", network_id
)
1914 print "http_put_network_id error while launching openflow rules"
1915 bottle
.abort(HTTP_Internal_Server_Error
, c
)
1916 if config_dic
.get("dhcp_server"):
1917 if network_id
in config_dic
["dhcp_nets"]:
1918 config_dic
["dhcp_nets"].remove(network_id
)
1919 print "dhcp_server: delete net", network_id
1920 if network
.get("name", network_old
["name"]) in config_dic
["dhcp_server"].get("nets", () ):
1921 config_dic
["dhcp_nets"].append(network_id
)
1922 print "dhcp_server: add new net", network_id
1924 net_bind
= network
.get("bind", network_old
["bind"] )
1925 if net_bind
and net_bind
[:7]=="bridge:" and net_bind
[7:] in config_dic
["dhcp_server"].get("bridge_ifaces", () ):
1926 config_dic
["dhcp_nets"].append(network_id
)
1927 print "dhcp_server: add new net", network_id
1928 return http_get_network_id(network_id
)
1930 bottle
.abort(-result
, content
)
1934 @bottle.route(url_base
+ '/networks/<network_id>', method
='DELETE')
1935 def http_delete_network_id(network_id
):
1936 '''delete a network_id from the database.'''
1937 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1939 #delete from the data base
1940 result
, content
= my
.db
.delete_row('nets', network_id
)
1943 bottle
.abort(HTTP_Not_Found
, content
)
1945 for brnet
in config_dic
['bridge_nets']:
1946 if brnet
[3]==network_id
:
1949 if config_dic
.get("dhcp_server") and network_id
in config_dic
["dhcp_nets"]:
1950 config_dic
["dhcp_nets"].remove(network_id
)
1951 print "dhcp_server: delete net", network_id
1952 data
={'result' : content
}
1953 return format_out(data
)
1955 print "http_delete_network_id error",result
, content
1956 bottle
.abort(-result
, content
)
1961 @bottle.route(url_base
+ '/networks/<network_id>/openflow', method
='GET')
1962 def http_get_openflow_id(network_id
):
1963 '''To obtain the list of openflow rules of a network
1965 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1967 if network_id
=='all':
1970 where_
={"net_id": network_id
}
1971 result
, content
= my
.db
.get_table(SELECT
=("name","net_id","priority","vlan_id","ingress_port","src_mac","dst_mac","actions"),
1972 WHERE
=where_
, FROM
='of_flows')
1974 bottle
.abort(-result
, content
)
1976 data
={'openflow-rules' : content
}
1977 return format_out(data
)
1979 @bottle.route(url_base
+ '/networks/<network_id>/openflow', method
='PUT')
1980 def http_put_openflow_id(network_id
):
1981 '''To make actions over the net. The action is to reinstall the openflow rules
1982 network_id can be 'all'
1984 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
1986 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
1989 if network_id
=='all':
1992 where_
={"uuid": network_id
}
1993 result
, content
= my
.db
.get_table(SELECT
=("uuid","type"), WHERE
=where_
, FROM
='nets')
1995 bottle
.abort(-result
, content
)
1999 if net
["type"]!="ptp" and net
["type"]!="data":
2002 r
,c
= config_dic
['of_thread'].insert_task("update-net", net
['uuid'])
2004 print "http_put_openflow_id error while launching openflow rules"
2005 bottle
.abort(HTTP_Internal_Server_Error
, c
)
2006 data
={'result' : str(result
)+" nets updates"}
2007 return format_out(data
)
2009 @bottle.route(url_base
+ '/networks/openflow/clear', method
='DELETE')
2010 @bottle.route(url_base
+ '/networks/clear/openflow', method
='DELETE')
2011 def http_clear_openflow_rules():
2012 '''To make actions over the net. The action is to delete ALL openflow rules
2014 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2016 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
2019 r
,c
= config_dic
['of_thread'].insert_task("clear-all")
2021 print "http_delete_openflow_id error while launching openflow rules"
2022 bottle
.abort(HTTP_Internal_Server_Error
, c
)
2025 data
={'result' : " Clearing openflow rules in process"}
2026 return format_out(data
)
2028 @bottle.route(url_base
+ '/networks/openflow/ports', method
='GET')
2029 def http_get_openflow_ports():
2030 '''Obtain switch ports names of openflow controller
2032 data
={'ports' : config_dic
['of_thread'].OF_connector
.pp2ofi
}
2033 return format_out(data
)
2040 @bottle.route(url_base
+ '/ports', method
='GET')
2041 def http_get_ports():
2043 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2044 select_
,where_
,limit_
= filter_query_string(bottle
.request
.query
, http2db_port
,
2045 ('id','name','tenant_id','network_id','vpci','mac_address','device_owner','device_id',
2046 'binding:switch_port','binding:vlan','bandwidth','status','admin_state_up','ip_address') )
2047 #result, content = my.db.get_ports(where_)
2048 result
, content
= my
.db
.get_table(SELECT
=select_
, WHERE
=where_
, FROM
='ports',LIMIT
=limit_
)
2050 print "http_get_ports Error", result
, content
2051 bottle
.abort(-result
, content
)
2054 convert_boolean(content
, ('admin_state_up',) )
2055 delete_nulls(content
)
2056 change_keys_http2db(content
, http2db_port
, reverse
=True)
2057 data
={'ports' : content
}
2058 return format_out(data
)
2060 @bottle.route(url_base
+ '/ports/<port_id>', method
='GET')
2061 def http_get_port_id(port_id
):
2062 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2064 result
, content
= my
.db
.get_table(WHERE
={'uuid': port_id
}, FROM
='ports')
2066 print "http_get_ports error", result
, content
2067 bottle
.abort(-result
, content
)
2069 print "http_get_ports port '%s' not found" % str(port_id
)
2070 bottle
.abort(HTTP_Not_Found
, 'port %s not found' % port_id
)
2072 convert_boolean(content
, ('admin_state_up',) )
2073 delete_nulls(content
)
2074 change_keys_http2db(content
, http2db_port
, reverse
=True)
2075 data
={'port' : content
[0]}
2076 return format_out(data
)
2079 @bottle.route(url_base
+ '/ports', method
='POST')
2080 def http_post_ports():
2081 '''insert an external port into the database.'''
2082 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2084 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
2086 http_content
= format_in( port_new_schema
)
2087 r
= remove_extra_items(http_content
, port_new_schema
)
2088 if r
is not None: print "http_post_ports: Warning: remove extra items ", r
2089 change_keys_http2db(http_content
['port'], http2db_port
)
2090 port
=http_content
['port']
2092 port
['type'] = 'external'
2093 if 'net_id' in port
and port
['net_id'] == None:
2096 if 'net_id' in port
:
2097 #check that new net has the correct type
2098 result
, new_net
= my
.db
.check_target_net(port
['net_id'], None, 'external' )
2100 bottle
.abort(HTTP_Bad_Request
, new_net
)
2102 #insert in data base
2103 result
, uuid
= my
.db
.new_row('ports', port
, True, True)
2105 if 'net_id' in port
:
2106 r
,c
= config_dic
['of_thread'].insert_task("update-net", port
['net_id'])
2108 print "http_post_ports error while launching openflow rules"
2109 bottle
.abort(HTTP_Internal_Server_Error
, c
)
2110 return http_get_port_id(uuid
)
2112 bottle
.abort(-result
, uuid
)
2115 @bottle.route(url_base
+ '/ports/<port_id>', method
='PUT')
2116 def http_put_port_id(port_id
):
2117 '''update a port_id into the database.'''
2119 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2121 http_content
= format_in( port_update_schema
)
2122 change_keys_http2db(http_content
['port'], http2db_port
)
2123 port_dict
=http_content
['port']
2125 #Look for the previous port data
2126 where_
= {'uuid': port_id
}
2127 result
, content
= my
.db
.get_table(FROM
="ports",WHERE
=where_
)
2129 print "http_put_port_id error", result
, content
2130 bottle
.abort(-result
, content
)
2133 print "http_put_port_id port '%s' not found" % port_id
2134 bottle
.abort(HTTP_Not_Found
, 'port %s not found' % port_id
)
2137 for k
in ('vlan','switch_port','mac_address', 'tenant_id'):
2138 if k
in port_dict
and not my
.admin
:
2139 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges for changing " + k
)
2143 #change_keys_http2db(port, http2db_port, reverse=True)
2147 if 'net_id' in port_dict
:
2149 old_net
= port
.get('net_id', None)
2150 new_net
= port_dict
['net_id']
2151 if old_net
!= new_net
:
2153 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
2154 if old_net
is not None: nets
.append(old_net
)
2155 if port
['type'] == 'instance:bridge':
2156 bottle
.abort(HTTP_Forbidden
, "bridge interfaces cannot be attached to a different net")
2158 elif port
['type'] == 'external':
2160 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
2164 #check that new net has the correct type
2165 result
, new_net_dict
= my
.db
.check_target_net(new_net
, None, port
['type'] )
2167 #change VLAN for SR-IOV ports
2168 if result
>=0 and port
["type"]=="instance:data" and port
["model"]=="VF": #TODO consider also VFnotShared
2170 port_dict
["vlan"] = None
2172 port_dict
["vlan"] = new_net_dict
["vlan"]
2173 #get host where this VM is allocated
2174 result
, content
= my
.db
.get_table(FROM
="instances",WHERE
={"uuid":port
["instance_id"]})
2176 print "http_put_port_id database error", content
2178 host_id
= content
[0]["host_id"]
2180 #insert in data base
2182 result
, content
= my
.db
.update_rows('ports', port_dict
, WHERE
={'uuid': port_id
}, log
=False )
2184 #Insert task to complete actions
2187 r
,v
= config_dic
['of_thread'].insert_task("update-net", net_id
)
2188 if r
<0: print "Error ********* http_put_port_id update_of_flows: ", v
2189 #TODO Do something if fails
2191 config_dic
['host_threads'][host_id
].insert_task("edit-iface", port_id
, old_net
, new_net
)
2194 return http_get_port_id(port_id
)
2196 bottle
.abort(HTTP_Bad_Request
, content
)
2200 @bottle.route(url_base
+ '/ports/<port_id>', method
='DELETE')
2201 def http_delete_port_id(port_id
):
2202 '''delete a port_id from the database.'''
2203 my
= config_dic
['http_threads'][ threading
.current_thread().name
]
2205 bottle
.abort(HTTP_Unauthorized
, "Needed admin privileges")
2208 #Look for the previous port data
2209 where_
= {'uuid': port_id
, "type": "external"}
2210 result
, ports
= my
.db
.get_table(WHERE
=where_
, FROM
='ports',LIMIT
=100)
2213 print "http_delete_port_id port '%s' not found" % port_id
2214 bottle
.abort(HTTP_Not_Found
, 'port %s not found or device_owner is not external' % port_id
)
2216 #delete from the data base
2217 result
, content
= my
.db
.delete_row('ports', port_id
)
2220 bottle
.abort(HTTP_Not_Found
, content
)
2222 network
= ports
[0].get('net_id', None)
2223 if network
is not None:
2225 r
,c
= config_dic
['of_thread'].insert_task("update-net", network
)
2226 if r
<0: print "!!!!!! http_delete_port_id update_of_flows error", r
, c
2227 data
={'result' : content
}
2228 return format_out(data
)
2230 print "http_delete_port_id error",result
, content
2231 bottle
.abort(-result
, content
)