The Assimilation Project  based on Assimilation version 1.1.7.1474836767
linkdiscovery.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # vim: smartindent tabstop=4 shiftwidth=4 expandtab number
3 #
4 # This file is part of the Assimilation Project.
5 #
6 # Author: Alan Robertson <alanr@unix.sh>
7 # Copyright (C) 2013 - Assimilation Systems Limited
8 #
9 # Free support is available from the Assimilation Project community - http://assimproj.org
10 # Paid support is available from Assimilation Systems Limited - http://assimilationsystems.com
11 #
12 # The Assimilation software is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # The Assimilation software is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with the Assimilation Project software. If not, see http://www.gnu.org/licenses/
24 #
25 #
26 
27 '''
28 Link (LLDP or CDP) Discovery Listener code.
29 This is the class for handling link discovery packets as they arrive.
30 It is a subclass of the DiscoveryListener class.
31 
32 More details are documented in the LinkDiscoveryListener class
33 '''
34 
35 import sys
36 from consts import CMAconsts
37 from store import Store
38 from AssimCclasses import pyNetAddr, pyConfigContext
39 from AssimCtypes import ADDR_FAMILY_IPV4, ADDR_FAMILY_IPV6, ADDR_FAMILY_802
40 from AssimCtypes import CONFIGNAME_INSTANCE
41 from AssimCtypes import CONFIGNAME_DEVNAME, CONFIGNAME_SWPROTOS
42 from discoverylistener import DiscoveryListener
43 from graphnodes import NICNode, IPaddrNode
44 from systemnode import SystemNode
45 from cmaconfig import ConfigFile
46 
48  'return True if this link is up-and-operational'
49  return ('operstate' in devinfo and 'carrier' in devinfo and 'address' in devinfo
50  and str(devinfo['operstate']) == 'up' and str(devinfo['carrier']) == 'True'
51  and str(devinfo['address']) != '00-00-00-00-00-00'
52  and str(devinfo['address']) != '')
53 
54 @SystemNode.add_json_processor
55 class LinkDiscoveryListener(DiscoveryListener):
56  '''Class for processing Link Discovery JSON messages
57  We create the System nodes for switches we discover and
58  any NIC nodes for the port we're connected to and any other
59  NICs that the switch tells us about.
60  We also create 'wiredto' relationships between host NICs and switch NICs.
61 
62  Note that all the CDP and LLDP packets are sent intact (in binary) from the
63  nanoprobes then post-processed by the CMA to create the JSON which we
64  receive here just as though the nanoprobe had sent us JSON in the first place.
65  '''
66 
67  prio = DiscoveryListener.PRI_OPTION
68  wantedpackets = ('__LinkDiscovery', 'netconfig')
69 
70  #R0914:684,4:LinkDiscoveryListener.processpkt: Too many local variables (25/15)
71  # pylint: disable=R0914
72 
73  def processpkt(self, drone, srcaddr, jsonobj, discoverychanged):
74  '''Trigger Switch discovery or add Low Level (Link Level) discovery data to the database.
75  '''
76  if not discoverychanged:
77  return
78  if jsonobj['discovertype'] == '__LinkDiscovery':
79  self.processpkt_linkdiscovery(drone, srcaddr, jsonobj)
80  elif jsonobj['discovertype'] == 'netconfig':
81  self.processpkt_netconfig(drone, srcaddr, jsonobj)
82  else:
83  print >> sys.stderr, 'OOPS! bad packet type [%s]', jsonobj['discovertype']
84 
85  def processpkt_netconfig(self, drone, _unused_srcaddr, jsonobj):
86  '''We want to trigger Switch discovery when we hear a 'netconfig' packet
87 
88  Build up the parameters for the discovery
89  action, then send it to drone.request_discovery(...)
90  To build up the parameters, you use ConfigFile.agent_params()
91  which will pull values from the system configuration.
92  '''
93  init_params = ConfigFile.agent_params(self.config, 'discovery', '#SWITCH'
94  , drone.designation)
95 
96  data = jsonobj['data'] # the data portion of the JSON message
97  discovery_args = []
98  for devname in data.keys():
99  devinfo = data[devname]
100  if discovery_indicates_link_is_up(devinfo):
101  params = pyConfigContext(init_params)
102  params[CONFIGNAME_INSTANCE] = '#SWITCH_' + devname
103  params[CONFIGNAME_DEVNAME] = devname
104  params[CONFIGNAME_SWPROTOS] = ["lldp", "cdp"]
105  #print >> sys.stderr, '***#SWITCH parameters:', params
106  discovery_args.append(params)
107  if discovery_args:
108  drone.request_discovery(discovery_args)
109 
110  def processpkt_linkdiscovery(self, drone, _unused_srcaddr, jsonobj):
111  'Add Low Level (Link Level) discovery data to the database'
112  #
113  # This code doesn't yet deal with moving network connections around
114  # it is certain that it won't delete the old information and replace it
115  # There are two possibilities:
116  # We are connecting to a switch port which is previously connected:
117  # Drop any wiredto connection that already exists to that port
118  # We are connecting to somewhere different
119  # Drop any wiredto relationship between the switch port and us
120  #
121  data = jsonobj['data']
122  #print >> sys.stderr, 'SWITCH JSON:', str(data)
123  if 'ChassisId' not in data:
124  self.log.warning('Chassis ID missing from discovery data from switch [%s]'
125  % (str(data)))
126  return
127  chassisid = data['ChassisId']
128  attrs = {}
129  for key in data.keys():
130  if key == 'ports' or key == 'SystemCapabilities':
131  continue
132  value = data[key]
133  if not isinstance(value, int) and not isinstance(value, float):
134  value = str(value)
135  attrs[key] = value
136  attrs['designation'] = chassisid
137  #### FIXME What should the domain of a switch default to?
138  attrs['domain'] = drone.domain
139  switch = self.store.load_or_create(SystemNode, **attrs)
140 
141  if 'SystemCapabilities' not in data:
142  switch.addrole(CMAconsts.ROLE_bridge)
143  else:
144  caps = data['SystemCapabilities']
145  for role in caps.keys():
146  if caps[role]:
147  switch.addrole(role)
148  #switch.addrole([role for role in caps.keys() if caps[role]])
149 
150 
151  if 'ManagementAddress' in attrs:
152  self._process_mgmt_addr(switch, chassisid, attrs)
153  self._process_ports(drone, switch, chassisid, data['ports'])
154 
155  def _process_mgmt_addr(self, switch, chassisid, attrs):
156  'Process the ManagementAddress field in the LLDP packet'
157  # FIXME - not sure if I know how I should do this now - no MAC address for mgmtaddr?
158  mgmtaddr = attrs['ManagementAddress']
159  mgmtnetaddr = pyNetAddr(mgmtaddr)
160  atype = mgmtnetaddr.addrtype()
161  if atype == ADDR_FAMILY_IPV4 or atype == ADDR_FAMILY_IPV6:
162  # MAC addresses are permitted, but IP addresses are preferred
163  chassisaddr = pyNetAddr(chassisid)
164  chassistype = chassisaddr.addrtype()
165  if chassistype == ADDR_FAMILY_802: # It might be an IP address instead
166  adminnic = self.store.load_or_create(NICNode, domain=switch.domain
167  , macaddr=chassisid, ifname='(adminNIC)')
168  mgmtip = self.store.load_or_create(IPaddrNode, domain=switch.domain
169  , cidrmask='unknown', ipaddr=mgmtaddr)
170  if Store.is_abstract(adminnic) or Store.is_abstract(switch):
171  self.store.relate(switch, CMAconsts.REL_nicowner, adminnic)
172  if Store.is_abstract(mgmtip) or Store.is_abstract(adminnic):
173  self.store.relate(adminnic, CMAconsts.REL_ipowner, mgmtip)
174  else:
175  self.log.info('LLDP ATTRS: %s' % str(attrs))
176  if mgmtnetaddr != chassisaddr:
177  # Not really sure what I should be doing in this case...
178  self.log.warning(
179  'Chassis ID [%s] not a MAC addr and not the same as mgmt addr [%s]'
180  % (chassisid, mgmtaddr))
181  self.log.warning('Chassis ID [%s] != mgmt addr [%s]'
182  % (str(mgmtnetaddr), str(chassisaddr)))
183  elif atype == ADDR_FAMILY_802:
184  mgmtnic = self.store.load_or_create(NICNode, domain=switch.domain
185  , macaddr=mgmtaddr, ifname='(ManagementAddress)')
186  if Store.is_abstract(mgmtnic) or Store.is_abstract(switch):
187  self.store.relate(switch, CMAconsts.REL_nicowner, mgmtnic)
188  def _process_ports(self, drone, switch, chassisid, ports):
189  'Process the ports listed in JSON data from switch discovery'
190 
191  for portname in ports.keys():
192  attrs = {}
193  thisport = ports[portname]
194  for key in thisport.keys():
195  value = thisport[key]
196  if isinstance(value, pyNetAddr):
197  value = str(value)
198  attrs[key] = value
199  if 'sourceMAC' in thisport:
200  nicmac = thisport['sourceMAC']
201  else:
202  nicmac = chassisid # Hope that works ;-)
203  nicnode = self.store.load_or_create(NICNode, domain=drone.domain
204  , macaddr=nicmac, json=str(thisport), ifname=thisport['PortId'], **attrs)
205  self.store.relate(switch, CMAconsts.REL_nicowner, nicnode, {'causes': True})
206  try:
207  assert thisport['ConnectsToHost'] == drone.designation
208  matchif = thisport['ConnectsToInterface']
209  niclist = self.store.load_related(drone, CMAconsts.REL_nicowner, NICNode)
210  for dronenic in niclist:
211  if dronenic.ifname == matchif:
212  self.store.relate_new(nicnode, CMAconsts.REL_wiredto, dronenic)
213  break
214  except KeyError:
215  self.log.error('OOPS! got an exception...')
def discovery_indicates_link_is_up(devinfo)