| # |
| # Copyright (c) 2012,2013 Big Switch Networks, Inc. |
| # |
| # Licensed under the Eclipse Public License, Version 1.0 (the |
| # "License"); you may not use this file except in compliance with the |
| # License. You may obtain a copy of the License at |
| # |
| # http://www.eclipse.org/legal/epl-v10.html |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| # implied. See the License for the specific language governing |
| # permissions and limitations under the License. |
| # |
| |
| import collections |
| import urllib |
| import error |
| import json |
| import modi |
| import utif |
| import url_cache |
| |
| # |
| # json based on non model based query |
| # |
| |
| def rest_to_model_init(bs, modi): |
| global sdnsh, mi |
| |
| sdnsh = bs |
| mi = modi |
| |
| # |
| # -------------------------------------------------------------------------------- |
| |
| def check_rest_result(result, message = None): |
| if isinstance(result, collections.Mapping): |
| error_type = result.get('error_type') |
| if error_type: |
| raise error.CommandRestError(result, message) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| |
| def get_model_from_url(obj_type, data): |
| """ |
| Intended to be used as a conversion tool, to provide a way |
| to move model requests to specific url requests |
| """ |
| |
| startswith = '__startswith' |
| data = dict(data) |
| |
| if mi.obj_type_has_model(obj_type): |
| print "MODEL_URL: %s not supported" % obj_type |
| return [] |
| # |
| # save sort field |
| sort = data.get('orderby', None) |
| if sort: |
| del data['orderby'] |
| |
| obj_type_url = mi.obj_type_has_url(obj_type) |
| url = "http://%s/rest/v1/" % sdnsh.controller + obj_type_url |
| |
| # for the devices query, be sensitive to the types of queries |
| # since the devices query doesn't understand the relationship |
| # between the vlan=='' for address_space == 'default'. |
| if obj_type_url == 'device': |
| if 'vlan' in data and data['vlan'] == '': |
| del data['vlan'] |
| |
| # data items need to be included into the query. |
| if data: |
| url += '?' |
| url += urllib.urlencode(data) |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: request ", obj_type, data, url |
| # |
| # cache small, short time results |
| entries = url_cache.get_cached_url(url) |
| if entries != None: |
| if sdnsh.description: # description debugging |
| print 'get_model_from_url: using cached result for ', url |
| else: |
| try: |
| result = sdnsh.store.rest_simple_request(url) |
| check_rest_result(result) |
| entries = json.loads(result) |
| except Exception, e: |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: failed request %s: '%s'" % (url, e) |
| entries = [] |
| |
| url_cache.save_url(url, entries) |
| |
| key = mi.pk(obj_type) |
| |
| # convert specific types XXX need better generic mechanism |
| result = [] |
| if obj_type == 'host-attachment-point': |
| for entry in entries: |
| # it is possible for multiple mac's to appear here |
| # as long as a single ip is associated with the device. |
| if len(entry['mac']) > 1: |
| raise error.CommandInternalError("host (attachment point): >1 mac") |
| mac = entry['mac'][0] |
| aps = entry['attachmentPoint'] |
| lastseen = entry['lastSeen'] |
| for ap in aps: |
| status = ap.get('errorStatus', '') |
| if status == None: |
| status = '' |
| if len(entry['vlan']) == 0: |
| result.append({'id' : mac + '|' + ap['switchDPID'] + |
| '|' + str(ap['port']), |
| 'mac' : mac, |
| 'switch' : ap['switchDPID'], |
| 'ingress-port' : ap['port'], |
| 'status' : status, |
| 'last-seen' : lastseen, |
| 'address-space': entry['entityClass'], |
| }) |
| else: |
| for vlan in entry['vlan']: |
| result.append({'id' : mac + '|' + ap['switchDPID'] + |
| '|' + str(ap['port']), |
| 'mac' : mac, |
| 'vlan' : vlan, |
| 'switch' : ap['switchDPID'], |
| 'ingress-port' : ap['port'], |
| 'status' : status, |
| 'last-seen' : lastseen, |
| 'address-space': entry['entityClass'], |
| }) |
| elif obj_type == 'host-network-address': |
| |
| for entry in entries: |
| if len(entry['mac']) > 1: |
| raise error.CommandInternalError("host (network-address): >1 mac") |
| mac = entry['mac'][0] |
| ips = entry['ipv4'] |
| lastseen = entry['lastSeen'] |
| for ip in ips: |
| if len(entry['vlan']) == 0: |
| result.append({'id' : mac + '|' + ip, |
| 'mac' : mac, |
| 'ip-address' : ip, |
| 'address-space' : entry['entityClass'], |
| 'last-seen' : lastseen }) |
| else: |
| for vlan in entry['vlan']: |
| result.append({'id' : mac + '|' + ip, |
| 'mac' : mac, |
| 'vlan' : vlan, |
| 'ip-address' : ip, |
| 'address-space' : entry['entityClass'], |
| 'last-seen' : lastseen }) |
| elif obj_type == 'host': |
| # For host queries, the device manager returns 'devices' which |
| # match specific criteria, for example, if you say ipv4 == 'xxx', |
| # the device which matches that criteris is returned. However, |
| # there may be multiple ip addresses associated. |
| # |
| # this means that two different groups of values for these |
| # entities are returned. fields like 'ipv4' is the collection |
| # of matching values, while 'ips' is the complee list. |
| # 'switch', 'port' are the match values, while 'attachment-points' |
| # is the complete list. |
| # |
| |
| ip_match = data.get('ipv4') |
| ip_prefix = data.get('ipv4__startswith') |
| |
| dpid_match = data.get('dpid') |
| dpid_prefix = data.get('dpid__startswith') |
| |
| port_match = data.get('port') |
| port_prefix = data.get('port__startswith') |
| |
| address_space_match = data.get('address-space') |
| address_space_prefix = data.get('address-space__startswith') |
| |
| for entry in entries: |
| if len(entry['mac']) > 1: |
| raise error.CommandInternalError("host: >1 mac") |
| mac = entry['mac'][0] |
| lastseen = entry['lastSeen'] |
| |
| if not ip_match and not ip_prefix: |
| ipv4 = entry['ipv4'] |
| elif ip_match: |
| ipv4 = [x for x in entry['ipv4'] if x == ip_match] |
| elif ip_prefix: |
| ipv4 = [x for x in entry['ipv4'] if x.startswith(ip_prefix)] |
| |
| ips = None |
| if len(entry['ipv4']): |
| ips = [{'ip-address' : x, 'last-seen' : lastseen} |
| for x in entry['ipv4'] ] |
| |
| aps = None |
| switch = [] |
| port = [] |
| |
| dpid_match = entry.get('dpid') |
| |
| if len(entry['attachmentPoint']): |
| aps = [{'switch' : x['switchDPID'], 'ingress-port' : x['port'] } |
| for x in entry['attachmentPoint']] |
| |
| if not dpid_match and not dpid_prefix: |
| switch = [x['switchDPID'] for x in entry['attachmentPoint']] |
| elif dpid_match: |
| switch = [x['switchDPID'] for x in entry['attachmentPoint'] |
| if x['switchDPID'] == dpid_match] |
| elif dpid_prefix: |
| switch = [x['switchDPID'] for x in entry['attachmentPoint'] |
| if x['switchDPID'].startswith(dpid_prefix)] |
| |
| if not port_match and not port_prefix: |
| port = [x['port'] for x in entry['attachmentPoint']] |
| elif port_match: |
| port = [x['port'] for x in entry['attachmentPoint'] |
| if x['port'] == port_match] |
| elif port_prefix: |
| port = [x['port'] for x in entry['attachmentPoint'] |
| if x['port'].startswith(port_prefix)] |
| |
| address_space = entry['entityClass'] |
| dhcp_client_name = entry.get('dhcpClientName', '') |
| if address_space_match and address_space != address_space_match: |
| continue |
| if address_space_prefix and not address_space.startswith(address_space_prefix): |
| continue |
| |
| if len(entry['vlan']) == 0: |
| id = '%s||%s' % (address_space, mac) |
| result.append({'id' : id, |
| 'mac' : mac, |
| 'ips' : ips, |
| 'ipv4' : ipv4, |
| 'attachment-points' : aps, |
| 'dpid' : switch, |
| 'port' : port, |
| 'address-space' : address_space, |
| 'dhcp-client-name' : dhcp_client_name, |
| 'last-seen' : lastseen}) |
| else: |
| for vlan in entry['vlan']: |
| if address_space != 'default': |
| id = '%s||%s' % (address_space, mac) |
| else: |
| id = '%s|%s|%s' % (address_space, vlan, mac) |
| result.append({'id' : id, |
| 'mac' : mac, |
| 'vlan' : vlan, |
| 'ips' : ips, |
| 'ipv4' : ipv4, |
| 'attachment-points' : aps, |
| 'dpid' : switch, |
| 'port' : port, |
| 'address-space' : address_space, |
| 'dhcp-client-name' : dhcp_client_name, |
| 'last-seen' : lastseen}) |
| |
| # Also need to add hostConfig entries. |
| if not mi.obj_type_has_model('host-config'): |
| raise error.CommandInternalError("host-config: not served via model") |
| host_config = sdnsh.rest_query_objects('host-config', data) |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: adding host-config ", data, host_config |
| known_ids = [x['id'] for x in result] |
| |
| for hc in host_config: |
| id = hc['id'] |
| if id not in known_ids: |
| # be sensitive to search fields: |
| query_match = True |
| if len(data): |
| for (d, dv) in data.items(): |
| # other ops aside from '='? |
| if d.endswith(startswith): |
| fn = d[:-len(startswith)] |
| if not fn in hc or not hc[fn].startswith(dv): |
| query_match = False |
| elif (not d in hc) or dv != hc[d]: |
| query_match = False |
| break |
| |
| if query_match: |
| hc['attachment-points'] = None |
| hc['dpid'] = None |
| hc['ips'] = None |
| hc['last-seen'] = None |
| result.append(hc) |
| |
| elif obj_type == 'vns-interface': |
| for entry in entries: |
| vns = entry['parentVNS']['name'] |
| rule = entry['parentRule'] |
| rule_name = 'default' |
| if rule and 'name' in rule: |
| rule_name = rule['name'] |
| |
| result.append({ key : vns + '|' + entry['name'], |
| 'vns' : vns, |
| 'address-space' : entry['parentVNS']['addressSpaceName'], |
| 'interface' : entry['name'], |
| 'rule' : rule_name, |
| 'last-seen' : entry['lastSeen'], |
| }) |
| |
| # also need to add vns-interface-config |
| if not mi.obj_type_has_model('vns-interface-config'): |
| raise error.CommandInternalError("vns-interface-config: not service via model") |
| vns_intf_config = sdnsh.rest_query_objects('vns-interface-config', data) |
| known_vns_intfs = [x[key] for x in result] |
| for vns_intf in vns_intf_config: |
| if not vns_intf[key] in known_vns_intfs: |
| vns_intf['rule'] = 'default' |
| result.append(vns_intf) |
| |
| elif obj_type == 'host-vns-interface': |
| |
| # mac matching works for the request |
| |
| vns_match = data.get('vns') |
| vns_prefix = data.get('vns__startswith') |
| |
| for entry in entries: |
| device = entry['device'] |
| if len(device['mac']) > 1: |
| raise error.CommandInternalError("host (vns-interface): >1 mac") |
| |
| device_last_seen = device['lastSeen'] |
| address_space = device['entityClass'] |
| mac = device['mac'][0] # currently, should only be one |
| |
| ips = None |
| if len(device['ipv4']): |
| ips = [{'ip-address' : x, 'last-seen' : device_last_seen} |
| for x in device['ipv4'] ] |
| |
| vlans = device.get('vlan', []) # currently, should only be one |
| if len(vlans) == 0: # iterate once when list is empty |
| vlans = [ '' ] |
| |
| aps = None |
| if len(device['attachmentPoint']): |
| aps = [{'switch' : x['switchDPID'], 'ingress-port' : x['port'] } |
| for x in device['attachmentPoint']] |
| |
| for iface in entry.get('iface', []): |
| vns = iface['parentVNS']['name'] |
| last_seen = iface['lastSeen'] |
| if vns_match and vns_match != vns: |
| continue |
| if vns_prefix and not vns.startswith(vns_prefix): |
| continue |
| |
| for vlan in vlans: # there's supposed to only be at most one vlan. |
| if address_space != 'default' or type(vlan) != int: |
| host = '%s||%s' % (address_space, mac) |
| else: |
| host = '%s|%s|%s' % (address_space, vlan, mac) |
| |
| result.append({key : mac + '|' + vns + '|' + iface['name'], |
| 'host' : host, |
| 'mac' : mac, |
| 'vlan' : vlan, |
| 'address-space' : address_space, |
| 'ips' : ips, |
| 'attachment-points' : aps, |
| 'vns' : vns, |
| 'interface' : vns + '|' + iface['name'], |
| 'last-seen' : device_last_seen}) |
| elif obj_type == 'switches': |
| switch_match = data.get('dpid') |
| switch_prefix = data.get('dpid__startswith') |
| |
| # this synthetic obj_type's name is 'switches' in an attempt |
| # to disabigutate it from 'class Switch' |
| for entry in entries: |
| if switch_match and switch_match != entry['dpid']: |
| continue |
| if switch_prefix and not entry['dpid'].startswith(switch_prefix): |
| continue |
| |
| attrs = entry['attributes'] |
| actions = entry['actions'] |
| capabilities = entry['capabilities'] |
| inet_address = entry.get('inetAddress') |
| ip_address = '' |
| tcp_port = '' |
| if inet_address: |
| # Current Java value looks like: /192.168.2.104:38420 |
| inet_parts = inet_address.split(':') |
| ip_address = inet_parts[0][1:] |
| tcp_port = inet_parts[1] |
| |
| result.append({ |
| 'dpid' : entry['dpid'], |
| 'connected-since' : entry['connectedSince'], |
| 'ip-address' : ip_address, |
| 'tcp-port' : tcp_port, |
| 'actions' : actions, |
| 'capabilities' : capabilities, |
| 'dp-desc' : attrs.get('DescriptionData', ''), |
| 'fast-wildcards' : attrs.get('FastWildcards', ''), |
| 'supports-nx-role' : attrs.get('supportsNxRole', ''), |
| 'supports-ofpp-flood' : attrs.get('supportsOfppFlood', ''), |
| 'supports-ofpp-table' : attrs.get('supportsOfppTable', ''), |
| 'core-switch' : False, |
| }) |
| |
| # now add switch-config |
| |
| switch_config = sdnsh.rest_query_objects('switch-config', data) |
| known_dpids = dict([[x['dpid'], x] for x in result]) |
| |
| for sw in switch_config: |
| dpid = sw['dpid'] |
| if not dpid in known_dpids: |
| # be sensitive to search fields: |
| query_match = True |
| if len(data): |
| for (d, dv) in data.items(): |
| # other ops aside from '='? |
| if d.endswith(startswith): |
| fn = d[:-len(startswith)] |
| if not fn in sw or not sw[fn].startswith(dv): |
| query_match = False |
| elif (not d in sw) or dv != sw[d]: |
| query_match = False |
| break |
| |
| if query_match: |
| sw['ip-address'] = '' |
| sw['tcp-port'] = '' |
| sw['connected-since'] = '' |
| result.append(sw) |
| else: |
| known_dpids[dpid].update(sw) |
| elif obj_type == 'interfaces': |
| |
| # These are called interfaces because the primary |
| # key is constructed with the interface name, not |
| # the port number. |
| |
| # the 'switches' query to sdnplatform currently |
| # doesn't support searching for the interface |
| # names. its done here instead |
| |
| switch_match = data.get('dpid') |
| switch_prefix = data.get('dpid__startswith') |
| |
| name_match = data.get('portName') |
| if name_match: |
| name_match = name_match.lower() |
| name_prefix = data.get('portName__startswith') |
| if name_prefix: |
| name_prefix = name_prefix.lower() |
| |
| # this synthetic obj_type's name is 'switches' in an attempt |
| # to disabigutate it from 'class Switch' |
| for entry in entries: |
| dpid = entry['dpid'] |
| |
| if switch_match and switch_match != dpid: |
| continue |
| if switch_prefix and not dpid.startswith(switch_prefix): |
| continue |
| |
| for p in entry['ports']: |
| portNumber = p['portNumber'] |
| name = p['name'] |
| |
| if name_match and name.lower() != name_match: |
| continue |
| if name_prefix and not name.lower().startswith(name_prefix): |
| continue |
| |
| result.append({ |
| 'id' : '%s|%s' % (dpid,name), |
| 'portNumber' : portNumber, |
| 'switch' : dpid, |
| 'portName' : p['name'], |
| 'config' : p['config'], |
| 'state' : p['state'], |
| 'advertisedFeatures' : p['advertisedFeatures'], |
| 'currentFeatures' : p['currentFeatures'], |
| 'hardwareAddress' : p['hardwareAddress'], |
| }) |
| |
| # |
| # order the result |
| if sort: |
| if sort[0] == '-': |
| sort = sort[1:] |
| # decreasing |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: order decreasing ", sort |
| result = sorted(result, key=lambda k:k.get(sort, ''), |
| cmp=lambda x,y : cmp(y,x)) |
| else: |
| # increasing |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: order increasing ", sort |
| result = sorted(result, key=lambda k:k.get(sort, ''), |
| cmp=lambda x,y : cmp(x,y)) |
| else: |
| # use tail-integer on the entries |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: pk ordering ", key |
| def sort_cmp(x,y): |
| for (idx, x_v) in enumerate(x): |
| c = cmp(utif.try_int(x_v), utif.try_int(y[idx])) |
| if c != 0: |
| return c |
| return 0 |
| result = sorted(result, key=lambda k:k.get(key, '').split('|'), |
| cmp=lambda x,y : cmp(utif.try_int(x),utif.try_int(y))) |
| |
| if sdnsh.description: # description debugging |
| print "get_model_from_url: result ", obj_type, url, len(result) |
| |
| return result |
| |
| |
| def validate_switch(): |
| """ |
| If /rest/v1/switches is cached, perform some validations on it. |
| |
| -- verify that the announced interfaces names are case insensitive |
| -- verify the names only appear once |
| """ |
| |
| def duplicate_port(entry, name): |
| dpid = entry['dpid'] |
| |
| print 'Warning: switch %s duplicate interface names: %s' % (dpid, name) |
| if sdnsh.debug_backtrace: |
| for port in entry['ports']: |
| if port['name'] == name: |
| print 'SWTICH %s:%s PORT %s' % (entry, name, port) |
| |
| def not_case_sensitive(entry, name): |
| dpid = entry['dpid'] |
| |
| ports = {} |
| for port in entry['ports']: |
| if port['name'].lower() == name: |
| ports[port['name']] = port |
| |
| print 'Warning: switch %s case insentive interface names: %s' % \ |
| (dpid, ' - '.join(ports.keys())) |
| if sdnsh.debug_backtrace: |
| for port in ports: |
| print 'SWTICH %s PORT %s' % (dpid, port) |
| |
| url = "http://%s/rest/v1/switches" % sdnsh.controller |
| entries = url_cache.get_cached_url(url) |
| if entries: |
| for entry in entries: |
| dpid = entry['dpid'] |
| |
| # verify that the port names are unique even when case |
| # sensitive |
| all_names = [p['name'] for p in entry['ports']] |
| one_case_names = utif.unique_list_from_list([x.lower() for x in all_names]) |
| if len(all_names) != len(one_case_names): |
| # Something is rotten, find out what. |
| for (i, port_name) in enumerate(all_names): |
| # use enumerate to drive upper-triangle comparison |
| for other_name in all_names[i+1:]: |
| if port_name == other_name: |
| duplicate_port(entry, port_name) |
| elif port_name.lower() == other_name.lower(): |
| not_case_sensitive(entry, port_name) |
| |
| |