test_dockermod.py 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032
  1. # -*- coding: utf-8 -*-
  2. """
  3. tests.unit.utils.test_dockermod
  4. ===============================
  5. Test the funcs in salt.utils.dockermod and salt.utils.dockermod.translate
  6. """
  7. # Import Python Libs
  8. from __future__ import absolute_import, print_function, unicode_literals
  9. import copy
  10. import functools
  11. import logging
  12. import os
  13. # Import salt libs
  14. import salt.config
  15. import salt.loader
  16. import salt.utils.dockermod.translate.container
  17. import salt.utils.dockermod.translate.network
  18. import salt.utils.platform
  19. from salt.exceptions import CommandExecutionError
  20. # Import 3rd-party libs
  21. from salt.ext import six
  22. from salt.utils.dockermod.translate import helpers as translate_helpers
  23. # Import Salt Testing Libs
  24. from tests.support.unit import TestCase
  25. log = logging.getLogger(__name__)
  26. class Assert(object):
  27. def __init__(self, translator):
  28. self.translator = translator
  29. def __call__(self, func):
  30. self.func = func
  31. return functools.wraps(func)(
  32. # pylint: disable=unnecessary-lambda
  33. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  34. # pylint: enable=unnecessary-lambda
  35. )
  36. def wrap(self, *args, **kwargs):
  37. raise NotImplementedError
  38. def test_stringlist(self, testcase, name):
  39. alias = self.translator.ALIASES_REVMAP.get(name)
  40. # Using file paths here because "volumes" must be passed through this
  41. # set of assertions and it requires absolute paths.
  42. if salt.utils.platform.is_windows():
  43. data = [r"c:\foo", r"c:\bar", r"c:\baz"]
  44. else:
  45. data = ["/foo", "/bar", "/baz"]
  46. for item in (name, alias):
  47. if item is None:
  48. continue
  49. testcase.assertEqual(
  50. salt.utils.dockermod.translate_input(
  51. self.translator, **{item: ",".join(data)}
  52. ),
  53. testcase.apply_defaults({name: data}),
  54. )
  55. testcase.assertEqual(
  56. salt.utils.dockermod.translate_input(self.translator, **{item: data}),
  57. testcase.apply_defaults({name: data}),
  58. )
  59. if name != "volumes":
  60. # Test coercing to string
  61. testcase.assertEqual(
  62. salt.utils.dockermod.translate_input(
  63. self.translator, **{item: ["one", 2]}
  64. ),
  65. testcase.apply_defaults({name: ["one", "2"]}),
  66. )
  67. if alias is not None:
  68. # Test collision
  69. # sorted() used here because we want to confirm that we discard the
  70. # alias' value and go with the unsorted version.
  71. test_kwargs = {name: data, alias: sorted(data)}
  72. testcase.assertEqual(
  73. salt.utils.dockermod.translate_input(
  74. self.translator, ignore_collisions=True, **test_kwargs
  75. ),
  76. testcase.apply_defaults({name: test_kwargs[name]}),
  77. )
  78. with testcase.assertRaisesRegex(
  79. CommandExecutionError, "is an alias for.+cannot both be used"
  80. ):
  81. salt.utils.dockermod.translate_input(
  82. self.translator, ignore_collisions=False, **test_kwargs
  83. )
  84. def test_key_value(self, testcase, name, delimiter):
  85. """
  86. Common logic for key/value pair testing. IP address validation is
  87. turned off here, and must be done separately in the wrapped function.
  88. """
  89. alias = self.translator.ALIASES_REVMAP.get(name)
  90. expected = {"foo": "bar", "baz": "qux"}
  91. vals = "foo{0}bar,baz{0}qux".format(delimiter)
  92. for item in (name, alias):
  93. if item is None:
  94. continue
  95. for val in (vals, vals.split(",")):
  96. testcase.assertEqual(
  97. salt.utils.dockermod.translate_input(
  98. self.translator, validate_ip_addrs=False, **{item: val}
  99. ),
  100. testcase.apply_defaults({name: expected}),
  101. )
  102. # Dictionary input
  103. testcase.assertEqual(
  104. salt.utils.dockermod.translate_input(
  105. self.translator, validate_ip_addrs=False, **{item: expected}
  106. ),
  107. testcase.apply_defaults({name: expected}),
  108. )
  109. # "Dictlist" input from states
  110. testcase.assertEqual(
  111. salt.utils.dockermod.translate_input(
  112. self.translator,
  113. validate_ip_addrs=False,
  114. **{item: [{"foo": "bar"}, {"baz": "qux"}]}
  115. ),
  116. testcase.apply_defaults({name: expected}),
  117. )
  118. if alias is not None:
  119. # Test collision
  120. test_kwargs = {name: vals, alias: "hello{0}world".format(delimiter)}
  121. testcase.assertEqual(
  122. salt.utils.dockermod.translate_input(
  123. self.translator,
  124. validate_ip_addrs=False,
  125. ignore_collisions=True,
  126. **test_kwargs
  127. ),
  128. testcase.apply_defaults({name: expected}),
  129. )
  130. with testcase.assertRaisesRegex(
  131. CommandExecutionError, "is an alias for.+cannot both be used"
  132. ):
  133. salt.utils.dockermod.translate_input(
  134. self.translator,
  135. validate_ip_addrs=False,
  136. ignore_collisions=False,
  137. **test_kwargs
  138. )
  139. class assert_bool(Assert):
  140. """
  141. Test a boolean value
  142. """
  143. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  144. # Strip off the "test_" from the function name
  145. name = self.func.__name__[5:]
  146. alias = self.translator.ALIASES_REVMAP.get(name)
  147. for item in (name, alias):
  148. if item is None:
  149. continue
  150. testcase.assertEqual(
  151. salt.utils.dockermod.translate_input(self.translator, **{item: True}),
  152. testcase.apply_defaults({name: True}),
  153. )
  154. # These two are contrived examples, but they will test bool-ifying
  155. # a non-bool value to ensure proper input format.
  156. testcase.assertEqual(
  157. salt.utils.dockermod.translate_input(self.translator, **{item: "foo"}),
  158. testcase.apply_defaults({name: True}),
  159. )
  160. testcase.assertEqual(
  161. salt.utils.dockermod.translate_input(self.translator, **{item: 0}),
  162. testcase.apply_defaults({name: False}),
  163. )
  164. if alias is not None:
  165. # Test collision
  166. test_kwargs = {name: True, alias: False}
  167. testcase.assertEqual(
  168. salt.utils.dockermod.translate_input(
  169. self.translator, ignore_collisions=True, **test_kwargs
  170. ),
  171. testcase.apply_defaults({name: test_kwargs[name]}),
  172. )
  173. with testcase.assertRaisesRegex(
  174. CommandExecutionError, "is an alias for.+cannot both be used"
  175. ):
  176. salt.utils.dockermod.translate_input(
  177. self.translator, ignore_collisions=False, **test_kwargs
  178. )
  179. return self.func(testcase, *args, **kwargs)
  180. class assert_int(Assert):
  181. """
  182. Test an integer value
  183. """
  184. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  185. # Strip off the "test_" from the function name
  186. name = self.func.__name__[5:]
  187. alias = self.translator.ALIASES_REVMAP.get(name)
  188. for item in (name, alias):
  189. if item is None:
  190. continue
  191. for val in (100, "100"):
  192. testcase.assertEqual(
  193. salt.utils.dockermod.translate_input(
  194. self.translator, **{item: val}
  195. ),
  196. testcase.apply_defaults({name: 100}),
  197. )
  198. # Error case: non-numeric value passed
  199. with testcase.assertRaisesRegex(
  200. CommandExecutionError, "'foo' is not an integer"
  201. ):
  202. salt.utils.dockermod.translate_input(self.translator, **{item: "foo"})
  203. if alias is not None:
  204. # Test collision
  205. test_kwargs = {name: 100, alias: 200}
  206. testcase.assertEqual(
  207. salt.utils.dockermod.translate_input(
  208. self.translator, ignore_collisions=True, **test_kwargs
  209. ),
  210. testcase.apply_defaults({name: test_kwargs[name]}),
  211. )
  212. with testcase.assertRaisesRegex(
  213. CommandExecutionError, "is an alias for.+cannot both be used"
  214. ):
  215. salt.utils.dockermod.translate_input(
  216. self.translator, ignore_collisions=False, **test_kwargs
  217. )
  218. return self.func(testcase, *args, **kwargs)
  219. class assert_string(Assert):
  220. """
  221. Test that item is a string or is converted to one
  222. """
  223. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  224. # Strip off the "test_" from the function name
  225. name = self.func.__name__[5:]
  226. alias = self.translator.ALIASES_REVMAP.get(name)
  227. # Using file paths here because "working_dir" must be passed through
  228. # this set of assertions and it requires absolute paths.
  229. if salt.utils.platform.is_windows():
  230. data = r"c:\foo"
  231. else:
  232. data = "/foo"
  233. for item in (name, alias):
  234. if item is None:
  235. continue
  236. testcase.assertEqual(
  237. salt.utils.dockermod.translate_input(self.translator, **{item: data}),
  238. testcase.apply_defaults({name: data}),
  239. )
  240. if name != "working_dir":
  241. # Test coercing to string
  242. testcase.assertEqual(
  243. salt.utils.dockermod.translate_input(
  244. self.translator, **{item: 123}
  245. ),
  246. testcase.apply_defaults({name: "123"}),
  247. )
  248. if alias is not None:
  249. # Test collision
  250. test_kwargs = {name: data, alias: data}
  251. testcase.assertEqual(
  252. salt.utils.dockermod.translate_input(
  253. self.translator, ignore_collisions=True, **test_kwargs
  254. ),
  255. testcase.apply_defaults({name: test_kwargs[name]}),
  256. )
  257. with testcase.assertRaisesRegex(
  258. CommandExecutionError, "is an alias for.+cannot both be used"
  259. ):
  260. salt.utils.dockermod.translate_input(
  261. self.translator, ignore_collisions=False, **test_kwargs
  262. )
  263. return self.func(testcase, *args, **kwargs)
  264. class assert_int_or_string(Assert):
  265. """
  266. Test an integer or string value
  267. """
  268. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  269. # Strip off the "test_" from the function name
  270. name = self.func.__name__[5:]
  271. alias = self.translator.ALIASES_REVMAP.get(name)
  272. for item in (name, alias):
  273. if item is None:
  274. continue
  275. testcase.assertEqual(
  276. salt.utils.dockermod.translate_input(self.translator, **{item: 100}),
  277. testcase.apply_defaults({name: 100}),
  278. )
  279. testcase.assertEqual(
  280. salt.utils.dockermod.translate_input(self.translator, **{item: "100M"}),
  281. testcase.apply_defaults({name: "100M"}),
  282. )
  283. if alias is not None:
  284. # Test collision
  285. test_kwargs = {name: 100, alias: "100M"}
  286. testcase.assertEqual(
  287. salt.utils.dockermod.translate_input(
  288. self.translator, ignore_collisions=True, **test_kwargs
  289. ),
  290. testcase.apply_defaults({name: test_kwargs[name]}),
  291. )
  292. with testcase.assertRaisesRegex(
  293. CommandExecutionError, "is an alias for.+cannot both be used"
  294. ):
  295. salt.utils.dockermod.translate_input(
  296. self.translator, ignore_collisions=False, **test_kwargs
  297. )
  298. return self.func(testcase, *args, **kwargs)
  299. class assert_stringlist(Assert):
  300. """
  301. Test a comma-separated or Python list of strings
  302. """
  303. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  304. # Strip off the "test_" from the function name
  305. name = self.func.__name__[5:]
  306. self.test_stringlist(testcase, name)
  307. return self.func(testcase, *args, **kwargs)
  308. class assert_dict(Assert):
  309. """
  310. Dictionaries should be untouched, dictlists should be repacked and end up
  311. as a single dictionary.
  312. """
  313. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  314. # Strip off the "test_" from the function name
  315. name = self.func.__name__[5:]
  316. alias = self.translator.ALIASES_REVMAP.get(name)
  317. expected = {"foo": "bar", "baz": "qux"}
  318. for item in (name, alias):
  319. if item is None:
  320. continue
  321. testcase.assertEqual(
  322. salt.utils.dockermod.translate_input(
  323. self.translator, **{item: expected}
  324. ),
  325. testcase.apply_defaults({name: expected}),
  326. )
  327. # "Dictlist" input from states
  328. testcase.assertEqual(
  329. salt.utils.dockermod.translate_input(
  330. self.translator,
  331. **{item: [{x: y} for x, y in six.iteritems(expected)]}
  332. ),
  333. testcase.apply_defaults({name: expected}),
  334. )
  335. # Error case: non-dictionary input
  336. with testcase.assertRaisesRegex(
  337. CommandExecutionError, "'foo' is not a dictionary"
  338. ):
  339. salt.utils.dockermod.translate_input(self.translator, **{item: "foo"})
  340. if alias is not None:
  341. # Test collision
  342. test_kwargs = {name: "foo", alias: "bar"}
  343. testcase.assertEqual(
  344. salt.utils.dockermod.translate_input(
  345. self.translator, ignore_collisions=True, **test_kwargs
  346. ),
  347. testcase.apply_defaults({name: test_kwargs[name]}),
  348. )
  349. with testcase.assertRaisesRegex(
  350. CommandExecutionError, "is an alias for.+cannot both be used"
  351. ):
  352. salt.utils.dockermod.translate_input(
  353. self.translator, ignore_collisions=False, **test_kwargs
  354. )
  355. return self.func(testcase, *args, **kwargs)
  356. class assert_cmd(Assert):
  357. """
  358. Test for a string, or a comma-separated or Python list of strings. This is
  359. different from a stringlist in that we do not do any splitting. This
  360. decorator is used both by the "command" and "entrypoint" arguments.
  361. """
  362. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  363. # Strip off the "test_" from the function name
  364. name = self.func.__name__[5:]
  365. alias = self.translator.ALIASES_REVMAP.get(name)
  366. for item in (name, alias):
  367. if item is None:
  368. continue
  369. testcase.assertEqual(
  370. salt.utils.dockermod.translate_input(
  371. self.translator, **{item: "foo bar"}
  372. ),
  373. testcase.apply_defaults({name: "foo bar"}),
  374. )
  375. testcase.assertEqual(
  376. salt.utils.dockermod.translate_input(
  377. self.translator, **{item: ["foo", "bar"]}
  378. ),
  379. testcase.apply_defaults({name: ["foo", "bar"]}),
  380. )
  381. # Test coercing to string
  382. testcase.assertEqual(
  383. salt.utils.dockermod.translate_input(self.translator, **{item: 123}),
  384. testcase.apply_defaults({name: "123"}),
  385. )
  386. testcase.assertEqual(
  387. salt.utils.dockermod.translate_input(
  388. self.translator, **{item: ["one", 2]}
  389. ),
  390. testcase.apply_defaults({name: ["one", "2"]}),
  391. )
  392. if alias is not None:
  393. # Test collision
  394. test_kwargs = {name: "foo", alias: "bar"}
  395. testcase.assertEqual(
  396. salt.utils.dockermod.translate_input(
  397. self.translator, ignore_collisions=True, **test_kwargs
  398. ),
  399. testcase.apply_defaults({name: test_kwargs[name]}),
  400. )
  401. with testcase.assertRaisesRegex(
  402. CommandExecutionError, "is an alias for.+cannot both be used"
  403. ):
  404. salt.utils.dockermod.translate_input(
  405. self.translator, ignore_collisions=False, **test_kwargs
  406. )
  407. return self.func(testcase, *args, **kwargs)
  408. class assert_key_colon_value(Assert):
  409. """
  410. Test a key/value pair with parameters passed as key:value pairs
  411. """
  412. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  413. # Strip off the "test_" from the function name
  414. name = self.func.__name__[5:]
  415. self.test_key_value(testcase, name, ":")
  416. return self.func(testcase, *args, **kwargs)
  417. class assert_key_equals_value(Assert):
  418. """
  419. Test a key/value pair with parameters passed as key=value pairs
  420. """
  421. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  422. # Strip off the "test_" from the function name
  423. name = self.func.__name__[5:]
  424. self.test_key_value(testcase, name, "=")
  425. if name == "labels":
  426. self.test_stringlist(testcase, name)
  427. return self.func(testcase, *args, **kwargs)
  428. class assert_labels(Assert):
  429. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  430. # Strip off the "test_" from the function name
  431. name = self.func.__name__[5:]
  432. alias = self.translator.ALIASES_REVMAP.get(name)
  433. labels = ["foo", "bar=baz", {"hello": "world"}]
  434. expected = {"foo": "", "bar": "baz", "hello": "world"}
  435. for item in (name, alias):
  436. if item is None:
  437. continue
  438. testcase.assertEqual(
  439. salt.utils.dockermod.translate_input(self.translator, **{item: labels}),
  440. testcase.apply_defaults({name: expected}),
  441. )
  442. # Error case: Passed a mutli-element dict in dictlist
  443. bad_labels = copy.deepcopy(labels)
  444. bad_labels[-1]["bad"] = "input"
  445. with testcase.assertRaisesRegex(
  446. CommandExecutionError, r"Invalid label\(s\)"
  447. ):
  448. salt.utils.dockermod.translate_input(
  449. self.translator, **{item: bad_labels}
  450. )
  451. return self.func(testcase, *args, **kwargs)
  452. class assert_device_rates(Assert):
  453. """
  454. Tests for device_{read,write}_{bps,iops}. The bps values have a "Rate"
  455. value expressed in bytes/kb/mb/gb, while the iops values have a "Rate"
  456. expressed as a simple integer.
  457. """
  458. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  459. # Strip off the "test_" from the function name
  460. name = self.func.__name__[5:]
  461. alias = self.translator.ALIASES_REVMAP.get(name)
  462. for item in (name, alias):
  463. if item is None:
  464. continue
  465. # Error case: Not an absolute path
  466. path = os.path.join("foo", "bar", "baz")
  467. with testcase.assertRaisesRegex(
  468. CommandExecutionError,
  469. "Path '{0}' is not absolute".format(path.replace("\\", "\\\\")),
  470. ):
  471. salt.utils.dockermod.translate_input(
  472. self.translator, **{item: "{0}:1048576".format(path)}
  473. )
  474. if name.endswith("_bps"):
  475. # Both integer bytes and a string providing a shorthand for kb,
  476. # mb, or gb can be used, so we need to test for both.
  477. expected = ({}, [])
  478. vals = "/dev/sda:1048576,/dev/sdb:1048576"
  479. for val in (vals, vals.split(",")):
  480. testcase.assertEqual(
  481. salt.utils.dockermod.translate_input(
  482. self.translator, **{item: val}
  483. ),
  484. testcase.apply_defaults(
  485. {
  486. name: [
  487. {"Path": "/dev/sda", "Rate": 1048576},
  488. {"Path": "/dev/sdb", "Rate": 1048576},
  489. ]
  490. }
  491. ),
  492. )
  493. vals = "/dev/sda:1mb,/dev/sdb:5mb"
  494. for val in (vals, vals.split(",")):
  495. testcase.assertEqual(
  496. salt.utils.dockermod.translate_input(
  497. self.translator, **{item: val}
  498. ),
  499. testcase.apply_defaults(
  500. {
  501. name: [
  502. {"Path": "/dev/sda", "Rate": "1mb"},
  503. {"Path": "/dev/sdb", "Rate": "5mb"},
  504. ]
  505. }
  506. ),
  507. )
  508. if alias is not None:
  509. # Test collision
  510. test_kwargs = {
  511. name: "/dev/sda:1048576,/dev/sdb:1048576",
  512. alias: "/dev/sda:1mb,/dev/sdb:5mb",
  513. }
  514. testcase.assertEqual(
  515. salt.utils.dockermod.translate_input(
  516. self.translator, ignore_collisions=True, **test_kwargs
  517. ),
  518. testcase.apply_defaults(
  519. {
  520. name: [
  521. {"Path": "/dev/sda", "Rate": 1048576},
  522. {"Path": "/dev/sdb", "Rate": 1048576},
  523. ]
  524. }
  525. ),
  526. )
  527. with testcase.assertRaisesRegex(
  528. CommandExecutionError, "is an alias for.+cannot both be used"
  529. ):
  530. salt.utils.dockermod.translate_input(
  531. self.translator, ignore_collisions=False, **test_kwargs
  532. )
  533. else:
  534. # The "Rate" value must be an integer
  535. vals = "/dev/sda:1000,/dev/sdb:500"
  536. for val in (vals, vals.split(",")):
  537. testcase.assertEqual(
  538. salt.utils.dockermod.translate_input(
  539. self.translator, **{item: val}
  540. ),
  541. testcase.apply_defaults(
  542. {
  543. name: [
  544. {"Path": "/dev/sda", "Rate": 1000},
  545. {"Path": "/dev/sdb", "Rate": 500},
  546. ]
  547. }
  548. ),
  549. )
  550. # Test non-integer input
  551. expected = (
  552. {},
  553. {item: "Rate '5mb' for path '/dev/sdb' is non-numeric"},
  554. [],
  555. )
  556. vals = "/dev/sda:1000,/dev/sdb:5mb"
  557. for val in (vals, vals.split(",")):
  558. with testcase.assertRaisesRegex(
  559. CommandExecutionError,
  560. "Rate '5mb' for path '/dev/sdb' is non-numeric",
  561. ):
  562. salt.utils.dockermod.translate_input(
  563. self.translator, **{item: val}
  564. )
  565. if alias is not None:
  566. # Test collision
  567. test_kwargs = {
  568. name: "/dev/sda:1000,/dev/sdb:500",
  569. alias: "/dev/sda:888,/dev/sdb:999",
  570. }
  571. testcase.assertEqual(
  572. salt.utils.dockermod.translate_input(
  573. self.translator, ignore_collisions=True, **test_kwargs
  574. ),
  575. testcase.apply_defaults(
  576. {
  577. name: [
  578. {"Path": "/dev/sda", "Rate": 1000},
  579. {"Path": "/dev/sdb", "Rate": 500},
  580. ]
  581. }
  582. ),
  583. )
  584. with testcase.assertRaisesRegex(
  585. CommandExecutionError, "is an alias for.+cannot both be used"
  586. ):
  587. salt.utils.dockermod.translate_input(
  588. self.translator, ignore_collisions=False, **test_kwargs
  589. )
  590. return self.func(testcase, *args, **kwargs)
  591. class assert_subnet(Assert):
  592. """
  593. Test an IPv4 or IPv6 subnet
  594. """
  595. def wrap(self, testcase, *args, **kwargs): # pylint: disable=arguments-differ
  596. # Strip off the "test_" from the function name
  597. name = self.func.__name__[5:]
  598. alias = self.translator.ALIASES_REVMAP.get(name)
  599. for item in (name, alias):
  600. if item is None:
  601. continue
  602. for val in ("127.0.0.1/32", "::1/128"):
  603. log.debug("Verifying '%s' is a valid subnet", val)
  604. testcase.assertEqual(
  605. salt.utils.dockermod.translate_input(
  606. self.translator, validate_ip_addrs=True, **{item: val}
  607. ),
  608. testcase.apply_defaults({name: val}),
  609. )
  610. # Error case: invalid subnet caught by validation
  611. for val in (
  612. "127.0.0.1",
  613. "999.999.999.999/24",
  614. "10.0.0.0/33",
  615. "::1",
  616. "feaz::1/128",
  617. "::1/129",
  618. ):
  619. log.debug("Verifying '%s' is not a valid subnet", val)
  620. with testcase.assertRaisesRegex(
  621. CommandExecutionError, "'{0}' is not a valid subnet".format(val)
  622. ):
  623. salt.utils.dockermod.translate_input(
  624. self.translator, validate_ip_addrs=True, **{item: val}
  625. )
  626. # This is not valid input but it will test whether or not subnet
  627. # validation happened
  628. val = "foo"
  629. testcase.assertEqual(
  630. salt.utils.dockermod.translate_input(
  631. self.translator, validate_ip_addrs=False, **{item: val}
  632. ),
  633. testcase.apply_defaults({name: val}),
  634. )
  635. if alias is not None:
  636. # Test collision
  637. test_kwargs = {name: "10.0.0.0/24", alias: "192.168.50.128/25"}
  638. testcase.assertEqual(
  639. salt.utils.dockermod.translate_input(
  640. self.translator, ignore_collisions=True, **test_kwargs
  641. ),
  642. testcase.apply_defaults({name: test_kwargs[name]}),
  643. )
  644. with testcase.assertRaisesRegex(
  645. CommandExecutionError, "is an alias for.+cannot both be used"
  646. ):
  647. salt.utils.dockermod.translate_input(
  648. self.translator, ignore_collisions=False, **test_kwargs
  649. )
  650. return self.func(testcase, *args, **kwargs)
  651. class TranslateBase(TestCase):
  652. maxDiff = None
  653. translator = None # Must be overridden in the subclass
  654. def apply_defaults(self, ret, skip_translate=None):
  655. if skip_translate is not True:
  656. defaults = getattr(self.translator, "DEFAULTS", {})
  657. for key, val in six.iteritems(defaults):
  658. if key not in ret:
  659. ret[key] = val
  660. return ret
  661. @staticmethod
  662. def normalize_ports(ret):
  663. """
  664. When we translate exposed ports, we can end up with a mixture of ints
  665. (representing TCP ports) and tuples (representing UDP ports). Python 2
  666. will sort an iterable containing these mixed types, but Python 3 will
  667. not. This helper is used to munge the ports in the return data so that
  668. the resulting list is sorted in a way that can reliably be compared to
  669. the expected results in the test.
  670. This helper should only be needed for port_bindings and ports.
  671. """
  672. if "ports" in ret[0]:
  673. tcp_ports = []
  674. udp_ports = []
  675. for item in ret[0]["ports"]:
  676. if isinstance(item, six.integer_types):
  677. tcp_ports.append(item)
  678. else:
  679. udp_ports.append(item)
  680. ret[0]["ports"] = sorted(tcp_ports) + sorted(udp_ports)
  681. return ret
  682. def tearDown(self):
  683. """
  684. Test skip_translate kwarg
  685. """
  686. name = self.id().split(".")[-1][5:]
  687. # The below is not valid input for the Docker API, but these
  688. # assertions confirm that we successfully skipped translation.
  689. for val in (True, name, [name]):
  690. self.assertEqual(
  691. salt.utils.dockermod.translate_input(
  692. self.translator, skip_translate=val, **{name: "foo"}
  693. ),
  694. self.apply_defaults({name: "foo"}, skip_translate=val),
  695. )
  696. class TranslateContainerInputTestCase(TranslateBase):
  697. """
  698. Tests for salt.utils.dockermod.translate_input(), invoked using
  699. salt.utils.dockermod.translate.container as the translator module.
  700. """
  701. translator = salt.utils.dockermod.translate.container
  702. @staticmethod
  703. def normalize_ports(ret):
  704. """
  705. When we translate exposed ports, we can end up with a mixture of ints
  706. (representing TCP ports) and tuples (representing UDP ports). Python 2
  707. will sort an iterable containing these mixed types, but Python 3 will
  708. not. This helper is used to munge the ports in the return data so that
  709. the resulting list is sorted in a way that can reliably be compared to
  710. the expected results in the test.
  711. This helper should only be needed for port_bindings and ports.
  712. """
  713. if "ports" in ret:
  714. tcp_ports = []
  715. udp_ports = []
  716. for item in ret["ports"]:
  717. if isinstance(item, six.integer_types):
  718. tcp_ports.append(item)
  719. else:
  720. udp_ports.append(item)
  721. ret["ports"] = sorted(tcp_ports) + sorted(udp_ports)
  722. return ret
  723. @assert_bool(salt.utils.dockermod.translate.container)
  724. def test_auto_remove(self):
  725. """
  726. Should be a bool or converted to one
  727. """
  728. def test_binds(self):
  729. """
  730. Test the "binds" kwarg. Any volumes not defined in the "volumes" kwarg
  731. should be added to the results.
  732. """
  733. self.assertEqual(
  734. salt.utils.dockermod.translate_input(
  735. self.translator, binds="/srv/www:/var/www:ro", volumes="/testing"
  736. ),
  737. {"binds": ["/srv/www:/var/www:ro"], "volumes": ["/testing", "/var/www"]},
  738. )
  739. self.assertEqual(
  740. salt.utils.dockermod.translate_input(
  741. self.translator, binds=["/srv/www:/var/www:ro"], volumes="/testing"
  742. ),
  743. {"binds": ["/srv/www:/var/www:ro"], "volumes": ["/testing", "/var/www"]},
  744. )
  745. self.assertEqual(
  746. salt.utils.dockermod.translate_input(
  747. self.translator,
  748. binds={"/srv/www": {"bind": "/var/www", "mode": "ro"}},
  749. volumes="/testing",
  750. ),
  751. {
  752. "binds": {"/srv/www": {"bind": "/var/www", "mode": "ro"}},
  753. "volumes": ["/testing", "/var/www"],
  754. },
  755. )
  756. @assert_int(salt.utils.dockermod.translate.container)
  757. def test_blkio_weight(self):
  758. """
  759. Should be an int or converted to one
  760. """
  761. def test_blkio_weight_device(self):
  762. """
  763. Should translate a list of PATH:WEIGHT pairs to a list of dictionaries
  764. with the following format: {'Path': PATH, 'Weight': WEIGHT}
  765. """
  766. for val in ("/dev/sda:100,/dev/sdb:200", ["/dev/sda:100", "/dev/sdb:200"]):
  767. self.assertEqual(
  768. salt.utils.dockermod.translate_input(
  769. self.translator, blkio_weight_device="/dev/sda:100,/dev/sdb:200"
  770. ),
  771. {
  772. "blkio_weight_device": [
  773. {"Path": "/dev/sda", "Weight": 100},
  774. {"Path": "/dev/sdb", "Weight": 200},
  775. ]
  776. },
  777. )
  778. # Error cases
  779. with self.assertRaisesRegex(
  780. CommandExecutionError, r"'foo' contains 1 value\(s\) \(expected 2\)"
  781. ):
  782. salt.utils.dockermod.translate_input(
  783. self.translator, blkio_weight_device="foo"
  784. )
  785. with self.assertRaisesRegex(
  786. CommandExecutionError, r"'foo:bar:baz' contains 3 value\(s\) \(expected 2\)"
  787. ):
  788. salt.utils.dockermod.translate_input(
  789. self.translator, blkio_weight_device="foo:bar:baz"
  790. )
  791. with self.assertRaisesRegex(
  792. CommandExecutionError, r"Weight 'foo' for path '/dev/sdb' is not an integer"
  793. ):
  794. salt.utils.dockermod.translate_input(
  795. self.translator, blkio_weight_device=["/dev/sda:100", "/dev/sdb:foo"]
  796. )
  797. @assert_stringlist(salt.utils.dockermod.translate.container)
  798. def test_cap_add(self):
  799. """
  800. Should be a list of strings or converted to one
  801. """
  802. @assert_stringlist(salt.utils.dockermod.translate.container)
  803. def test_cap_drop(self):
  804. """
  805. Should be a list of strings or converted to one
  806. """
  807. @assert_cmd(salt.utils.dockermod.translate.container)
  808. def test_command(self):
  809. """
  810. Can either be a string or a comma-separated or Python list of strings.
  811. """
  812. @assert_string(salt.utils.dockermod.translate.container)
  813. def test_cpuset_cpus(self):
  814. """
  815. Should be a string or converted to one
  816. """
  817. @assert_string(salt.utils.dockermod.translate.container)
  818. def test_cpuset_mems(self):
  819. """
  820. Should be a string or converted to one
  821. """
  822. @assert_int(salt.utils.dockermod.translate.container)
  823. def test_cpu_group(self):
  824. """
  825. Should be an int or converted to one
  826. """
  827. @assert_int(salt.utils.dockermod.translate.container)
  828. def test_cpu_period(self):
  829. """
  830. Should be an int or converted to one
  831. """
  832. @assert_int(salt.utils.dockermod.translate.container)
  833. def test_cpu_shares(self):
  834. """
  835. Should be an int or converted to one
  836. """
  837. @assert_bool(salt.utils.dockermod.translate.container)
  838. def test_detach(self):
  839. """
  840. Should be a bool or converted to one
  841. """
  842. @assert_device_rates(salt.utils.dockermod.translate.container)
  843. def test_device_read_bps(self):
  844. """
  845. CLI input is a list of PATH:RATE pairs, but the API expects a list of
  846. dictionaries in the format [{'Path': path, 'Rate': rate}]
  847. """
  848. @assert_device_rates(salt.utils.dockermod.translate.container)
  849. def test_device_read_iops(self):
  850. """
  851. CLI input is a list of PATH:RATE pairs, but the API expects a list of
  852. dictionaries in the format [{'Path': path, 'Rate': rate}]
  853. """
  854. @assert_device_rates(salt.utils.dockermod.translate.container)
  855. def test_device_write_bps(self):
  856. """
  857. CLI input is a list of PATH:RATE pairs, but the API expects a list of
  858. dictionaries in the format [{'Path': path, 'Rate': rate}]
  859. """
  860. @assert_device_rates(salt.utils.dockermod.translate.container)
  861. def test_device_write_iops(self):
  862. """
  863. CLI input is a list of PATH:RATE pairs, but the API expects a list of
  864. dictionaries in the format [{'Path': path, 'Rate': rate}]
  865. """
  866. @assert_stringlist(salt.utils.dockermod.translate.container)
  867. def test_devices(self):
  868. """
  869. Should be a list of strings or converted to one
  870. """
  871. @assert_stringlist(salt.utils.dockermod.translate.container)
  872. def test_dns_opt(self):
  873. """
  874. Should be a list of strings or converted to one
  875. """
  876. @assert_stringlist(salt.utils.dockermod.translate.container)
  877. def test_dns_search(self):
  878. """
  879. Should be a list of strings or converted to one
  880. """
  881. def test_dns(self):
  882. """
  883. While this is a stringlist, it also supports IP address validation, so
  884. it can't use the test_stringlist decorator because we need to test both
  885. with and without validation, and it isn't necessary to make all other
  886. stringlist tests also do that same kind of testing.
  887. """
  888. for val in ("8.8.8.8,8.8.4.4", ["8.8.8.8", "8.8.4.4"]):
  889. self.assertEqual(
  890. salt.utils.dockermod.translate_input(
  891. self.translator, dns=val, validate_ip_addrs=True,
  892. ),
  893. {"dns": ["8.8.8.8", "8.8.4.4"]},
  894. )
  895. # Error case: invalid IP address caught by validation
  896. for val in ("8.8.8.888,8.8.4.4", ["8.8.8.888", "8.8.4.4"]):
  897. with self.assertRaisesRegex(
  898. CommandExecutionError, r"'8.8.8.888' is not a valid IP address"
  899. ):
  900. salt.utils.dockermod.translate_input(
  901. self.translator, dns=val, validate_ip_addrs=True,
  902. )
  903. # This is not valid input but it will test whether or not IP address
  904. # validation happened.
  905. for val in ("foo,bar", ["foo", "bar"]):
  906. self.assertEqual(
  907. salt.utils.dockermod.translate_input(
  908. self.translator, dns=val, validate_ip_addrs=False,
  909. ),
  910. {"dns": ["foo", "bar"]},
  911. )
  912. @assert_string(salt.utils.dockermod.translate.container)
  913. def test_domainname(self):
  914. """
  915. Should be a list of strings or converted to one
  916. """
  917. @assert_cmd(salt.utils.dockermod.translate.container)
  918. def test_entrypoint(self):
  919. """
  920. Can either be a string or a comma-separated or Python list of strings.
  921. """
  922. @assert_key_equals_value(salt.utils.dockermod.translate.container)
  923. def test_environment(self):
  924. """
  925. Can be passed in several formats but must end up as a dictionary
  926. mapping keys to values
  927. """
  928. def test_extra_hosts(self):
  929. """
  930. Can be passed as a list of key:value pairs but can't be simply tested
  931. using @assert_key_colon_value since we need to test both with and without
  932. IP address validation.
  933. """
  934. for val in ("web1:10.9.8.7,web2:10.9.8.8", ["web1:10.9.8.7", "web2:10.9.8.8"]):
  935. self.assertEqual(
  936. salt.utils.dockermod.translate_input(
  937. self.translator, extra_hosts=val, validate_ip_addrs=True,
  938. ),
  939. {"extra_hosts": {"web1": "10.9.8.7", "web2": "10.9.8.8"}},
  940. )
  941. # Error case: invalid IP address caught by validation
  942. for val in (
  943. "web1:10.9.8.299,web2:10.9.8.8",
  944. ["web1:10.9.8.299", "web2:10.9.8.8"],
  945. ):
  946. with self.assertRaisesRegex(
  947. CommandExecutionError, r"'10.9.8.299' is not a valid IP address"
  948. ):
  949. salt.utils.dockermod.translate_input(
  950. self.translator, extra_hosts=val, validate_ip_addrs=True,
  951. )
  952. # This is not valid input but it will test whether or not IP address
  953. # validation happened.
  954. for val in ("foo:bar,baz:qux", ["foo:bar", "baz:qux"]):
  955. self.assertEqual(
  956. salt.utils.dockermod.translate_input(
  957. self.translator, extra_hosts=val, validate_ip_addrs=False,
  958. ),
  959. {"extra_hosts": {"foo": "bar", "baz": "qux"}},
  960. )
  961. @assert_stringlist(salt.utils.dockermod.translate.container)
  962. def test_group_add(self):
  963. """
  964. Should be a list of strings or converted to one
  965. """
  966. @assert_string(salt.utils.dockermod.translate.container)
  967. def test_hostname(self):
  968. """
  969. Should be a string or converted to one
  970. """
  971. @assert_string(salt.utils.dockermod.translate.container)
  972. def test_ipc_mode(self):
  973. """
  974. Should be a string or converted to one
  975. """
  976. @assert_string(salt.utils.dockermod.translate.container)
  977. def test_isolation(self):
  978. """
  979. Should be a string or converted to one
  980. """
  981. @assert_labels(salt.utils.dockermod.translate.container)
  982. def test_labels(self):
  983. """
  984. Can be passed as a list of key=value pairs or a dictionary, and must
  985. ultimately end up as a dictionary.
  986. """
  987. @assert_key_colon_value(salt.utils.dockermod.translate.container)
  988. def test_links(self):
  989. """
  990. Can be passed as a list of key:value pairs or a dictionary, and must
  991. ultimately end up as a dictionary.
  992. """
  993. def test_log_config(self):
  994. """
  995. This is a mixture of log_driver and log_opt, which get combined into a
  996. dictionary.
  997. log_driver is a simple string, but log_opt can be passed in several
  998. ways, so we need to test them all.
  999. """
  1000. expected = (
  1001. {"log_config": {"Type": "foo", "Config": {"foo": "bar", "baz": "qux"}}},
  1002. {},
  1003. [],
  1004. )
  1005. for val in (
  1006. "foo=bar,baz=qux",
  1007. ["foo=bar", "baz=qux"],
  1008. [{"foo": "bar"}, {"baz": "qux"}],
  1009. {"foo": "bar", "baz": "qux"},
  1010. ):
  1011. self.assertEqual(
  1012. salt.utils.dockermod.translate_input(
  1013. self.translator, log_driver="foo", log_opt="foo=bar,baz=qux"
  1014. ),
  1015. {"log_config": {"Type": "foo", "Config": {"foo": "bar", "baz": "qux"}}},
  1016. )
  1017. # Ensure passing either `log_driver` or `log_opt` alone works
  1018. self.assertEqual(
  1019. salt.utils.dockermod.translate_input(self.translator, log_driver="foo"),
  1020. {"log_config": {"Type": "foo", "Config": {}}},
  1021. )
  1022. self.assertEqual(
  1023. salt.utils.dockermod.translate_input(
  1024. self.translator, log_opt={"foo": "bar", "baz": "qux"}
  1025. ),
  1026. {"log_config": {"Type": "none", "Config": {"foo": "bar", "baz": "qux"}}},
  1027. )
  1028. @assert_key_equals_value(salt.utils.dockermod.translate.container)
  1029. def test_lxc_conf(self):
  1030. """
  1031. Can be passed as a list of key=value pairs or a dictionary, and must
  1032. ultimately end up as a dictionary.
  1033. """
  1034. @assert_string(salt.utils.dockermod.translate.container)
  1035. def test_mac_address(self):
  1036. """
  1037. Should be a string or converted to one
  1038. """
  1039. @assert_int_or_string(salt.utils.dockermod.translate.container)
  1040. def test_mem_limit(self):
  1041. """
  1042. Should be a string or converted to one
  1043. """
  1044. @assert_int(salt.utils.dockermod.translate.container)
  1045. def test_mem_swappiness(self):
  1046. """
  1047. Should be an int or converted to one
  1048. """
  1049. @assert_int_or_string(salt.utils.dockermod.translate.container)
  1050. def test_memswap_limit(self):
  1051. """
  1052. Should be a string or converted to one
  1053. """
  1054. @assert_string(salt.utils.dockermod.translate.container)
  1055. def test_name(self):
  1056. """
  1057. Should be a string or converted to one
  1058. """
  1059. @assert_bool(salt.utils.dockermod.translate.container)
  1060. def test_network_disabled(self):
  1061. """
  1062. Should be a bool or converted to one
  1063. """
  1064. @assert_string(salt.utils.dockermod.translate.container)
  1065. def test_network_mode(self):
  1066. """
  1067. Should be a string or converted to one
  1068. """
  1069. @assert_bool(salt.utils.dockermod.translate.container)
  1070. def test_oom_kill_disable(self):
  1071. """
  1072. Should be a bool or converted to one
  1073. """
  1074. @assert_int(salt.utils.dockermod.translate.container)
  1075. def test_oom_score_adj(self):
  1076. """
  1077. Should be an int or converted to one
  1078. """
  1079. @assert_string(salt.utils.dockermod.translate.container)
  1080. def test_pid_mode(self):
  1081. """
  1082. Should be a string or converted to one
  1083. """
  1084. @assert_int(salt.utils.dockermod.translate.container)
  1085. def test_pids_limit(self):
  1086. """
  1087. Should be an int or converted to one
  1088. """
  1089. def test_port_bindings(self):
  1090. """
  1091. This has several potential formats and can include port ranges. It
  1092. needs its own test.
  1093. """
  1094. # ip:hostPort:containerPort - Bind a specific IP and port on the host
  1095. # to a specific port within the container.
  1096. bindings = (
  1097. "10.1.2.3:8080:80,10.1.2.3:8888:80,10.4.5.6:3333:3333,"
  1098. "10.7.8.9:14505-14506:4505-4506,10.1.2.3:8080:81/udp,"
  1099. "10.1.2.3:8888:81/udp,10.4.5.6:3334:3334/udp,"
  1100. "10.7.8.9:15505-15506:5505-5506/udp"
  1101. )
  1102. for val in (bindings, bindings.split(",")):
  1103. self.assertEqual(
  1104. self.normalize_ports(
  1105. salt.utils.dockermod.translate_input(
  1106. self.translator, port_bindings=val,
  1107. )
  1108. ),
  1109. {
  1110. "port_bindings": {
  1111. 80: [("10.1.2.3", 8080), ("10.1.2.3", 8888)],
  1112. 3333: ("10.4.5.6", 3333),
  1113. 4505: ("10.7.8.9", 14505),
  1114. 4506: ("10.7.8.9", 14506),
  1115. "81/udp": [("10.1.2.3", 8080), ("10.1.2.3", 8888)],
  1116. "3334/udp": ("10.4.5.6", 3334),
  1117. "5505/udp": ("10.7.8.9", 15505),
  1118. "5506/udp": ("10.7.8.9", 15506),
  1119. },
  1120. "ports": [
  1121. 80,
  1122. 3333,
  1123. 4505,
  1124. 4506,
  1125. (81, "udp"),
  1126. (3334, "udp"),
  1127. (5505, "udp"),
  1128. (5506, "udp"),
  1129. ],
  1130. },
  1131. )
  1132. # ip::containerPort - Bind a specific IP and an ephemeral port to a
  1133. # specific port within the container.
  1134. bindings = (
  1135. "10.1.2.3::80,10.1.2.3::80,10.4.5.6::3333,10.7.8.9::4505-4506,"
  1136. "10.1.2.3::81/udp,10.1.2.3::81/udp,10.4.5.6::3334/udp,"
  1137. "10.7.8.9::5505-5506/udp"
  1138. )
  1139. for val in (bindings, bindings.split(",")):
  1140. self.assertEqual(
  1141. self.normalize_ports(
  1142. salt.utils.dockermod.translate_input(
  1143. self.translator, port_bindings=val,
  1144. )
  1145. ),
  1146. {
  1147. "port_bindings": {
  1148. 80: [("10.1.2.3",), ("10.1.2.3",)],
  1149. 3333: ("10.4.5.6",),
  1150. 4505: ("10.7.8.9",),
  1151. 4506: ("10.7.8.9",),
  1152. "81/udp": [("10.1.2.3",), ("10.1.2.3",)],
  1153. "3334/udp": ("10.4.5.6",),
  1154. "5505/udp": ("10.7.8.9",),
  1155. "5506/udp": ("10.7.8.9",),
  1156. },
  1157. "ports": [
  1158. 80,
  1159. 3333,
  1160. 4505,
  1161. 4506,
  1162. (81, "udp"),
  1163. (3334, "udp"),
  1164. (5505, "udp"),
  1165. (5506, "udp"),
  1166. ],
  1167. },
  1168. )
  1169. # hostPort:containerPort - Bind a specific port on all of the host's
  1170. # interfaces to a specific port within the container.
  1171. bindings = (
  1172. "8080:80,8888:80,3333:3333,14505-14506:4505-4506,8080:81/udp,"
  1173. "8888:81/udp,3334:3334/udp,15505-15506:5505-5506/udp"
  1174. )
  1175. for val in (bindings, bindings.split(",")):
  1176. self.assertEqual(
  1177. self.normalize_ports(
  1178. salt.utils.dockermod.translate_input(
  1179. self.translator, port_bindings=val,
  1180. )
  1181. ),
  1182. {
  1183. "port_bindings": {
  1184. 80: [8080, 8888],
  1185. 3333: 3333,
  1186. 4505: 14505,
  1187. 4506: 14506,
  1188. "81/udp": [8080, 8888],
  1189. "3334/udp": 3334,
  1190. "5505/udp": 15505,
  1191. "5506/udp": 15506,
  1192. },
  1193. "ports": [
  1194. 80,
  1195. 3333,
  1196. 4505,
  1197. 4506,
  1198. (81, "udp"),
  1199. (3334, "udp"),
  1200. (5505, "udp"),
  1201. (5506, "udp"),
  1202. ],
  1203. },
  1204. )
  1205. # containerPort - Bind an ephemeral port on all of the host's
  1206. # interfaces to a specific port within the container.
  1207. bindings = "80,3333,4505-4506,81/udp,3334/udp,5505-5506/udp"
  1208. for val in (bindings, bindings.split(",")):
  1209. self.assertEqual(
  1210. self.normalize_ports(
  1211. salt.utils.dockermod.translate_input(
  1212. self.translator, port_bindings=val,
  1213. )
  1214. ),
  1215. {
  1216. "port_bindings": {
  1217. 80: None,
  1218. 3333: None,
  1219. 4505: None,
  1220. 4506: None,
  1221. "81/udp": None,
  1222. "3334/udp": None,
  1223. "5505/udp": None,
  1224. "5506/udp": None,
  1225. },
  1226. "ports": [
  1227. 80,
  1228. 3333,
  1229. 4505,
  1230. 4506,
  1231. (81, "udp"),
  1232. (3334, "udp"),
  1233. (5505, "udp"),
  1234. (5506, "udp"),
  1235. ],
  1236. },
  1237. )
  1238. # Test a mixture of different types of input
  1239. bindings = (
  1240. "10.1.2.3:8080:80,10.4.5.6::3333,14505-14506:4505-4506,"
  1241. "9999-10001,10.1.2.3:8080:81/udp,10.4.5.6::3334/udp,"
  1242. "15505-15506:5505-5506/udp,19999-20001/udp"
  1243. )
  1244. for val in (bindings, bindings.split(",")):
  1245. self.assertEqual(
  1246. self.normalize_ports(
  1247. salt.utils.dockermod.translate_input(
  1248. self.translator, port_bindings=val,
  1249. )
  1250. ),
  1251. {
  1252. "port_bindings": {
  1253. 80: ("10.1.2.3", 8080),
  1254. 3333: ("10.4.5.6",),
  1255. 4505: 14505,
  1256. 4506: 14506,
  1257. 9999: None,
  1258. 10000: None,
  1259. 10001: None,
  1260. "81/udp": ("10.1.2.3", 8080),
  1261. "3334/udp": ("10.4.5.6",),
  1262. "5505/udp": 15505,
  1263. "5506/udp": 15506,
  1264. "19999/udp": None,
  1265. "20000/udp": None,
  1266. "20001/udp": None,
  1267. },
  1268. "ports": [
  1269. 80,
  1270. 3333,
  1271. 4505,
  1272. 4506,
  1273. 9999,
  1274. 10000,
  1275. 10001,
  1276. (81, "udp"),
  1277. (3334, "udp"),
  1278. (5505, "udp"),
  1279. (5506, "udp"),
  1280. (19999, "udp"),
  1281. (20000, "udp"),
  1282. (20001, "udp"),
  1283. ],
  1284. },
  1285. )
  1286. # Error case: too many items (max 3)
  1287. with self.assertRaisesRegex(
  1288. CommandExecutionError,
  1289. r"'10.1.2.3:8080:80:123' is an invalid port binding "
  1290. r"definition \(at most 3 components are allowed, found 4\)",
  1291. ):
  1292. salt.utils.dockermod.translate_input(
  1293. self.translator, port_bindings="10.1.2.3:8080:80:123"
  1294. )
  1295. # Error case: port range start is greater than end
  1296. for val in (
  1297. "10.1.2.3:5555-5554:1111-1112",
  1298. "10.1.2.3:1111-1112:5555-5554",
  1299. "10.1.2.3::5555-5554",
  1300. "5555-5554:1111-1112",
  1301. "1111-1112:5555-5554",
  1302. "5555-5554",
  1303. ):
  1304. with self.assertRaisesRegex(
  1305. CommandExecutionError,
  1306. r"Start of port range \(5555\) cannot be greater than end "
  1307. r"of port range \(5554\)",
  1308. ):
  1309. salt.utils.dockermod.translate_input(
  1310. self.translator, port_bindings=val,
  1311. )
  1312. # Error case: non-numeric port range
  1313. for val in (
  1314. "10.1.2.3:foo:1111-1112",
  1315. "10.1.2.3:1111-1112:foo",
  1316. "10.1.2.3::foo",
  1317. "foo:1111-1112",
  1318. "1111-1112:foo",
  1319. "foo",
  1320. ):
  1321. with self.assertRaisesRegex(
  1322. CommandExecutionError, "'foo' is non-numeric or an invalid port range"
  1323. ):
  1324. salt.utils.dockermod.translate_input(
  1325. self.translator, port_bindings=val,
  1326. )
  1327. # Error case: misatched port range
  1328. for val in ("10.1.2.3:1111-1113:1111-1112", "1111-1113:1111-1112"):
  1329. with self.assertRaisesRegex(
  1330. CommandExecutionError,
  1331. r"Host port range \(1111-1113\) does not have the same "
  1332. r"number of ports as the container port range \(1111-1112\)",
  1333. ):
  1334. salt.utils.dockermod.translate_input(self.translator, port_bindings=val)
  1335. for val in ("10.1.2.3:1111-1112:1111-1113", "1111-1112:1111-1113"):
  1336. with self.assertRaisesRegex(
  1337. CommandExecutionError,
  1338. r"Host port range \(1111-1112\) does not have the same "
  1339. r"number of ports as the container port range \(1111-1113\)",
  1340. ):
  1341. salt.utils.dockermod.translate_input(
  1342. self.translator, port_bindings=val,
  1343. )
  1344. # Error case: empty host port or container port
  1345. with self.assertRaisesRegex(
  1346. CommandExecutionError, "Empty host port in port binding definition ':1111'"
  1347. ):
  1348. salt.utils.dockermod.translate_input(self.translator, port_bindings=":1111")
  1349. with self.assertRaisesRegex(
  1350. CommandExecutionError,
  1351. "Empty container port in port binding definition '1111:'",
  1352. ):
  1353. salt.utils.dockermod.translate_input(self.translator, port_bindings="1111:")
  1354. with self.assertRaisesRegex(
  1355. CommandExecutionError, "Empty port binding definition found"
  1356. ):
  1357. salt.utils.dockermod.translate_input(self.translator, port_bindings="")
  1358. def test_ports(self):
  1359. """
  1360. Ports can be passed as a comma-separated or Python list of port
  1361. numbers, with '/tcp' being optional for TCP ports. They must ultimately
  1362. be a list of port definitions, in which an integer denotes a TCP port,
  1363. and a tuple in the format (port_num, 'udp') denotes a UDP port. Also,
  1364. the port numbers must end up as integers. None of the decorators will
  1365. suffice so this one must be tested specially.
  1366. """
  1367. for val in (
  1368. "1111,2222/tcp,3333/udp,4505-4506",
  1369. [1111, "2222/tcp", "3333/udp", "4505-4506"],
  1370. ["1111", "2222/tcp", "3333/udp", "4505-4506"],
  1371. ):
  1372. self.assertEqual(
  1373. self.normalize_ports(
  1374. salt.utils.dockermod.translate_input(self.translator, ports=val,)
  1375. ),
  1376. {"ports": [1111, 2222, 4505, 4506, (3333, "udp")]},
  1377. )
  1378. # Error case: non-integer and non/string value
  1379. for val in (1.0, [1.0]):
  1380. with self.assertRaisesRegex(
  1381. CommandExecutionError, "'1.0' is not a valid port definition"
  1382. ):
  1383. salt.utils.dockermod.translate_input(
  1384. self.translator, ports=val,
  1385. )
  1386. # Error case: port range start is greater than end
  1387. with self.assertRaisesRegex(
  1388. CommandExecutionError,
  1389. r"Start of port range \(5555\) cannot be greater than end of "
  1390. r"port range \(5554\)",
  1391. ):
  1392. salt.utils.dockermod.translate_input(
  1393. self.translator, ports="5555-5554",
  1394. )
  1395. @assert_bool(salt.utils.dockermod.translate.container)
  1396. def test_privileged(self):
  1397. """
  1398. Should be a bool or converted to one
  1399. """
  1400. @assert_bool(salt.utils.dockermod.translate.container)
  1401. def test_publish_all_ports(self):
  1402. """
  1403. Should be a bool or converted to one
  1404. """
  1405. @assert_bool(salt.utils.dockermod.translate.container)
  1406. def test_read_only(self):
  1407. """
  1408. Should be a bool or converted to one
  1409. """
  1410. def test_restart_policy(self):
  1411. """
  1412. Input is in the format "name[:retry_count]", but the API wants it
  1413. in the format {'Name': name, 'MaximumRetryCount': retry_count}
  1414. """
  1415. name = "restart_policy"
  1416. alias = "restart"
  1417. for item in (name, alias):
  1418. # Test with retry count
  1419. self.assertEqual(
  1420. salt.utils.dockermod.translate_input(
  1421. self.translator, **{item: "on-failure:5"}
  1422. ),
  1423. {name: {"Name": "on-failure", "MaximumRetryCount": 5}},
  1424. )
  1425. # Test without retry count
  1426. self.assertEqual(
  1427. salt.utils.dockermod.translate_input(
  1428. self.translator, **{item: "on-failure"}
  1429. ),
  1430. {name: {"Name": "on-failure", "MaximumRetryCount": 0}},
  1431. )
  1432. # Error case: more than one policy passed
  1433. with self.assertRaisesRegex(
  1434. CommandExecutionError, "Only one policy is permitted"
  1435. ):
  1436. salt.utils.dockermod.translate_input(
  1437. self.translator, **{item: "on-failure,always"}
  1438. )
  1439. # Test collision
  1440. test_kwargs = {name: "on-failure:5", alias: "always"}
  1441. self.assertEqual(
  1442. salt.utils.dockermod.translate_input(
  1443. self.translator, ignore_collisions=True, **test_kwargs
  1444. ),
  1445. {name: {"Name": "on-failure", "MaximumRetryCount": 5}},
  1446. )
  1447. with self.assertRaisesRegex(
  1448. CommandExecutionError, "'restart' is an alias for 'restart_policy'"
  1449. ):
  1450. salt.utils.dockermod.translate_input(
  1451. self.translator, ignore_collisions=False, **test_kwargs
  1452. )
  1453. @assert_stringlist(salt.utils.dockermod.translate.container)
  1454. def test_security_opt(self):
  1455. """
  1456. Should be a list of strings or converted to one
  1457. """
  1458. @assert_int_or_string(salt.utils.dockermod.translate.container)
  1459. def test_shm_size(self):
  1460. """
  1461. Should be a string or converted to one
  1462. """
  1463. @assert_bool(salt.utils.dockermod.translate.container)
  1464. def test_stdin_open(self):
  1465. """
  1466. Should be a bool or converted to one
  1467. """
  1468. @assert_string(salt.utils.dockermod.translate.container)
  1469. def test_stop_signal(self):
  1470. """
  1471. Should be a string or converted to one
  1472. """
  1473. @assert_int(salt.utils.dockermod.translate.container)
  1474. def test_stop_timeout(self):
  1475. """
  1476. Should be an int or converted to one
  1477. """
  1478. @assert_key_equals_value(salt.utils.dockermod.translate.container)
  1479. def test_storage_opt(self):
  1480. """
  1481. Can be passed in several formats but must end up as a dictionary
  1482. mapping keys to values
  1483. """
  1484. @assert_key_equals_value(salt.utils.dockermod.translate.container)
  1485. def test_sysctls(self):
  1486. """
  1487. Can be passed in several formats but must end up as a dictionary
  1488. mapping keys to values
  1489. """
  1490. @assert_dict(salt.utils.dockermod.translate.container)
  1491. def test_tmpfs(self):
  1492. """
  1493. Can be passed in several formats but must end up as a dictionary
  1494. mapping keys to values
  1495. """
  1496. @assert_bool(salt.utils.dockermod.translate.container)
  1497. def test_tty(self):
  1498. """
  1499. Should be a bool or converted to one
  1500. """
  1501. def test_ulimits(self):
  1502. """
  1503. Input is in the format "name=soft_limit[:hard_limit]", but the API
  1504. wants it in the format
  1505. {'Name': name, 'Soft': soft_limit, 'Hard': hard_limit}
  1506. """
  1507. # Test with and without hard limit
  1508. ulimits = "nofile=1024:2048,nproc=50"
  1509. for val in (ulimits, ulimits.split(",")):
  1510. self.assertEqual(
  1511. salt.utils.dockermod.translate_input(self.translator, ulimits=val,),
  1512. {
  1513. "ulimits": [
  1514. {"Name": "nofile", "Soft": 1024, "Hard": 2048},
  1515. {"Name": "nproc", "Soft": 50, "Hard": 50},
  1516. ]
  1517. },
  1518. )
  1519. # Error case: Invalid format
  1520. with self.assertRaisesRegex(
  1521. CommandExecutionError,
  1522. r"Ulimit definition 'nofile:1024:2048' is not in the format "
  1523. r"type=soft_limit\[:hard_limit\]",
  1524. ):
  1525. salt.utils.dockermod.translate_input(
  1526. self.translator, ulimits="nofile:1024:2048"
  1527. )
  1528. # Error case: Invalid format
  1529. with self.assertRaisesRegex(
  1530. CommandExecutionError,
  1531. r"Limit 'nofile=foo:2048' contains non-numeric value\(s\)",
  1532. ):
  1533. salt.utils.dockermod.translate_input(
  1534. self.translator, ulimits="nofile=foo:2048"
  1535. )
  1536. def test_user(self):
  1537. """
  1538. Must be either username (string) or uid (int). An int passed as a
  1539. string (e.g. '0') should be converted to an int.
  1540. """
  1541. # Username passed as string
  1542. self.assertEqual(
  1543. salt.utils.dockermod.translate_input(self.translator, user="foo"),
  1544. {"user": "foo"},
  1545. )
  1546. for val in (0, "0"):
  1547. self.assertEqual(
  1548. salt.utils.dockermod.translate_input(self.translator, user=val),
  1549. {"user": 0},
  1550. )
  1551. # Error case: non string/int passed
  1552. with self.assertRaisesRegex(
  1553. CommandExecutionError, "Value must be a username or uid"
  1554. ):
  1555. salt.utils.dockermod.translate_input(self.translator, user=["foo"])
  1556. # Error case: negative int passed
  1557. with self.assertRaisesRegex(CommandExecutionError, "'-1' is an invalid uid"):
  1558. salt.utils.dockermod.translate_input(self.translator, user=-1)
  1559. @assert_string(salt.utils.dockermod.translate.container)
  1560. def test_userns_mode(self):
  1561. """
  1562. Should be a bool or converted to one
  1563. """
  1564. @assert_string(salt.utils.dockermod.translate.container)
  1565. def test_volume_driver(self):
  1566. """
  1567. Should be a bool or converted to one
  1568. """
  1569. @assert_stringlist(salt.utils.dockermod.translate.container)
  1570. def test_volumes(self):
  1571. """
  1572. Should be a list of absolute paths
  1573. """
  1574. # Error case: Not an absolute path
  1575. path = os.path.join("foo", "bar", "baz")
  1576. with self.assertRaisesRegex(
  1577. CommandExecutionError,
  1578. "'{0}' is not an absolute path".format(path.replace("\\", "\\\\")),
  1579. ):
  1580. salt.utils.dockermod.translate_input(self.translator, volumes=path)
  1581. @assert_stringlist(salt.utils.dockermod.translate.container)
  1582. def test_volumes_from(self):
  1583. """
  1584. Should be a list of strings or converted to one
  1585. """
  1586. @assert_string(salt.utils.dockermod.translate.container)
  1587. def test_working_dir(self):
  1588. """
  1589. Should be a single absolute path
  1590. """
  1591. # Error case: Not an absolute path
  1592. path = os.path.join("foo", "bar", "baz")
  1593. with self.assertRaisesRegex(
  1594. CommandExecutionError,
  1595. "'{0}' is not an absolute path".format(path.replace("\\", "\\\\")),
  1596. ):
  1597. salt.utils.dockermod.translate_input(self.translator, working_dir=path)
  1598. class TranslateNetworkInputTestCase(TranslateBase):
  1599. """
  1600. Tests for salt.utils.dockermod.translate_input(), invoked using
  1601. salt.utils.dockermod.translate.network as the translator module.
  1602. """
  1603. translator = salt.utils.dockermod.translate.network
  1604. ip_addrs = {
  1605. True: ("10.1.2.3", "::1"),
  1606. False: ("FOO", "0.9.800.1000", "feaz::1", "aj01::feac"),
  1607. }
  1608. @assert_string(salt.utils.dockermod.translate.network)
  1609. def test_driver(self):
  1610. """
  1611. Should be a string or converted to one
  1612. """
  1613. @assert_key_equals_value(salt.utils.dockermod.translate.network)
  1614. def test_options(self):
  1615. """
  1616. Can be passed in several formats but must end up as a dictionary
  1617. mapping keys to values
  1618. """
  1619. @assert_dict(salt.utils.dockermod.translate.network)
  1620. def test_ipam(self):
  1621. """
  1622. Must be a dict
  1623. """
  1624. @assert_bool(salt.utils.dockermod.translate.network)
  1625. def test_check_duplicate(self):
  1626. """
  1627. Should be a bool or converted to one
  1628. """
  1629. @assert_bool(salt.utils.dockermod.translate.network)
  1630. def test_internal(self):
  1631. """
  1632. Should be a bool or converted to one
  1633. """
  1634. @assert_labels(salt.utils.dockermod.translate.network)
  1635. def test_labels(self):
  1636. """
  1637. Can be passed as a list of key=value pairs or a dictionary, and must
  1638. ultimately end up as a dictionary.
  1639. """
  1640. @assert_bool(salt.utils.dockermod.translate.network)
  1641. def test_enable_ipv6(self):
  1642. """
  1643. Should be a bool or converted to one
  1644. """
  1645. @assert_bool(salt.utils.dockermod.translate.network)
  1646. def test_attachable(self):
  1647. """
  1648. Should be a bool or converted to one
  1649. """
  1650. @assert_bool(salt.utils.dockermod.translate.network)
  1651. def test_ingress(self):
  1652. """
  1653. Should be a bool or converted to one
  1654. """
  1655. @assert_string(salt.utils.dockermod.translate.network)
  1656. def test_ipam_driver(self):
  1657. """
  1658. Should be a bool or converted to one
  1659. """
  1660. @assert_key_equals_value(salt.utils.dockermod.translate.network)
  1661. def test_ipam_opts(self):
  1662. """
  1663. Can be passed in several formats but must end up as a dictionary
  1664. mapping keys to values
  1665. """
  1666. def ipam_pools(self):
  1667. """
  1668. Must be a list of dictionaries (not a dictlist)
  1669. """
  1670. good_pool = {
  1671. "subnet": "10.0.0.0/24",
  1672. "iprange": "10.0.0.128/25",
  1673. "gateway": "10.0.0.254",
  1674. "aux_addresses": {
  1675. "foo.bar.tld": "10.0.0.20",
  1676. "hello.world.tld": "10.0.0.21",
  1677. },
  1678. }
  1679. bad_pools = [
  1680. {
  1681. "subnet": "10.0.0.0/33",
  1682. "iprange": "10.0.0.128/25",
  1683. "gateway": "10.0.0.254",
  1684. "aux_addresses": {
  1685. "foo.bar.tld": "10.0.0.20",
  1686. "hello.world.tld": "10.0.0.21",
  1687. },
  1688. },
  1689. {
  1690. "subnet": "10.0.0.0/24",
  1691. "iprange": "foo/25",
  1692. "gateway": "10.0.0.254",
  1693. "aux_addresses": {
  1694. "foo.bar.tld": "10.0.0.20",
  1695. "hello.world.tld": "10.0.0.21",
  1696. },
  1697. },
  1698. {
  1699. "subnet": "10.0.0.0/24",
  1700. "iprange": "10.0.0.128/25",
  1701. "gateway": "10.0.0.256",
  1702. "aux_addresses": {
  1703. "foo.bar.tld": "10.0.0.20",
  1704. "hello.world.tld": "10.0.0.21",
  1705. },
  1706. },
  1707. {
  1708. "subnet": "10.0.0.0/24",
  1709. "iprange": "10.0.0.128/25",
  1710. "gateway": "10.0.0.254",
  1711. "aux_addresses": {
  1712. "foo.bar.tld": "10.0.0.20",
  1713. "hello.world.tld": "999.0.0.21",
  1714. },
  1715. },
  1716. ]
  1717. self.assertEqual(
  1718. salt.utils.dockermod.translate_input(
  1719. self.translator, ipam_pools=[good_pool],
  1720. ),
  1721. {"ipam_pools": [good_pool]},
  1722. )
  1723. for bad_pool in bad_pools:
  1724. with self.assertRaisesRegex(CommandExecutionError, "not a valid"):
  1725. salt.utils.dockermod.translate_input(
  1726. self.translator, ipam_pools=[good_pool, bad_pool]
  1727. )
  1728. @assert_subnet(salt.utils.dockermod.translate.network)
  1729. def test_subnet(self):
  1730. """
  1731. Must be an IPv4 or IPv6 subnet
  1732. """
  1733. @assert_subnet(salt.utils.dockermod.translate.network)
  1734. def test_iprange(self):
  1735. """
  1736. Must be an IPv4 or IPv6 subnet
  1737. """
  1738. def test_gateway(self):
  1739. """
  1740. Must be an IPv4 or IPv6 address
  1741. """
  1742. for val in self.ip_addrs[True]:
  1743. self.assertEqual(
  1744. salt.utils.dockermod.translate_input(
  1745. self.translator, validate_ip_addrs=True, gateway=val,
  1746. ),
  1747. self.apply_defaults({"gateway": val}),
  1748. )
  1749. for val in self.ip_addrs[False]:
  1750. with self.assertRaisesRegex(
  1751. CommandExecutionError, "'{0}' is not a valid IP address".format(val)
  1752. ):
  1753. salt.utils.dockermod.translate_input(
  1754. self.translator, validate_ip_addrs=True, gateway=val,
  1755. )
  1756. self.assertEqual(
  1757. salt.utils.dockermod.translate_input(
  1758. self.translator, validate_ip_addrs=False, gateway=val,
  1759. ),
  1760. self.apply_defaults(
  1761. {
  1762. "gateway": val
  1763. if isinstance(val, six.string_types)
  1764. else six.text_type(val)
  1765. }
  1766. ),
  1767. )
  1768. @assert_key_equals_value(salt.utils.dockermod.translate.network)
  1769. def test_aux_addresses(self):
  1770. """
  1771. Must be a mapping of hostnames to IP addresses
  1772. """
  1773. name = "aux_addresses"
  1774. alias = "aux_address"
  1775. for item in (name, alias):
  1776. for val in self.ip_addrs[True]:
  1777. addresses = {"foo.bar.tld": val}
  1778. self.assertEqual(
  1779. salt.utils.dockermod.translate_input(
  1780. self.translator, validate_ip_addrs=True, **{item: addresses}
  1781. ),
  1782. self.apply_defaults({name: addresses}),
  1783. )
  1784. for val in self.ip_addrs[False]:
  1785. addresses = {"foo.bar.tld": val}
  1786. with self.assertRaisesRegex(
  1787. CommandExecutionError, "'{0}' is not a valid IP address".format(val)
  1788. ):
  1789. salt.utils.dockermod.translate_input(
  1790. self.translator, validate_ip_addrs=True, **{item: addresses}
  1791. )
  1792. self.assertEqual(
  1793. salt.utils.dockermod.translate_input(
  1794. self.translator,
  1795. validate_ip_addrs=False,
  1796. aux_addresses=addresses,
  1797. ),
  1798. self.apply_defaults({name: addresses}),
  1799. )
  1800. class DockerTranslateHelperTestCase(TestCase):
  1801. """
  1802. Tests for a couple helper functions in salt.utils.dockermod.translate
  1803. """
  1804. def test_get_port_def(self):
  1805. """
  1806. Test translation of port definition (1234, '1234/tcp', '1234/udp',
  1807. etc.) into the format which docker-py uses (integer for TCP ports,
  1808. 'port_num/udp' for UDP ports).
  1809. """
  1810. # Test TCP port (passed as int, no protocol passed)
  1811. self.assertEqual(translate_helpers.get_port_def(2222), 2222)
  1812. # Test TCP port (passed as str, no protocol passed)
  1813. self.assertEqual(translate_helpers.get_port_def("2222"), 2222)
  1814. # Test TCP port (passed as str, with protocol passed)
  1815. self.assertEqual(translate_helpers.get_port_def("2222", "tcp"), 2222)
  1816. # Test TCP port (proto passed in port_num, with passed proto ignored).
  1817. # This is a contrived example as we would never invoke the function in
  1818. # this way, but it tests that we are taking the port number from the
  1819. # port_num argument and ignoring the passed protocol.
  1820. self.assertEqual(translate_helpers.get_port_def("2222/tcp", "udp"), 2222)
  1821. # Test UDP port (passed as int)
  1822. self.assertEqual(translate_helpers.get_port_def(2222, "udp"), (2222, "udp"))
  1823. # Test UDP port (passed as string)
  1824. self.assertEqual(translate_helpers.get_port_def("2222", "udp"), (2222, "udp"))
  1825. # Test UDP port (proto passed in port_num
  1826. self.assertEqual(translate_helpers.get_port_def("2222/udp"), (2222, "udp"))
  1827. def test_get_port_range(self):
  1828. """
  1829. Test extracting the start and end of a port range from a port range
  1830. expression (e.g. 4505-4506)
  1831. """
  1832. # Passing a single int should return the start and end as the same value
  1833. self.assertEqual(translate_helpers.get_port_range(2222), (2222, 2222))
  1834. # Same as above but with port number passed as a string
  1835. self.assertEqual(translate_helpers.get_port_range("2222"), (2222, 2222))
  1836. # Passing a port range
  1837. self.assertEqual(translate_helpers.get_port_range("2222-2223"), (2222, 2223))
  1838. # Error case: port range start is greater than end
  1839. with self.assertRaisesRegex(
  1840. ValueError,
  1841. r"Start of port range \(2222\) cannot be greater than end of "
  1842. r"port range \(2221\)",
  1843. ):
  1844. translate_helpers.get_port_range("2222-2221")
  1845. # Error case: non-numeric input
  1846. with self.assertRaisesRegex(
  1847. ValueError, "'2222-bar' is non-numeric or an invalid port range"
  1848. ):
  1849. translate_helpers.get_port_range("2222-bar")