ipsec: IPSec protection for multi-point tunnel interfaces
[vpp.git] / test / remote_test.py
1 #!/usr/bin/env python3
2
3 import inspect
4 import os
5 import unittest
6 from framework import VppTestCase
7 from multiprocessing import Process, Pipe
8 from pickle import dumps
9 import six
10 from six import moves
11 import sys
12
13 if sys.version_info < (3,):
14     from aenum import IntEnum, IntFlag
15 else:
16     from enum import IntEnum, IntFlag
17
18
19 class SerializableClassCopy(object):
20     """
21     Empty class used as a basis for a serializable copy of another class.
22     """
23     pass
24
25     def __repr__(self):
26         return '<SerializableClassCopy dict=%s>' % self.__dict__
27
28
29 class RemoteClassAttr(object):
30     """
31     Wrapper around attribute of a remotely executed class.
32     """
33
34     def __init__(self, remote, attr):
35         self._path = [attr] if attr else []
36         self._remote = remote
37
38     def path_to_str(self):
39         return '.'.join(self._path)
40
41     def get_remote_value(self):
42         return self._remote._remote_exec(RemoteClass.GET, self.path_to_str())
43
44     def __repr__(self):
45         return self._remote._remote_exec(RemoteClass.REPR, self.path_to_str())
46
47     def __str__(self):
48         return self._remote._remote_exec(RemoteClass.STR, self.path_to_str())
49
50     def __getattr__(self, attr):
51         if attr[0] == '_':
52             if not (attr.startswith('__') and attr.endswith('__')):
53                 raise AttributeError('tried to get private attribute: %s ',
54                                      attr)
55         self._path.append(attr)
56         return self
57
58     def __setattr__(self, attr, val):
59         if attr[0] == '_':
60             if not (attr.startswith('__') and attr.endswith('__')):
61                 super(RemoteClassAttr, self).__setattr__(attr, val)
62                 return
63         self._path.append(attr)
64         self._remote._remote_exec(RemoteClass.SETATTR, self.path_to_str(),
65                                   value=val)
66
67     def __call__(self, *args, **kwargs):
68         return self._remote._remote_exec(RemoteClass.CALL, self.path_to_str(),
69                                          *args, **kwargs)
70
71
72 class RemoteClass(Process):
73     """
74     This class can wrap around and adapt the interface of another class,
75     and then delegate its execution to a newly forked child process.
76     Usage:
77         # Create a remotely executed instance of MyClass
78         object = RemoteClass(MyClass, arg1='foo', arg2='bar')
79         object.start_remote()
80         # Access the object normally as if it was an instance of your class.
81         object.my_attribute = 20
82         print object.my_attribute
83         print object.my_method(object.my_attribute)
84         object.my_attribute.nested_attribute = 'test'
85         # If you need the value of a remote attribute, use .get_remote_value
86         method. This method is automatically called when needed in the context
87         of a remotely executed class. E.g.:
88         if (object.my_attribute.get_remote_value() > 20):
89             object.my_attribute2 = object.my_attribute
90         # Destroy the instance
91         object.quit_remote()
92         object.terminate()
93     """
94
95     GET = 0       # Get attribute remotely
96     CALL = 1      # Call method remotely
97     SETATTR = 2   # Set attribute remotely
98     REPR = 3      # Get representation of a remote object
99     STR = 4       # Get string representation of a remote object
100     QUIT = 5      # Quit remote execution
101
102     PIPE_PARENT = 0  # Parent end of the pipe
103     PIPE_CHILD = 1  # Child end of the pipe
104
105     DEFAULT_TIMEOUT = 2  # default timeout for an operation to execute
106
107     def __init__(self, cls, *args, **kwargs):
108         super(RemoteClass, self).__init__()
109         self._cls = cls
110         self._args = args
111         self._kwargs = kwargs
112         self._timeout = RemoteClass.DEFAULT_TIMEOUT
113         self._pipe = Pipe()  # pipe for input/output arguments
114
115     def __repr__(self):
116         return moves.reprlib.repr(RemoteClassAttr(self, None))
117
118     def __str__(self):
119         return str(RemoteClassAttr(self, None))
120
121     def __call__(self, *args, **kwargs):
122         return self.RemoteClassAttr(self, None)()
123
124     def __getattr__(self, attr):
125         if attr[0] == '_' or not self.is_alive():
126             if not (attr.startswith('__') and attr.endswith('__')):
127                 if hasattr(super(RemoteClass, self), '__getattr__'):
128                     return super(RemoteClass, self).__getattr__(attr)
129                 raise AttributeError('missing: %s', attr)
130         return RemoteClassAttr(self, attr)
131
132     def __setattr__(self, attr, val):
133         if attr[0] == '_' or not self.is_alive():
134             if not (attr.startswith('__') and attr.endswith('__')):
135                 super(RemoteClass, self).__setattr__(attr, val)
136                 return
137         setattr(RemoteClassAttr(self, None), attr, val)
138
139     def _remote_exec(self, op, path=None, *args, **kwargs):
140         """
141         Execute given operation on a given, possibly nested, member remotely.
142         """
143         # automatically resolve remote objects in the arguments
144         mutable_args = list(args)
145         for i, val in enumerate(mutable_args):
146             if isinstance(val, RemoteClass) or \
147                     isinstance(val, RemoteClassAttr):
148                 mutable_args[i] = val.get_remote_value()
149         args = tuple(mutable_args)
150         for key, val in six.iteritems(kwargs):
151             if isinstance(val, RemoteClass) or \
152                     isinstance(val, RemoteClassAttr):
153                 kwargs[key] = val.get_remote_value()
154         # send request
155         args = self._make_serializable(args)
156         kwargs = self._make_serializable(kwargs)
157         self._pipe[RemoteClass.PIPE_PARENT].send((op, path, args, kwargs))
158         timeout = self._timeout
159         # adjust timeout specifically for the .sleep method
160         if path is not None and path.split('.')[-1] == 'sleep':
161             if args and isinstance(args[0], (long, int)):
162                 timeout += args[0]
163             elif 'timeout' in kwargs:
164                 timeout += kwargs['timeout']
165         if not self._pipe[RemoteClass.PIPE_PARENT].poll(timeout):
166             return None
167         try:
168             rv = self._pipe[RemoteClass.PIPE_PARENT].recv()
169             rv = self._deserialize(rv)
170             return rv
171         except EOFError:
172             return None
173
174     def _get_local_object(self, path):
175         """
176         Follow the path to obtain a reference on the addressed nested attribute
177         """
178         obj = self._instance
179         for attr in path:
180             obj = getattr(obj, attr)
181         return obj
182
183     def _get_local_value(self, path):
184         try:
185             return self._get_local_object(path)
186         except AttributeError:
187             return None
188
189     def _call_local_method(self, path, *args, **kwargs):
190         try:
191             method = self._get_local_object(path)
192             return method(*args, **kwargs)
193         except AttributeError:
194             return None
195
196     def _set_local_attr(self, path, value):
197         try:
198             obj = self._get_local_object(path[:-1])
199             setattr(obj, path[-1], value)
200         except AttributeError:
201             pass
202         return None
203
204     def _get_local_repr(self, path):
205         try:
206             obj = self._get_local_object(path)
207             return moves.reprlib.repr(obj)
208         except AttributeError:
209             return None
210
211     def _get_local_str(self, path):
212         try:
213             obj = self._get_local_object(path)
214             return str(obj)
215         except AttributeError:
216             return None
217
218     def _serializable(self, obj):
219         """ Test if the given object is serializable """
220         try:
221             dumps(obj)
222             return True
223         except:
224             return False
225
226     def _make_obj_serializable(self, obj):
227         """
228         Make a serializable copy of an object.
229         Members which are difficult/impossible to serialize are stripped.
230         """
231         if self._serializable(obj):
232             return obj  # already serializable
233
234         copy = SerializableClassCopy()
235
236         """
237         Dictionaries can hold complex values, so we split keys and values into
238         separate lists and serialize them individually.
239         """
240         if (type(obj) is dict):
241             copy.type = type(obj)
242             copy.k_list = list()
243             copy.v_list = list()
244             for k, v in obj.items():
245                 copy.k_list.append(self._make_serializable(k))
246                 copy.v_list.append(self._make_serializable(v))
247             return copy
248
249         # copy at least serializable attributes and properties
250         for name, member in inspect.getmembers(obj):
251             # skip private members and non-writable dunder methods.
252             if name[0] == '_':
253                 if name in ['__weakref__']:
254                     continue
255                 if name in ['__dict__']:
256                     continue
257                 if not (name.startswith('__') and name.endswith('__')):
258                     continue
259             if callable(member) and not isinstance(member, property):
260                 continue
261             if not self._serializable(member):
262                 member = self._make_serializable(member)
263             setattr(copy, name, member)
264         return copy
265
266     def _make_serializable(self, obj):
267         """
268         Make a serializable copy of an object or a list/tuple of objects.
269         Members which are difficult/impossible to serialize are stripped.
270         """
271         if (type(obj) is list) or (type(obj) is tuple):
272             rv = []
273             for item in obj:
274                 rv.append(self._make_serializable(item))
275             if type(obj) is tuple:
276                 rv = tuple(rv)
277             return rv
278         elif (isinstance(obj, IntEnum) or isinstance(obj, IntFlag)):
279             return obj.value
280         else:
281             return self._make_obj_serializable(obj)
282
283     def _deserialize_obj(self, obj):
284         if (hasattr(obj, 'type')):
285             if obj.type is dict:
286                 _obj = dict()
287                 for k, v in zip(obj.k_list, obj.v_list):
288                     _obj[self._deserialize(k)] = self._deserialize(v)
289             return _obj
290         return obj
291
292     def _deserialize(self, obj):
293         if (type(obj) is list) or (type(obj) is tuple):
294             rv = []
295             for item in obj:
296                 rv.append(self._deserialize(item))
297             if type(obj) is tuple:
298                 rv = tuple(rv)
299             return rv
300         else:
301             return self._deserialize_obj(obj)
302
303     def start_remote(self):
304         """ Start remote execution """
305         self.start()
306
307     def quit_remote(self):
308         """ Quit remote execution """
309         self._remote_exec(RemoteClass.QUIT, None)
310
311     def get_remote_value(self):
312         """ Get value of a remotely held object """
313         return RemoteClassAttr(self, None).get_remote_value()
314
315     def set_request_timeout(self, timeout):
316         """ Change request timeout """
317         self._timeout = timeout
318
319     def run(self):
320         """
321         Create instance of the wrapped class and execute operations
322         on it as requested by the parent process.
323         """
324         self._instance = self._cls(*self._args, **self._kwargs)
325         while True:
326             try:
327                 rv = None
328                 # get request from the parent process
329                 (op, path, args,
330                  kwargs) = self._pipe[RemoteClass.PIPE_CHILD].recv()
331                 args = self._deserialize(args)
332                 kwargs = self._deserialize(kwargs)
333                 path = path.split('.') if path else []
334                 if op == RemoteClass.GET:
335                     rv = self._get_local_value(path)
336                 elif op == RemoteClass.CALL:
337                     rv = self._call_local_method(path, *args, **kwargs)
338                 elif op == RemoteClass.SETATTR and 'value' in kwargs:
339                     self._set_local_attr(path, kwargs['value'])
340                 elif op == RemoteClass.REPR:
341                     rv = self._get_local_repr(path)
342                 elif op == RemoteClass.STR:
343                     rv = self._get_local_str(path)
344                 elif op == RemoteClass.QUIT:
345                     break
346                 else:
347                     continue
348                 # send return value
349                 if not self._serializable(rv):
350                     rv = self._make_serializable(rv)
351                 self._pipe[RemoteClass.PIPE_CHILD].send(rv)
352             except EOFError:
353                 break
354         self._instance = None  # destroy the instance
355
356
357 @unittest.skip("Remote Vpp Test Case Class")
358 class RemoteVppTestCase(VppTestCase):
359     """ Re-use VppTestCase to create remote VPP segment
360
361         In your test case:
362
363         @classmethod
364         def setUpClass(cls):
365             # fork new process before client connects to VPP
366             cls.remote_test = RemoteClass(RemoteVppTestCase)
367
368             # start remote process
369             cls.remote_test.start_remote()
370
371             # set up your test case
372             super(MyTestCase, cls).setUpClass()
373
374             # set up remote test
375             cls.remote_test.setUpClass(cls.tempdir)
376
377         @classmethod
378         def tearDownClass(cls):
379             # tear down remote test
380             cls.remote_test.tearDownClass()
381
382             # stop remote process
383             cls.remote_test.quit_remote()
384
385             # tear down your test case
386             super(MyTestCase, cls).tearDownClass()
387     """
388
389     def __init__(self):
390         super(RemoteVppTestCase, self).__init__("emptyTest")
391
392     # Note: __del__ is a 'Finalizer" not a 'Destructor'.
393     # https://docs.python.org/3/reference/datamodel.html#object.__del__
394     def __del__(self):
395         if hasattr(self, "vpp"):
396             self.vpp.poll()
397             if self.vpp.returncode is None:
398                 self.vpp.terminate()
399                 self.vpp.communicate()
400
401     @classmethod
402     def setUpClass(cls, tempdir):
403         # disable features unsupported in remote VPP
404         orig_env = dict(os.environ)
405         if 'STEP' in os.environ:
406             del os.environ['STEP']
407         if 'DEBUG' in os.environ:
408             del os.environ['DEBUG']
409         cls.tempdir_prefix = os.path.basename(tempdir) + "/"
410         super(RemoteVppTestCase, cls).setUpClass()
411         os.environ = orig_env
412
413     @classmethod
414     def tearDownClass(cls):
415         super(RemoteVppTestCase, cls).tearDownClass()
416
417     @unittest.skip("Empty test")
418     def emptyTest(self):
419         """ Do nothing """
420         pass
421
422     def setTestFunctionInfo(self, name, doc):
423         """
424         Store the name and documentation string of currently executed test
425         in the main VPP for logging purposes.
426         """
427         self._testMethodName = name
428         self._testMethodDoc = doc