Chip Boling | f5af85d | 2019-02-12 15:36:17 -0600 | [diff] [blame] | 1 | # CCopyright 2017-present Adtran, Inc. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | from twisted.internet.defer import inlineCallbacks, returnValue |
| 16 | import xmltodict |
| 17 | import structlog |
| 18 | from pyvoltha.protos.openflow_13_pb2 import OFPPF_1GB_FD, OFPPF_10GB_FD, OFPPF_40GB_FD, OFPPF_100GB_FD |
| 19 | from pyvoltha.protos.openflow_13_pb2 import OFPPF_FIBER, OFPPF_COPPER |
| 20 | from pyvoltha.protos.openflow_13_pb2 import OFPPS_LIVE, OFPPC_PORT_DOWN, OFPPS_LINK_DOWN, OFPPF_OTHER |
| 21 | from pyvoltha.protos.common_pb2 import OperStatus, AdminState |
| 22 | |
| 23 | log = structlog.get_logger() |
| 24 | |
| 25 | _ietf_interfaces_config_rpc = """ |
| 26 | <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> |
| 27 | <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> |
| 28 | <interface/> |
| 29 | </interfaces> |
| 30 | </filter> |
| 31 | """ |
| 32 | |
| 33 | _ietf_interfaces_state_rpc = """ |
| 34 | <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> |
| 35 | <interfaces-state xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces"> |
| 36 | <interface> |
| 37 | <name/> |
| 38 | <type/> |
| 39 | <admin-status/> |
| 40 | <oper-status/> |
| 41 | <last-change/> |
| 42 | <phys-address/> |
| 43 | <speed/> |
| 44 | </interface> |
| 45 | </interfaces-state> |
| 46 | </filter> |
| 47 | """ |
| 48 | |
| 49 | _allowed_with_default_types = ['report-all', 'report-all-tagged', 'trim', 'explicit'] |
| 50 | |
| 51 | # TODO: Centralize the item below as a function in a core util module |
| 52 | |
| 53 | |
| 54 | def _with_defaults(default_type=None): |
| 55 | if default_type is None: |
| 56 | return "" |
| 57 | |
| 58 | assert(default_type in _allowed_with_default_types) |
| 59 | return """ |
| 60 | <with-defaults xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults"> |
| 61 | {}</with-defaults>""".format(default_type) |
| 62 | |
| 63 | |
| 64 | class IetfInterfacesConfig(object): |
| 65 | def __init__(self, session): |
| 66 | self._session = session |
| 67 | |
| 68 | @inlineCallbacks |
| 69 | def get_config(self, source='running', with_defaults=None): |
| 70 | |
| 71 | filter = _ietf_interfaces_config_rpc + _with_defaults(with_defaults) |
| 72 | |
| 73 | request = self._session.get(source, filter=filter) |
| 74 | rpc_reply = yield request |
| 75 | returnValue(rpc_reply) |
| 76 | |
| 77 | def get_interfaces(self, rpc_reply, interface_type=None): |
| 78 | """ |
| 79 | Get the physical entities of a particular type |
| 80 | :param rpc_reply: Reply from previous get or request |
| 81 | :param interface_type: (String or List) The type of interface (case-insensitive) |
| 82 | :return: list) of OrderDict interface entries |
| 83 | """ |
| 84 | result_dict = xmltodict.parse(rpc_reply.data_xml) |
| 85 | |
| 86 | entries = result_dict['data']['interfaces'] |
| 87 | |
| 88 | if interface_type is None: |
| 89 | return entries |
| 90 | |
| 91 | # for entry in entries: |
| 92 | # import pprint |
| 93 | # log.info(pprint.PrettyPrinter(indent=2).pformat(entry)) |
| 94 | |
| 95 | def _matches(entry, value): |
| 96 | if 'type' in entry and '#text' in entry['type']: |
| 97 | text_val = entry['type']['#text'].lower() |
| 98 | if isinstance(value, list): |
| 99 | return any(v.lower() in text_val for v in value) |
| 100 | return value.lower() in text_val |
| 101 | return False |
| 102 | |
| 103 | return [entry for entry in entries if _matches(entry, interface_type)] |
| 104 | |
| 105 | |
| 106 | class IetfInterfacesState(object): |
| 107 | def __init__(self, session): |
| 108 | self._session = session |
| 109 | |
| 110 | @inlineCallbacks |
| 111 | def get_state(self): |
| 112 | try: |
| 113 | request = self._session.get(_ietf_interfaces_state_rpc) |
| 114 | rpc_reply = yield request |
| 115 | returnValue(rpc_reply) |
| 116 | |
| 117 | except Exception as e: |
| 118 | log.exception('get_state', e=e) |
| 119 | raise |
| 120 | |
| 121 | @staticmethod |
| 122 | def get_interfaces(self, rpc_reply, key='type', key_value=None): |
| 123 | """ |
| 124 | Get the physical entities of a particular type |
| 125 | :param key_value: (String or List) The type of interface (case-insensitive) |
| 126 | :return: list) of OrderDict interface entries |
| 127 | """ |
| 128 | result_dict = xmltodict.parse(rpc_reply.data_xml) |
| 129 | entries = result_dict['data']['interfaces-state']['interface'] |
| 130 | |
| 131 | if key_value is None: |
| 132 | return entries |
| 133 | |
| 134 | for entry in entries: |
| 135 | import pprint |
| 136 | log.info(pprint.PrettyPrinter(indent=2).pformat(entry)) |
| 137 | |
| 138 | def _matches(entry, key, value): |
| 139 | if key in entry and '#text' in entry[key]: |
| 140 | text_val = entry[key]['#text'].lower() |
| 141 | if isinstance(value, list): |
| 142 | return any(v.lower() in text_val for v in value) |
| 143 | return value.lower() in text_val |
| 144 | return False |
| 145 | |
| 146 | return [entry for entry in entries if _matches(entry, key, key_value)] |
| 147 | |
| 148 | @staticmethod |
| 149 | def _get_admin_state(entry): |
| 150 | state_map = { |
| 151 | 'up': AdminState.ENABLED, |
| 152 | 'down': AdminState.DISABLED, |
| 153 | 'testing': AdminState.DISABLED |
| 154 | } |
| 155 | return state_map.get(entry.get('admin-status', 'down'), |
| 156 | AdminState.UNKNOWN) |
| 157 | |
| 158 | @staticmethod |
| 159 | def _get_oper_status(entry): |
| 160 | state_map = { |
| 161 | 'up': OperStatus.ACTIVE, |
| 162 | 'down': OperStatus.FAILED, |
| 163 | 'testing': OperStatus.TESTING, |
| 164 | 'unknown': OperStatus.UNKNOWN, |
| 165 | 'dormant': OperStatus.DISCOVERED, |
| 166 | 'not-present': OperStatus.UNKNOWN, |
| 167 | 'lower-layer-down': OperStatus.FAILED |
| 168 | } |
| 169 | return state_map.get(entry.get('oper-status', 'down'), |
| 170 | OperStatus.UNKNOWN) |
| 171 | |
| 172 | @staticmethod |
| 173 | def _get_mac_addr(entry): |
| 174 | mac_addr = entry.get('phys-address', None) |
| 175 | if mac_addr is None: |
| 176 | import random |
| 177 | # TODO: Get with qumram team about phys addr |
| 178 | mac_addr = '08:00:{}{}:{}{}:{}{}:00'.format(random.randint(0, 9), |
| 179 | random.randint(0, 9), |
| 180 | random.randint(0, 9), |
| 181 | random.randint(0, 9), |
| 182 | random.randint(0, 9), |
| 183 | random.randint(0, 9)) |
| 184 | return mac_addr |
| 185 | |
| 186 | @staticmethod |
| 187 | def _get_speed_value(entry): |
| 188 | speed = entry.get('speed') or IetfInterfacesState._get_speed_via_name(entry.get('name')) |
| 189 | if isinstance(speed, str): |
| 190 | return long(speed) |
| 191 | return speed |
| 192 | |
| 193 | @staticmethod |
| 194 | def _get_speed_via_name(name): |
| 195 | speed_map = { |
| 196 | 'terabit': 1000000000000, |
| 197 | 'hundred-gigabit': 100000000000, |
| 198 | 'fourty-gigabit': 40000000000, |
| 199 | 'ten-gigabit': 10000000000, |
| 200 | 'gigabit': 1000000000, |
| 201 | } |
| 202 | for n,v in speed_map.iteritems(): |
| 203 | if n in name.lower(): |
| 204 | return v |
| 205 | return 0 |
| 206 | |
| 207 | @staticmethod |
| 208 | def _get_of_state(entry): |
| 209 | # If port up and ready: OFPPS_LIVE |
| 210 | # If port config bit is down: OFPPC_PORT_DOWN |
| 211 | # If port state bit is down: OFPPS_LINK_DOWN |
| 212 | # if IetfInterfacesState._get_admin_state(entry) == AdminState.ENABLED: |
| 213 | # return OFPPS_LIVE \ |
| 214 | # if IetfInterfacesState._get_oper_status(entry) == OperStatus.ACTIVE \ |
| 215 | # else OFPPS_LINK_DOWN |
| 216 | # |
| 217 | # return OFPPC_PORT_DOWN |
| 218 | # TODO: Update of openflow port state is not supported, so always say we are alive |
| 219 | return OFPPS_LIVE |
| 220 | |
| 221 | @staticmethod |
| 222 | def _get_of_capabilities(entry): |
| 223 | # The capabilities field is a bitmap that uses a combination of the following flags : |
| 224 | # Capabilities supported by the datapath |
| 225 | # enum ofp_capabilities { |
| 226 | # OFPC_FLOW_STATS = 1 << 0, /* Flow statistics. */ |
| 227 | # OFPC_TABLE_STATS = 1 << 1, /* Table statistics. */ |
| 228 | # OFPC_PORT_STATS = 1 << 2, /* Port statistics. */ |
| 229 | # OFPC_GROUP_STATS = 1 << 3, /* Group statistics. */ |
| 230 | # OFPC_IP_REASM = 1 << 5, /* Can reassemble IP fragments. */ |
| 231 | # OFPC_QUEUE_STATS = 1 << 6, /* Queue statistics. */ |
| 232 | # OFPC_PORT_BLOCKED = 1 << 8, /* Switch will block looping ports. */ |
| 233 | # OFPC_BUNDLES = 1 << 9, /* Switch supports bundles. */ |
| 234 | # OFPC_FLOW_MONITORING = 1 << 10, /* Switch supports flow monitoring. */ |
| 235 | # } |
| 236 | # enum ofp_port_features { |
| 237 | # OFPPF_10MB_HD = 1 << 0, /* 10 Mb half-duplex rate support. */ |
| 238 | # OFPPF_10MB_FD = 1 << 1, /* 10 Mb full-duplex rate support. */ |
| 239 | # OFPPF_100MB_HD = 1 << 2, /* 100 Mb half-duplex rate support. */ |
| 240 | # OFPPF_100MB_FD = 1 << 3, /* 100 Mb full-duplex rate support. */ |
| 241 | # OFPPF_1GB_HD = 1 << 4, /* 1 Gb half-duplex rate support. */ |
| 242 | # OFPPF_1GB_FD = 1 << 5, /* 1 Gb full-duplex rate support. */ |
| 243 | # OFPPF_10GB_FD = 1 << 6, /* 10 Gb full-duplex rate support. */ |
| 244 | # OFPPF_40GB_FD = 1 << 7, /* 40 Gb full-duplex rate support. */ |
| 245 | # OFPPF_100GB_FD = 1 << 8, /* 100 Gb full-duplex rate support. */ |
| 246 | # OFPPF_1TB_FD = 1 << 9, /* 1 Tb full-duplex rate support. */ |
| 247 | # OFPPF_OTHER = 1 << 10, /* Other rate, not in the list. */ |
| 248 | # OFPPF_COPPER = 1 << 11, /* Copper medium. */ |
| 249 | # OFPPF_FIBER = 1 << 12, /* Fiber medium. */ |
| 250 | # OFPPF_AUTONEG = 1 << 13, /* Auto-negotiation. */ |
| 251 | # OFPPF_PAUSE = 1 << 14, /* Pause. */ |
| 252 | # OFPPF_PAUSE_ASYM = 1 << 15 /* Asymmetric pause. */ |
| 253 | # } |
| 254 | # TODO: Look into adtran-physical-entities and decode xSFP type any other settings |
| 255 | return IetfInterfacesState._get_of_speed(entry) | OFPPF_FIBER |
| 256 | |
| 257 | @staticmethod |
| 258 | def _get_of_speed(entry): |
| 259 | speed = IetfInterfacesState._get_speed_value(entry) |
| 260 | speed_map = { |
| 261 | 1000000000: OFPPF_1GB_FD, |
| 262 | 10000000000: OFPPF_10GB_FD, |
| 263 | 40000000000: OFPPF_40GB_FD, |
| 264 | 100000000000: OFPPF_100GB_FD, |
| 265 | } |
| 266 | # return speed_map.get(speed, OFPPF_OTHER) |
| 267 | # TODO: For now, force 100 GB |
| 268 | return OFPPF_100GB_FD |
| 269 | |
| 270 | @staticmethod |
| 271 | def _get_port_number(name, if_index): |
| 272 | import re |
| 273 | |
| 274 | formats = [ |
| 275 | 'xpon \d/{1,2}\d', # OLT version 3 (Feb 2018++) |
| 276 | 'Hundred-Gigabit-Ethernet \d/\d/{1,2}\d', # OLT version 2 |
| 277 | 'XPON \d/\d/{1,2}\d', # OLT version 2 |
| 278 | 'hundred-gigabit-ethernet \d/{1,2}\d', # OLT version 1 |
| 279 | 'channel-termination {1,2}\d', # OLT version 1 |
| 280 | ] |
| 281 | p2 = re.compile('\d+') |
| 282 | |
| 283 | for regex in formats: |
| 284 | p = re.compile(regex, re.IGNORECASE) |
| 285 | match = p.match(name) |
| 286 | if match is not None: |
| 287 | return int(p2.findall(name)[-1]) |
| 288 | |
| 289 | @staticmethod |
| 290 | def get_port_entries(rpc_reply, port_type): |
| 291 | """ |
| 292 | Get the port entries that make up the northbound and |
| 293 | southbound interfaces |
| 294 | |
| 295 | :param rpc_reply: |
| 296 | :param port_type: |
| 297 | :return: |
| 298 | """ |
| 299 | ports = dict() |
| 300 | result_dict = xmltodict.parse(rpc_reply.data_xml) |
| 301 | entries = result_dict['data']['interfaces-state']['interface'] |
| 302 | if not isinstance(entries, list): |
| 303 | entries = [entries] |
| 304 | port_entries = [entry for entry in entries if 'name' in entry and |
| 305 | port_type.lower() in entry['name'].lower()] |
| 306 | |
| 307 | for entry in port_entries: |
| 308 | port = { |
| 309 | 'port_no': IetfInterfacesState._get_port_number(entry.get('name'), |
| 310 | entry.get('ifindex')), |
| 311 | 'name': entry.get('name', 'unknown'), |
| 312 | 'ifIndex': entry.get('ifIndex'), |
| 313 | # 'label': None, |
| 314 | 'mac_address': IetfInterfacesState._get_mac_addr(entry), |
| 315 | 'admin_state': IetfInterfacesState._get_admin_state(entry), |
| 316 | 'oper_status': IetfInterfacesState._get_oper_status(entry), |
| 317 | 'ofp_state': IetfInterfacesState._get_of_state(entry), |
| 318 | 'ofp_capabilities': IetfInterfacesState._get_of_capabilities(entry), |
| 319 | 'current_speed': IetfInterfacesState._get_of_speed(entry), |
| 320 | 'max_speed': IetfInterfacesState._get_of_speed(entry), |
| 321 | } |
| 322 | port_no = port['port_no'] |
| 323 | if port_no not in ports: |
| 324 | ports[port_no] = port |
| 325 | else: |
| 326 | ports[port_no].update(port) |
| 327 | |
| 328 | return ports |