1#
2# Copyright (c) 2014, Arista Networks, Inc.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9#   Redistributions of source code must retain the above copyright notice,
10#   this list of conditions and the following disclaimer.
11#
12#   Redistributions in binary form must reproduce the above copyright
13#   notice, this list of conditions and the following disclaimer in the
14#   documentation and/or other materials provided with the distribution.
15#
16#   Neither the name of Arista Networks nor the names of its
17#   contributors may be used to endorse or promote products derived from
18#   this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
24# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#
32"""Module for working with EOS static routes
33
34The staticroute resource provides configuration management of static
35route resources on an EOS node. It provides the following class
36implementations:
37
38    * StaticRoute - Configure static routes in EOS
39
40StaticRoute Attributes:
41    ip_dest (string): The ip address of the destination in the
42        form of A.B.C.D/E
43
44    next_hop (string): The next hop interface or ip address
45    next_hop_ip (string): The next hop address on destination interface
46    distance (int): Administrative distance for this route
47    tag (int): Route tag
48    route_name (string): Route name
49
50Notes:
51    The 'default' prefix function of the 'ip route' command,
52    'default ip route ...', currently equivalent to the 'no ip route ...'
53    command.
54"""
55
56import re
57
58from pyeapi.api import EntityCollection
59
60# Define the regex to match ip route lines (by lines in regex):
61#   'ip route' header
62#   ip_dest
63#   next_hop
64#   next_hop_ip
65#   distance
66#   tag
67#   name
68ROUTES_RE = re.compile(r'(?<=^ip route)'
69                       r' (\d+\.\d+\.\d+\.\d+\/\d+)'
70                       r' (\d+\.\d+\.\d+\.\d+|\S+)'
71                       r'(?: (\d+\.\d+\.\d+\.\d+))?'
72                       r' (\d+)'
73                       r'(?: tag (\d+))?'
74                       r'(?: name (\S+))?', re.M)
75
76
77class StaticRoute(EntityCollection):
78    """The StaticRoute class provides a configuration instance
79    for working with static routes
80
81    """
82
83    def __str__(self):
84        return 'StaticRoute'
85
86    def get(self, name):
87        """Retrieves the ip route information for the destination
88        ip address specified.
89
90        Args:
91            name (string): The ip address of the destination in the
92                form of A.B.C.D/E
93
94        Returns:
95            dict: An dict object of static route entries in the form::
96
97                { ip_dest:
98                    { next_hop:
99                        { next_hop_ip:
100                            { distance:
101                                { 'tag': tag,
102                                  'route_name': route_name
103                                }
104                            }
105                        }
106                    }
107                }
108
109            If the ip address specified does not have any associated
110            static routes, then None is returned.
111
112        Notes:
113            The keys ip_dest, next_hop, next_hop_ip, and distance in
114            the returned dictionary are the values of those components
115            of the ip route specification. If a route does not contain
116            a next_hop_ip, then that key value will be set as 'None'.
117        """
118
119        # Return the route configurations for the specified ip address,
120        # or None if its not found
121        return self.getall().get(name)
122
123    def getall(self):
124        """Return all ip routes configured on the switch as a resource dict
125
126        Returns:
127            dict: An dict object of static route entries in the form::
128
129                { ip_dest:
130                    { next_hop:
131                        { next_hop_ip:
132                            { distance:
133                                { 'tag': tag,
134                                  'route_name': route_name
135                                }
136                            }
137                        }
138                    }
139                }
140
141            If the ip address specified does not have any associated
142            static routes, then None is returned.
143
144        Notes:
145            The keys ip_dest, next_hop, next_hop_ip, and distance in
146            the returned dictionary are the values of those components
147            of the ip route specification. If a route does not contain
148            a next_hop_ip, then that key value will be set as 'None'.
149        """
150
151        # Find all the ip routes in the config
152        matches = ROUTES_RE.findall(self.config)
153
154        # Parse the routes and add them to the routes dict
155        routes = dict()
156        for match in matches:
157
158            # Get the four identifying components
159            ip_dest = match[0]
160            next_hop = match[1]
161            next_hop_ip = None if match[2] == '' else match[2]
162            distance = int(match[3])
163
164            # Create the data dict with the remaining components
165            data = {}
166            data['tag'] = None if match[4] == '' else int(match[4])
167            data['route_name'] = None if match[5] == '' else match[5]
168
169            # Build the complete dict entry from the four components
170            # and the data.
171            # temp_dict = parent_dict[key] = parent_dict.get(key, {})
172            # This creates the keyed dict in the parent_dict if it doesn't
173            # exist, or reuses the existing keyed dict.
174            # The temp_dict is used to make things more readable.
175            ip_dict = routes[ip_dest] = routes.get(ip_dest, {})
176            nh_dict = ip_dict[next_hop] = ip_dict.get(next_hop, {})
177            nhip_dict = nh_dict[next_hop_ip] = nh_dict.get(next_hop_ip, {})
178            nhip_dict[distance] = data
179
180        return routes
181
182    def create(self, ip_dest, next_hop, **kwargs):
183        """Create a static route
184
185        Args:
186            ip_dest (string): The ip address of the destination in the
187                form of A.B.C.D/E
188            next_hop (string): The next hop interface or ip address
189            **kwargs['next_hop_ip'] (string): The next hop address on
190                destination interface
191            **kwargs['distance'] (string): Administrative distance for this
192                route
193            **kwargs['tag'] (string): Route tag
194            **kwargs['route_name'] (string): Route name
195
196        Returns:
197            True if the operation succeeds, otherwise False.
198        """
199
200        # Call _set_route with delete and default set to False
201        return self._set_route(ip_dest, next_hop, **kwargs)
202
203    def delete(self, ip_dest, next_hop, **kwargs):
204        """Delete a static route
205
206        Args:
207            ip_dest (string): The ip address of the destination in the
208                form of A.B.C.D/E
209            next_hop (string): The next hop interface or ip address
210            **kwargs['next_hop_ip'] (string): The next hop address on
211                destination interface
212            **kwargs['distance'] (string): Administrative distance for this
213                route
214            **kwargs['tag'] (string): Route tag
215            **kwargs['route_name'] (string): Route name
216
217        Returns:
218            True if the operation succeeds, otherwise False.
219        """
220
221        # Call _set_route with the delete flag set to True
222        kwargs.update({'delete': True})
223        return self._set_route(ip_dest, next_hop, **kwargs)
224
225    def default(self, ip_dest, next_hop, **kwargs):
226        """Set a static route to default (i.e. delete the matching route)
227
228        Args:
229            ip_dest (string): The ip address of the destination in the
230                form of A.B.C.D/E
231            next_hop (string): The next hop interface or ip address
232            **kwargs['next_hop_ip'] (string): The next hop address on
233                destination interface
234            **kwargs['distance'] (string): Administrative distance for this
235                route
236            **kwargs['tag'] (string): Route tag
237            **kwargs['route_name'] (string): Route name
238
239        Returns:
240            True if the operation succeeds, otherwise False.
241        """
242
243        # Call _set_route with the default flag set to True
244        kwargs.update({'default': True})
245        return self._set_route(ip_dest, next_hop, **kwargs)
246
247    def set_tag(self, ip_dest, next_hop, **kwargs):
248        """Set the tag value for the specified route
249
250        Args:
251            ip_dest (string): The ip address of the destination in the
252                form of A.B.C.D/E
253            next_hop (string): The next hop interface or ip address
254            **kwargs['next_hop_ip'] (string): The next hop address on
255                destination interface
256            **kwargs['distance'] (string): Administrative distance for this
257                route
258            **kwargs['tag'] (string): Route tag
259            **kwargs['route_name'] (string): Route name
260
261        Returns:
262            True if the operation succeeds, otherwise False.
263
264        Notes:
265            Any existing route_name value must be included in call to
266                set_tag, otherwise the tag will be reset
267                by the call to EOS.
268        """
269
270        # Call _set_route with the new tag information
271        return self._set_route(ip_dest, next_hop, **kwargs)
272
273    def set_route_name(self, ip_dest, next_hop, **kwargs):
274        """Set the route_name value for the specified route
275
276        Args:
277            ip_dest (string): The ip address of the destination in the
278                form of A.B.C.D/E
279            next_hop (string): The next hop interface or ip address
280            **kwargs['next_hop_ip'] (string): The next hop address on
281                destination interface
282            **kwargs['distance'] (string): Administrative distance for this
283                route
284            **kwargs['tag'] (string): Route tag
285            **kwargs['route_name'] (string): Route name
286
287        Returns:
288            True if the operation succeeds, otherwise False.
289
290        Notes:
291            Any existing tag value must be included in call to
292                set_route_name, otherwise the tag will be reset
293                by the call to EOS.
294        """
295
296        # Call _set_route with the new route_name information
297        return self._set_route(ip_dest, next_hop, **kwargs)
298
299    def _build_commands(self, ip_dest, next_hop, **kwargs):
300        """Build the EOS command string for ip route interactions.
301
302        Args:
303            ip_dest (string): The ip address of the destination in the
304                form of A.B.C.D/E
305            next_hop (string): The next hop interface or ip address
306            **kwargs['next_hop_ip'] (string): The next hop address on
307                destination interface
308            **kwargs['distance'] (string): Administrative distance for this
309                route
310            **kwargs['tag'] (string): Route tag
311            **kwargs['route_name'] (string): Route name
312
313        Returns the ip route command string to be sent to the switch for
314        the given set of parameters.
315        """
316
317        commands = "ip route %s %s" % (ip_dest, next_hop)
318
319        next_hop_ip = kwargs.get('next_hop_ip', None)
320        distance = kwargs.get('distance', None)
321        tag = kwargs.get('tag', None)
322        route_name = kwargs.get('route_name', None)
323
324        if next_hop_ip is not None:
325            commands += " %s" % next_hop_ip
326        if distance is not None:
327            commands += " %s" % distance
328        if tag is not None:
329            commands += " tag %s" % tag
330        if route_name is not None:
331            commands += " name %s" % route_name
332
333        return commands
334
335    def _set_route(self, ip_dest, next_hop, **kwargs):
336        """Configure a static route
337
338        Args:
339            ip_dest (string): The ip address of the destination in the
340                form of A.B.C.D/E
341            next_hop (string): The next hop interface or ip address
342            **kwargs['next_hop_ip'] (string): The next hop address on
343                destination interface
344            **kwargs['distance'] (string): Administrative distance for this
345                route
346            **kwargs['tag'] (string): Route tag
347            **kwargs['route_name'] (string): Route name
348            **kwargs['delete'] (boolean): If true, deletes the specified route
349                instead of creating or setting values for the route
350            **kwargs['default'] (boolean): If true, defaults the specified
351                route instead of creating or setting values for the route
352
353        Returns:
354            True if the operation succeeds, otherwise False.
355        """
356
357        commands = self._build_commands(ip_dest, next_hop, **kwargs)
358
359        delete = kwargs.get('delete', False)
360        default = kwargs.get('default', False)
361
362        # Prefix with 'no' if delete is set
363        if delete:
364            commands = "no " + commands
365        # Or with 'default' if default is setting
366        else:
367            if default:
368                commands = "default " + commands
369
370        return self.configure(commands)
371
372
373def instance(node):
374    """Returns an instance of StaticRoute
375
376    This method will create and return an instance of the StaticRoute
377    object passing the value of API to the object. The instance method
378    is required for the resource to be autoloaded by the Node object
379
380    Args:
381        node (Node): The node argument passes an instance of Node to the
382            resource
383    """
384    return StaticRoute(node)
385