1"""
2Management of zc.buildout
3
4.. versionadded:: 2014.1.0
5
6.. _`minitage's buildout maker`: https://github.com/minitage/minitage/blob/master/src/minitage/core/makers/buildout.py
7
8This module is inspired by `minitage's buildout maker`_
9
10.. note::
11
12    The zc.buildout integration is still in beta; the API is subject to change
13
14General notes
15-------------
16
17You have those following methods:
18
19* upgrade_bootstrap
20* bootstrap
21* run_buildout
22* buildout
23"""
24
25
26import copy
27import logging
28import os
29import re
30import sys
31import traceback
32import urllib.request
33
34import salt.utils.files
35import salt.utils.path
36import salt.utils.stringutils
37from salt.exceptions import CommandExecutionError
38
39INVALID_RESPONSE = "Unexpected response from buildout"
40VALID_RESPONSE = ""
41NOTSET = object()
42HR = "{}\n".format("-" * 80)
43RE_F = re.S | re.M | re.U
44BASE_STATUS = {
45    "status": None,
46    "logs": [],
47    "comment": "",
48    "out": None,
49    "logs_by_level": {},
50    "outlog": None,
51    "outlog_by_level": None,
52}
53_URL_VERSIONS = {
54    1: "http://downloads.buildout.org/1/bootstrap.py",
55    2: "http://downloads.buildout.org/2/bootstrap.py",
56}
57DEFAULT_VER = 2
58_logger = logging.getLogger(__name__)
59
60# Define the module's virtual name
61__virtualname__ = "buildout"
62
63
64def __virtual__():
65    """
66    Only load if buildout libs are present
67    """
68    return __virtualname__
69
70
71def _salt_callback(func, **kwargs):
72    LOG.clear()
73
74    def _call_callback(*a, **kw):
75        # cleanup the module kwargs before calling it from the
76        # decorator
77        kw = copy.deepcopy(kw)
78        for k in [ar for ar in kw if "__pub" in ar]:
79            kw.pop(k, None)
80        st = BASE_STATUS.copy()
81        directory = kw.get("directory", ".")
82        onlyif = kw.get("onlyif", None)
83        unless = kw.get("unless", None)
84        runas = kw.get("runas", None)
85        env = kw.get("env", ())
86        status = BASE_STATUS.copy()
87        try:
88            # may rise _ResultTransmission
89            status = _check_onlyif_unless(
90                onlyif, unless, directory=directory, runas=runas, env=env
91            )
92            # if onlyif/unless returns, we are done
93            if status is None:
94                status = BASE_STATUS.copy()
95                comment, st = "", True
96                out = func(*a, **kw)
97                # we may have already final statuses not to be touched
98                # merged_statuses flag is there to check that !
99                if not isinstance(out, dict):
100                    status = _valid(status, out=out)
101                else:
102                    if out.get("merged_statuses", False):
103                        status = out
104                    else:
105                        status = _set_status(
106                            status,
107                            status=out.get("status", True),
108                            comment=out.get("comment", ""),
109                            out=out.get("out", out),
110                        )
111        except Exception:  # pylint: disable=broad-except
112            trace = traceback.format_exc()
113            LOG.error(trace)
114            _invalid(status)
115        LOG.clear()
116        # before returning, trying to compact the log output
117        for k in ["comment", "out", "outlog"]:
118            if status[k] and isinstance(status[k], str):
119                status[k] = "\n".join(
120                    [log for log in status[k].split("\n") if log.strip()]
121                )
122        return status
123
124    _call_callback.__doc__ = func.__doc__
125    return _call_callback
126
127
128class _Logger:
129    levels = ("info", "warn", "debug", "error")
130
131    def __init__(self):
132        self._msgs = []
133        self._by_level = {}
134
135    def _log(self, level, msg):
136        if not isinstance(msg, str):
137            msg = msg.decode("utf-8")
138        if level not in self._by_level:
139            self._by_level[level] = []
140        self._msgs.append((level, msg))
141        self._by_level[level].append(msg)
142
143    def debug(self, msg):
144        self._log("debug", msg)
145
146    def info(self, msg):
147        self._log("info", msg)
148
149    def error(self, msg):
150        self._log("error", msg)
151
152    def warn(self, msg):
153        self._log("warn", msg)
154
155    warning = warn
156
157    def clear(self):
158        for i in self._by_level:
159            self._by_level[i] = []
160        for i in self._msgs[:]:
161            self._msgs.pop()
162
163    def get_logs(self, level):
164        return self._by_level.get(level, [])
165
166    @property
167    def messages(self):
168        return self._msgs
169
170    @property
171    def by_level(self):
172        return self._by_level
173
174
175LOG = _Logger()
176
177
178def _encode_status(status):
179    if status["out"] is None:
180        status["out"] = None
181    else:
182        status["out"] = salt.utils.stringutils.to_unicode(status["out"])
183    status["outlog_by_level"] = salt.utils.stringutils.to_unicode(
184        status["outlog_by_level"]
185    )
186    if status["logs"]:
187        for i, data in enumerate(status["logs"][:]):
188            status["logs"][i] = (data[0], salt.utils.stringutils.to_unicode(data[1]))
189        for logger in "error", "warn", "info", "debug":
190            logs = status["logs_by_level"].get(logger, [])[:]
191            if logs:
192                for i, log in enumerate(logs):
193                    status["logs_by_level"][logger][
194                        i
195                    ] = salt.utils.stringutils.to_unicode(log)
196    return status
197
198
199def _set_status(m, comment=INVALID_RESPONSE, status=False, out=None):
200    """
201    Assign status data to a dict.
202    """
203    m["out"] = out
204    m["status"] = status
205    m["logs"] = LOG.messages[:]
206    m["logs_by_level"] = LOG.by_level.copy()
207    outlog, outlog_by_level = "", ""
208    m["comment"] = comment
209    if out and isinstance(out, str):
210        outlog += HR
211        outlog += "OUTPUT:\n"
212        outlog += "{}\n".format(salt.utils.stringutils.to_unicode(out))
213        outlog += HR
214    if m["logs"]:
215        outlog += HR
216        outlog += "Log summary:\n"
217        outlog += HR
218        outlog_by_level += HR
219        outlog_by_level += "Log summary by level:\n"
220        outlog_by_level += HR
221        for level, msg in m["logs"]:
222            outlog += "\n{}: {}\n".format(
223                level.upper(), salt.utils.stringutils.to_unicode(msg)
224            )
225        for logger in "error", "warn", "info", "debug":
226            logs = m["logs_by_level"].get(logger, [])
227            if logs:
228                outlog_by_level += "\n{}:\n".format(logger.upper())
229                for idx, log in enumerate(logs[:]):
230                    logs[idx] = salt.utils.stringutils.to_unicode(log)
231                outlog_by_level += "\n".join(logs)
232                outlog_by_level += "\n"
233        outlog += HR
234    m["outlog"] = outlog
235    m["outlog_by_level"] = outlog_by_level
236    return _encode_status(m)
237
238
239def _invalid(m, comment=INVALID_RESPONSE, out=None):
240    """
241    Return invalid status.
242    """
243    return _set_status(m, status=False, comment=comment, out=out)
244
245
246def _valid(m, comment=VALID_RESPONSE, out=None):
247    """
248    Return valid status.
249    """
250    return _set_status(m, status=True, comment=comment, out=out)
251
252
253def _Popen(
254    command,
255    output=False,
256    directory=".",
257    runas=None,
258    env=(),
259    exitcode=0,
260    use_vt=False,
261    loglevel=None,
262):
263    """
264    Run a command.
265
266    output
267        return output if true
268
269    directory
270        directory to execute in
271
272    runas
273        user used to run buildout as
274
275    env
276        environment variables to set when running
277
278    exitcode
279        fails if cmd does not return this exit code
280        (set to None to disable check)
281
282    use_vt
283        Use the new salt VT to stream output [experimental]
284
285    """
286    ret = None
287    directory = os.path.abspath(directory)
288    if isinstance(command, list):
289        command = " ".join(command)
290    LOG.debug("Running {}".format(command))  # pylint: disable=str-format-in-logging
291    if not loglevel:
292        loglevel = "debug"
293    ret = __salt__["cmd.run_all"](
294        command,
295        cwd=directory,
296        output_loglevel=loglevel,
297        runas=runas,
298        env=env,
299        use_vt=use_vt,
300        python_shell=False,
301    )
302    out = ret["stdout"] + "\n\n" + ret["stderr"]
303    if (exitcode is not None) and (ret["retcode"] != exitcode):
304        raise _BuildoutError(out)
305    ret["output"] = out
306    if output:
307        ret = out
308    return ret
309
310
311class _BuildoutError(CommandExecutionError):
312    """
313    General Buildout Error.
314    """
315
316
317def _has_old_distribute(python=sys.executable, runas=None, env=()):
318    old_distribute = False
319    try:
320        cmd = [
321            python,
322            "-c",
323            "'import pkg_resources;"
324            "print pkg_resources."
325            'get_distribution("distribute").location\'',
326        ]
327        ret = _Popen(cmd, runas=runas, env=env, output=True)
328        if "distribute-0.6" in ret:
329            old_distribute = True
330    except Exception:  # pylint: disable=broad-except
331        old_distribute = False
332    return old_distribute
333
334
335def _has_setuptools7(python=sys.executable, runas=None, env=()):
336    new_st = False
337    try:
338        cmd = [
339            python,
340            "-c",
341            "'import pkg_resources;"
342            "print not pkg_resources."
343            'get_distribution("setuptools").version.startswith("0.6")\'',
344        ]
345        ret = _Popen(cmd, runas=runas, env=env, output=True)
346        if "true" in ret.lower():
347            new_st = True
348    except Exception:  # pylint: disable=broad-except
349        new_st = False
350    return new_st
351
352
353def _find_cfgs(path, cfgs=None):
354    """
355    Find all buildout configs in a subdirectory.
356    only buildout.cfg and etc/buildout.cfg are valid in::
357
358    path
359        directory where to start to search
360
361    cfg
362        a optional list to append to
363
364            .
365            ├── buildout.cfg
366            ├── etc
367            │   └── buildout.cfg
368            ├── foo
369            │   └── buildout.cfg
370            └── var
371                └── buildout.cfg
372    """
373    ignored = ["var", "parts"]
374    dirs = []
375    if not cfgs:
376        cfgs = []
377    for i in os.listdir(path):
378        fi = os.path.join(path, i)
379        if fi.endswith(".cfg") and os.path.isfile(fi):
380            cfgs.append(fi)
381        if os.path.isdir(fi) and (i not in ignored):
382            dirs.append(fi)
383    for fpath in dirs:
384        for p, ids, ifs in salt.utils.path.os_walk(fpath):
385            for i in ifs:
386                if i.endswith(".cfg"):
387                    cfgs.append(os.path.join(p, i))
388    return cfgs
389
390
391def _get_bootstrap_content(directory="."):
392    """
393    Get the current bootstrap.py script content
394    """
395    try:
396        with salt.utils.files.fopen(
397            os.path.join(os.path.abspath(directory), "bootstrap.py")
398        ) as fic:
399            oldcontent = salt.utils.stringutils.to_unicode(fic.read())
400    except OSError:
401        oldcontent = ""
402    return oldcontent
403
404
405def _get_buildout_ver(directory="."):
406    """Check for buildout versions.
407
408    In any cases, check for a version pinning
409    Also check for buildout.dumppickedversions which is buildout1 specific
410    Also check for the version targeted by the local bootstrap file
411    Take as default buildout2
412
413    directory
414        directory to execute in
415    """
416    directory = os.path.abspath(directory)
417    buildoutver = 2
418    try:
419        files = _find_cfgs(directory)
420        for f in files:
421            with salt.utils.files.fopen(f) as fic:
422                buildout1re = re.compile(r"^zc\.buildout\s*=\s*1", RE_F)
423                dfic = salt.utils.stringutils.to_unicode(fic.read())
424                if ("buildout.dumppick" in dfic) or (buildout1re.search(dfic)):
425                    buildoutver = 1
426        bcontent = _get_bootstrap_content(directory)
427        if (
428            "--download-base" in bcontent
429            or "--setup-source" in bcontent
430            or "--distribute" in bcontent
431        ):
432            buildoutver = 1
433    except OSError:
434        pass
435    return buildoutver
436
437
438def _get_bootstrap_url(directory):
439    """
440    Get the most appropriate download URL for the bootstrap script.
441
442    directory
443        directory to execute in
444
445    """
446    v = _get_buildout_ver(directory)
447    return _URL_VERSIONS.get(v, _URL_VERSIONS[DEFAULT_VER])
448
449
450def _dot_buildout(directory):
451    """
452    Get the local marker directory.
453
454    directory
455        directory to execute in
456    """
457    return os.path.join(os.path.abspath(directory), ".buildout")
458
459
460@_salt_callback
461def upgrade_bootstrap(
462    directory=".",
463    onlyif=None,
464    unless=None,
465    runas=None,
466    env=(),
467    offline=False,
468    buildout_ver=None,
469):
470    """
471    Upgrade current bootstrap.py with the last released one.
472
473    Indeed, when we first run a buildout, a common source of problem
474    is to have a locally stale bootstrap, we just try to grab a new copy
475
476    directory
477        directory to execute in
478
479    offline
480        are we executing buildout in offline mode
481
482    buildout_ver
483        forcing to use a specific buildout version (1 | 2)
484
485    onlyif
486        Only execute cmd if statement on the host return 0
487
488    unless
489        Do not execute cmd if statement on the host return 0
490
491    CLI Example:
492
493    .. code-block:: bash
494
495        salt '*' buildout.upgrade_bootstrap /srv/mybuildout
496    """
497    if buildout_ver:
498        booturl = _URL_VERSIONS[buildout_ver]
499    else:
500        buildout_ver = _get_buildout_ver(directory)
501        booturl = _get_bootstrap_url(directory)
502    LOG.debug("Using {}".format(booturl))  # pylint: disable=str-format-in-logging
503    # try to download an up-to-date bootstrap
504    # set defaulttimeout
505    # and add possible content
506    directory = os.path.abspath(directory)
507    b_py = os.path.join(directory, "bootstrap.py")
508    comment = ""
509    try:
510        oldcontent = _get_bootstrap_content(directory)
511        dbuild = _dot_buildout(directory)
512        data = oldcontent
513        updated = False
514        dled = False
515        if not offline:
516            try:
517                if not os.path.isdir(dbuild):
518                    os.makedirs(dbuild)
519                # only try to download once per buildout checkout
520                with salt.utils.files.fopen(
521                    os.path.join(dbuild, "{}.updated_bootstrap".format(buildout_ver))
522                ):
523                    pass
524            except OSError:
525                LOG.info("Bootstrap updated from repository")
526                data = urllib.request.urlopen(booturl).read()
527                updated = True
528                dled = True
529        if "socket.setdefaulttimeout" not in data:
530            updated = True
531            ldata = data.splitlines()
532            ldata.insert(1, "import socket;socket.setdefaulttimeout(2)")
533            data = "\n".join(ldata)
534        if updated:
535            comment = "Bootstrap updated"
536            with salt.utils.files.fopen(b_py, "w") as fic:
537                fic.write(salt.utils.stringutils.to_str(data))
538        if dled:
539            with salt.utils.files.fopen(
540                os.path.join(dbuild, "{}.updated_bootstrap".format(buildout_ver)), "w"
541            ) as afic:
542                afic.write("foo")
543    except OSError:
544        if oldcontent:
545            with salt.utils.files.fopen(b_py, "w") as fic:
546                fic.write(salt.utils.stringutils.to_str(oldcontent))
547
548    return {"comment": comment}
549
550
551@_salt_callback
552def bootstrap(
553    directory=".",
554    config="buildout.cfg",
555    python=sys.executable,
556    onlyif=None,
557    unless=None,
558    runas=None,
559    env=(),
560    distribute=None,
561    buildout_ver=None,
562    test_release=False,
563    offline=False,
564    new_st=None,
565    use_vt=False,
566    loglevel=None,
567):
568    """
569    Run the buildout bootstrap dance (python bootstrap.py).
570
571    directory
572        directory to execute in
573
574    config
575        alternative buildout configuration file to use
576
577    runas
578        User used to run buildout as
579
580    env
581        environment variables to set when running
582
583    buildout_ver
584        force a specific buildout version (1 | 2)
585
586    test_release
587        buildout accept test release
588
589    offline
590        are we executing buildout in offline mode
591
592    distribute
593        Forcing use of distribute
594
595    new_st
596        Forcing use of setuptools >= 0.7
597
598    python
599        path to a python executable to use in place of default (salt one)
600
601    onlyif
602        Only execute cmd if statement on the host return 0
603
604    unless
605        Do not execute cmd if statement on the host return 0
606
607    use_vt
608        Use the new salt VT to stream output [experimental]
609
610    CLI Example:
611
612    .. code-block:: bash
613
614        salt '*' buildout.bootstrap /srv/mybuildout
615    """
616    directory = os.path.abspath(directory)
617    dbuild = _dot_buildout(directory)
618    bootstrap_args = ""
619    has_distribute = _has_old_distribute(python=python, runas=runas, env=env)
620    has_new_st = _has_setuptools7(python=python, runas=runas, env=env)
621    if has_distribute and has_new_st and not distribute and new_st:
622        new_st = True
623        distribute = False
624    if has_distribute and has_new_st and not distribute and new_st:
625        new_st = True
626        distribute = False
627    if has_distribute and has_new_st and distribute and not new_st:
628        new_st = True
629        distribute = False
630    if has_distribute and has_new_st and not distribute and not new_st:
631        new_st = True
632        distribute = False
633
634    if not has_distribute and has_new_st and not distribute and new_st:
635        new_st = True
636        distribute = False
637    if not has_distribute and has_new_st and not distribute and new_st:
638        new_st = True
639        distribute = False
640    if not has_distribute and has_new_st and distribute and not new_st:
641        new_st = True
642        distribute = False
643    if not has_distribute and has_new_st and not distribute and not new_st:
644        new_st = True
645        distribute = False
646
647    if has_distribute and not has_new_st and not distribute and new_st:
648        new_st = True
649        distribute = False
650    if has_distribute and not has_new_st and not distribute and new_st:
651        new_st = True
652        distribute = False
653    if has_distribute and not has_new_st and distribute and not new_st:
654        new_st = False
655        distribute = True
656    if has_distribute and not has_new_st and not distribute and not new_st:
657        new_st = False
658        distribute = True
659
660    if not has_distribute and not has_new_st and not distribute and new_st:
661        new_st = True
662        distribute = False
663    if not has_distribute and not has_new_st and not distribute and new_st:
664        new_st = True
665        distribute = False
666    if not has_distribute and not has_new_st and distribute and not new_st:
667        new_st = False
668        distribute = True
669    if not has_distribute and not has_new_st and not distribute and not new_st:
670        new_st = True
671        distribute = False
672
673    if new_st and distribute:
674        distribute = False
675    if new_st:
676        distribute = False
677        LOG.warning("Forcing to use setuptools as we have setuptools >= 0.7")
678    if distribute:
679        new_st = False
680        if buildout_ver == 1:
681            LOG.warning("Using distribute !")
682            bootstrap_args += " --distribute"
683    if not os.path.isdir(dbuild):
684        os.makedirs(dbuild)
685    upgrade_bootstrap(directory, offline=offline, buildout_ver=buildout_ver)
686    # be sure which buildout bootstrap we have
687    b_py = os.path.join(directory, "bootstrap.py")
688    with salt.utils.files.fopen(b_py) as fic:
689        content = salt.utils.stringutils.to_unicode(fic.read())
690    if (test_release is not False) and " --accept-buildout-test-releases" in content:
691        bootstrap_args += " --accept-buildout-test-releases"
692    if config and '"-c"' in content:
693        bootstrap_args += " -c {}".format(config)
694    # be sure that the bootstrap belongs to the running user
695    try:
696        if runas:
697            uid = __salt__["user.info"](runas)["uid"]
698            gid = __salt__["user.info"](runas)["gid"]
699            os.chown("bootstrap.py", uid, gid)
700    except OSError as exc:
701        # don't block here, try to execute it if can pass
702        _logger.error(
703            "BUILDOUT bootstrap permissions error: %s",
704            exc,
705            exc_info=_logger.isEnabledFor(logging.DEBUG),
706        )
707    cmd = "{} bootstrap.py {}".format(python, bootstrap_args)
708    ret = _Popen(
709        cmd, directory=directory, runas=runas, loglevel=loglevel, env=env, use_vt=use_vt
710    )
711    output = ret["output"]
712    return {"comment": cmd, "out": output}
713
714
715@_salt_callback
716def run_buildout(
717    directory=".",
718    config="buildout.cfg",
719    parts=None,
720    onlyif=None,
721    unless=None,
722    offline=False,
723    newest=True,
724    runas=None,
725    env=(),
726    verbose=False,
727    debug=False,
728    use_vt=False,
729    loglevel=None,
730):
731    """
732    Run a buildout in a directory.
733
734    directory
735        directory to execute in
736
737    config
738        alternative buildout configuration file to use
739
740    offline
741        are we executing buildout in offline mode
742
743    runas
744        user used to run buildout as
745
746    env
747        environment variables to set when running
748
749    onlyif
750        Only execute cmd if statement on the host return 0
751
752    unless
753        Do not execute cmd if statement on the host return 0
754
755    newest
756        run buildout in newest mode
757
758    force
759        run buildout unconditionally
760
761    verbose
762        run buildout in verbose mode (-vvvvv)
763
764    use_vt
765        Use the new salt VT to stream output [experimental]
766
767    CLI Example:
768
769    .. code-block:: bash
770
771        salt '*' buildout.run_buildout /srv/mybuildout
772    """
773    directory = os.path.abspath(directory)
774    bcmd = os.path.join(directory, "bin", "buildout")
775    installed_cfg = os.path.join(directory, ".installed.cfg")
776    argv = []
777    if verbose:
778        LOG.debug("Buildout is running in verbose mode!")
779        argv.append("-vvvvvvv")
780    if not newest and os.path.exists(installed_cfg):
781        LOG.debug("Buildout is running in non newest mode!")
782        argv.append("-N")
783    if newest:
784        LOG.debug("Buildout is running in newest mode!")
785        argv.append("-n")
786    if offline:
787        LOG.debug("Buildout is running in offline mode!")
788        argv.append("-o")
789    if debug:
790        LOG.debug("Buildout is running in debug mode!")
791        argv.append("-D")
792    cmds, outputs = [], []
793    if parts:
794        for part in parts:
795            LOG.info(
796                "Installing single part: {}".format(part)
797            )  # pylint: disable=str-format-in-logging
798            cmd = "{} -c {} {} install {}".format(bcmd, config, " ".join(argv), part)
799            cmds.append(cmd)
800            outputs.append(
801                _Popen(
802                    cmd,
803                    directory=directory,
804                    runas=runas,
805                    env=env,
806                    output=True,
807                    loglevel=loglevel,
808                    use_vt=use_vt,
809                )
810            )
811    else:
812        LOG.info("Installing all buildout parts")
813        cmd = "{} -c {} {}".format(bcmd, config, " ".join(argv))
814        cmds.append(cmd)
815        outputs.append(
816            _Popen(
817                cmd,
818                directory=directory,
819                runas=runas,
820                loglevel=loglevel,
821                env=env,
822                output=True,
823                use_vt=use_vt,
824            )
825        )
826
827    return {"comment": "\n".join(cmds), "out": "\n".join(outputs)}
828
829
830def _merge_statuses(statuses):
831    status = BASE_STATUS.copy()
832    status["status"] = None
833    status["merged_statuses"] = True
834    status["out"] = ""
835    for st in statuses:
836        if status["status"] is not False:
837            status["status"] = st["status"]
838        out = st["out"]
839        comment = salt.utils.stringutils.to_unicode(st["comment"])
840        logs = st["logs"]
841        logs_by_level = st["logs_by_level"]
842        outlog_by_level = st["outlog_by_level"]
843        outlog = st["outlog"]
844        if out:
845            if not status["out"]:
846                status["out"] = ""
847            status["out"] += "\n"
848            status["out"] += HR
849            out = salt.utils.stringutils.to_unicode(out)
850            status["out"] += "{}\n".format(out)
851            status["out"] += HR
852        if comment:
853            if not status["comment"]:
854                status["comment"] = ""
855            status["comment"] += "\n{}\n".format(
856                salt.utils.stringutils.to_unicode(comment)
857            )
858        if outlog:
859            if not status["outlog"]:
860                status["outlog"] = ""
861            outlog = salt.utils.stringutils.to_unicode(outlog)
862            status["outlog"] += "\n{}".format(HR)
863            status["outlog"] += outlog
864        if outlog_by_level:
865            if not status["outlog_by_level"]:
866                status["outlog_by_level"] = ""
867            status["outlog_by_level"] += "\n{}".format(HR)
868            status["outlog_by_level"] += salt.utils.stringutils.to_unicode(
869                outlog_by_level
870            )
871        status["logs"].extend(
872            [(a[0], salt.utils.stringutils.to_unicode(a[1])) for a in logs]
873        )
874        for log in logs_by_level:
875            if log not in status["logs_by_level"]:
876                status["logs_by_level"][log] = []
877            status["logs_by_level"][log].extend(
878                [salt.utils.stringutils.to_unicode(a) for a in logs_by_level[log]]
879            )
880    return _encode_status(status)
881
882
883@_salt_callback
884def buildout(
885    directory=".",
886    config="buildout.cfg",
887    parts=None,
888    runas=None,
889    env=(),
890    buildout_ver=None,
891    test_release=False,
892    distribute=None,
893    new_st=None,
894    offline=False,
895    newest=False,
896    python=sys.executable,
897    debug=False,
898    verbose=False,
899    onlyif=None,
900    unless=None,
901    use_vt=False,
902    loglevel=None,
903):
904    """
905    Run buildout in a directory.
906
907    directory
908        directory to execute in
909
910    config
911        buildout config to use
912
913    parts
914        specific buildout parts to run
915
916    runas
917        user used to run buildout as
918
919    env
920        environment variables to set when running
921
922    buildout_ver
923        force a specific buildout version (1 | 2)
924
925    test_release
926        buildout accept test release
927
928    new_st
929        Forcing use of setuptools >= 0.7
930
931    distribute
932        use distribute over setuptools if possible
933
934    offline
935        does buildout run offline
936
937    python
938        python to use
939
940    debug
941        run buildout with -D debug flag
942
943    onlyif
944        Only execute cmd if statement on the host return 0
945
946    unless
947        Do not execute cmd if statement on the host return 0
948    newest
949        run buildout in newest mode
950
951    verbose
952        run buildout in verbose mode (-vvvvv)
953
954    use_vt
955        Use the new salt VT to stream output [experimental]
956
957    CLI Example:
958
959    .. code-block:: bash
960
961        salt '*' buildout.buildout /srv/mybuildout
962    """
963    LOG.info(
964        "Running buildout in {} ({})".format(directory, config)
965    )  # pylint: disable=str-format-in-logging
966    boot_ret = bootstrap(
967        directory,
968        config=config,
969        buildout_ver=buildout_ver,
970        test_release=test_release,
971        offline=offline,
972        new_st=new_st,
973        env=env,
974        runas=runas,
975        distribute=distribute,
976        python=python,
977        use_vt=use_vt,
978        loglevel=loglevel,
979    )
980    buildout_ret = run_buildout(
981        directory=directory,
982        config=config,
983        parts=parts,
984        offline=offline,
985        newest=newest,
986        runas=runas,
987        env=env,
988        verbose=verbose,
989        debug=debug,
990        use_vt=use_vt,
991        loglevel=loglevel,
992    )
993    # signal the decorator or our return
994    return _merge_statuses([boot_ret, buildout_ret])
995
996
997def _check_onlyif_unless(onlyif, unless, directory, runas=None, env=()):
998    ret = None
999    status = BASE_STATUS.copy()
1000    if os.path.exists(directory):
1001        directory = os.path.abspath(directory)
1002        status["status"] = False
1003        retcode = __salt__["cmd.retcode"]
1004        if onlyif is not None:
1005            if not isinstance(onlyif, str):
1006                if not onlyif:
1007                    _valid(status, "onlyif condition is false")
1008            elif isinstance(onlyif, str):
1009                if retcode(onlyif, cwd=directory, runas=runas, env=env) != 0:
1010                    _valid(status, "onlyif condition is false")
1011        if unless is not None:
1012            if not isinstance(unless, str):
1013                if unless:
1014                    _valid(status, "unless condition is true")
1015            elif isinstance(unless, str):
1016                if (
1017                    retcode(
1018                        unless, cwd=directory, runas=runas, env=env, python_shell=False
1019                    )
1020                    == 0
1021                ):
1022                    _valid(status, "unless condition is true")
1023    if status["status"]:
1024        ret = status
1025    return ret
1026
1027
1028# vim:set et sts=4 ts=4 tw=80:
1029