1""" 2Functions to translate input for container creation 3""" 4 5import os 6 7from salt.exceptions import SaltInvocationError 8 9from . import helpers 10 11ALIASES = { 12 "cmd": "command", 13 "cpuset": "cpuset_cpus", 14 "dns_option": "dns_opt", 15 "env": "environment", 16 "expose": "ports", 17 "interactive": "stdin_open", 18 "ipc": "ipc_mode", 19 "label": "labels", 20 "memory": "mem_limit", 21 "memory_swap": "memswap_limit", 22 "publish": "port_bindings", 23 "publish_all": "publish_all_ports", 24 "restart": "restart_policy", 25 "rm": "auto_remove", 26 "sysctl": "sysctls", 27 "security_opts": "security_opt", 28 "ulimit": "ulimits", 29 "user_ns_mode": "userns_mode", 30 "volume": "volumes", 31 "workdir": "working_dir", 32} 33ALIASES_REVMAP = {y: x for x, y in ALIASES.items()} 34 35 36def _merge_keys(kwargs): 37 """ 38 The log_config is a mixture of the CLI options --log-driver and --log-opt 39 (which we support in Salt as log_driver and log_opt, respectively), but it 40 must be submitted to the host config in the format {'Type': log_driver, 41 'Config': log_opt}. So, we need to construct this argument to be passed to 42 the API from those two arguments. 43 """ 44 log_driver = kwargs.pop("log_driver", helpers.NOTSET) 45 log_opt = kwargs.pop("log_opt", helpers.NOTSET) 46 if "log_config" not in kwargs: 47 if log_driver is not helpers.NOTSET or log_opt is not helpers.NOTSET: 48 kwargs["log_config"] = { 49 "Type": log_driver if log_driver is not helpers.NOTSET else "none", 50 "Config": log_opt if log_opt is not helpers.NOTSET else {}, 51 } 52 53 54def _post_processing(kwargs, skip_translate, invalid): 55 """ 56 Additional container-specific post-translation processing 57 """ 58 # Don't allow conflicting options to be set 59 if kwargs.get("port_bindings") is not None and kwargs.get("publish_all_ports"): 60 kwargs.pop("port_bindings") 61 invalid["port_bindings"] = "Cannot be used when publish_all_ports=True" 62 if kwargs.get("hostname") is not None and kwargs.get("network_mode") == "host": 63 kwargs.pop("hostname") 64 invalid["hostname"] = "Cannot be used when network_mode=True" 65 66 # Make sure volumes and ports are defined to match the binds and port_bindings 67 if kwargs.get("binds") is not None and ( 68 skip_translate is True 69 or all(x not in skip_translate for x in ("binds", "volume", "volumes")) 70 ): 71 # Make sure that all volumes defined in "binds" are included in the 72 # "volumes" param. 73 auto_volumes = [] 74 if isinstance(kwargs["binds"], dict): 75 for val in kwargs["binds"].values(): 76 try: 77 if "bind" in val: 78 auto_volumes.append(val["bind"]) 79 except TypeError: 80 continue 81 else: 82 if isinstance(kwargs["binds"], list): 83 auto_volume_defs = kwargs["binds"] 84 else: 85 try: 86 auto_volume_defs = helpers.split(kwargs["binds"]) 87 except AttributeError: 88 auto_volume_defs = [] 89 for val in auto_volume_defs: 90 try: 91 auto_volumes.append(helpers.split(val, ":")[1]) 92 except IndexError: 93 continue 94 if auto_volumes: 95 actual_volumes = kwargs.setdefault("volumes", []) 96 actual_volumes.extend([x for x in auto_volumes if x not in actual_volumes]) 97 # Sort list to make unit tests more reliable 98 actual_volumes.sort() 99 100 if kwargs.get("port_bindings") is not None and all( 101 x not in skip_translate for x in ("port_bindings", "expose", "ports") 102 ): 103 # Make sure that all ports defined in "port_bindings" are included in 104 # the "ports" param. 105 ports_to_bind = list(kwargs["port_bindings"]) 106 if ports_to_bind: 107 ports_to_open = set(kwargs.get("ports", [])) 108 ports_to_open.update([helpers.get_port_def(x) for x in ports_to_bind]) 109 kwargs["ports"] = list(ports_to_open) 110 111 if "ports" in kwargs and all(x not in skip_translate for x in ("expose", "ports")): 112 # TCP ports should only be passed as the port number. Normalize the 113 # input so a port definition of 80/tcp becomes just 80 instead of 114 # (80, 'tcp'). 115 for index, _ in enumerate(kwargs["ports"]): 116 try: 117 if kwargs["ports"][index][1] == "tcp": 118 kwargs["ports"][index] = ports_to_open[index][0] 119 except TypeError: 120 continue 121 122 123# Functions below must match names of docker-py arguments 124def auto_remove(val, **kwargs): # pylint: disable=unused-argument 125 return helpers.translate_bool(val) 126 127 128def binds(val, **kwargs): # pylint: disable=unused-argument 129 """ 130 On the CLI, these are passed as multiple instances of a given CLI option. 131 In Salt, we accept these as a comma-delimited list but the API expects a 132 Python list. 133 """ 134 if not isinstance(val, dict): 135 if not isinstance(val, list): 136 try: 137 val = helpers.split(val) 138 except AttributeError: 139 raise SaltInvocationError( 140 "'{}' is not a dictionary or list of bind definitions".format(val) 141 ) 142 return val 143 144 145def blkio_weight(val, **kwargs): # pylint: disable=unused-argument 146 return helpers.translate_int(val) 147 148 149def blkio_weight_device(val, **kwargs): # pylint: disable=unused-argument 150 """ 151 CLI input is a list of PATH:WEIGHT pairs, but the API expects a list of 152 dictionaries in the format [{'Path': path, 'Weight': weight}] 153 """ 154 val = helpers.map_vals(val, "Path", "Weight") 155 for item in val: 156 try: 157 item["Weight"] = int(item["Weight"]) 158 except (TypeError, ValueError): 159 raise SaltInvocationError( 160 "Weight '{Weight}' for path '{Path}' is not an integer".format(**item) 161 ) 162 return val 163 164 165def cap_add(val, **kwargs): # pylint: disable=unused-argument 166 return helpers.translate_stringlist(val) 167 168 169def cap_drop(val, **kwargs): # pylint: disable=unused-argument 170 return helpers.translate_stringlist(val) 171 172 173def command(val, **kwargs): # pylint: disable=unused-argument 174 return helpers.translate_command(val) 175 176 177def cpuset_cpus(val, **kwargs): # pylint: disable=unused-argument 178 return helpers.translate_str(val) 179 180 181def cpuset_mems(val, **kwargs): # pylint: disable=unused-argument 182 return helpers.translate_str(val) 183 184 185def cpu_group(val, **kwargs): # pylint: disable=unused-argument 186 return helpers.translate_int(val) 187 188 189def cpu_period(val, **kwargs): # pylint: disable=unused-argument 190 return helpers.translate_int(val) 191 192 193def cpu_shares(val, **kwargs): # pylint: disable=unused-argument 194 return helpers.translate_int(val) 195 196 197def detach(val, **kwargs): # pylint: disable=unused-argument 198 return helpers.translate_bool(val) 199 200 201def device_read_bps(val, **kwargs): # pylint: disable=unused-argument 202 return helpers.translate_device_rates(val, numeric_rate=False) 203 204 205def device_read_iops(val, **kwargs): # pylint: disable=unused-argument 206 return helpers.translate_device_rates(val, numeric_rate=True) 207 208 209def device_write_bps(val, **kwargs): # pylint: disable=unused-argument 210 return helpers.translate_device_rates(val, numeric_rate=False) 211 212 213def device_write_iops(val, **kwargs): # pylint: disable=unused-argument 214 return helpers.translate_device_rates(val, numeric_rate=True) 215 216 217def devices(val, **kwargs): # pylint: disable=unused-argument 218 return helpers.translate_stringlist(val) 219 220 221def dns_opt(val, **kwargs): # pylint: disable=unused-argument 222 return helpers.translate_stringlist(val) 223 224 225def dns_search(val, **kwargs): # pylint: disable=unused-argument 226 return helpers.translate_stringlist(val) 227 228 229def dns(val, **kwargs): 230 val = helpers.translate_stringlist(val) 231 if kwargs.get("validate_ip_addrs", True): 232 for item in val: 233 helpers.validate_ip(item) 234 return val 235 236 237def domainname(val, **kwargs): # pylint: disable=unused-argument 238 return helpers.translate_str(val) 239 240 241def entrypoint(val, **kwargs): # pylint: disable=unused-argument 242 return helpers.translate_command(val) 243 244 245def environment(val, **kwargs): # pylint: disable=unused-argument 246 return helpers.translate_key_val(val, delimiter="=") 247 248 249def extra_hosts(val, **kwargs): 250 val = helpers.translate_key_val(val, delimiter=":") 251 if kwargs.get("validate_ip_addrs", True): 252 for key in val: 253 helpers.validate_ip(val[key]) 254 return val 255 256 257def group_add(val, **kwargs): # pylint: disable=unused-argument 258 return helpers.translate_stringlist(val) 259 260 261def host_config(val, **kwargs): # pylint: disable=unused-argument 262 return helpers.translate_dict(val) 263 264 265def hostname(val, **kwargs): # pylint: disable=unused-argument 266 return helpers.translate_str(val) 267 268 269def ipc_mode(val, **kwargs): # pylint: disable=unused-argument 270 return helpers.translate_str(val) 271 272 273def isolation(val, **kwargs): # pylint: disable=unused-argument 274 return helpers.translate_str(val) 275 276 277def labels(val, **kwargs): # pylint: disable=unused-argument 278 return helpers.translate_labels(val) 279 280 281def links(val, **kwargs): # pylint: disable=unused-argument 282 return helpers.translate_key_val(val, delimiter=":") 283 284 285def log_driver(val, **kwargs): # pylint: disable=unused-argument 286 return helpers.translate_str(val) 287 288 289def log_opt(val, **kwargs): # pylint: disable=unused-argument 290 return helpers.translate_key_val(val, delimiter="=") 291 292 293def lxc_conf(val, **kwargs): # pylint: disable=unused-argument 294 return helpers.translate_key_val(val, delimiter="=") 295 296 297def mac_address(val, **kwargs): # pylint: disable=unused-argument 298 return helpers.translate_str(val) 299 300 301def mem_limit(val, **kwargs): # pylint: disable=unused-argument 302 return helpers.translate_bytes(val) 303 304 305def mem_swappiness(val, **kwargs): # pylint: disable=unused-argument 306 return helpers.translate_int(val) 307 308 309def memswap_limit(val, **kwargs): # pylint: disable=unused-argument 310 return helpers.translate_bytes(val) 311 312 313def name(val, **kwargs): # pylint: disable=unused-argument 314 return helpers.translate_str(val) 315 316 317def network_disabled(val, **kwargs): # pylint: disable=unused-argument 318 return helpers.translate_bool(val) 319 320 321def network_mode(val, **kwargs): # pylint: disable=unused-argument 322 return helpers.translate_str(val) 323 324 325def oom_kill_disable(val, **kwargs): # pylint: disable=unused-argument 326 return helpers.translate_bool(val) 327 328 329def oom_score_adj(val, **kwargs): # pylint: disable=unused-argument 330 return helpers.translate_int(val) 331 332 333def pid_mode(val, **kwargs): # pylint: disable=unused-argument 334 return helpers.translate_str(val) 335 336 337def pids_limit(val, **kwargs): # pylint: disable=unused-argument 338 return helpers.translate_int(val) 339 340 341def port_bindings(val, **kwargs): 342 """ 343 On the CLI, these are passed as multiple instances of a given CLI option. 344 In Salt, we accept these as a comma-delimited list but the API expects a 345 Python dictionary mapping ports to their bindings. The format the API 346 expects is complicated depending on whether or not the external port maps 347 to a different internal port, or if the port binding is for UDP instead of 348 TCP (the default). For reference, see the "Port bindings" section in the 349 docker-py documentation at the following URL: 350 http://docker-py.readthedocs.io/en/stable/api.html 351 """ 352 validate_ip_addrs = kwargs.get("validate_ip_addrs", True) 353 if not isinstance(val, dict): 354 if not isinstance(val, list): 355 try: 356 val = helpers.split(val) 357 except AttributeError: 358 val = helpers.split(str(val)) 359 360 for idx, item in enumerate(val): 361 if not isinstance(item, str): 362 val[idx] = str(item) 363 364 def _format_port(port_num, proto): 365 return str(port_num) + "/udp" if proto.lower() == "udp" else port_num 366 367 bindings = {} 368 for binding in val: 369 bind_parts = helpers.split(binding, ":") 370 num_bind_parts = len(bind_parts) 371 if num_bind_parts == 1: 372 # Single port or port range being passed through (no 373 # special mapping) 374 container_port = str(bind_parts[0]) 375 if container_port == "": 376 raise SaltInvocationError("Empty port binding definition found") 377 container_port, _, proto = container_port.partition("/") 378 try: 379 start, end = helpers.get_port_range(container_port) 380 except ValueError as exc: 381 # Using __str__() to avoid deprecation warning for using 382 # the message attribute of the ValueError. 383 raise SaltInvocationError(exc.__str__()) 384 bind_vals = [ 385 (_format_port(port_num, proto), None) 386 for port_num in range(start, end + 1) 387 ] 388 elif num_bind_parts == 2: 389 if bind_parts[0] == "": 390 raise SaltInvocationError( 391 "Empty host port in port binding definition '{}'".format( 392 binding 393 ) 394 ) 395 if bind_parts[1] == "": 396 raise SaltInvocationError( 397 "Empty container port in port binding definition '{}'".format( 398 binding 399 ) 400 ) 401 container_port, _, proto = bind_parts[1].partition("/") 402 try: 403 cport_start, cport_end = helpers.get_port_range(container_port) 404 hport_start, hport_end = helpers.get_port_range(bind_parts[0]) 405 except ValueError as exc: 406 # Using __str__() to avoid deprecation warning for 407 # using the message attribute of the ValueError. 408 raise SaltInvocationError(exc.__str__()) 409 if (hport_end - hport_start) != (cport_end - cport_start): 410 # Port range is mismatched 411 raise SaltInvocationError( 412 "Host port range ({}) does not have the same " 413 "number of ports as the container port range " 414 "({})".format(bind_parts[0], container_port) 415 ) 416 cport_list = list(range(cport_start, cport_end + 1)) 417 hport_list = list(range(hport_start, hport_end + 1)) 418 bind_vals = [ 419 (_format_port(item, proto), hport_list[ind]) 420 for ind, item in enumerate(cport_list) 421 ] 422 elif num_bind_parts == 3: 423 host_ip, host_port = bind_parts[0:2] 424 if validate_ip_addrs: 425 helpers.validate_ip(host_ip) 426 container_port, _, proto = bind_parts[2].partition("/") 427 try: 428 cport_start, cport_end = helpers.get_port_range(container_port) 429 except ValueError as exc: 430 # Using __str__() to avoid deprecation warning for 431 # using the message attribute of the ValueError. 432 raise SaltInvocationError(exc.__str__()) 433 cport_list = list(range(cport_start, cport_end + 1)) 434 if host_port == "": 435 hport_list = [None] * len(cport_list) 436 else: 437 try: 438 hport_start, hport_end = helpers.get_port_range(host_port) 439 except ValueError as exc: 440 # Using __str__() to avoid deprecation warning for 441 # using the message attribute of the ValueError. 442 raise SaltInvocationError(exc.__str__()) 443 hport_list = list(range(hport_start, hport_end + 1)) 444 445 if (hport_end - hport_start) != (cport_end - cport_start): 446 # Port range is mismatched 447 raise SaltInvocationError( 448 "Host port range ({}) does not have the same " 449 "number of ports as the container port range " 450 "({})".format(host_port, container_port) 451 ) 452 453 bind_vals = [ 454 ( 455 _format_port(val, proto), 456 (host_ip,) 457 if hport_list[idx] is None 458 else (host_ip, hport_list[idx]), 459 ) 460 for idx, val in enumerate(cport_list) 461 ] 462 else: 463 raise SaltInvocationError( 464 "'{}' is an invalid port binding definition (at most " 465 "3 components are allowed, found {})".format( 466 binding, num_bind_parts 467 ) 468 ) 469 470 for cport, bind_def in bind_vals: 471 if cport not in bindings: 472 bindings[cport] = bind_def 473 else: 474 if isinstance(bindings[cport], list): 475 # Append to existing list of bindings for this 476 # container port. 477 bindings[cport].append(bind_def) 478 else: 479 bindings[cport] = [bindings[cport], bind_def] 480 for idx, val in enumerate(bindings[cport]): 481 if val is None: 482 # Now that we are adding multiple 483 # bindings 484 try: 485 # Convert 1234/udp to 1234 486 bindings[cport][idx] = int(cport.split("/")[0]) 487 except AttributeError: 488 # Port was tcp, the AttributeError 489 # signifies that the split failed 490 # because the port number was 491 # already defined as an integer. 492 # Just use the cport. 493 bindings[cport][idx] = cport 494 val = bindings 495 return val 496 497 498def ports(val, **kwargs): # pylint: disable=unused-argument 499 """ 500 Like cap_add, cap_drop, etc., this option can be specified multiple times, 501 and each time can be a port number or port range. Ultimately, the API 502 expects a list, but elements in the list are ints when the port is TCP, and 503 a tuple (port_num, 'udp') when the port is UDP. 504 """ 505 if not isinstance(val, list): 506 try: 507 val = helpers.split(val) 508 except AttributeError: 509 if isinstance(val, int): 510 val = [val] 511 else: 512 raise SaltInvocationError( 513 "'{}' is not a valid port definition".format(val) 514 ) 515 new_ports = set() 516 for item in val: 517 if isinstance(item, int): 518 new_ports.add(item) 519 continue 520 try: 521 item, _, proto = item.partition("/") 522 except AttributeError: 523 raise SaltInvocationError( 524 "'{}' is not a valid port definition".format(item) 525 ) 526 try: 527 range_start, range_end = helpers.get_port_range(item) 528 except ValueError as exc: 529 # Using __str__() to avoid deprecation warning for using 530 # the "message" attribute of the ValueError. 531 raise SaltInvocationError(exc.__str__()) 532 new_ports.update( 533 [helpers.get_port_def(x, proto) for x in range(range_start, range_end + 1)] 534 ) 535 return list(new_ports) 536 537 538def privileged(val, **kwargs): # pylint: disable=unused-argument 539 return helpers.translate_bool(val) 540 541 542def publish_all_ports(val, **kwargs): # pylint: disable=unused-argument 543 return helpers.translate_bool(val) 544 545 546def read_only(val, **kwargs): # pylint: disable=unused-argument 547 return helpers.translate_bool(val) 548 549 550def restart_policy(val, **kwargs): # pylint: disable=unused-argument 551 """ 552 CLI input is in the format NAME[:RETRY_COUNT] but the API expects {'Name': 553 name, 'MaximumRetryCount': retry_count}. We will use the 'fill' kwarg here 554 to make sure the mapped result uses '0' for the count if this optional 555 value was omitted. 556 """ 557 val = helpers.map_vals(val, "Name", "MaximumRetryCount", fill="0") 558 # map_vals() converts the input into a list of dicts, but the API 559 # wants just a dict, so extract the value from the single-element 560 # list. If there was more than one element in the list, then 561 # invalid input was passed (i.e. a comma-separated list, when what 562 # we wanted was a single value). 563 if len(val) != 1: 564 raise SaltInvocationError("Only one policy is permitted") 565 val = val[0] 566 try: 567 # The count needs to be an integer 568 val["MaximumRetryCount"] = int(val["MaximumRetryCount"]) 569 except (TypeError, ValueError): 570 # Non-numeric retry count passed 571 raise SaltInvocationError( 572 "Retry count '{}' is non-numeric".format(val["MaximumRetryCount"]) 573 ) 574 return val 575 576 577def security_opt(val, **kwargs): # pylint: disable=unused-argument 578 return helpers.translate_stringlist(val) 579 580 581def shm_size(val, **kwargs): # pylint: disable=unused-argument 582 return helpers.translate_bytes(val) 583 584 585def stdin_open(val, **kwargs): # pylint: disable=unused-argument 586 return helpers.translate_bool(val) 587 588 589def stop_signal(val, **kwargs): # pylint: disable=unused-argument 590 return helpers.translate_str(val) 591 592 593def stop_timeout(val, **kwargs): # pylint: disable=unused-argument 594 return helpers.translate_int(val) 595 596 597def storage_opt(val, **kwargs): # pylint: disable=unused-argument 598 return helpers.translate_key_val(val, delimiter="=") 599 600 601def sysctls(val, **kwargs): # pylint: disable=unused-argument 602 return helpers.translate_key_val(val, delimiter="=") 603 604 605def tmpfs(val, **kwargs): # pylint: disable=unused-argument 606 return helpers.translate_dict(val) 607 608 609def tty(val, **kwargs): # pylint: disable=unused-argument 610 return helpers.translate_bool(val) 611 612 613def ulimits(val, **kwargs): # pylint: disable=unused-argument 614 val = helpers.translate_stringlist(val) 615 for idx, item in enumerate(val): 616 if not isinstance(item, dict): 617 try: 618 ulimit_name, limits = helpers.split(item, "=", 1) 619 comps = helpers.split(limits, ":", 1) 620 except (AttributeError, ValueError): 621 raise SaltInvocationError( 622 "Ulimit definition '{}' is not in the format " 623 "type=soft_limit[:hard_limit]".format(item) 624 ) 625 if len(comps) == 1: 626 comps *= 2 627 soft_limit, hard_limit = comps 628 try: 629 val[idx] = { 630 "Name": ulimit_name, 631 "Soft": int(soft_limit), 632 "Hard": int(hard_limit), 633 } 634 except (TypeError, ValueError): 635 raise SaltInvocationError( 636 "Limit '{}' contains non-numeric value(s)".format(item) 637 ) 638 return val 639 640 641def user(val, **kwargs): # pylint: disable=unused-argument 642 """ 643 This can be either a string or a numeric uid 644 """ 645 if not isinstance(val, int): 646 # Try to convert to integer. This will fail if the value is a 647 # username. This is OK, as we check below to make sure that the 648 # value is either a string or integer. Trying to convert to an 649 # integer first though will allow us to catch the edge case in 650 # which a quoted uid is passed (e.g. '1000'). 651 try: 652 val = int(val) 653 except (TypeError, ValueError): 654 pass 655 if not isinstance(val, (int, str)): 656 raise SaltInvocationError("Value must be a username or uid") 657 elif isinstance(val, int) and val < 0: 658 raise SaltInvocationError("'{}' is an invalid uid".format(val)) 659 return val 660 661 662def userns_mode(val, **kwargs): # pylint: disable=unused-argument 663 return helpers.translate_str(val) 664 665 666def volume_driver(val, **kwargs): # pylint: disable=unused-argument 667 return helpers.translate_str(val) 668 669 670def volumes(val, **kwargs): # pylint: disable=unused-argument 671 """ 672 Should be a list of absolute paths 673 """ 674 val = helpers.translate_stringlist(val) 675 for item in val: 676 if not os.path.isabs(item): 677 raise SaltInvocationError("'{}' is not an absolute path".format(item)) 678 return val 679 680 681def volumes_from(val, **kwargs): # pylint: disable=unused-argument 682 return helpers.translate_stringlist(val) 683 684 685def working_dir(val, **kwargs): # pylint: disable=unused-argument 686 """ 687 Must be an absolute path 688 """ 689 try: 690 is_abs = os.path.isabs(val) 691 except AttributeError: 692 is_abs = False 693 if not is_abs: 694 raise SaltInvocationError("'{}' is not an absolute path".format(val)) 695 return val 696