python/blackbox: add rpcd_witness_samba_only.py test
[samba.git] / python / samba / tests / blackbox / rpcd_witness_samba_only.py
1 #!/usr/bin/env python3
2 # Unix SMB/CIFS implementation.
3 #
4 # Copyright © 2024 Stefan Metzmacher <metze@samba.org>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import sys
21 import os
22
23 sys.path.insert(0, "bin/python")
24 os.environ["PYTHONUNBUFFERED"] = "1"
25
26 import json
27
28 import samba.tests
29 from samba.credentials import Credentials
30 from samba.ndr import ndr_print
31 from samba.dcerpc import witness
32 from samba.tests import DynamicTestCase, BlackboxTestCase
33 from samba.common import get_string
34 from samba import werror, WERRORError
35
36 @DynamicTestCase
37 class RpcdWitnessSambaTests(BlackboxTestCase):
38     @classmethod
39     def setUpDynamicTestCases(cls):
40         cls.num_nodes = int(samba.tests.env_get_var_value('NUM_NODES'))
41
42         def _define_tests(idx1, idx2, ndr64=False):
43             cls._define_GetInterfaceList_test(idx1, idx2, ndr64)
44             if idx1 == 0 and idx2 != -1:
45                 cls._define_ResourceChangeCTDB_tests(idx1, idx2, ndr64)
46
47         for idx1 in range(0, cls.num_nodes):
48             _define_tests(idx1, -1, ndr64=False)
49             _define_tests(idx1, -1, ndr64=True)
50             for idx2 in range(0, cls.num_nodes):
51                 _define_tests(idx1, idx2, ndr64=False)
52                 _define_tests(idx1, idx2, ndr64=True)
53
54     def setUp(self):
55         super().setUp()
56
57         # ctdb/tests/local_daemons.sh doesn't like CTDB_SOCKET to be set already
58         # and it doesn't need CTDB_BASE, so we stash them away
59         self.saved_CTDB_SOCKET = samba.tests.env_get_var_value('CTDB_SOCKET',
60                                                                allow_missing=True)
61         if self.saved_CTDB_SOCKET is not None:
62             del os.environ["CTDB_SOCKET"]
63         self.saved_CTDB_BASE = samba.tests.env_get_var_value('CTDB_BASE',
64                                                              allow_missing=True)
65         if self.saved_CTDB_BASE is not None:
66             del os.environ["CTDB_BASE"]
67
68         self.disabled_idx = -1
69
70         # set this to True in order to get verbose output
71         self.verbose = False
72
73         self.ctdb_prefix = samba.tests.env_get_var_value('CTDB_PREFIX')
74
75         self.cluster_share = samba.tests.env_get_var_value('CLUSTER_SHARE')
76
77         self.lp = self.get_loadparm(s3=True)
78         self.remote_domain = samba.tests.env_get_var_value('DOMAIN')
79         self.remote_user = samba.tests.env_get_var_value('USERNAME')
80         self.remote_password = samba.tests.env_get_var_value('PASSWORD')
81         self.remote_creds = Credentials()
82         self.remote_creds.guess(self.lp)
83         self.remote_creds.set_username(self.remote_user)
84         self.remote_creds.set_domain(self.remote_domain)
85         self.remote_creds.set_password(self.remote_password)
86
87         self.server_hostname = samba.tests.env_get_var_value('SERVER_HOSTNAME')
88         self.interface_group_name = samba.tests.env_get_var_value('INTERFACE_GROUP_NAME')
89
90         common_binding_args = "spnego,sign,target_hostname=%s" % (
91             self.server_hostname)
92         if self.verbose:
93             common_binding_args += ",print"
94
95         common_binding_args32 = common_binding_args
96         common_binding_args64 = common_binding_args + ",ndr64"
97
98         self.nodes = []
99         for node_idx in range(0, self.num_nodes):
100             node = {}
101
102             name_var = 'CTDB_SERVER_NAME_NODE%u' % node_idx
103             node["name"] = samba.tests.env_get_var_value(name_var)
104
105             ip_var = 'CTDB_IFACE_IP_NODE%u' % node_idx
106             node["ip"] = samba.tests.env_get_var_value(ip_var)
107
108             node["binding_string32"] = "ncacn_ip_tcp:%s[%s]" % (
109                     node["ip"], common_binding_args32)
110             node["binding_string64"] = "ncacn_ip_tcp:%s[%s]" % (
111                     node["ip"], common_binding_args64)
112             self.nodes.append(node)
113
114     def tearDown(self):
115         if self.disabled_idx != -1:
116             self.enable_node(self.disabled_idx)
117
118         if self.saved_CTDB_SOCKET is not None:
119             os.environ["CTDB_SOCKET"] = self.saved_CTDB_SOCKET
120             self.saved_CTDB_SOCKET = None
121         if self.saved_CTDB_BASE is not None:
122             os.environ["CTDB_BASE"] = self.saved_CTDB_BASE
123             self.saved_CTDB_BASE = None
124
125         super().tearDown()
126
127     def call_onnode(self, nodes, cmd):
128         COMMAND = "ctdb/tests/local_daemons.sh"
129
130         argv = "%s '%s' onnode %s '%s'" % (COMMAND, self.ctdb_prefix, nodes, cmd)
131
132         try:
133             if self.verbose:
134                 print("Calling: %s" % argv)
135             out = self.check_output(argv)
136         except samba.tests.BlackboxProcessError as e:
137             self.fail("Error calling [%s]: %s" % (argv, e))
138
139         out_str = get_string(out)
140         return out_str
141
142     def dump_ctdb_status_all(self):
143         for node_idx in range(0, self.num_nodes):
144             print("%s" % self.call_onnode(str(node_idx), "ctdb status"))
145
146     def disable_node(self, node_idx, dump_status=False):
147         if dump_status:
148             self.dump_ctdb_status_all()
149
150         self.assertEqual(self.disabled_idx, -1)
151         self.call_onnode(str(node_idx), "ctdb disable")
152         self.disabled_idx = node_idx
153
154         if dump_status:
155             self.dump_ctdb_status_all()
156
157     def enable_node(self, node_idx, dump_status=False):
158         if dump_status:
159             self.dump_ctdb_status_all()
160
161         self.assertEqual(self.disabled_idx, node_idx)
162         self.call_onnode(str(node_idx), "ctdb enable")
163         self.disabled_idx = -1
164
165         if dump_status:
166             self.dump_ctdb_status_all()
167
168     @classmethod
169     def _define_GetInterfaceList_test(cls, conn_idx, disable_idx, ndr64=False):
170         if disable_idx != -1:
171             disable_name = "%u_disabled" % disable_idx
172         else:
173             disable_name = "all_enabled"
174
175         if ndr64:
176             ndr_name = "NDR64"
177         else:
178             ndr_name = "NDR32"
179
180         name = "Node%u_%s_%s" % (conn_idx, disable_name, ndr_name)
181         args = {
182             'conn_idx': conn_idx,
183             'disable_idx': disable_idx,
184             'ndr64': ndr64,
185         }
186         cls.generate_dynamic_test('test_GetInterfaceList', name, args)
187
188     def _test_GetInterfaceList_with_args(self, args):
189         conn_idx = args.pop('conn_idx')
190         disable_idx = args.pop('disable_idx')
191         ndr64 = args.pop('ndr64')
192         self.assertEqual(len(args.keys()), 0)
193
194         conn_node = self.nodes[conn_idx]
195         if ndr64:
196             binding_string = conn_node["binding_string64"]
197         else:
198             binding_string = conn_node["binding_string32"]
199
200         if disable_idx != -1:
201             self.disable_node(disable_idx)
202
203         conn = witness.witness(binding_string, self.lp, self.remote_creds)
204         interface_list = conn.GetInterfaceList()
205
206         if disable_idx != -1:
207             self.enable_node(disable_idx)
208
209         self.assertIsNotNone(interface_list)
210         self.assertEqual(interface_list.num_interfaces, len(self.nodes))
211         for idx in range(0, interface_list.num_interfaces):
212             iface = interface_list.interfaces[idx]
213             node = self.nodes[idx]
214
215             expected_flags = 0
216             expected_flags |= witness.WITNESS_INFO_IPv4_VALID
217             if conn_idx != idx:
218                 expected_flags |= witness.WITNESS_INFO_WITNESS_IF
219
220             if disable_idx == idx:
221                 expected_state = witness.WITNESS_STATE_UNAVAILABLE
222             else:
223                 expected_state = witness.WITNESS_STATE_AVAILABLE
224
225             self.assertIsNotNone(iface.group_name)
226             self.assertEqual(iface.group_name, self.interface_group_name)
227
228             self.assertEqual(iface.version, witness.WITNESS_V2)
229             self.assertEqual(iface.state, expected_state)
230
231             self.assertIsNotNone(iface.ipv4)
232             self.assertEqual(iface.ipv4, node["ip"])
233
234             self.assertIsNotNone(iface.ipv6)
235             self.assertEqual(iface.ipv6,
236                     "0000:0000:0000:0000:0000:0000:0000:0000")
237
238             self.assertEqual(iface.flags, expected_flags)
239
240     def assertResourceChanges(self, response, expected_resource_changes):
241         self.assertIsNotNone(response)
242         self.assertEqual(response.type,
243                 witness.WITNESS_NOTIFY_RESOURCE_CHANGE)
244         self.assertEqual(response.num, len(expected_resource_changes))
245         self.assertEqual(len(response.messages), len(expected_resource_changes))
246         for ri in range(0, len(expected_resource_changes)):
247             expected_resource_change = expected_resource_changes[ri]
248             resource_change = response.messages[ri]
249             self.assertIsNotNone(resource_change)
250
251             expected_type = witness.WITNESS_RESOURCE_STATE_UNAVAILABLE
252             expected_type = expected_resource_change.get('type', expected_type)
253
254             expected_name = expected_resource_change.get('name')
255
256             self.assertEqual(resource_change.type, expected_type)
257             self.assertIsNotNone(resource_change.name)
258             self.assertEqual(resource_change.name, expected_name)
259
260     def assertResourceChange(self, response, expected_type, expected_name):
261         expected_resource_change = {
262             'type': expected_type,
263             'name': expected_name,
264         }
265         expected_resource_changes = [expected_resource_change]
266         self.assertResourceChanges(response, expected_resource_changes)
267
268     @classmethod
269     def _define_ResourceChangeCTDB_tests(cls, conn_idx, monitor_idx, ndr64=False):
270         if ndr64:
271             ndr_name = "NDR64"
272         else:
273             ndr_name = "NDR32"
274
275         name_suffix = "WNode%u_RNode%u_%s" % (conn_idx, monitor_idx, ndr_name)
276         base_args = {
277             'conn_idx': conn_idx,
278             'monitor_idx': monitor_idx,
279             'ndr64': ndr64,
280         }
281
282         name = "v1_disabled_after_%s" % name_suffix
283         args = base_args.copy()
284         args['reg_v1'] = True
285         args['disable_after_reg'] = True
286         args['explicit_unregister'] = False
287         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
288
289         name = "v1_disabled_after_enabled_after_%s" % name_suffix
290         args = base_args.copy()
291         args['reg_v1'] = True
292         args['disable_after_reg'] = True
293         args['enable_after_reg'] = True
294         args['explicit_unregister'] = False
295         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
296
297         name = "v2_disabled_before_enable_after_%s" % name_suffix
298         args = base_args.copy()
299         args['disable_before_reg'] = True
300         args['enable_after_reg'] = True
301         args['wait_for_timeout'] = True
302         args['timeout'] = 6
303         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
304
305         name = "v2_disabled_after_%s" % name_suffix
306         args = base_args.copy()
307         args['disable_after_reg'] = True
308         args['wait_for_not_found'] = True
309         args['explicit_unregister'] = False
310         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
311
312         name = "v2_disabled_after_enabled_after_%s" % name_suffix
313         args = base_args.copy()
314         args['disable_after_reg'] = True
315         args['enable_after_reg'] = True
316         args['wait_for_not_found'] = True
317         args['explicit_unregister'] = False
318         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
319
320         name = "share_v2_disabled_before_enable_after_%s" % name_suffix
321         args = base_args.copy()
322         args['share_reg'] = True
323         args['disable_before_reg'] = True
324         args['enable_after_reg'] = True
325         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
326
327         name = "share_v2_disabled_after_%s" % name_suffix
328         args = base_args.copy()
329         args['share_reg'] = True
330         args['disable_after_reg'] = True
331         args['explicit_unregister'] = False
332         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
333
334         name = "share_v2_disabled_after_enabled_after_%s" % name_suffix
335         args = base_args.copy()
336         args['share_reg'] = True
337         args['disable_after_reg'] = True
338         args['enable_after_reg'] = True
339         args['explicit_unregister'] = False
340         cls.generate_dynamic_test('test_ResourceChangeCTDB', name, args)
341
342     def _test_ResourceChangeCTDB_with_args(self, args):
343         conn_idx = args.pop('conn_idx')
344         monitor_idx = args.pop('monitor_idx')
345         ndr64 = args.pop('ndr64')
346         timeout = int(args.pop('timeout', 15))
347         reg_v1 = args.pop('reg_v1', False)
348         share_reg = args.pop('share_reg', False)
349         disable_before_reg = args.pop('disable_before_reg', False)
350         disable_after_reg = args.pop('disable_after_reg', False)
351         enable_after_reg = args.pop('enable_after_reg', False)
352         explicit_unregister = args.pop('explicit_unregister', True)
353         wait_for_not_found = args.pop('wait_for_not_found', False)
354         wait_for_timeout = args.pop('wait_for_timeout', False)
355         self.assertEqual(len(args.keys()), 0)
356
357         conn_node = self.nodes[conn_idx]
358         if ndr64:
359             binding_string = conn_node["binding_string64"]
360         else:
361             binding_string = conn_node["binding_string32"]
362         monitor_node = self.nodes[monitor_idx]
363
364         computer_name = "test-rpcd-witness-samba-only-client-computer"
365
366         conn = witness.witness(binding_string, self.lp, self.remote_creds)
367
368         if disable_before_reg:
369             self.assertFalse(disable_after_reg)
370             self.disable_node(monitor_idx)
371
372         if reg_v1:
373             self.assertFalse(wait_for_timeout)
374             self.assertFalse(share_reg)
375
376             reg_context = conn.Register(witness.WITNESS_V1,
377                                         self.server_hostname,
378                                         monitor_node["ip"],
379                                         computer_name)
380         else:
381             if share_reg:
382                 share_name = self.cluster_share
383             else:
384                 share_name = None
385
386             reg_context = conn.RegisterEx(witness.WITNESS_V2,
387                                           self.server_hostname,
388                                           share_name,
389                                           monitor_node["ip"],
390                                           computer_name,
391                                           witness.WITNESS_REGISTER_NONE,
392                                           timeout)
393
394         if disable_after_reg:
395             self.assertFalse(disable_before_reg)
396             self.disable_node(monitor_idx)
397
398         if enable_after_reg:
399             self.enable_node(monitor_idx)
400
401         if disable_after_reg:
402             response_unavailable = conn.AsyncNotify(reg_context)
403             self.assertResourceChange(response_unavailable,
404                                       witness.WITNESS_RESOURCE_STATE_UNAVAILABLE,
405                                       monitor_node["ip"])
406
407         if enable_after_reg:
408             response_available = conn.AsyncNotify(reg_context)
409             self.assertResourceChange(response_available,
410                                       witness.WITNESS_RESOURCE_STATE_AVAILABLE,
411                                       monitor_node["ip"])
412
413         if wait_for_timeout:
414             self.assertFalse(wait_for_not_found)
415             self.assertFalse(disable_after_reg)
416             try:
417                 _ = conn.AsyncNotify(reg_context)
418                 self.fail()
419             except WERRORError as e:
420                 (num, string) = e.args
421                 if num != werror.WERR_TIMEOUT:
422                     raise
423
424         if wait_for_not_found:
425             self.assertFalse(wait_for_timeout)
426             self.assertTrue(disable_after_reg)
427             self.assertFalse(explicit_unregister)
428             try:
429                 _ = conn.AsyncNotify(reg_context)
430                 self.fail()
431             except WERRORError as e:
432                 (num, string) = e.args
433                 if num != werror.WERR_NOT_FOUND:
434                     raise
435
436         if not explicit_unregister:
437             return
438
439         conn.UnRegister(reg_context)
440
441         try:
442             _ = conn.AsyncNotify(reg_context)
443             self.fail()
444         except WERRORError as e:
445             (num, string) = e.args
446             if num != werror.WERR_NOT_FOUND:
447                 raise
448
449         try:
450             conn.UnRegister(reg_context)
451             self.fail()
452         except WERRORError as e:
453             (num, string) = e.args
454             if num != werror.WERR_NOT_FOUND:
455                 raise
456
457 if __name__ == "__main__":
458     import unittest
459     unittest.main()