e5cd40d0169bf68898cf2e43342ab56012256348
[osm/openvim.git] / ovim.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, Leonardo Mirabal"
30 __date__ = "$06-Feb-2017 12:07:15$"
31
32 import threading
33 import vim_db
34 import logging
35 import threading
36 import imp
37 import host_thread as ht
38 import dhcp_thread as dt
39 import openflow_thread as oft
40
41 HTTP_Bad_Request = 400
42 HTTP_Unauthorized = 401
43 HTTP_Not_Found = 404
44 HTTP_Forbidden = 403
45 HTTP_Method_Not_Allowed = 405
46 HTTP_Not_Acceptable = 406
47 HTTP_Request_Timeout = 408
48 HTTP_Conflict = 409
49 HTTP_Service_Unavailable = 503
50 HTTP_Internal_Server_Error= 500
51
52
53 def convert_boolean(data, items):
54 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
55 It assumes that bandwidth is well formed
56 Attributes:
57 'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid
58 'items': tuple of keys to convert
59 Return:
60 None
61 '''
62 if type(data) is dict:
63 for k in data.keys():
64 if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
65 convert_boolean(data[k], items)
66 if k in items:
67 if type(data[k]) is str:
68 if data[k] == "false":
69 data[k] = False
70 elif data[k] == "true":
71 data[k] = True
72 if type(data) is tuple or type(data) is list:
73 for k in data:
74 if type(k) is dict or type(k) is tuple or type(k) is list:
75 convert_boolean(k, items)
76
77
78
79 class ovimException(Exception):
80 def __init__(self, message, http_code=HTTP_Bad_Request):
81 self.http_code = http_code
82 Exception.__init__(self, message)
83
84
85 class ovim():
86 running_info = {} #TODO OVIM move the info of running threads from config_dic to this static variable
87 def __init__(self, configuration):
88 self.config = configuration
89 self.logger = logging.getLogger(configuration["logger_name"])
90 self.db = None
91 self.db = self._create_database_connection()
92
93 def _create_database_connection(self):
94 db = vim_db.vim_db((self.config["network_vlan_range_start"], self.config["network_vlan_range_end"]),
95 self.config['log_level_db']);
96 if db.connect(self.config['db_host'], self.config['db_user'], self.config['db_passwd'],
97 self.config['db_name']) == -1:
98 # self.logger.error("Cannot connect to database %s at %s@%s", self.config['db_name'], self.config['db_user'],
99 # self.config['db_host'])
100 raise ovimException("Cannot connect to database {} at {}@{}".format(self.config['db_name'],
101 self.config['db_user'],
102 self.config['db_host']) )
103 return db
104
105
106 def start_service(self):
107 #if self.running_info:
108 # return #TODO service can be checked and rebuild broken threads
109 r = self.db.get_db_version()
110 if r[0]<0:
111 raise ovimException("DATABASE is not a VIM one or it is a '0.0' version. Try to upgrade to version '{}' with "\
112 "'./database_utils/migrate_vim_db.sh'".format(self.config["database_version"]) )
113 elif r[1]!=self.config["database_version"]:
114 raise ovimException("DATABASE wrong version '{}'. Try to upgrade/downgrade to version '{}' with "\
115 "'./database_utils/migrate_vim_db.sh'".format(r[1], self.config["database_version"]) )
116
117 # create database connection for openflow threads
118 db_of = self._create_database_connection()
119 self.config["db"] = db_of
120 db_lock = threading.Lock()
121 self.config["db_lock"] = db_lock
122
123 # precreate interfaces; [bridge:<host_bridge_name>, VLAN used at Host, uuid of network camping in this bridge, speed in Gbit/s
124 self.config['dhcp_nets'] = []
125 self.config['bridge_nets'] = []
126 for bridge, vlan_speed in self.config["bridge_ifaces"].items():
127 # skip 'development_bridge'
128 if self.config['mode'] == 'development' and self.config['development_bridge'] == bridge:
129 continue
130 self.config['bridge_nets'].append([bridge, vlan_speed[0], vlan_speed[1], None])
131
132 # check if this bridge is already used (present at database) for a network)
133 used_bridge_nets = []
134 for brnet in self.config['bridge_nets']:
135 r, nets = db_of.get_table(SELECT=('uuid',), FROM='nets', WHERE={'provider': "bridge:" + brnet[0]})
136 if r > 0:
137 brnet[3] = nets[0]['uuid']
138 used_bridge_nets.append(brnet[0])
139 if self.config.get("dhcp_server"):
140 if brnet[0] in self.config["dhcp_server"]["bridge_ifaces"]:
141 self.config['dhcp_nets'].append(nets[0]['uuid'])
142 if len(used_bridge_nets) > 0:
143 self.logger.info("found used bridge nets: " + ",".join(used_bridge_nets))
144 # get nets used by dhcp
145 if self.config.get("dhcp_server"):
146 for net in self.config["dhcp_server"].get("nets", ()):
147 r, nets = db_of.get_table(SELECT=('uuid',), FROM='nets', WHERE={'name': net})
148 if r > 0:
149 self.config['dhcp_nets'].append(nets[0]['uuid'])
150
151 # get host list from data base before starting threads
152 r, hosts = db_of.get_table(SELECT=('name', 'ip_name', 'user', 'uuid'), FROM='hosts', WHERE={'status': 'ok'})
153 if r < 0:
154 raise ovimException("Cannot get hosts from database {}".format(hosts))
155 # create connector to the openflow controller
156 of_test_mode = False if self.config['mode'] == 'normal' or self.config['mode'] == "OF only" else True
157
158 if of_test_mode:
159 OF_conn = oft.of_test_connector({"of_debug": self.config['log_level_of']})
160 else:
161 # load other parameters starting by of_ from config dict in a temporal dict
162 temp_dict = {"of_ip": self.config['of_controller_ip'],
163 "of_port": self.config['of_controller_port'],
164 "of_dpid": self.config['of_controller_dpid'],
165 "of_debug": self.config['log_level_of']
166 }
167 for k, v in self.config.iteritems():
168 if type(k) is str and k[0:3] == "of_" and k[0:13] != "of_controller":
169 temp_dict[k] = v
170 if self.config['of_controller'] == 'opendaylight':
171 module = "ODL"
172 elif "of_controller_module" in self.config:
173 module = self.config["of_controller_module"]
174 else:
175 module = self.config['of_controller']
176 module_info = None
177 try:
178 module_info = imp.find_module(module)
179
180 OF_conn = imp.load_module("OF_conn", *module_info)
181 try:
182 OF_conn = OF_conn.OF_conn(temp_dict)
183 except Exception as e:
184 self.logger.error("Cannot open the Openflow controller '%s': %s", type(e).__name__, str(e))
185 if module_info and module_info[0]:
186 file.close(module_info[0])
187 exit(-1)
188 except (IOError, ImportError) as e:
189 if module_info and module_info[0]:
190 file.close(module_info[0])
191 self.logger.error(
192 "Cannot open openflow controller module '%s'; %s: %s; revise 'of_controller' field of configuration file.",
193 module, type(e).__name__, str(e))
194 raise ovimException("Cannot open openflow controller module '{}'; {}: {}; revise 'of_controller' field of configuration file.".fromat(
195 module, type(e).__name__, str(e)))
196
197
198 # create openflow thread
199 thread = oft.openflow_thread(OF_conn, of_test=of_test_mode, db=db_of, db_lock=db_lock,
200 pmp_with_same_vlan=self.config['of_controller_nets_with_same_vlan'],
201 debug=self.config['log_level_of'])
202 r, c = thread.OF_connector.obtain_port_correspondence()
203 if r < 0:
204 raise ovimException("Cannot get openflow information %s", c)
205 thread.start()
206 self.config['of_thread'] = thread
207
208 # create dhcp_server thread
209 host_test_mode = True if self.config['mode'] == 'test' or self.config['mode'] == "OF only" else False
210 dhcp_params = self.config.get("dhcp_server")
211 if dhcp_params:
212 thread = dt.dhcp_thread(dhcp_params=dhcp_params, test=host_test_mode, dhcp_nets=self.config["dhcp_nets"],
213 db=db_of, db_lock=db_lock, debug=self.config['log_level_of'])
214 thread.start()
215 self.config['dhcp_thread'] = thread
216
217 # Create one thread for each host
218 host_test_mode = True if self.config['mode'] == 'test' or self.config['mode'] == "OF only" else False
219 host_develop_mode = True if self.config['mode'] == 'development' else False
220 host_develop_bridge_iface = self.config.get('development_bridge', None)
221 self.config['host_threads'] = {}
222 for host in hosts:
223 host['image_path'] = '/opt/VNF/images/openvim'
224 thread = ht.host_thread(name=host['name'], user=host['user'], host=host['ip_name'], db=db_of, db_lock=db_lock,
225 test=host_test_mode, image_path=self.config['image_path'], version=self.config['version'],
226 host_id=host['uuid'], develop_mode=host_develop_mode,
227 develop_bridge_iface=host_develop_bridge_iface)
228 thread.start()
229 self.config['host_threads'][host['uuid']] = thread
230
231 def stop_service(self):
232 threads = self.config.get('host_threads', {})
233 if 'of_thread' in self.config:
234 threads['of'] = (self.config['of_thread'])
235 if 'dhcp_thread' in self.config:
236 threads['dhcp'] = (self.config['dhcp_thread'])
237
238 for thread in threads.values():
239 thread.insert_task("exit")
240 for thread in threads.values():
241 thread.join()
242 # http_thread.join()
243 # if http_thread_admin is not None:
244 # http_thread_admin.join()
245
246
247 def get_ports(self, columns=None, filter={}, limit=None):
248 # result, content = my.db.get_ports(where_)
249 result, content = self.db.get_table(SELECT=columns, WHERE=filter, FROM='ports', LIMIT=limit)
250 if result < 0:
251 self.logger.error("http_get_ports Error %d %s", result, content)
252 raise ovimException(str(content), -result)
253 else:
254 convert_boolean(content, ('admin_state_up',))
255 return content
256
257
258 def new_port(self, port_data):
259 port_data['type'] = 'external'
260 if port_data.get('net_id'):
261 # check that new net has the correct type
262 result, new_net = self.db.check_target_net(port_data['net_id'], None, 'external')
263 if result < 0:
264 raise ovimException(str(new_net), -result)
265 # insert in data base
266 result, uuid = self.db.new_row('ports', port_data, True, True)
267 if result > 0:
268 if 'net_id' in port_data:
269 r, c = self.config['of_thread'].insert_task("update-net", port_data['net_id'])
270 if r < 0:
271 self.logger.error("Cannot insert a task for updating network '$s' %s", port_data['net_id'], c)
272 #TODO put network in error status
273 return uuid
274 else:
275 raise ovimException(str(uuid), -result)
276
277 def delete_port(self, port_id):
278 # Look for the previous port data
279 result, ports = self.db.get_table(WHERE={'uuid': port_id, "type": "external"}, FROM='ports')
280 if result < 0:
281 raise ovimException("Cannot get port info from database: {}".format(ports), http_code=-result)
282 # delete from the data base
283 result, content = self.db.delete_row('ports', port_id)
284 if result == 0:
285 raise ovimException("External port '{}' not found".format(port_id), http_code=HTTP_Not_Found)
286 elif result < 0:
287 raise ovimException("Cannot delete port from database: {}".format(content), http_code=-result)
288 # update network
289 network = ports[0].get('net_id', None)
290 if network:
291 # change of net.
292 r, c = self.config['of_thread'].insert_task("update-net", network)
293 if r < 0:
294 self.logger.error("Cannot insert a task for updating network '$s' %s", network, c)
295 return content
296
297
298 def edit_port(self, port_id, port_data, admin=True):
299 # Look for the previous port data
300 result, content = self.db.get_table(FROM="ports", WHERE={'uuid': port_id})
301 if result < 0:
302 raise ovimException("Cannot get port info from database: {}".format(content), http_code=-result)
303 elif result == 0:
304 raise ovimException("Port '{}' not found".format(port_id), http_code=HTTP_Not_Found)
305 port = content[0]
306 nets = []
307 host_id = None
308 result = 1
309 if 'net_id' in port_data:
310 # change of net.
311 old_net = port.get('net_id', None)
312 new_net = port_data['net_id']
313 if old_net != new_net:
314
315 if new_net:
316 nets.append(new_net) # put first the new net, so that new openflow rules are created before removing the old ones
317 if old_net:
318 nets.append(old_net)
319 if port['type'] == 'instance:bridge' or port['type'] == 'instance:ovs':
320 raise ovimException("bridge interfaces cannot be attached to a different net", http_code=HTTP_Forbidden)
321 elif port['type'] == 'external' and not admin:
322 raise ovimException("Needed admin privileges",http_code=HTTP_Unauthorized)
323 if new_net:
324 # check that new net has the correct type
325 result, new_net_dict = self.db.check_target_net(new_net, None, port['type'])
326 if result < 0:
327 raise ovimException("Error {}".format(new_net_dict), http_code=HTTP_Conflict)
328 # change VLAN for SR-IOV ports
329 if result >= 0 and port["type"] == "instance:data" and port["model"] == "VF": # TODO consider also VFnotShared
330 if new_net:
331 port_data["vlan"] = None
332 else:
333 port_data["vlan"] = new_net_dict["vlan"]
334 # get host where this VM is allocated
335 result, content = self.db.get_table(FROM="instances", WHERE={"uuid": port["instance_id"]})
336 if result > 0:
337 host_id = content[0]["host_id"]
338
339 # insert in data base
340 if result >= 0:
341 result, content = self.db.update_rows('ports', port_data, WHERE={'uuid': port_id}, log=False)
342
343 # Insert task to complete actions
344 if result > 0:
345 for net_id in nets:
346 r, v = self.config['of_thread'].insert_task("update-net", net_id)
347 if r < 0:
348 self.logger.error("Error updating network '{}' {}".format(r,v))
349 # TODO Do something if fails
350 if host_id:
351 r, v = self.config['host_threads'][host_id].insert_task("edit-iface", port_id, old_net, new_net)
352 if r < 0:
353 self.logger.error("Error updating network '{}' {}".format(r,v))
354 # TODO Do something if fails
355 if result >= 0:
356 return port_id
357 else:
358 raise ovimException("Error {}".format(content), http_code=-result)