1"""
2RPM Package builder system
3
4.. versionadded:: 2015.8.0
5
6This system allows for all of the components to build rpms safely in chrooted
7environments. This also provides a function to generate yum repositories
8
9This module implements the pkgbuild interface
10"""
11
12
13import errno
14import functools
15import logging
16import os
17import re
18import shutil
19import tempfile
20import time
21import traceback
22import urllib.parse
23
24import salt.utils.files
25import salt.utils.path
26import salt.utils.user
27import salt.utils.vt
28from salt.exceptions import CommandExecutionError, SaltInvocationError
29
30HAS_LIBS = False
31
32try:
33    import gnupg  # pylint: disable=unused-import
34    import salt.modules.gpg
35
36    HAS_LIBS = True
37except ImportError:
38    pass
39
40log = logging.getLogger(__name__)
41
42__virtualname__ = "pkgbuild"
43
44
45def __virtual__():
46    """
47    Confirm this module is on a RPM based system, and has required utilities
48    """
49    missing_util = False
50    utils_reqd = ["gpg", "rpm", "rpmbuild", "mock", "createrepo"]
51    for named_util in utils_reqd:
52        if not salt.utils.path.which(named_util):
53            missing_util = True
54            break
55
56    if HAS_LIBS and not missing_util:
57        if __grains__.get("os_family", False) in ("RedHat", "Suse"):
58            return __virtualname__
59        else:
60            # The module will be exposed as `rpmbuild` on non-RPM based systems
61            return "rpmbuild"
62    else:
63        return (
64            False,
65            "The rpmbuild module could not be loaded: requires python-gnupg, "
66            "gpg, rpm, rpmbuild, mock and createrepo utilities to be installed",
67        )
68
69
70def _create_rpmmacros(runas="root"):
71    """
72    Create the .rpmmacros file in user's home directory
73    """
74    home = os.path.expanduser("~" + runas)
75    rpmbuilddir = os.path.join(home, "rpmbuild")
76    if not os.path.isdir(rpmbuilddir):
77        __salt__["file.makedirs_perms"](name=rpmbuilddir, user=runas, group="mock")
78
79    mockdir = os.path.join(home, "mock")
80    if not os.path.isdir(mockdir):
81        __salt__["file.makedirs_perms"](name=mockdir, user=runas, group="mock")
82
83    rpmmacros = os.path.join(home, ".rpmmacros")
84    with salt.utils.files.fopen(rpmmacros, "w") as afile:
85        afile.write(salt.utils.stringutils.to_str("%_topdir {}\n".format(rpmbuilddir)))
86        afile.write("%signature gpg\n")
87        afile.write("%_source_filedigest_algorithm 8\n")
88        afile.write("%_binary_filedigest_algorithm 8\n")
89        afile.write("%_gpg_name packaging@saltstack.com\n")
90
91
92def _mk_tree(runas="root"):
93    """
94    Create the rpm build tree
95    """
96    basedir = tempfile.mkdtemp()
97    paths = ["BUILD", "RPMS", "SOURCES", "SPECS", "SRPMS"]
98    for path in paths:
99        full = os.path.join(basedir, path)
100        __salt__["file.makedirs_perms"](name=full, user=runas, group="mock")
101    return basedir
102
103
104def _get_spec(tree_base, spec, template, saltenv="base"):
105    """
106    Get the spec file and place it in the SPECS dir
107    """
108    spec_tgt = os.path.basename(spec)
109    dest = os.path.join(tree_base, "SPECS", spec_tgt)
110    return __salt__["cp.get_url"](spec, dest, saltenv=saltenv)
111
112
113def _get_src(tree_base, source, saltenv="base", runas="root"):
114    """
115    Get the named sources and place them into the tree_base
116    """
117    parsed = urllib.parse.urlparse(source)
118    sbase = os.path.basename(source)
119    dest = os.path.join(tree_base, "SOURCES", sbase)
120    if parsed.scheme:
121        lsrc = __salt__["cp.get_url"](source, dest, saltenv=saltenv)
122    else:
123        shutil.copy(source, dest)
124    __salt__["file.chown"](path=dest, user=runas, group="mock")
125
126
127def _get_distset(tgt):
128    """
129    Get the distribution string for use with rpmbuild and mock
130    """
131    # Centos adds 'centos' string to rpm names, removing that to have
132    # consistent naming on Centos and Redhat, and allow for Amazon naming
133    tgtattrs = tgt.split("-")
134    if tgtattrs[0] == "amzn2":
135        distset = '--define "dist .{}"'.format(tgtattrs[0])
136    elif tgtattrs[1] in ["6", "7", "8"]:
137        distset = '--define "dist .el{}"'.format(tgtattrs[1])
138    else:
139        distset = ""
140
141    return distset
142
143
144def _get_deps(deps, tree_base, saltenv="base"):
145    """
146    Get include string for list of dependent rpms to build package
147    """
148    deps_list = ""
149    if deps is None:
150        return deps_list
151    if not isinstance(deps, list):
152        raise SaltInvocationError(
153            "'deps' must be a Python list or comma-separated string"
154        )
155    for deprpm in deps:
156        parsed = urllib.parse._urlparse(deprpm)
157        depbase = os.path.basename(deprpm)
158        dest = os.path.join(tree_base, depbase)
159        if parsed.scheme:
160            __salt__["cp.get_url"](deprpm, dest, saltenv=saltenv)
161        else:
162            shutil.copy(deprpm, dest)
163
164        deps_list += " {}".format(dest)
165
166    return deps_list
167
168
169def _check_repo_gpg_phrase_utils():
170    """
171    Check for /usr/libexec/gpg-preset-passphrase is installed
172    """
173    util_name = "/usr/libexec/gpg-preset-passphrase"
174    if __salt__["file.file_exists"](util_name):
175        return True
176    else:
177        raise CommandExecutionError(
178            "utility '{}' needs to be installed".format(util_name)
179        )
180
181
182def _get_gpg_key_resources(keyid, env, use_passphrase, gnupghome, runas):
183    """
184    Obtain gpg key resource infomation to sign repo files with
185
186    keyid
187
188        Optional Key ID to use in signing packages and repository.
189        Utilizes Public and Private keys associated with keyid which have
190        been loaded into the minion's Pillar data.
191
192    env
193
194        A dictionary of environment variables to be utilized in creating the
195        repository.
196
197    use_passphrase : False
198
199        Use a passphrase with the signing key presented in ``keyid``.
200        Passphrase is received from Pillar data which could be passed on the
201        command line with ``pillar`` parameter.
202
203    gnupghome : /etc/salt/gpgkeys
204
205        Location where GPG related files are stored, used with ``keyid``.
206
207    runas : root
208
209        User to create the repository as, and optionally sign packages.
210
211        .. note::
212
213            Ensure the user has correct permissions to any files and
214            directories which are to be utilized.
215
216
217    Returns:
218        tuple
219            use_gpg_agent       True | False, Redhat 8 now makes use of a gpg-agent similar ot Debian
220            local_keyid         key id to use in signing
221            define_gpg_name     string containing definition to use with addsign (use_gpg_agent False)
222            phrase              pass phrase (may not be used)
223
224    """
225    local_keygrip_to_use = None
226    local_key_fingerprint = None
227    local_keyid = None
228    local_uids = None
229    define_gpg_name = ""
230    phrase = ""
231    retrc = 0
232    use_gpg_agent = False
233
234    if (
235        __grains__.get("os_family") == "RedHat"
236        and __grains__.get("osmajorrelease") >= 8
237    ):
238        use_gpg_agent = True
239
240    if keyid is not None:
241        # import_keys
242        pkg_pub_key_file = "{}/{}".format(
243            gnupghome, __salt__["pillar.get"]("gpg_pkg_pub_keyname", None)
244        )
245        pkg_priv_key_file = "{}/{}".format(
246            gnupghome, __salt__["pillar.get"]("gpg_pkg_priv_keyname", None)
247        )
248
249        if pkg_pub_key_file is None or pkg_priv_key_file is None:
250            raise SaltInvocationError(
251                "Pillar data should contain Public and Private keys associated with"
252                " 'keyid'"
253            )
254        try:
255            __salt__["gpg.import_key"](
256                user=runas, filename=pkg_pub_key_file, gnupghome=gnupghome
257            )
258            __salt__["gpg.import_key"](
259                user=runas, filename=pkg_priv_key_file, gnupghome=gnupghome
260            )
261
262        except SaltInvocationError:
263            raise SaltInvocationError(
264                "Public and Private key files associated with Pillar data and 'keyid' "
265                "{} could not be found".format(keyid)
266            )
267
268        # gpg keys should have been loaded as part of setup
269        # retrieve specified key and preset passphrase
270        local_keys = __salt__["gpg.list_keys"](user=runas, gnupghome=gnupghome)
271        for gpg_key in local_keys:
272            if keyid == gpg_key["keyid"][8:]:
273                local_uids = gpg_key["uids"]
274                local_keyid = gpg_key["keyid"]
275                if use_gpg_agent:
276                    local_keygrip_to_use = gpg_key["fingerprint"]
277                    local_key_fingerprint = gpg_key["fingerprint"]
278                break
279
280        if use_gpg_agent:
281            cmd = "gpg --with-keygrip --list-secret-keys"
282            local_keys2_keygrip = __salt__["cmd.run"](cmd, runas=runas, env=env)
283            local_keys2 = iter(local_keys2_keygrip.splitlines())
284            try:
285                for line in local_keys2:
286                    if line.startswith("sec"):
287                        line_fingerprint = next(local_keys2).lstrip().rstrip()
288                        if local_key_fingerprint == line_fingerprint:
289                            lkeygrip = next(local_keys2).split("=")
290                            local_keygrip_to_use = lkeygrip[1].lstrip().rstrip()
291                            break
292            except StopIteration:
293                raise SaltInvocationError(
294                    "unable to find keygrip associated with fingerprint '{}' for keyid"
295                    " '{}'".format(local_key_fingerprint, local_keyid)
296                )
297
298        if local_keyid is None:
299            raise SaltInvocationError(
300                "The key ID '{}' was not found in GnuPG keyring at '{}'".format(
301                    keyid, gnupghome
302                )
303            )
304
305        if use_passphrase:
306            phrase = __salt__["pillar.get"]("gpg_passphrase")
307            if use_gpg_agent:
308                _check_repo_gpg_phrase_utils()
309                cmd = (
310                    "/usr/libexec/gpg-preset-passphrase --verbose --preset "
311                    '--passphrase "{}" {}'.format(phrase, local_keygrip_to_use)
312                )
313                retrc = __salt__["cmd.retcode"](cmd, runas=runas, env=env)
314                if retrc != 0:
315                    raise SaltInvocationError(
316                        "Failed to preset passphrase, error {1}, "
317                        "check logs for further details".format(retrc)
318                    )
319
320        if local_uids:
321            define_gpg_name = (
322                "--define='%_signature gpg' --define='%_gpg_name {}'".format(
323                    local_uids[0]
324                )
325            )
326
327        # need to update rpm with public key
328        cmd = "rpm --import {}".format(pkg_pub_key_file)
329        retrc = __salt__["cmd.retcode"](cmd, runas=runas, use_vt=True)
330        if retrc != 0:
331            raise SaltInvocationError(
332                "Failed to import public key from file {} with return "
333                "error {}, check logs for further details".format(
334                    pkg_pub_key_file, retrc
335                )
336            )
337
338    return (use_gpg_agent, local_keyid, define_gpg_name, phrase)
339
340
341def _sign_file(runas, define_gpg_name, phrase, abs_file, timeout):
342    """
343    Sign file with provided key and definition
344    """
345    SIGN_PROMPT_RE = re.compile(r"Enter pass phrase: ", re.M)
346
347    # interval of 0.125 is really too fast on some systems
348    interval = 0.5
349    number_retries = timeout / interval
350    times_looped = 0
351    error_msg = "Failed to sign file {}".format(abs_file)
352
353    cmd = "rpm {} --addsign {}".format(define_gpg_name, abs_file)
354    preexec_fn = functools.partial(salt.utils.user.chugid_and_umask, runas, None)
355    try:
356        stdout, stderr = None, None
357        proc = salt.utils.vt.Terminal(
358            cmd,
359            shell=True,
360            preexec_fn=preexec_fn,
361            stream_stdout=True,
362            stream_stderr=True,
363        )
364        while proc.has_unread_data:
365            stdout, stderr = proc.recv()
366            if stdout and SIGN_PROMPT_RE.search(stdout):
367                # have the prompt for inputting the passphrase
368                proc.sendline(phrase)
369            else:
370                times_looped += 1
371
372            if times_looped > number_retries:
373                raise SaltInvocationError(
374                    "Attemping to sign file {} failed, timed out after {} seconds".format(
375                        abs_file, int(times_looped * interval)
376                    )
377                )
378            time.sleep(interval)
379
380        proc_exitstatus = proc.exitstatus
381        if proc_exitstatus != 0:
382            raise SaltInvocationError(
383                "Signing file {} failed with proc.status {}".format(
384                    abs_file, proc_exitstatus
385                )
386            )
387    except salt.utils.vt.TerminalException as err:
388        trace = traceback.format_exc()
389        log.error(error_msg, err, trace)
390    finally:
391        proc.close(terminate=True, kill=True)
392
393
394def _sign_files_with_gpg_agent(runas, local_keyid, abs_file, repodir, env, timeout):
395    """
396    Sign file with provided key utilizing gpg-agent
397    """
398    cmd = "rpmsign --verbose  --key-id={} --addsign {}".format(local_keyid, abs_file)
399    retrc = __salt__["cmd.retcode"](cmd, runas=runas, cwd=repodir, use_vt=True, env=env)
400    if retrc != 0:
401        raise SaltInvocationError(
402            "Signing encountered errors for command '{}', "
403            "return error {}, check logs for further details".format(cmd, retrc)
404        )
405
406
407def make_src_pkg(
408    dest_dir, spec, sources, env=None, template=None, saltenv="base", runas="root"
409):
410    """
411    Create a source rpm from the given spec file and sources
412
413    CLI Example:
414
415    .. code-block:: bash
416
417        salt '*' pkgbuild.make_src_pkg /var/www/html/
418                https://raw.githubusercontent.com/saltstack/libnacl/master/pkg/rpm/python-libnacl.spec
419                https://pypi.python.org/packages/source/l/libnacl/libnacl-1.3.5.tar.gz
420
421    This example command should build the libnacl SOURCE package and place it in
422    /var/www/html/ on the minion
423
424    .. versionchanged:: 2017.7.0
425
426    dest_dir
427        The directory on the minion to place the built package(s)
428
429    spec
430        The location of the spec file (used for rpms)
431
432    sources
433        The list of package sources
434
435    env
436        A dictionary of environment variables to be set prior to execution.
437
438    template
439        Run the spec file through a templating engine
440        Optional argument, allows for no templating engine used to be
441        if none is desired.
442
443    saltenv
444        The saltenv to use for files downloaded from the salt filesever
445
446    runas
447        The user to run the build process as
448
449        .. versionadded:: 2018.3.3
450
451
452    .. note::
453
454        using SHA256 as digest and minimum level dist el6
455
456    """
457    _create_rpmmacros(runas)
458    tree_base = _mk_tree(runas)
459    spec_path = _get_spec(tree_base, spec, template, saltenv)
460    __salt__["file.chown"](path=spec_path, user=runas, group="mock")
461    __salt__["file.chown"](path=tree_base, user=runas, group="mock")
462
463    if isinstance(sources, str):
464        sources = sources.split(",")
465    for src in sources:
466        _get_src(tree_base, src, saltenv, runas)
467
468    # make source rpms for dist el6 with SHA256, usable with mock on other dists
469    cmd = 'rpmbuild --verbose --define "_topdir {}" -bs --define "dist .el6" {}'.format(
470        tree_base, spec_path
471    )
472    retrc = __salt__["cmd.retcode"](cmd, runas=runas)
473    if retrc != 0:
474        raise SaltInvocationError(
475            "Make source package for destination directory {}, spec {}, sources {},"
476            " failed with return error {}, check logs for further details".format(
477                dest_dir, spec, sources, retrc
478            )
479        )
480
481    srpms = os.path.join(tree_base, "SRPMS")
482    ret = []
483    if not os.path.isdir(dest_dir):
484        __salt__["file.makedirs_perms"](name=dest_dir, user=runas, group="mock")
485    for fn_ in os.listdir(srpms):
486        full = os.path.join(srpms, fn_)
487        tgt = os.path.join(dest_dir, fn_)
488        shutil.copy(full, tgt)
489        ret.append(tgt)
490    return ret
491
492
493def build(
494    runas,
495    tgt,
496    dest_dir,
497    spec,
498    sources,
499    deps,
500    env,
501    template,
502    saltenv="base",
503    log_dir="/var/log/salt/pkgbuild",
504):
505    """
506    Given the package destination directory, the spec file source and package
507    sources, use mock to safely build the rpm defined in the spec file
508
509    CLI Example:
510
511    .. code-block:: bash
512
513        salt '*' pkgbuild.build mock epel-7-x86_64 /var/www/html
514                    https://raw.githubusercontent.com/saltstack/libnacl/master/pkg/rpm/python-libnacl.spec
515                    https://pypi.python.org/packages/source/l/libnacl/libnacl-1.3.5.tar.gz
516
517    This example command should build the libnacl package for rhel 7 using user
518    mock and place it in /var/www/html/ on the minion
519    """
520    ret = {}
521    try:
522        __salt__["file.chown"](path=dest_dir, user=runas, group="mock")
523    except OSError as exc:
524        if exc.errno != errno.EEXIST:
525            raise
526    srpm_dir = os.path.join(dest_dir, "SRPMS")
527    srpm_build_dir = tempfile.mkdtemp()
528    try:
529        srpms = make_src_pkg(
530            srpm_build_dir, spec, sources, env, template, saltenv, runas
531        )
532    except Exception as exc:  # pylint: disable=broad-except
533        shutil.rmtree(srpm_build_dir)
534        log.error("Failed to make src package")
535        return ret
536
537    distset = _get_distset(tgt)
538
539    noclean = ""
540    deps_dir = tempfile.mkdtemp()
541    deps_list = _get_deps(deps, deps_dir, saltenv)
542
543    retrc = 0
544    for srpm in srpms:
545        dbase = os.path.dirname(srpm)
546        results_dir = tempfile.mkdtemp()
547        try:
548            __salt__["file.chown"](path=dbase, user=runas, group="mock")
549            __salt__["file.chown"](path=results_dir, user=runas, group="mock")
550            cmd = "mock --root={} --resultdir={} --init".format(tgt, results_dir)
551            retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
552            if deps_list and not deps_list.isspace():
553                cmd = "mock --root={} --resultdir={} --install {} {}".format(
554                    tgt, results_dir, deps_list, noclean
555                )
556                retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
557                noclean += " --no-clean"
558
559            cmd = "mock --root={} --resultdir={} {} {} {}".format(
560                tgt, results_dir, distset, noclean, srpm
561            )
562            retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
563            cmdlist = [
564                "rpm",
565                "-qp",
566                "--queryformat",
567                "{0}/%{{name}}/%{{version}}-%{{release}}".format(log_dir),
568                srpm,
569            ]
570            log_dest = __salt__["cmd.run_stdout"](cmdlist, python_shell=False)
571            for filename in os.listdir(results_dir):
572                full = os.path.join(results_dir, filename)
573                if filename.endswith("src.rpm"):
574                    sdest = os.path.join(srpm_dir, filename)
575                    try:
576                        __salt__["file.makedirs_perms"](
577                            name=srpm_dir, user=runas, group="mock"
578                        )
579                    except OSError as exc:
580                        if exc.errno != errno.EEXIST:
581                            raise
582                    shutil.copy(full, sdest)
583                    ret.setdefault("Source Packages", []).append(sdest)
584                elif filename.endswith(".rpm"):
585                    bdist = os.path.join(dest_dir, filename)
586                    shutil.copy(full, bdist)
587                    ret.setdefault("Packages", []).append(bdist)
588                else:
589                    log_file = os.path.join(log_dest, filename)
590                    try:
591                        __salt__["file.makedirs_perms"](
592                            name=log_dest, user=runas, group="mock"
593                        )
594                    except OSError as exc:
595                        if exc.errno != errno.EEXIST:
596                            raise
597                    shutil.copy(full, log_file)
598                    ret.setdefault("Log Files", []).append(log_file)
599        except Exception as exc:  # pylint: disable=broad-except
600            log.error("Error building from %s: %s", srpm, exc)
601        finally:
602            shutil.rmtree(results_dir)
603    if retrc != 0:
604        raise SaltInvocationError(
605            "Building packages for destination directory {}, spec {}, sources {},"
606            " failed with return error {}, check logs for further details".format(
607                dest_dir, spec, sources, retrc
608            )
609        )
610    shutil.rmtree(deps_dir)
611    shutil.rmtree(srpm_build_dir)
612    return ret
613
614
615def make_repo(
616    repodir,
617    keyid=None,
618    env=None,
619    use_passphrase=False,
620    gnupghome="/etc/salt/gpgkeys",
621    runas="root",
622    timeout=15.0,
623):
624    """
625    Make a package repository and optionally sign packages present
626
627    Given the repodir, create a ``yum`` repository out of the rpms therein
628    and optionally sign it and packages present, the name is directory to
629    turn into a repo. This state is best used with onchanges linked to
630    your package building states.
631
632    repodir
633        The directory to find packages that will be in the repository.
634
635    keyid
636        .. versionchanged:: 2016.3.0
637
638        Optional Key ID to use in signing packages and repository.
639        Utilizes Public and Private keys associated with keyid which have
640        been loaded into the minion's Pillar data.
641
642        For example, contents from a Pillar data file with named Public
643        and Private keys as follows:
644
645        .. code-block:: yaml
646
647            gpg_pkg_priv_key: |
648              -----BEGIN PGP PRIVATE KEY BLOCK-----
649              Version: GnuPG v1
650
651              lQO+BFciIfQBCADAPCtzx7I5Rl32escCMZsPzaEKWe7bIX1em4KCKkBoX47IG54b
652              w82PCE8Y1jF/9Uk2m3RKVWp3YcLlc7Ap3gj6VO4ysvVz28UbnhPxsIkOlf2cq8qc
653              .
654              .
655              Ebe+8JCQTwqSXPRTzXmy/b5WXDeM79CkLWvuGpXFor76D+ECMRPv/rawukEcNptn
656              R5OmgHqvydEnO4pWbn8JzQO9YX/Us0SMHBVzLC8eIi5ZIopzalvX
657              =JvW8
658              -----END PGP PRIVATE KEY BLOCK-----
659
660            gpg_pkg_priv_keyname: gpg_pkg_key.pem
661
662            gpg_pkg_pub_key: |
663              -----BEGIN PGP PUBLIC KEY BLOCK-----
664              Version: GnuPG v1
665
666              mQENBFciIfQBCADAPCtzx7I5Rl32escCMZsPzaEKWe7bIX1em4KCKkBoX47IG54b
667              w82PCE8Y1jF/9Uk2m3RKVWp3YcLlc7Ap3gj6VO4ysvVz28UbnhPxsIkOlf2cq8qc
668              .
669              .
670              bYP7t5iwJmQzRMyFInYRt77wkJBPCpJc9FPNebL9vlZcN4zv0KQta+4alcWivvoP
671              4QIxE+/+trC6QRw2m2dHk6aAeq/J0Sc7ilZufwnNA71hf9SzRIwcFXMsLx4iLlki
672              inNqW9c=
673              =s1CX
674              -----END PGP PUBLIC KEY BLOCK-----
675
676            gpg_pkg_pub_keyname: gpg_pkg_key.pub
677
678    env
679        .. versionchanged:: 2016.3.0
680
681        A dictionary of environment variables to be utilized in creating the
682        repository.
683
684        .. note::
685
686            This parameter is not used for making ``yum`` repositories.
687
688    use_passphrase : False
689        .. versionadded:: 2016.3.0
690
691        Use a passphrase with the signing key presented in ``keyid``.
692        Passphrase is received from Pillar data which could be passed on the
693        command line with ``pillar`` parameter.
694
695        .. code-block:: bash
696
697            pillar='{ "gpg_passphrase" : "my_passphrase" }'
698
699        .. versionadded:: 3001.1
700
701        RHEL 8 and above leverages gpg-agent and gpg-preset-passphrase for
702        caching keys, etc.
703
704    gnupghome : /etc/salt/gpgkeys
705        .. versionadded:: 2016.3.0
706
707        Location where GPG related files are stored, used with ``keyid``.
708
709    runas : root
710        .. versionadded:: 2016.3.0
711
712        User to create the repository as, and optionally sign packages.
713
714        .. note::
715
716            Ensure the user has correct permissions to any files and
717            directories which are to be utilized.
718
719    timeout : 15.0
720        .. versionadded:: 2016.3.4
721
722        Timeout in seconds to wait for the prompt for inputting the passphrase.
723
724    CLI Example:
725
726    .. code-block:: bash
727
728        salt '*' pkgbuild.make_repo /var/www/html/
729
730    """
731    home = os.path.expanduser("~" + runas)
732    rpmmacros = os.path.join(home, ".rpmmacros")
733    if not os.path.exists(rpmmacros):
734        _create_rpmmacros(runas)
735
736    if gnupghome and env is None:
737        env = {}
738        env["GNUPGHOME"] = gnupghome
739
740    use_gpg_agent, local_keyid, define_gpg_name, phrase = _get_gpg_key_resources(
741        keyid, env, use_passphrase, gnupghome, runas
742    )
743
744    # sign_it_here
745    for fileused in os.listdir(repodir):
746        if fileused.endswith(".rpm"):
747            abs_file = os.path.join(repodir, fileused)
748            if use_gpg_agent:
749                _sign_files_with_gpg_agent(
750                    runas, local_keyid, abs_file, repodir, env, timeout
751                )
752            else:
753                _sign_file(runas, define_gpg_name, phrase, abs_file, timeout)
754
755    cmd = "createrepo --update {}".format(repodir)
756    retrc = __salt__["cmd.run_all"](cmd, runas=runas)
757    return retrc
758