Srikanth Vavilapalli | 1725e49 | 2014-12-01 17:50:52 -0800 | [diff] [blame] | 1 | # |
| 2 | # Copyright (c) 2012,2013 Big Switch Networks, Inc. |
| 3 | # |
| 4 | # Licensed under the Eclipse Public License, Version 1.0 (the |
| 5 | # "License"); you may not use this file except in compliance with the |
| 6 | # License. You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.eclipse.org/legal/epl-v10.html |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| 13 | # implied. See the License for the specific language governing |
| 14 | # permissions and limitations under the License. |
| 15 | # |
| 16 | |
| 17 | # The database/model descriptions exist to meet particular |
| 18 | # needs, for example, switch-alias exist to provide an |
| 19 | # alternate name space from dpid's, to allow for a more |
| 20 | # readable and human usable form of the same dpid. Aliases |
| 21 | # would then naturally need a alias->dpid conversion, and |
| 22 | # at the same time, a dpid->alias (at least for the display |
| 23 | # of dpid's). |
| 24 | # |
| 25 | # The functions in this file provide these type of abstractions, |
| 26 | # taking the output from model lookup's in the rest api, and |
| 27 | # supplying some service used by the cli. |
| 28 | # |
| 29 | |
| 30 | import rest_to_model |
| 31 | import fmtcnv |
| 32 | import json |
| 33 | import utif |
| 34 | |
| 35 | def init_midware(bs, modi): |
| 36 | global sdnsh, mi |
| 37 | sdnsh = bs |
| 38 | mi = modi |
| 39 | |
| 40 | # |
| 41 | # -------------------------------------------------------------------------------- |
| 42 | |
| 43 | def create_obj_type_dict(obj_type, field, key = None, value = None): |
| 44 | """ |
| 45 | Return a dictionary from a table search, where the key is one of the |
| 46 | fields. This doesn't manage multiple field matches. |
| 47 | |
| 48 | Typically, the field selected is a foreign key for the obj_type. |
| 49 | |
| 50 | For ('host-network-address', 'host'), this creates a dict |
| 51 | indexed by the mac address, returning the row in the table associated |
| 52 | with the mac (since the primary key for 'host' is a mac address). |
| 53 | |
| 54 | For ('tag-mapping', 'host'), this creates a dict indexed by |
| 55 | the mac, returning the matching row in the table. |
| 56 | |
| 57 | note: This gets the whole table |
| 58 | """ |
| 59 | if not mi.obj_type_has_field(obj_type, field): |
| 60 | return {} |
| 61 | |
| 62 | if not mi.obj_type_has_model(obj_type): |
| 63 | data = {} |
| 64 | if key and value: |
| 65 | data[key] = value |
| 66 | rows = rest_to_model.get_model_from_url(obj_type, data) |
| 67 | elif not type(key) is dict: |
| 68 | try: |
| 69 | rows = sdnsh.get_table_from_store(obj_type, key, value) |
| 70 | except Exception, e: |
| 71 | errors = sdnsh.rest_error_to_dict(e) |
| 72 | print sdnsh.rest_error_dict_to_message(errors) |
| 73 | rows = [] |
| 74 | else: |
| 75 | try: |
| 76 | rows = sdnsh.rest_query_objects(obj_type, key) |
| 77 | except Exception, e: |
| 78 | errors = sdnsh.rest_error_to_dict(e) |
| 79 | print sdnsh.rest_error_dict_to_message(errors) |
| 80 | rows = [] |
| 81 | |
| 82 | s_dict = {} |
| 83 | for row in rows: |
| 84 | if row[field] in s_dict: |
| 85 | s_dict[row[field]].append(row) |
| 86 | else: |
| 87 | s_dict[row[field]] = [row] |
| 88 | |
| 89 | return s_dict |
| 90 | |
| 91 | |
| 92 | # |
| 93 | # ALIAS |
| 94 | # |
| 95 | |
| 96 | # |
| 97 | # -------------------------------------------------------------------------------- |
| 98 | |
| 99 | def alias_lookup(alias_obj_type, alias_id): |
| 100 | """ |
| 101 | Return the value for the alias replacement by looking it up in the store. |
| 102 | When there is no alias replacement, return None. |
| 103 | """ |
| 104 | field = mi.alias_obj_type_field(alias_obj_type) |
| 105 | if not field: |
| 106 | print sdnsh.error_msg("Error: no field for alias") |
| 107 | return None |
| 108 | try: |
| 109 | alias_key = mi.pk(alias_obj_type) |
| 110 | # use an exact search instead of a 'get_object...()' since |
| 111 | # a miss for an exact search can lead to a 404 error, which |
| 112 | # gets recorded in the error logs |
| 113 | alias_row = sdnsh.get_table_from_store(alias_obj_type, |
| 114 | alias_key, |
| 115 | alias_id, |
| 116 | "exact") |
| 117 | if len(alias_row) == 1: |
| 118 | return alias_row[0][field] |
| 119 | # only len(alias_row) == 0 at this point |
| 120 | except: |
| 121 | pass |
| 122 | return None |
| 123 | |
| 124 | # |
| 125 | # -------------------------------------------------------------------------------- |
| 126 | |
| 127 | def convert_alias_to_object_key(obj_type, name_or_alias): |
| 128 | """ |
| 129 | For a specific obj_type (table/model) which may have an alias 'row', |
| 130 | return the alias when it exists for this name_or_alias. |
| 131 | """ |
| 132 | if obj_type in mi.alias_obj_type_xref: |
| 133 | if name_or_alias in sdnsh.reserved_words: |
| 134 | return name_or_alias |
| 135 | for alias in mi.alias_obj_type_xref[obj_type]: |
| 136 | alias_value = alias_lookup(alias, name_or_alias) |
| 137 | if alias_value: |
| 138 | return alias_value |
| 139 | return name_or_alias |
| 140 | |
| 141 | # |
| 142 | # -------------------------------------------------------------------------------- |
| 143 | |
| 144 | def alias_choices_for_alias_obj_type(entries, obj_type, text): |
| 145 | """ |
| 146 | Used to return all choices of entries for an alias. Remove any original |
| 147 | items which appear in the entries list passed in preventing duplication |
| 148 | of entries |
| 149 | |
| 150 | Also see cp_alias_choices(), which is similar, but includes |
| 151 | the current mode. |
| 152 | """ |
| 153 | if obj_type in mi.alias_obj_type_xref: |
| 154 | for alias in mi.alias_obj_type_xref[obj_type]: |
| 155 | try: |
| 156 | key = mi.pk(alias) |
| 157 | alias_dict = create_obj_type_dict(alias, key, key, text) |
| 158 | # |
| 159 | # remove the alias name if the dpid is in the |
| 160 | # list of entries... In all cases the alias is added, |
| 161 | # especially since the alias_dict may only contain selected |
| 162 | # entries from the 'text' query, and entries may already |
| 163 | # exclude those items. |
| 164 | alias_field = mi.alias_obj_type_field(alias) |
| 165 | if not alias_field: |
| 166 | continue |
| 167 | for item in alias_dict: |
| 168 | if alias_dict[item][0][alias_field] in entries: |
| 169 | entries.remove(alias_dict[item][0][alias_field]) |
| 170 | entries.append(item) |
| 171 | |
| 172 | except Exception, e: |
| 173 | pass |
| 174 | return entries |
| 175 | |
| 176 | # |
| 177 | # -------------------------------------------------------------------------------- |
| 178 | |
| 179 | def alias_lookup_with_foreign_key(alias_obj_type, foreign_key): |
| 180 | """ |
| 181 | Find the alias name for some alias based on the foreign key's |
| 182 | value it's associaed with. |
| 183 | """ |
| 184 | foreign_field = mi.alias_obj_type_field(alias_obj_type) |
| 185 | try: |
| 186 | rows = sdnsh.get_table_from_store(alias_obj_type, |
| 187 | foreign_field, |
| 188 | foreign_key, |
| 189 | "exact") |
| 190 | except Exception, e: |
| 191 | errors = sdnsh.rest_error_to_dict(e) |
| 192 | print sdnsh.rest_error_dict_to_message(errors) |
| 193 | rows = [] |
| 194 | if len(rows) == 1: |
| 195 | return rows[0][mi.pk(alias_obj_type)] |
| 196 | return None |
| 197 | |
| 198 | |
| 199 | # |
| 200 | # Interface between the cli and table output requires dictionaries |
| 201 | # which map between low item type values (for example, dpid's) and |
| 202 | # alias names for the items (for example, switch aliases), to be |
| 203 | # updated before display. If cassandra could provide some version |
| 204 | # number (or hash of the complete table), the lookup could be avoided |
| 205 | # by valiating that the current result is up-to-date. |
| 206 | # |
| 207 | |
| 208 | # |
| 209 | # -------------------------------------------------------------------------------- |
| 210 | |
| 211 | def update_show_alias(obj_type): |
| 212 | """ |
| 213 | Update alias associations for the pretty printer, used for the |
| 214 | 'show' of tables |
| 215 | """ |
| 216 | if obj_type in mi.alias_obj_type_xref: |
| 217 | for alias in mi.alias_obj_type_xref[obj_type]: |
| 218 | field = mi.alias_obj_type_field(alias) |
| 219 | if not field: |
| 220 | print sdnsh.error_msg("update show alias alias_obj_type_field") |
| 221 | return |
| 222 | try: |
| 223 | table = sdnsh.get_table_from_store(alias) |
| 224 | except Exception, e: |
| 225 | table = [] |
| 226 | |
| 227 | new_dict = {} |
| 228 | key = mi.pk(alias) |
| 229 | # (foreign_obj, foreign_field) = \ |
| 230 | # mi.foreign_key_references(alias, field) |
| 231 | for row in table: |
| 232 | new_dict[row[field]] = row[key] |
| 233 | fmtcnv.update_alias_dict(obj_type, new_dict) |
| 234 | return |
| 235 | |
| 236 | # |
| 237 | # -------------------------------------------------------------------------------- |
| 238 | |
| 239 | def update_switch_alias_cache(): |
| 240 | """ |
| 241 | Update the cliModeInfo prettyprinting switch table |
| 242 | """ |
| 243 | return update_show_alias('switch-config') |
| 244 | |
| 245 | # |
| 246 | # -------------------------------------------------------------------------------- |
| 247 | |
| 248 | def update_switch_port_name_cache(): |
| 249 | """ |
| 250 | Update the cliModeInfo prettyprinting portNames table |
| 251 | """ |
| 252 | # return update_show_alias('port') |
| 253 | |
| 254 | errors = None |
| 255 | switch_port_to_name_dict = {} |
| 256 | |
| 257 | try: |
| 258 | ports = rest_to_model.get_model_from_url('interfaces', {}) |
| 259 | except Exception, e: |
| 260 | errors = sdnsh.rest_error_to_dict(e) |
| 261 | |
| 262 | if errors: |
| 263 | print sdnsh.rest_error_dict_to_message(errors) |
| 264 | return |
| 265 | |
| 266 | for port in ports: |
| 267 | key_string = port['switch'] + "." + "%d" % port['portNumber'] |
| 268 | switch_port_to_name_dict[key_string] = port['portName'] |
| 269 | fmtcnv.update_alias_dict("portNames", switch_port_to_name_dict) |
| 270 | |
| 271 | # |
| 272 | # -------------------------------------------------------------------------------- |
| 273 | |
| 274 | def update_host_alias_cache(): |
| 275 | """ |
| 276 | Update the cliModeInfo prettyprinting host table |
| 277 | """ |
| 278 | return update_show_alias('host-config') |
| 279 | |
| 280 | |
| 281 | # |
| 282 | # -------------------------------------------------------------------------------- |
| 283 | # update_flow_cookie_hash |
| 284 | |
| 285 | def update_flow_cookie_hash(): |
| 286 | """ |
| 287 | The formatter keeps a map for static flow entries. |
| 288 | """ |
| 289 | # iterate through all the static flows and get their hashes once |
| 290 | flow_map = {} |
| 291 | prime = 211 |
| 292 | for sf in sdnsh.get_table_from_store("flow-entry"): |
| 293 | flow_hash = 2311 |
| 294 | for i in range(0, len(sf['name'])): |
| 295 | flow_hash = flow_hash * prime + ord(sf['name'][i]) |
| 296 | flow_hash = flow_hash & ( (1 << 20) - 1) |
| 297 | flow_map[flow_hash] = sf['name'] |
| 298 | |
| 299 | fmtcnv.update_alias_dict("staticflow", flow_map) |
| 300 | |
| 301 | fmtcnv.callout_flow_encoders(sdnsh) |
| 302 | |
| 303 | |
| 304 | # |
| 305 | # -------------------------------------------------------------------------------- |
| 306 | # |
| 307 | |
| 308 | def update_controller_node_alias_cache(): |
| 309 | return update_show_alias('controller-node') |
| 310 | |
| 311 | # |
| 312 | # -------------------------------------------------------------------------------- |
| 313 | # |
| 314 | def obj_type_show_alias_update(obj_type): |
| 315 | """ |
| 316 | When some item is about to be displayed, particular 'alias' |
| 317 | items for the display may require updating. instead of just |
| 318 | updating everything all the time, peek at the different formatting |
| 319 | functions and use those function names to determine what needs to |
| 320 | be updated. |
| 321 | |
| 322 | Also see formatter_to_update in climodelinfo, since it may |
| 323 | need to include new formatting functions. |
| 324 | """ |
| 325 | update = {} |
| 326 | sdnsh.pp.format_to_alias_update(obj_type, update) |
| 327 | |
| 328 | # select objects from 'update' dict |
| 329 | if 'host' in update: |
| 330 | update_host_alias_cache() |
| 331 | if 'switch' in update: |
| 332 | update_switch_alias_cache() |
| 333 | if 'port' in update: |
| 334 | update_switch_port_name_cache() |
| 335 | if 'flow' in update: |
| 336 | update_flow_cookie_hash() |
| 337 | if 'controller-node' in update: |
| 338 | update_controller_node_alias_cache() |
| 339 | |
| 340 | |
| 341 | # |
| 342 | # OBJECTs middleware. |
| 343 | # |
| 344 | |
| 345 | |
| 346 | # |
| 347 | # -------------------------------------------------------------------------------- |
| 348 | |
| 349 | def objects_starting_with(obj_type, text = "", key = None): |
| 350 | """ |
| 351 | The function returns a list of matching keys from table/model |
| 352 | identified by the 'obj_type' parameter |
| 353 | |
| 354 | If the table/model has a 'alias' field, then this field's |
| 355 | values are also examined for matches |
| 356 | |
| 357 | The first argument is the name of a table/model in the store, |
| 358 | while the second argument is a prefix to filter the results. |
| 359 | The filter is applied to the key of the table/model, which |
| 360 | was previously populated. |
| 361 | """ |
| 362 | |
| 363 | if key: |
| 364 | if not mi.obj_type_has_field(obj_type, key): |
| 365 | sdnsh.warning("objects_starting_with: %s doesn't have field %s" % |
| 366 | (obj_type, key)) |
| 367 | else: |
| 368 | key = mi.pk(obj_type) |
| 369 | if key == None: |
| 370 | sdnsh.warning("objects_starting_with: %s doesn't have pk" % |
| 371 | (obj_type)) |
| 372 | key_entries = [] |
| 373 | |
| 374 | # Next, find the object |
| 375 | # Deal with any changes to the lookup name based on the 'contenation' |
| 376 | # of the config mode name to the named identifer. |
| 377 | # |
| 378 | |
| 379 | |
| 380 | case = mi.get_obj_type_field_case_sensitive(obj_type, key) |
| 381 | id_value = utif.convert_case(case, text) |
| 382 | |
| 383 | if mi.obj_type_has_model(obj_type): |
| 384 | # from the database |
| 385 | try: |
| 386 | entries = sdnsh.get_table_from_store(obj_type, key, id_value) |
| 387 | errors = None |
| 388 | except Exception, e: |
| 389 | errors = sdnsh.rest_error_to_dict(e) |
| 390 | if errors: |
| 391 | print sdnsh.rest_error_dict_to_message(errors) |
| 392 | return key_entries |
| 393 | else: |
| 394 | if id_value == '': |
| 395 | entries = rest_to_model.get_model_from_url(obj_type, {}) |
| 396 | else: |
| 397 | entries = rest_to_model.get_model_from_url(obj_type, { key + "__startswith" : id_value }) |
| 398 | |
| 399 | if key and entries: |
| 400 | # Expand any key values which are lists (hosts, for example) |
| 401 | items = [x[key] for x in entries if x.get(key)] |
| 402 | entries = [] |
| 403 | for item in items: |
| 404 | if type(item) == list: |
| 405 | entries += item |
| 406 | else: |
| 407 | entries.append(item) |
| 408 | key_entries = [sdnsh.quote_item(obj_type, x) |
| 409 | for x in entries if x.startswith(id_value)] |
| 410 | # |
| 411 | # for some specific tables which have id's concatenated from multiple other |
| 412 | # components, only part of the id is available for completion. |
| 413 | # |
| 414 | if mi.is_compound_key(obj_type, key): |
| 415 | separator_character = mi.compound_key_separator(obj_type, key) |
| 416 | keyDict = {} |
| 417 | for key in key_entries: |
| 418 | # keyDict[key.split(separator_character)[0]] = '' |
| 419 | keyDict[key] = '' |
| 420 | key_entries = keyDict.keys() |
| 421 | |
| 422 | alias_obj_type = obj_type |
| 423 | if key != mi.pk(alias_obj_type): |
| 424 | # if this is a forgeign key, use the obj_type of the fk. |
| 425 | if mi.is_foreign_key(alias_obj_type, key): |
| 426 | (alias_obj_type, fk_name) = mi.foreign_key_references(alias_obj_type, key) |
| 427 | else: |
| 428 | # XXX possibly other choices to determine alias_obj_type? |
| 429 | alias_obj_type = None |
| 430 | |
| 431 | if alias_obj_type: |
| 432 | obj_type_config = mi.obj_type_related_config_obj_type(alias_obj_type) |
| 433 | |
| 434 | # alias_choices_for_alias_obj_type() removes switch dpid's which |
| 435 | # have associated alias names, |
| 436 | key_entries = alias_choices_for_alias_obj_type(key_entries, |
| 437 | obj_type_config, |
| 438 | text) |
| 439 | |
| 440 | return key_entries |
| 441 | |
| 442 | |
| 443 | # |
| 444 | # -------------------------------------------------------------------------------- |
| 445 | |
| 446 | def local_interfaces_firewall_open(protos, ports, controller_id = None): |
| 447 | """ |
| 448 | Return a list of interfaces, which have the proto and port currently enabled |
| 449 | |
| 450 | @param proto a string, or list of strings, identifying the protocol |
| 451 | @param port a strings, or list of strings or ints |
| 452 | """ |
| 453 | |
| 454 | # first collect all associated rules |
| 455 | if type(protos) != list: |
| 456 | protos = [protos] |
| 457 | if type(ports) != list: |
| 458 | ports = [ports] |
| 459 | |
| 460 | rules = [] |
| 461 | for proto in protos: |
| 462 | for port in ports: |
| 463 | query_dict = { 'proto' : proto, 'port' : port } |
| 464 | rules += sdnsh.rest_query_objects('firewall-rule', query_dict) |
| 465 | |
| 466 | # create a dictionary indexed by the interface, which is part of the pk 'id' |
| 467 | rules_of_interface = dict([[x['interface'], x] for x in rules]) |
| 468 | |
| 469 | if controller_id == None: |
| 470 | # request 'this' controller |
| 471 | controller_url = "http://%s/rest/v1/system/controller" % sdnsh.controller |
| 472 | result = sdnsh.store.rest_simple_request(controller_url) |
| 473 | sdnsh.check_rest_result(result) |
| 474 | controller_id = json.loads(result) |
| 475 | |
| 476 | if controller_id != 'all': |
| 477 | query_dict = { 'controller' : controller_id['id'] } |
| 478 | |
| 479 | ifs = sdnsh.rest_query_objects('controller-interface', query_dict) |
| 480 | |
| 481 | return [ifn for ifn in ifs if ifn['id'] in rules_of_interface] |
| 482 | |
| 483 | # |
| 484 | # -------------------------------------------------------------------------------- |
| 485 | |
| 486 | |
| 487 | def log_url(ip_and_port = None, log = None): |
| 488 | """ |
| 489 | Returns the url of the log's on the named ip_and_port. |
| 490 | """ |
| 491 | log_path = 'http://%s/rest/v1/system/log' % ip_and_port |
| 492 | if log: |
| 493 | log_path += '/' + log |
| 494 | return log_path |
| 495 | |
| 496 | |
| 497 | # |
| 498 | # -------------------------------------------------------------------------------- |
| 499 | |
| 500 | |
| 501 | def controller_ip_and_port(controller): |
| 502 | """ |
| 503 | Return a list of ip:port values for named controllers, |
| 504 | to use to build urls for REST API's. If a controller of 'all' |
| 505 | is passed in, then all the controllers ar enumerated. |
| 506 | |
| 507 | If both port 80, and 8000 are open, then two ip:port |
| 508 | pairs will be returned for a controller. This returns |
| 509 | ALL values which match, not a single ip:port for each |
| 510 | controller. |
| 511 | """ |
| 512 | url = 'http://%s/rest/v1/system/controller' % sdnsh.controller |
| 513 | rest_dict = sdnsh.rest_simple_request_to_dict(url) |
| 514 | this_controller = rest_dict['id'] |
| 515 | |
| 516 | ips_80 = [x for x in local_interfaces_firewall_open('tcp', 80, |
| 517 | controller) |
| 518 | if (x['ip'] != '' or x['discovered-ip'] != '')] |
| 519 | |
| 520 | ips_8000 = [x for x in local_interfaces_firewall_open('tcp', 8000, |
| 521 | controller) |
| 522 | if (x['ip'] != '' or x['discovered-ip'] != '')] |
| 523 | |
| 524 | return ['%s:80' % '127.0.0.1' if x['controller'] == this_controller else |
| 525 | x['discovered-ip'] if x['discovered-ip'] != '' else x['ip'] |
| 526 | for x in ips_80] + ['%s:8000' % |
| 527 | '127.0.0.1' if x['controller'] == this_controller else |
| 528 | x['discovered-ip'] if x['discovered-ip'] != '' else x['ip'] |
| 529 | for x in ips_8000] |