Switch to using IPs for inter-node communication, DNS sometimes fails (and with 100...
[nepi.git] / src / nepi / testbeds / planetlab / node.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID
5 import plcapi
6 import operator
7 import rspawn
8 import time
9 import os
10 import collections
11 import cStringIO
12 import resourcealloc
13 import socket
14 import sys
15 import logging
16 import ipaddr
17 import operator
18
19 from nepi.util import server
20 from nepi.util import parallel
21
22 import application
23
24 MAX_VROUTE_ROUTES = 5
25
26 class UnresponsiveNodeError(RuntimeError):
27     pass
28
29 def _castproperty(typ, propattr):
30     def _get(self):
31         return getattr(self, propattr)
32     def _set(self, value):
33         if value is not None or (isinstance(value, basestring) and not value):
34             value = typ(value)
35         return setattr(self, propattr, value)
36     def _del(self, value):
37         return delattr(self, propattr)
38     _get.__name__ = propattr + '_get'
39     _set.__name__ = propattr + '_set'
40     _del.__name__ = propattr + '_del'
41     return property(_get, _set, _del)
42
43 class Node(object):
44     BASEFILTERS = {
45         # Map Node attribute to plcapi filter name
46         'hostname' : 'hostname',
47     }
48     
49     TAGFILTERS = {
50         # Map Node attribute to (<tag name>, <plcapi filter expression>)
51         #   There are replacements that are applied with string formatting,
52         #   so '%' has to be escaped as '%%'.
53         'architecture' : ('arch','value'),
54         'operatingSystem' : ('fcdistro','value'),
55         'pl_distro' : ('pldistro','value'),
56         'city' : ('city','value'),
57         'country' : ('country','value'),
58         'region' : ('region','value'),
59         'minReliability' : ('reliability%(timeframe)s', ']value'),
60         'maxReliability' : ('reliability%(timeframe)s', '[value'),
61         'minBandwidth' : ('bw%(timeframe)s', ']value'),
62         'maxBandwidth' : ('bw%(timeframe)s', '[value'),
63         'minLoad' : ('load%(timeframe)s', ']value'),
64         'maxLoad' : ('load%(timeframe)s', '[value'),
65         'minCpu' : ('cpu%(timeframe)s', ']value'),
66         'maxCpu' : ('cpu%(timeframe)s', '[value'),
67     }
68     
69     RATE_FACTORS = (
70         # (<tag name>, <weight>, <default>)
71         ('bw%(timeframe)s', -0.001, 1024.0),
72         ('cpu%(timeframe)s', 0.1, 40.0),
73         ('load%(timeframe)s', -0.2, 3.0),
74         ('reliability%(timeframe)s', 1, 100.0),
75     )
76     
77     DEPENDS_PIDFILE = '/tmp/nepi-depends.pid'
78     DEPENDS_LOGFILE = '/tmp/nepi-depends.log'
79     RPM_FUSION_URL = 'http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-stable.noarch.rpm'
80     RPM_FUSION_URL_F12 = 'http://download1.rpmfusion.org/free/fedora/releases/12/Everything/x86_64/os/rpmfusion-free-release-12-1.noarch.rpm'
81     
82     minReliability = _castproperty(float, '_minReliability')
83     maxReliability = _castproperty(float, '_maxReliability')
84     minBandwidth = _castproperty(float, '_minBandwidth')
85     maxBandwidth = _castproperty(float, '_maxBandwidth')
86     minCpu = _castproperty(float, '_minCpu')
87     maxCpu = _castproperty(float, '_maxCpu')
88     minLoad = _castproperty(float, '_minLoad')
89     maxLoad = _castproperty(float, '_maxLoad')
90     
91     def __init__(self, api=None):
92         if not api:
93             api = plcapi.PLCAPI()
94         self._api = api
95         
96         # Attributes
97         self.hostname = None
98         self.architecture = None
99         self.operatingSystem = None
100         self.pl_distro = None
101         self.site = None
102         self.city = None
103         self.country = None
104         self.region = None
105         self.minReliability = None
106         self.maxReliability = None
107         self.minBandwidth = None
108         self.maxBandwidth = None
109         self.minCpu = None
110         self.maxCpu = None
111         self.minLoad = None
112         self.maxLoad = None
113         self.min_num_external_ifaces = None
114         self.max_num_external_ifaces = None
115         self.timeframe = 'm'
116         
117         # Applications and routes add requirements to connected nodes
118         self.required_packages = set()
119         self.required_vsys = set()
120         self.pythonpath = []
121         self.rpmFusion = False
122         self.env = collections.defaultdict(list)
123         
124         # Some special applications - initialized when connected
125         self.multicast_forwarder = None
126         
127         # Testbed-derived attributes
128         self.slicename = None
129         self.ident_path = None
130         self.server_key = None
131         self.home_path = None
132         self.enable_cleanup = False
133         
134         # Those are filled when an actual node is allocated
135         self._node_id = None
136         self._yum_dependencies = None
137         self._installed = False
138
139         # Logging
140         self._logger = logging.getLogger('nepi.testbeds.planetlab')
141     
142     def _nepi_testbed_environment_setup_get(self):
143         command = cStringIO.StringIO()
144         command.write('export PYTHONPATH=$PYTHONPATH:%s' % (
145             ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.pythonpath])
146         ))
147         command.write(' ; export PATH=$PATH:%s' % (
148             ':'.join(["${HOME}/"+server.shell_escape(s) for s in self.pythonpath])
149         ))
150         if self.env:
151             for envkey, envvals in self.env.iteritems():
152                 for envval in envvals:
153                     command.write(' ; export %s=%s' % (envkey, envval))
154         return command.getvalue()
155     def _nepi_testbed_environment_setup_set(self, value):
156         pass
157     _nepi_testbed_environment_setup = property(
158         _nepi_testbed_environment_setup_get,
159         _nepi_testbed_environment_setup_set)
160     
161     def build_filters(self, target_filters, filter_map):
162         for attr, tag in filter_map.iteritems():
163             value = getattr(self, attr, None)
164             if value is not None:
165                 target_filters[tag] = value
166         return target_filters
167     
168     @property
169     def applicable_filters(self):
170         has = lambda att : getattr(self,att,None) is not None
171         return (
172             filter(has, self.BASEFILTERS.iterkeys())
173             + filter(has, self.TAGFILTERS.iterkeys())
174         )
175     
176     def find_candidates(self, filter_slice_id=None):
177         self._logger.info("Finding candidates for %s", self.make_filter_description())
178         
179         fields = ('node_id',)
180         replacements = {'timeframe':self.timeframe}
181         
182         # get initial candidates (no tag filters)
183         basefilters = self.build_filters({}, self.BASEFILTERS)
184         rootfilters = basefilters.copy()
185         if filter_slice_id:
186             basefilters['|slice_ids'] = (filter_slice_id,)
187         
188         # only pick healthy nodes
189         basefilters['run_level'] = 'boot'
190         basefilters['boot_state'] = 'boot'
191         basefilters['node_type'] = 'regular' # nepi can only handle regular nodes (for now)
192         basefilters['>last_contact'] = int(time.time()) - 5*3600 # allow 5h out of contact, for timezone discrepancies
193         
194         # keyword-only "pseudofilters"
195         extra = {}
196         if self.site:
197             extra['peer'] = self.site
198             
199         candidates = set(map(operator.itemgetter('node_id'), 
200             self._api.GetNodes(filters=basefilters, fields=fields, **extra)))
201         
202         # filter by tag, one tag at a time
203         applicable = self.applicable_filters
204         for tagfilter in self.TAGFILTERS.iteritems():
205             attr, (tagname, expr) = tagfilter
206             
207             # don't bother if there's no filter defined
208             if attr in applicable:
209                 tagfilter = rootfilters.copy()
210                 tagfilter['tagname'] = tagname % replacements
211                 tagfilter[expr % replacements] = getattr(self,attr)
212                 tagfilter['node_id'] = list(candidates)
213                 
214                 candidates &= set(map(operator.itemgetter('node_id'),
215                     self._api.GetNodeTags(filters=tagfilter, fields=fields)))
216         
217         # filter by vsys tags - special case since it doesn't follow
218         # the usual semantics
219         if self.required_vsys:
220             newcandidates = collections.defaultdict(set)
221             
222             vsys_tags = self._api.GetNodeTags(
223                 tagname='vsys', 
224                 node_id = list(candidates), 
225                 fields = ['node_id','value'])
226             
227             vsys_tags = map(
228                 operator.itemgetter(['node_id','value']),
229                 vsys_tags)
230             
231             required_vsys = self.required_vsys
232             for node_id, value in vsys_tags:
233                 if value in required_vsys:
234                     newcandidates[value].add(node_id)
235             
236             # take only those that have all the required vsys tags
237             newcandidates = reduce(
238                 lambda accum, new : accum & new,
239                 newcandidates.itervalues(),
240                 candidates)
241         
242         # filter by iface count
243         if self.min_num_external_ifaces is not None or self.max_num_external_ifaces is not None:
244             # fetch interfaces for all, in one go
245             filters = basefilters.copy()
246             filters['node_id'] = list(candidates)
247             ifaces = dict(map(operator.itemgetter('node_id','interface_ids'),
248                 self._api.GetNodes(filters=basefilters, fields=('node_id','interface_ids')) ))
249             
250             # filter candidates by interface count
251             if self.min_num_external_ifaces is not None and self.max_num_external_ifaces is not None:
252                 predicate = ( lambda node_id : 
253                     self.min_num_external_ifaces <= len(ifaces.get(node_id,())) <= self.max_num_external_ifaces )
254             elif self.min_num_external_ifaces is not None:
255                 predicate = ( lambda node_id : 
256                     self.min_num_external_ifaces <= len(ifaces.get(node_id,())) )
257             else:
258                 predicate = ( lambda node_id : 
259                     len(ifaces.get(node_id,())) <= self.max_num_external_ifaces )
260             
261             candidates = set(filter(predicate, candidates))
262         
263         # make sure hostnames are resolvable
264         if candidates:
265             self._logger.info("  Found %s candidates. Checking for reachability...", len(candidates))
266             
267             hostnames = dict(map(operator.itemgetter('node_id','hostname'),
268                 self._api.GetNodes(list(candidates), ['node_id','hostname'])
269             ))
270             def resolvable(node_id):
271                 try:
272                     addr = socket.gethostbyname(hostnames[node_id])
273                     return addr is not None
274                 except:
275                     return False
276             candidates = set(parallel.pfilter(resolvable, candidates,
277                 maxthreads = 16))
278
279             self._logger.info("  Found %s reachable candidates.", len(candidates))
280             
281         return candidates
282     
283     def make_filter_description(self):
284         """
285         Makes a human-readable description of filtering conditions
286         for find_candidates.
287         """
288         
289         # get initial candidates (no tag filters)
290         filters = self.build_filters({}, self.BASEFILTERS)
291         
292         # keyword-only "pseudofilters"
293         if self.site:
294             filters['peer'] = self.site
295             
296         # filter by tag, one tag at a time
297         applicable = self.applicable_filters
298         for tagfilter in self.TAGFILTERS.iteritems():
299             attr, (tagname, expr) = tagfilter
300             
301             # don't bother if there's no filter defined
302             if attr in applicable:
303                 filters[attr] = getattr(self,attr)
304         
305         # filter by vsys tags - special case since it doesn't follow
306         # the usual semantics
307         if self.required_vsys:
308             filters['vsys'] = ','.join(list(self.required_vsys))
309         
310         # filter by iface count
311         if self.min_num_external_ifaces is not None or self.max_num_external_ifaces is not None:
312             filters['num_ifaces'] = '-'.join([
313                 str(self.min_num_external_ifaces or '0'),
314                 str(self.max_num_external_ifaces or 'inf')
315             ])
316             
317         return '; '.join(map('%s: %s'.__mod__,filters.iteritems()))
318
319     def assign_node_id(self, node_id):
320         self._node_id = node_id
321         self.fetch_node_info()
322     
323     def unassign_node(self):
324         self._node_id = None
325         self.hostip = None
326         
327         try:
328             orig_attrs = self.__orig_attrs
329         except AttributeError:
330             return
331             
332         for key, value in orig_attrs.iteritems():
333             setattr(self, key, value)
334         del self.__orig_attrs
335     
336     def rate_nodes(self, nodes):
337         rates = collections.defaultdict(int)
338         tags = collections.defaultdict(dict)
339         replacements = {'timeframe':self.timeframe}
340         tagnames = [ tagname % replacements 
341                      for tagname, weight, default in self.RATE_FACTORS ]
342         
343         taginfo = self._api.GetNodeTags(
344             node_id=list(nodes), 
345             tagname=tagnames,
346             fields=('node_id','tagname','value'))
347         
348         unpack = operator.itemgetter('node_id','tagname','value')
349         for value in taginfo:
350             node, tagname, value = unpack(value)
351             if value and value.lower() != 'n/a':
352                 tags[tagname][int(node)] = float(value)
353         
354         for tagname, weight, default in self.RATE_FACTORS:
355             taginfo = tags[tagname % replacements].get
356             for node in nodes:
357                 rates[node] += weight * taginfo(node,default)
358         
359         return map(rates.__getitem__, nodes)
360             
361     def fetch_node_info(self):
362         orig_attrs = {}
363         
364         self._api.StartMulticall()
365         info = self._api.GetNodes(self._node_id)
366         tags = self._api.GetNodeTags(node_id=self._node_id, fields=('tagname','value'))
367         info, tags = self._api.FinishMulticall()
368         info = info[0]
369         
370         tags = dict( (t['tagname'],t['value'])
371                      for t in tags )
372
373         orig_attrs['min_num_external_ifaces'] = self.min_num_external_ifaces
374         orig_attrs['max_num_external_ifaces'] = self.max_num_external_ifaces
375         self.min_num_external_ifaces = None
376         self.max_num_external_ifaces = None
377         self.timeframe = 'm'
378         
379         replacements = {'timeframe':self.timeframe}
380         for attr, tag in self.BASEFILTERS.iteritems():
381             if tag in info:
382                 value = info[tag]
383                 if hasattr(self, attr):
384                     orig_attrs[attr] = getattr(self, attr)
385                 setattr(self, attr, value)
386         for attr, (tag,_) in self.TAGFILTERS.iteritems():
387             tag = tag % replacements
388             if tag in tags:
389                 value = tags[tag]
390                 if hasattr(self, attr):
391                     orig_attrs[attr] = getattr(self, attr)
392                 if not value or value.lower() == 'n/a':
393                     value = None
394                 setattr(self, attr, value)
395         
396         if 'peer_id' in info:
397             orig_attrs['site'] = self.site
398             self.site = self._api.peer_map[info['peer_id']]
399         
400         if 'interface_ids' in info:
401             self.min_num_external_ifaces = \
402             self.max_num_external_ifaces = len(info['interface_ids'])
403         
404         if 'ssh_rsa_key' in info:
405             orig_attrs['server_key'] = self.server_key
406             self.server_key = info['ssh_rsa_key']
407         
408         self.hostip = socket.gethostbyname(self.hostname)
409         
410         try:
411             self.__orig_attrs
412         except AttributeError:
413             self.__orig_attrs = orig_attrs
414
415     def validate(self):
416         if self.home_path is None:
417             raise AssertionError, "Misconfigured node: missing home path"
418         if self.ident_path is None or not os.access(self.ident_path, os.R_OK):
419             raise AssertionError, "Misconfigured node: missing slice SSH key"
420         if self.slicename is None:
421             raise AssertionError, "Misconfigured node: unspecified slice"
422
423     def recover(self):
424         # Mark dependencies installed
425         self._installed = True
426         
427         # Clear load attributes, they impair re-discovery
428         self.minReliability = \
429         self.maxReliability = \
430         self.minBandwidth = \
431         self.maxBandwidth = \
432         self.minCpu = \
433         self.maxCpu = \
434         self.minLoad = \
435         self.maxLoad = None
436
437     def install_dependencies(self):
438         if self.required_packages and not self._installed:
439             # If we need rpmfusion, we must install the repo definition and the gpg keys
440             if self.rpmFusion:
441                 if self.operatingSystem == 'f12':
442                     # Fedora 12 requires a different rpmfusion package
443                     RPM_FUSION_URL = self.RPM_FUSION_URL_F12
444                 else:
445                     # This one works for f13+
446                     RPM_FUSION_URL = self.RPM_FUSION_URL
447                     
448                 rpmFusion = (
449                   'sudo -S rpm -q $(rpm -q -p %(RPM_FUSION_URL)s) || rpm -i %(RPM_FUSION_URL)s'
450                 ) % {
451                     'RPM_FUSION_URL' : RPM_FUSION_URL
452                 }
453             else:
454                 rpmFusion = ''
455             
456             if rpmFusion:
457                 (out,err),proc = server.popen_ssh_command(
458                     rpmFusion,
459                     host = self.hostname,
460                     port = None,
461                     user = self.slicename,
462                     agent = None,
463                     ident_key = self.ident_path,
464                     server_key = self.server_key,
465                     timeout = 600,
466                     )
467                 
468                 if proc.wait():
469                     raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
470             
471             # Launch p2p yum dependency installer
472             self._yum_dependencies.async_setup()
473     
474     def wait_provisioning(self, timeout = 20*60):
475         # Wait for the p2p installer
476         sleeptime = 1.0
477         totaltime = 0.0
478         while not self.is_alive():
479             time.sleep(sleeptime)
480             totaltime += sleeptime
481             sleeptime = min(30.0, sleeptime*1.5)
482             
483             if totaltime > timeout:
484                 # PlanetLab has a 15' delay on configuration propagation
485                 # If we're above that delay, the unresponsiveness is not due
486                 # to this delay.
487                 if not self.is_alive(verbose=True):
488                     raise UnresponsiveNodeError, "Unresponsive host %s" % (self.hostname,)
489         
490         # Ensure the node is clean (no apps running that could interfere with operations)
491         if self.enable_cleanup:
492             self.do_cleanup()
493     
494     def wait_dependencies(self, pidprobe=1, probe=0.5, pidmax=10, probemax=10):
495         # Wait for the p2p installer
496         if self._yum_dependencies and not self._installed:
497             self._yum_dependencies.async_setup_wait()
498             self._installed = True
499         
500     def is_alive(self, verbose = False):
501         # Make sure all the paths are created where 
502         # they have to be created for deployment
503         (out,err),proc = server.eintr_retry(server.popen_ssh_command)(
504             "echo 'ALIVE'",
505             host = self.hostname,
506             port = None,
507             user = self.slicename,
508             agent = None,
509             ident_key = self.ident_path,
510             server_key = self.server_key,
511             timeout = 60,
512             err_on_timeout = False,
513             persistent = False
514             )
515         
516         if proc.wait():
517             if verbose:
518                 self._logger.warn("Unresponsive node %s got:\n%s%s", self.hostname, out, err)
519             return False
520         elif not err and out.strip() == 'ALIVE':
521             return True
522         else:
523             if verbose:
524                 self._logger.warn("Unresponsive node %s got:\n%s%s", self.hostname, out, err)
525             return False
526     
527     def destroy(self):
528         if self.enable_cleanup:
529             self.do_cleanup()
530     
531     def blacklist(self):
532         if self._node_id:
533             self._logger.warn("Blacklisting malfunctioning node %s", self.hostname)
534             import util
535             util.appendBlacklist(self._node_id)
536     
537     def do_cleanup(self):
538         if self.testbed().recovering:
539             # WOW - not now
540             return
541             
542         self._logger.info("Cleaning up %s", self.hostname)
543         
544         cmds = [
545             "sudo -S killall python tcpdump || /bin/true ; "
546             "sudo -S killall python tcpdump || /bin/true ; "
547             "sudo -S kill $(ps -N -T -o pid --no-heading | grep -v $PPID | sort) || /bin/true ",
548             "sudo -S killall -u %(slicename)s || /bin/true ",
549             "sudo -S killall -u root || /bin/true ",
550             "sudo -S killall -u %(slicename)s || /bin/true ",
551             "sudo -S killall -u root || /bin/true ",
552         ]
553
554         for cmd in cmds:
555             (out,err),proc = server.popen_ssh_command(
556                 # Some apps need two kills
557                 cmd % {
558                     'slicename' : self.slicename ,
559                 },
560                 host = self.hostname,
561                 port = None,
562                 user = self.slicename,
563                 agent = None,
564                 ident_key = self.ident_path,
565                 server_key = self.server_key,
566                 tty = True, # so that ps -N -T works as advertised...
567                 timeout = 60,
568                 retry = 3
569                 )
570             proc.wait()
571     
572     def prepare_dependencies(self):
573         # Configure p2p yum dependency installer
574         if self.required_packages and not self._installed:
575             self._yum_dependencies = application.YumDependency(self._api)
576             self._yum_dependencies.node = self
577             self._yum_dependencies.home_path = "nepi-yumdep"
578             self._yum_dependencies.depends = ' '.join(self.required_packages)
579
580     def routing_method(self, routes, vsys_vnet):
581         """
582         There are two methods, vroute and sliceip.
583         
584         vroute:
585             Modifies the node's routing table directly, validating that the IP
586             range lies within the network given by the slice's vsys_vnet tag.
587             This method is the most scalable for very small routing tables
588             that need not modify other routes (including the default)
589         
590         sliceip:
591             Uses policy routing and iptables filters to create per-sliver
592             routing tables. It's the most flexible way, but it doesn't scale
593             as well since only 155 routing tables can be created this way.
594         
595         This method will return the most appropriate routing method, which will
596         prefer vroute for small routing tables.
597         """
598         
599         # For now, sliceip results in kernel panics
600         # so we HAVE to use vroute
601         return 'vroute'
602         
603         # We should not make the routing table grow too big
604         if len(routes) > MAX_VROUTE_ROUTES:
605             return 'sliceip'
606         
607         vsys_vnet = ipaddr.IPNetwork(vsys_vnet)
608         for route in routes:
609             dest, prefix, nexthop, metric = route
610             dest = ipaddr.IPNetwork("%s/%d" % (dest,prefix))
611             nexthop = ipaddr.IPAddress(nexthop)
612             if dest not in vsys_vnet or nexthop not in vsys_vnet:
613                 return 'sliceip'
614         
615         return 'vroute'
616     
617     def format_route(self, route, dev, method, action):
618         dest, prefix, nexthop, metric = route
619         if method == 'vroute':
620             return (
621                 "%s %s%s gw %s %s" % (
622                     action,
623                     dest,
624                     (("/%d" % (prefix,)) if prefix and prefix != 32 else ""),
625                     nexthop,
626                     dev,
627                 )
628             )
629         elif method == 'sliceip':
630             return (
631                 "route %s to %s%s via %s metric %s dev %s" % (
632                     action,
633                     dest,
634                     (("/%d" % (prefix,)) if prefix and prefix != 32 else ""),
635                     nexthop,
636                     metric or 1,
637                     dev,
638                 )
639             )
640         else:
641             raise AssertionError, "Unknown method"
642     
643     def _annotate_routes_with_devs(self, routes, devs, method):
644         dev_routes = []
645         for route in routes:
646             for dev in devs:
647                 if dev.routes_here(route):
648                     dev_routes.append(tuple(route) + (dev.if_name,))
649                     
650                     # Stop checking
651                     break
652             else:
653                 if method == 'sliceip':
654                     dev_routes.append(tuple(route) + ('eth0',))
655                 else:
656                     raise RuntimeError, "Route %s cannot be bound to any virtual interface " \
657                         "- PL can only handle rules over virtual interfaces. Candidates are: %s" % (route,devs)
658         return dev_routes
659     
660     def configure_routes(self, routes, devs, vsys_vnet):
661         """
662         Add the specified routes to the node's routing table
663         """
664         rules = []
665         method = self.routing_method(routes, vsys_vnet)
666         tdevs = set()
667         
668         # annotate routes with devices
669         dev_routes = self._annotate_routes_with_devs(routes, devs, method)
670         for route in dev_routes:
671             route, dev = route[:-1], route[-1]
672             
673             # Schedule rule
674             tdevs.add(dev)
675             rules.append(self.format_route(route, dev, method, 'add'))
676         
677         if method == 'sliceip':
678             rules = map('enable '.__add__, tdevs) + rules
679         
680         self._logger.info("Setting up routes for %s using %s", self.hostname, method)
681         self._logger.debug("Routes for %s:\n\t%s", self.hostname, '\n\t'.join(rules))
682         
683         self.apply_route_rules(rules, method)
684         
685         self._configured_routes = set(routes)
686         self._configured_devs = tdevs
687         self._configured_method = method
688     
689     def reconfigure_routes(self, routes, devs, vsys_vnet):
690         """
691         Updates the routes in the node's routing table to match
692         the given route list
693         """
694         method = self._configured_method
695         
696         dev_routes = self._annotate_routes_with_devs(routes, devs, method)
697
698         current = self._configured_routes
699         current_devs = self._configured_devs
700         
701         new = set(dev_routes)
702         new_devs = set(map(operator.itemgetter(-1), dev_routes))
703         
704         deletions = current - new
705         insertions = new - current
706         
707         dev_deletions = current_devs - new_devs
708         dev_insertions = new_devs - current_devs
709         
710         # Generate rules
711         rules = []
712         
713         # Rule deletions first
714         for route in deletions:
715             route, dev = route[:-1], route[-1]
716             rules.append(self.format_route(route, dev, method, 'del'))
717         
718         if method == 'sliceip':
719             # Dev deletions now
720             rules.extend(map('disable '.__add__, dev_deletions))
721
722             # Dev insertions now
723             rules.extend(map('enable '.__add__, dev_insertions))
724
725         # Rule insertions now
726         for route in insertions:
727             route, dev = route[:-1], dev[-1]
728             rules.append(self.format_route(route, dev, method, 'add'))
729         
730         # Apply
731         self.apply_route_rules(rules, method)
732         
733         self._configured_routes = dev_routes
734         self._configured_devs = new_devs
735         
736     def apply_route_rules(self, rules, method):
737         (out,err),proc = server.popen_ssh_command(
738             "( sudo -S bash -c 'cat /vsys/%(method)s.out >&2' & ) ; sudo -S bash -c 'cat > /vsys/%(method)s.in' ; sleep 0.5" % dict(
739                 home = server.shell_escape(self.home_path),
740                 method = method),
741             host = self.hostname,
742             port = None,
743             user = self.slicename,
744             agent = None,
745             ident_key = self.ident_path,
746             server_key = self.server_key,
747             stdin = '\n'.join(rules),
748             timeout = 300
749             )
750         
751         if proc.wait() or err:
752             raise RuntimeError, "Could not set routes (%s) errors: %s%s" % (rules,out,err)
753         elif out or err:
754             logger.debug("%s said: %s%s", method, out, err)
755