test_docker_network.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Integration tests for the docker_network states
  4. '''
  5. # Import Python Libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import errno
  8. import functools
  9. import logging
  10. import os
  11. import subprocess
  12. import tempfile
  13. import pytest
  14. # Import Salt Testing Libs
  15. from tests.support.runtime import RUNTIME_VARS
  16. from tests.support.unit import skipIf
  17. from tests.support.case import ModuleCase
  18. from tests.support.docker import with_network, random_name
  19. from tests.support.helpers import requires_system_grains
  20. from tests.support.mixins import SaltReturnAssertsMixin
  21. # Import Salt Libs
  22. import salt.utils.files
  23. import salt.utils.network
  24. import salt.utils.path
  25. from salt.exceptions import CommandExecutionError
  26. log = logging.getLogger(__name__)
  27. IMAGE_NAME = random_name(prefix='salt_busybox_')
  28. IPV6_ENABLED = bool(salt.utils.network.ip_addrs6(include_loopback=True))
  29. def network_name(func):
  30. '''
  31. Generate a randomized name for a network and clean it up afterward
  32. '''
  33. @functools.wraps(func)
  34. def wrapper(self, *args, **kwargs):
  35. name = random_name(prefix='salt_net_')
  36. try:
  37. return func(self, name, *args, **kwargs)
  38. finally:
  39. self.run_function(
  40. 'docker.disconnect_all_containers_from_network', [name])
  41. try:
  42. self.run_function('docker.remove_network', [name])
  43. except CommandExecutionError as exc:
  44. if 'No such network' not in exc.__str__():
  45. raise
  46. return wrapper
  47. def container_name(func):
  48. '''
  49. Generate a randomized name for a container and clean it up afterward
  50. '''
  51. def build_image():
  52. # Create temp dir
  53. image_build_rootdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  54. script_path = os.path.join(RUNTIME_VARS.BASE_FILES, 'mkimage-busybox-static')
  55. cmd = [script_path, image_build_rootdir, IMAGE_NAME]
  56. log.debug('Running \'%s\' to build busybox image', ' '.join(cmd))
  57. process = subprocess.Popen(
  58. cmd,
  59. close_fds=True,
  60. stdout=subprocess.PIPE,
  61. stderr=subprocess.STDOUT)
  62. output = process.communicate()[0]
  63. log.debug('Output from mkimge-busybox-static:\n%s', output)
  64. if process.returncode != 0:
  65. raise Exception('Failed to build image')
  66. try:
  67. salt.utils.files.rm_rf(image_build_rootdir)
  68. except OSError as exc:
  69. if exc.errno != errno.ENOENT:
  70. raise
  71. @functools.wraps(func)
  72. def wrapper(self, *args, **kwargs):
  73. try:
  74. self.run_function('docker.inspect_image', [IMAGE_NAME])
  75. except CommandExecutionError:
  76. pass
  77. else:
  78. build_image()
  79. name = random_name(prefix='salt_test_')
  80. self.run_function(
  81. 'docker.create',
  82. name=name,
  83. image=IMAGE_NAME,
  84. command='sleep 600',
  85. start=True)
  86. try:
  87. return func(self, name, *args, **kwargs)
  88. finally:
  89. try:
  90. self.run_function('docker.rm', [name], force=True)
  91. except CommandExecutionError as exc:
  92. if 'No such container' not in exc.__str__():
  93. raise
  94. return wrapper
  95. @pytest.mark.destructive_test
  96. @skipIf(not salt.utils.path.which('dockerd'), 'Docker not installed')
  97. class DockerNetworkTestCase(ModuleCase, SaltReturnAssertsMixin):
  98. '''
  99. Test docker_network states
  100. '''
  101. @classmethod
  102. def tearDownClass(cls):
  103. '''
  104. Remove test image if present. Note that this will run a docker rmi even
  105. if no test which required the image was run.
  106. '''
  107. cmd = ['docker', 'rmi', '--force', IMAGE_NAME]
  108. log.debug('Running \'%s\' to destroy busybox image', ' '.join(cmd))
  109. process = subprocess.Popen(
  110. cmd,
  111. close_fds=True,
  112. stdout=subprocess.PIPE,
  113. stderr=subprocess.STDOUT)
  114. output = process.communicate()[0]
  115. log.debug('Output from %s:\n%s', ' '.join(cmd), output)
  116. if process.returncode != 0 and 'No such image' not in output:
  117. raise Exception('Failed to destroy image')
  118. def run_state(self, function, **kwargs):
  119. ret = super(DockerNetworkTestCase, self).run_state(function, **kwargs)
  120. log.debug('ret = %s', ret)
  121. return ret
  122. @with_network(create=False)
  123. def test_absent(self, net):
  124. self.assertSaltTrueReturn(
  125. self.run_state('docker_network.present', name=net.name))
  126. ret = self.run_state('docker_network.absent', name=net.name)
  127. self.assertSaltTrueReturn(ret)
  128. ret = ret[next(iter(ret))]
  129. self.assertEqual(ret['changes'], {'removed': True})
  130. self.assertEqual(
  131. ret['comment'],
  132. 'Removed network \'{0}\''.format(net.name)
  133. )
  134. @container_name
  135. @with_network(create=False)
  136. def test_absent_with_disconnected_container(self, net, container_name):
  137. self.assertSaltTrueReturn(
  138. self.run_state('docker_network.present',
  139. name=net.name,
  140. containers=[container_name])
  141. )
  142. ret = self.run_state('docker_network.absent', name=net.name)
  143. self.assertSaltTrueReturn(ret)
  144. ret = ret[next(iter(ret))]
  145. self.assertEqual(
  146. ret['changes'],
  147. {
  148. 'removed': True,
  149. 'disconnected': [container_name],
  150. }
  151. )
  152. self.assertEqual(
  153. ret['comment'],
  154. 'Removed network \'{0}\''.format(net.name)
  155. )
  156. @with_network(create=False)
  157. def test_absent_when_not_present(self, net):
  158. ret = self.run_state('docker_network.absent', name=net.name)
  159. self.assertSaltTrueReturn(ret)
  160. ret = ret[next(iter(ret))]
  161. self.assertEqual(ret['changes'], {})
  162. self.assertEqual(
  163. ret['comment'],
  164. 'Network \'{0}\' already absent'.format(net.name)
  165. )
  166. @with_network(create=False)
  167. def test_present(self, net):
  168. ret = self.run_state('docker_network.present', name=net.name)
  169. self.assertSaltTrueReturn(ret)
  170. ret = ret[next(iter(ret))]
  171. # Make sure the state return is what we expect
  172. self.assertEqual(ret['changes'], {'created': True})
  173. self.assertEqual(
  174. ret['comment'],
  175. 'Network \'{0}\' created'.format(net.name)
  176. )
  177. # Now check to see that the network actually exists. If it doesn't,
  178. # this next function call will raise an exception.
  179. self.run_function('docker.inspect_network', [net.name])
  180. @container_name
  181. @with_network(create=False)
  182. def test_present_with_containers(self, net, container_name):
  183. ret = self.run_state(
  184. 'docker_network.present',
  185. name=net.name,
  186. containers=[container_name])
  187. self.assertSaltTrueReturn(ret)
  188. ret = ret[next(iter(ret))]
  189. self.assertEqual(
  190. ret['changes'],
  191. {
  192. 'created': True,
  193. 'connected': [container_name],
  194. }
  195. )
  196. self.assertEqual(
  197. ret['comment'],
  198. 'Network \'{0}\' created'.format(net.name)
  199. )
  200. # Now check to see that the network actually exists. If it doesn't,
  201. # this next function call will raise an exception.
  202. self.run_function('docker.inspect_network', [net.name])
  203. def _test_present_reconnect(self, net, container_name, reconnect=True):
  204. ret = self.run_state(
  205. 'docker_network.present',
  206. name=net.name,
  207. driver='bridge')
  208. self.assertSaltTrueReturn(ret)
  209. ret = ret[next(iter(ret))]
  210. self.assertEqual(ret['changes'], {'created': True})
  211. self.assertEqual(
  212. ret['comment'],
  213. 'Network \'{0}\' created'.format(net.name)
  214. )
  215. # Connect the container
  216. self.run_function(
  217. 'docker.connect_container_to_network',
  218. [container_name, net.name]
  219. )
  220. # Change the driver to force the network to be replaced
  221. ret = self.run_state(
  222. 'docker_network.present',
  223. name=net.name,
  224. driver='macvlan',
  225. reconnect=reconnect)
  226. self.assertSaltTrueReturn(ret)
  227. ret = ret[next(iter(ret))]
  228. self.assertEqual(
  229. ret['changes'],
  230. {
  231. 'recreated': True,
  232. 'reconnected' if reconnect else 'disconnected': [container_name],
  233. net.name: {
  234. 'Driver': {
  235. 'old': 'bridge',
  236. 'new': 'macvlan',
  237. }
  238. }
  239. }
  240. )
  241. self.assertEqual(
  242. ret['comment'],
  243. 'Network \'{0}\' was replaced with updated config'.format(net.name)
  244. )
  245. @container_name
  246. @with_network(create=False)
  247. def test_present_with_reconnect(self, net, container_name):
  248. '''
  249. Test reconnecting with containers not passed to state
  250. '''
  251. self._test_present_reconnect(net, container_name, reconnect=True)
  252. @container_name
  253. @with_network(create=False)
  254. def test_present_with_no_reconnect(self, net, container_name):
  255. '''
  256. Test reconnecting with containers not passed to state
  257. '''
  258. self._test_present_reconnect(net, container_name, reconnect=False)
  259. @with_network()
  260. def test_present_internal(self, net):
  261. self.assertSaltTrueReturn(
  262. self.run_state(
  263. 'docker_network.present',
  264. name=net.name,
  265. internal=True,
  266. )
  267. )
  268. net_info = self.run_function('docker.inspect_network', [net.name])
  269. self.assertIs(net_info['Internal'], True)
  270. @with_network()
  271. def test_present_labels(self, net):
  272. # Test a mix of different ways of specifying labels
  273. self.assertSaltTrueReturn(
  274. self.run_state(
  275. 'docker_network.present',
  276. name=net.name,
  277. labels=[
  278. 'foo',
  279. 'bar=baz',
  280. {'hello': 'world'},
  281. ],
  282. )
  283. )
  284. net_info = self.run_function('docker.inspect_network', [net.name])
  285. self.assertEqual(
  286. net_info['Labels'],
  287. {'foo': '',
  288. 'bar': 'baz',
  289. 'hello': 'world'},
  290. )
  291. @with_network(subnet='fe3f:2180:26:1::/123')
  292. @with_network(subnet='10.247.197.96/27')
  293. @skipIf(not IPV6_ENABLED, 'IPv6 not enabled')
  294. def test_present_enable_ipv6(self, net1, net2):
  295. self.assertSaltTrueReturn(
  296. self.run_state(
  297. 'docker_network.present',
  298. name=net1.name,
  299. enable_ipv6=True,
  300. ipam_pools=[
  301. {'subnet': net1.subnet},
  302. {'subnet': net2.subnet},
  303. ],
  304. )
  305. )
  306. net_info = self.run_function('docker.inspect_network', [net1.name])
  307. self.assertIs(net_info['EnableIPv6'], True)
  308. @requires_system_grains
  309. @with_network()
  310. def test_present_attachable(self, net, grains):
  311. if grains['os_family'] == 'RedHat' \
  312. and grains.get('osmajorrelease', 0) <= 7:
  313. self.skipTest('Cannot reliably manage attachable on RHEL <= 7')
  314. self.assertSaltTrueReturn(
  315. self.run_state(
  316. 'docker_network.present',
  317. name=net.name,
  318. attachable=True,
  319. )
  320. )
  321. net_info = self.run_function('docker.inspect_network', [net.name])
  322. self.assertIs(net_info['Attachable'], True)
  323. @skipIf(True, 'Skip until we can set up docker swarm testing')
  324. @with_network()
  325. def test_present_scope(self, net):
  326. self.assertSaltTrueReturn(
  327. self.run_state(
  328. 'docker_network.present',
  329. name=net.name,
  330. scope='global',
  331. )
  332. )
  333. net_info = self.run_function('docker.inspect_network', [net.name])
  334. self.assertIs(net_info['Scope'], 'global')
  335. @skipIf(True, 'Skip until we can set up docker swarm testing')
  336. @with_network()
  337. def test_present_ingress(self, net):
  338. self.assertSaltTrueReturn(
  339. self.run_state(
  340. 'docker_network.present',
  341. name=net.name,
  342. ingress=True,
  343. )
  344. )
  345. net_info = self.run_function('docker.inspect_network', [net.name])
  346. self.assertIs(net_info['Ingress'], True)
  347. @with_network(subnet='10.247.197.128/27')
  348. @with_network(subnet='10.247.197.96/27')
  349. def test_present_with_custom_ipv4(self, net1, net2):
  350. # First run will test passing the IPAM arguments individually
  351. self.assertSaltTrueReturn(
  352. self.run_state(
  353. 'docker_network.present',
  354. name=net1.name,
  355. subnet=net1.subnet,
  356. gateway=net1.gateway,
  357. )
  358. )
  359. # Second run will pass them in the ipam_pools argument
  360. ret = self.run_state(
  361. 'docker_network.present',
  362. name=net1.name, # We want to keep the same network name
  363. ipam_pools=[
  364. {'subnet': net2.subnet,
  365. 'gateway': net2.gateway},
  366. ],
  367. )
  368. self.assertSaltTrueReturn(ret)
  369. ret = ret[next(iter(ret))]
  370. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  371. # provided. So, there will be both an IPv4 and IPv6 pool in the
  372. # configuration.
  373. expected = {
  374. 'recreated': True,
  375. net1.name: {
  376. 'IPAM': {
  377. 'Config': {
  378. 'old': [
  379. {'Subnet': net1.subnet,
  380. 'Gateway': net1.gateway},
  381. ],
  382. 'new': [
  383. {'Subnet': net2.subnet,
  384. 'Gateway': net2.gateway},
  385. ],
  386. }
  387. }
  388. }
  389. }
  390. self.assertEqual(ret['changes'], expected)
  391. self.assertEqual(
  392. ret['comment'],
  393. 'Network \'{0}\' was replaced with updated config'.format(
  394. net1.name
  395. )
  396. )
  397. @with_network(subnet='fe3f:2180:26:1::20/123')
  398. @with_network(subnet='fe3f:2180:26:1::/123')
  399. @with_network(subnet='10.247.197.96/27')
  400. @skipIf(not IPV6_ENABLED, 'IPv6 not enabled')
  401. def test_present_with_custom_ipv6(self, ipv4_net, ipv6_net1, ipv6_net2):
  402. self.assertSaltTrueReturn(
  403. self.run_state(
  404. 'docker_network.present',
  405. name=ipv4_net.name,
  406. enable_ipv6=True,
  407. ipam_pools=[
  408. {'subnet': ipv4_net.subnet,
  409. 'gateway': ipv4_net.gateway},
  410. {'subnet': ipv6_net1.subnet,
  411. 'gateway': ipv6_net1.gateway}
  412. ],
  413. )
  414. )
  415. ret = self.run_state(
  416. 'docker_network.present',
  417. name=ipv4_net.name, # We want to keep the same network name
  418. enable_ipv6=True,
  419. ipam_pools=[
  420. {'subnet': ipv4_net.subnet,
  421. 'gateway': ipv4_net.gateway},
  422. {'subnet': ipv6_net2.subnet,
  423. 'gateway': ipv6_net2.gateway}
  424. ],
  425. )
  426. self.assertSaltTrueReturn(ret)
  427. ret = ret[next(iter(ret))]
  428. # Docker requires there to be IPv4, even when only an IPv6 subnet was
  429. # provided. So, there will be both an IPv4 and IPv6 pool in the
  430. # configuration.
  431. expected = {
  432. 'recreated': True,
  433. ipv4_net.name: {
  434. 'IPAM': {
  435. 'Config': {
  436. 'old': [
  437. {'Subnet': ipv4_net.subnet,
  438. 'Gateway': ipv4_net.gateway},
  439. {'Subnet': ipv6_net1.subnet,
  440. 'Gateway': ipv6_net1.gateway}
  441. ],
  442. 'new': [
  443. {'Subnet': ipv4_net.subnet,
  444. 'Gateway': ipv4_net.gateway},
  445. {'Subnet': ipv6_net2.subnet,
  446. 'Gateway': ipv6_net2.gateway}
  447. ],
  448. }
  449. }
  450. }
  451. }
  452. self.assertEqual(ret['changes'], expected)
  453. self.assertEqual(
  454. ret['comment'],
  455. 'Network \'{0}\' was replaced with updated config'.format(
  456. ipv4_net.name
  457. )
  458. )