test_ssdp.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: :email:`Bo Maryniuk <bo@suse.de>`
  4. '''
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import datetime
  7. from tests.support.unit import TestCase, skipIf
  8. from tests.support.mock import (
  9. NO_MOCK,
  10. NO_MOCK_REASON,
  11. MagicMock,
  12. patch)
  13. from salt.ext.six.moves import zip
  14. from salt.ext import six
  15. import salt.utils.ssdp as ssdp
  16. import salt.utils.stringutils
  17. try:
  18. import pytest
  19. except ImportError:
  20. pytest = None
  21. class Mocks(object):
  22. def get_socket_mock(self, expected_ip, expected_hostname):
  23. '''
  24. Get a mock of a socket
  25. :return:
  26. '''
  27. sck = MagicMock()
  28. sck.getsockname = MagicMock(return_value=(expected_ip, 123456))
  29. sock_mock = MagicMock()
  30. sock_mock.socket = MagicMock(return_value=sck)
  31. sock_mock.gethostbyname = MagicMock(return_value=expected_hostname)
  32. return sock_mock
  33. @skipIf(NO_MOCK, NO_MOCK_REASON)
  34. @skipIf(pytest is None, 'PyTest is missing')
  35. class SSDPBaseTestCase(TestCase, Mocks):
  36. '''
  37. TestCase for SSDP-related parts.
  38. '''
  39. @staticmethod
  40. def exception_generic(*args, **kwargs):
  41. '''
  42. Side effect
  43. :return:
  44. '''
  45. raise Exception('some network error')
  46. @staticmethod
  47. def exception_attr_error(*args, **kwargs):
  48. '''
  49. Side effect
  50. :return:
  51. '''
  52. raise AttributeError('attribute error: {0}. {1}'.format(args, kwargs))
  53. @patch('salt.utils.ssdp._json', None)
  54. @patch('salt.utils.ssdp.asyncio', None)
  55. def test_base_avail(self):
  56. '''
  57. Test SSDP base class availability method.
  58. :return:
  59. '''
  60. base = ssdp.SSDPBase()
  61. assert not base._is_available()
  62. with patch('salt.utils.ssdp._json', True):
  63. assert not base._is_available()
  64. with patch('salt.utils.ssdp.asyncio', True):
  65. assert not base._is_available()
  66. with patch('salt.utils.ssdp._json', True), patch('salt.utils.ssdp.asyncio', True):
  67. assert base._is_available()
  68. def test_base_protocol_settings(self):
  69. '''
  70. Tests default constants data.
  71. :return:
  72. '''
  73. base = ssdp.SSDPBase()
  74. v_keys = ['signature', 'answer', 'port', 'listen_ip', 'timeout']
  75. v_vals = ['__salt_master_service', {}, 4520, '0.0.0.0', 3]
  76. for key in v_keys:
  77. assert key in base.DEFAULTS
  78. for key in base.DEFAULTS:
  79. assert key in v_keys
  80. for key, value in zip(v_keys, v_vals):
  81. assert base.DEFAULTS[key] == value
  82. def test_base_self_ip(self):
  83. '''
  84. Test getting self IP method.
  85. :return:
  86. '''
  87. base = ssdp.SSDPBase()
  88. expected_ip = '192.168.1.10'
  89. expected_host = 'oxygen'
  90. sock_mock = self.get_socket_mock(expected_ip, expected_host)
  91. with patch('salt.utils.ssdp.socket', sock_mock):
  92. assert base.get_self_ip() == expected_ip
  93. sock_mock.socket().getsockname.side_effect = SSDPBaseTestCase.exception_generic
  94. with patch('salt.utils.ssdp.socket', sock_mock):
  95. assert base.get_self_ip() == expected_host
  96. @skipIf(NO_MOCK, NO_MOCK_REASON)
  97. @skipIf(pytest is None, 'PyTest is missing')
  98. class SSDPFactoryTestCase(TestCase):
  99. '''
  100. Test socket protocol
  101. '''
  102. @patch('salt.utils.ssdp.socket.gethostbyname', MagicMock(return_value='10.10.10.10'))
  103. def test_attr_check(self):
  104. '''
  105. Tests attributes are set to the base class
  106. :return:
  107. '''
  108. config = {
  109. ssdp.SSDPBase.SIGNATURE: '-signature-',
  110. ssdp.SSDPBase.ANSWER: {'this-is': 'the-answer'}
  111. }
  112. factory = ssdp.SSDPFactory(**config)
  113. for attr in [ssdp.SSDPBase.SIGNATURE, ssdp.SSDPBase.ANSWER]:
  114. assert hasattr(factory, attr)
  115. assert getattr(factory, attr) == config[attr]
  116. assert not factory.disable_hidden
  117. assert factory.my_ip == '10.10.10.10'
  118. def test_transport_sendto_success(self):
  119. '''
  120. Test transport send_to.
  121. :return:
  122. '''
  123. transport = MagicMock()
  124. log = MagicMock()
  125. factory = ssdp.SSDPFactory()
  126. with patch.object(factory, 'transport', transport), patch.object(factory, 'log', log):
  127. data = {'some': 'data'}
  128. addr = '10.10.10.10'
  129. factory._sendto(data=data, addr=addr)
  130. assert factory.transport.sendto.called
  131. assert factory.transport.sendto.mock_calls[0][1][0]['some'] == 'data'
  132. assert factory.transport.sendto.mock_calls[0][2]['addr'] == '10.10.10.10'
  133. assert factory.log.debug.called
  134. assert factory.log.debug.mock_calls[0][1][0] == 'Sent successfully'
  135. @patch('salt.utils.ssdp.time.sleep', MagicMock())
  136. def test_transport_sendto_retry(self):
  137. '''
  138. Test transport send_to.
  139. :return:
  140. '''
  141. transport = MagicMock()
  142. transport.sendto = MagicMock(side_effect=SSDPBaseTestCase.exception_attr_error)
  143. log = MagicMock()
  144. factory = ssdp.SSDPFactory()
  145. with patch.object(factory, 'transport', transport), patch.object(factory, 'log', log):
  146. data = {'some': 'data'}
  147. addr = '10.10.10.10'
  148. factory._sendto(data=data, addr=addr)
  149. assert factory.transport.sendto.called
  150. assert ssdp.time.sleep.called
  151. assert ssdp.time.sleep.call_args[0][0] > 0 and ssdp.time.sleep.call_args[0][0] < 0.5
  152. assert factory.log.debug.called
  153. assert 'Permission error' in factory.log.debug.mock_calls[0][1][0]
  154. def test_datagram_signature_bad(self):
  155. '''
  156. Test datagram_received on bad signature
  157. :return:
  158. '''
  159. factory = ssdp.SSDPFactory()
  160. data = 'nonsense'
  161. addr = '10.10.10.10', 'foo.suse.de'
  162. with patch.object(factory, 'log', MagicMock()):
  163. factory.datagram_received(data=data, addr=addr)
  164. assert factory.log.debug.called
  165. assert 'Received bad signature from' in factory.log.debug.call_args[0][0]
  166. assert factory.log.debug.call_args[0][1] == addr[0]
  167. assert factory.log.debug.call_args[0][2] == addr[1]
  168. def test_datagram_signature_wrong_timestamp_quiet(self):
  169. '''
  170. Test datagram receives a wrong timestamp (no reply).
  171. :return:
  172. '''
  173. factory = ssdp.SSDPFactory()
  174. data = '{}nonsense'.format(ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE])
  175. addr = '10.10.10.10', 'foo.suse.de'
  176. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto', MagicMock()):
  177. factory.datagram_received(data=data, addr=addr)
  178. assert factory.log.debug.called
  179. assert 'Received invalid timestamp in package' in factory.log.debug.call_args[0][0]
  180. assert not factory._sendto.called
  181. def test_datagram_signature_wrong_timestamp_reply(self):
  182. '''
  183. Test datagram receives a wrong timestamp.
  184. :return:
  185. '''
  186. factory = ssdp.SSDPFactory()
  187. factory.disable_hidden = True
  188. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  189. data = '{}nonsense'.format(signature)
  190. addr = '10.10.10.10', 'foo.suse.de'
  191. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto', MagicMock()):
  192. factory.datagram_received(data=data, addr=addr)
  193. assert factory.log.debug.called
  194. assert 'Received invalid timestamp in package' in factory.log.debug.call_args[0][0]
  195. assert factory._sendto.called
  196. assert '{}:E:Invalid timestamp'.format(signature) == factory._sendto.call_args[0][0]
  197. def test_datagram_signature_outdated_timestamp_quiet(self):
  198. '''
  199. Test if datagram processing reacts on outdated message (more than 20 seconds). Quiet mode.
  200. :return:
  201. '''
  202. factory = ssdp.SSDPFactory()
  203. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  204. data = '{}{}'.format(signature, '1516623820')
  205. addr = '10.10.10.10', 'foo.suse.de'
  206. ahead_dt = datetime.datetime.fromtimestamp(1516623841)
  207. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  208. delta = datetime.timedelta(0, 20)
  209. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  210. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  211. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  212. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  213. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  214. factory.datagram_received(data=data, addr=addr)
  215. assert factory.log.debug.called
  216. assert not factory.disable_hidden
  217. assert not factory._sendto.called
  218. assert 'Received outdated package' in factory.log.debug.call_args[0][0]
  219. def test_datagram_signature_outdated_timestamp_reply(self):
  220. '''
  221. Test if datagram processing reacts on outdated message (more than 20 seconds). Reply mode.
  222. :return:
  223. '''
  224. factory = ssdp.SSDPFactory()
  225. factory.disable_hidden = True
  226. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  227. data = '{}{}'.format(signature, '1516623820')
  228. addr = '10.10.10.10', 'foo.suse.de'
  229. ahead_dt = datetime.datetime.fromtimestamp(1516623841)
  230. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  231. delta = datetime.timedelta(0, 20)
  232. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  233. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  234. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  235. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  236. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  237. factory.datagram_received(data=data, addr=addr)
  238. assert factory.log.debug.called
  239. assert factory.disable_hidden
  240. assert factory._sendto.called
  241. assert factory._sendto.call_args[0][0] == '{}:E:Timestamp is too old'.format(signature)
  242. assert 'Received outdated package' in factory.log.debug.call_args[0][0]
  243. def test_datagram_signature_correct_timestamp_reply(self):
  244. '''
  245. Test if datagram processing sends out correct reply within 20 seconds.
  246. :return:
  247. '''
  248. factory = ssdp.SSDPFactory()
  249. factory.disable_hidden = True
  250. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  251. data = '{}{}'.format(signature, '1516623820')
  252. addr = '10.10.10.10', 'foo.suse.de'
  253. ahead_dt = datetime.datetime.fromtimestamp(1516623840)
  254. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  255. delta = datetime.timedelta(0, 20)
  256. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  257. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  258. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  259. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  260. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  261. factory.datagram_received(data=data, addr=addr)
  262. assert factory.log.debug.called
  263. assert factory.disable_hidden
  264. assert factory._sendto.called
  265. assert factory._sendto.call_args[0][0] == salt.utils.stringutils.to_bytes("{}:@:{{}}".format(signature))
  266. assert 'Received "%s" from %s:%s' in factory.log.debug.call_args[0][0]
  267. @skipIf(NO_MOCK, NO_MOCK_REASON)
  268. @skipIf(pytest is None, 'PyTest is missing')
  269. class SSDPServerTestCase(TestCase):
  270. '''
  271. Server-related test cases
  272. '''
  273. def test_config_detached(self):
  274. '''
  275. Test if configuration is not a reference.
  276. :return:
  277. '''
  278. old_ip = '10.10.10.10'
  279. new_ip = '20.20.20.20'
  280. config = {'answer': {'master': old_ip}}
  281. with patch('salt.utils.ssdp.SSDPDiscoveryServer.get_self_ip', MagicMock(return_value=new_ip)):
  282. srv = ssdp.SSDPDiscoveryServer(**config)
  283. assert srv._config['answer']['master'] == new_ip
  284. assert config['answer']['master'] == old_ip
  285. @patch('salt.utils.ssdp.SSDPFactory', MagicMock())
  286. def test_run(self):
  287. '''
  288. Test server runner.
  289. :return:
  290. '''
  291. config = {'answer': {'master': '10.10.10.10'},
  292. ssdp.SSDPBase.LISTEN_IP: '10.10.10.10',
  293. ssdp.SSDPBase.PORT: 12345}
  294. srv = ssdp.SSDPDiscoveryServer(**config)
  295. srv.create_datagram_endpoint = MagicMock()
  296. srv.log = MagicMock()
  297. trnsp = MagicMock()
  298. proto = MagicMock()
  299. loop = MagicMock()
  300. loop.run_until_complete = MagicMock(return_value=(trnsp, proto))
  301. io = MagicMock()
  302. io.ported = False
  303. io.get_event_loop = MagicMock(return_value=loop)
  304. with patch('salt.utils.ssdp.asyncio', io):
  305. srv.run()
  306. cde_args = io.get_event_loop().create_datagram_endpoint.call_args[1]
  307. cfg_ip_addr, cfg_port = cde_args['local_addr']
  308. assert io.get_event_loop.called
  309. assert io.get_event_loop().run_until_complete.called
  310. assert io.get_event_loop().create_datagram_endpoint.called
  311. assert io.get_event_loop().run_forever.called
  312. assert trnsp.close.called
  313. assert loop.close.called
  314. assert srv.log.info.called
  315. assert srv.log.info.call_args[0][0] == 'Stopping service discovery listener.'
  316. assert 'allow_broadcast' in cde_args
  317. assert cde_args['allow_broadcast']
  318. assert 'local_addr' in cde_args
  319. assert not cfg_ip_addr == ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.LISTEN_IP] and cfg_ip_addr == '10.10.10.10'
  320. assert not cfg_port == ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.PORT] and cfg_port == 12345
  321. @skipIf(NO_MOCK, NO_MOCK_REASON)
  322. @skipIf(pytest is None, 'PyTest is missing')
  323. class SSDPClientTestCase(TestCase):
  324. '''
  325. Client-related test cases
  326. '''
  327. class Resource(object):
  328. '''
  329. Fake network reader
  330. '''
  331. def __init__(self):
  332. self.pool = [('some', '10.10.10.10'),
  333. ('data', '20.20.20.20'),
  334. ('data', '10.10.10.10'),
  335. (None, None)]
  336. def read(self, *args, **kwargs):
  337. return self.pool.pop(0)
  338. def test_config_passed(self):
  339. '''
  340. Test if the configuration is passed.
  341. :return:
  342. '''
  343. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server',
  344. ssdp.SSDPBase.TIMEOUT: 5, ssdp.SSDPBase.PORT: 12345}
  345. clnt = ssdp.SSDPDiscoveryClient(**config)
  346. assert clnt._config[ssdp.SSDPBase.SIGNATURE] == config[ssdp.SSDPBase.SIGNATURE]
  347. assert clnt._config[ssdp.SSDPBase.PORT] == config[ssdp.SSDPBase.PORT]
  348. assert clnt._config[ssdp.SSDPBase.TIMEOUT] == config[ssdp.SSDPBase.TIMEOUT]
  349. def test_config_detached(self):
  350. '''
  351. Test if the passed configuration is not a reference.
  352. :return:
  353. '''
  354. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server', }
  355. clnt = ssdp.SSDPDiscoveryClient(**config)
  356. clnt._config['foo'] = 'bar'
  357. assert 'foo' in clnt._config
  358. assert 'foo' not in config
  359. def test_query(self):
  360. '''
  361. Test if client queries the broadcast
  362. :return:
  363. '''
  364. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server',
  365. ssdp.SSDPBase.PORT: 4000}
  366. f_time = 1111
  367. _socket = MagicMock()
  368. with patch('salt.utils.ssdp.socket', _socket),\
  369. patch('salt.utils.ssdp.time.time', MagicMock(return_value=f_time)):
  370. clnt = ssdp.SSDPDiscoveryClient(**config)
  371. clnt._query()
  372. assert clnt._socket.sendto.called
  373. message, target = clnt._socket.sendto.call_args[0]
  374. assert message == salt.utils.stringutils.to_bytes(
  375. '{}{}'.format(config[ssdp.SSDPBase.SIGNATURE], f_time)
  376. )
  377. assert target[0] == '<broadcast>'
  378. assert target[1] == config[ssdp.SSDPBase.PORT]
  379. def test_get_masters_map(self):
  380. '''
  381. Test getting map of the available masters on the network
  382. :return:
  383. '''
  384. _socket = MagicMock()
  385. response = {}
  386. with patch('salt.utils.ssdp.socket', _socket):
  387. clnt = ssdp.SSDPDiscoveryClient()
  388. clnt._socket.recvfrom = SSDPClientTestCase.Resource().read
  389. clnt.log = MagicMock()
  390. clnt._collect_masters_map(response=response)
  391. assert '10.10.10.10' in response
  392. assert '20.20.20.20' in response
  393. assert response['10.10.10.10'] == ['some', 'data']
  394. assert response['20.20.20.20'] == ['data']
  395. def test_get_masters_map_error_handling(self):
  396. '''
  397. Test getting map handles timeout network exception
  398. :return:
  399. '''
  400. _socket = MagicMock()
  401. response = {}
  402. error_msg = 'fake testing timeout just had happened'
  403. with patch('salt.utils.ssdp.socket', _socket):
  404. clnt = ssdp.SSDPDiscoveryClient()
  405. clnt._socket.recvfrom = MagicMock(side_effect=Exception(error_msg))
  406. clnt.log = MagicMock()
  407. clnt._collect_masters_map(response=response)
  408. assert clnt.log.error.called
  409. assert 'Discovery master collection failure' in clnt.log.error.call_args[0][0]
  410. assert error_msg == six.text_type(clnt.log.error.call_args[0][1])
  411. assert not response
  412. def test_discover_no_masters(self):
  413. '''
  414. Test discover available master on the network (none found).
  415. :return:
  416. '''
  417. clnt = ssdp.SSDPDiscoveryClient()
  418. clnt._query = MagicMock()
  419. clnt._collect_masters_map = MagicMock()
  420. clnt.log = MagicMock()
  421. clnt.discover()
  422. assert clnt.log.info.called
  423. assert clnt.log.info.call_args[0][0] == 'No master has been discovered.'
  424. def test_discover_general_error(self):
  425. '''
  426. Test discover available master on the network (erroneous found)
  427. :return:
  428. '''
  429. _socket = MagicMock()
  430. error = 'Admins on strike due to broken coffee machine'
  431. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  432. fake_resource = SSDPClientTestCase.Resource()
  433. fake_resource.pool = [('{}:E:{}'.format(signature, error), '10.10.10.10'),
  434. (None, None)]
  435. with patch('salt.utils.ssdp.socket', _socket):
  436. clnt = ssdp.SSDPDiscoveryClient()
  437. clnt._socket.recvfrom = fake_resource.read
  438. clnt._query = MagicMock()
  439. clnt.log = MagicMock()
  440. clnt.discover()
  441. assert len(clnt.log.error.mock_calls) == 1
  442. assert 'Error response from the service publisher' in clnt.log.error.call_args[0][0]
  443. assert '10.10.10.10' == clnt.log.error.call_args[0][1]
  444. assert clnt.log.error.call_args[1] == {}
  445. assert clnt.log.error.call_args[0][2] == error
  446. def test_discover_timestamp_error(self):
  447. '''
  448. Test discover available master on the network (outdated timestamp)
  449. :return:
  450. '''
  451. _socket = MagicMock()
  452. error = 'We only support a 1200 bps connection. Routing timestamp problems on neural net.'
  453. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  454. fake_resource = SSDPClientTestCase.Resource()
  455. fake_resource.pool = [('{}:E:{}'.format(signature, error), '10.10.10.10'),
  456. (None, None)]
  457. with patch('salt.utils.ssdp.socket', _socket):
  458. clnt = ssdp.SSDPDiscoveryClient()
  459. clnt._socket.recvfrom = fake_resource.read
  460. clnt._query = MagicMock()
  461. clnt.log = MagicMock()
  462. clnt.discover()
  463. assert len(clnt.log.error.mock_calls) == 2
  464. assert 'Error response from the service publisher' in clnt.log.error.mock_calls[0][1][0]
  465. assert clnt.log.error.mock_calls[0][1][2] == error
  466. assert clnt.log.error.mock_calls[0][2] == {}
  467. assert 'Publisher sent shifted timestamp' in clnt.log.error.mock_calls[1][1][0]
  468. assert clnt.log.error.mock_calls[1][1][1] == clnt.log.error.mock_calls[0][1][1] == '10.10.10.10'