"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "salt/modules/rpmbuild_pkgbuild.py" between
salt-3002.1.tar.gz and salt-3002.2.tar.gz

About: SaltStack is a systems management software for data center automation, cloud orchestration, server provisioning, configuration management and more. Community version.

rpmbuild_pkgbuild.py  (salt-3002.1):rpmbuild_pkgbuild.py  (salt-3002.2)
# -*- coding: utf-8 -*-
""" """
RPM Package builder system RPM Package builder system
.. versionadded:: 2015.8.0 .. versionadded:: 2015.8.0
This system allows for all of the components to build rpms safely in chrooted This system allows for all of the components to build rpms safely in chrooted
environments. This also provides a function to generate yum repositories environments. This also provides a function to generate yum repositories
This module implements the pkgbuild interface This module implements the pkgbuild interface
""" """
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import errno import errno
import functools import functools
import logging import logging
import os import os
import re import re
import shutil import shutil
import tempfile import tempfile
import time import time
import traceback import traceback
import salt.utils.files import salt.utils.files
import salt.utils.path import salt.utils.path
import salt.utils.user import salt.utils.user
import salt.utils.vt import salt.utils.vt
# Import salt libs
from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.exceptions import CommandExecutionError, SaltInvocationError
# Import 3rd-party libs
from salt.ext import six
from salt.ext.six.moves.urllib.parse import urlparse as _urlparse from salt.ext.six.moves.urllib.parse import urlparse as _urlparse
HAS_LIBS = False HAS_LIBS = False
try: try:
import gnupg # pylint: disable=unused-import import gnupg # pylint: disable=unused-import
import salt.modules.gpg import salt.modules.gpg
HAS_LIBS = True HAS_LIBS = True
except ImportError: except ImportError:
skipping to change at line 91 skipping to change at line 82
rpmbuilddir = os.path.join(home, "rpmbuild") rpmbuilddir = os.path.join(home, "rpmbuild")
if not os.path.isdir(rpmbuilddir): if not os.path.isdir(rpmbuilddir):
__salt__["file.makedirs_perms"](name=rpmbuilddir, user=runas, group="moc k") __salt__["file.makedirs_perms"](name=rpmbuilddir, user=runas, group="moc k")
mockdir = os.path.join(home, "mock") mockdir = os.path.join(home, "mock")
if not os.path.isdir(mockdir): if not os.path.isdir(mockdir):
__salt__["file.makedirs_perms"](name=mockdir, user=runas, group="mock") __salt__["file.makedirs_perms"](name=mockdir, user=runas, group="mock")
rpmmacros = os.path.join(home, ".rpmmacros") rpmmacros = os.path.join(home, ".rpmmacros")
with salt.utils.files.fopen(rpmmacros, "w") as afile: with salt.utils.files.fopen(rpmmacros, "w") as afile:
afile.write(salt.utils.stringutils.to_str("%_topdir {0}\n".format(rpmbui lddir))) afile.write(salt.utils.stringutils.to_str("%_topdir {}\n".format(rpmbuil ddir)))
afile.write("%signature gpg\n") afile.write("%signature gpg\n")
afile.write("%_source_filedigest_algorithm 8\n") afile.write("%_source_filedigest_algorithm 8\n")
afile.write("%_binary_filedigest_algorithm 8\n") afile.write("%_binary_filedigest_algorithm 8\n")
afile.write("%_gpg_name packaging@saltstack.com\n") afile.write("%_gpg_name packaging@saltstack.com\n")
def _mk_tree(runas="root"): def _mk_tree(runas="root"):
""" """
Create the rpm build tree Create the rpm build tree
""" """
basedir = tempfile.mkdtemp() basedir = tempfile.mkdtemp()
skipping to change at line 137 skipping to change at line 128
__salt__["file.chown"](path=dest, user=runas, group="mock") __salt__["file.chown"](path=dest, user=runas, group="mock")
def _get_distset(tgt): def _get_distset(tgt):
""" """
Get the distribution string for use with rpmbuild and mock Get the distribution string for use with rpmbuild and mock
""" """
# Centos adds 'centos' string to rpm names, removing that to have # Centos adds 'centos' string to rpm names, removing that to have
# consistent naming on Centos and Redhat, and allow for Amazon naming # consistent naming on Centos and Redhat, and allow for Amazon naming
tgtattrs = tgt.split("-") tgtattrs = tgt.split("-")
if tgtattrs[0] == "amzn2": if tgtattrs[0] == "amzn2":
distset = '--define "dist .{0}"'.format(tgtattrs[0]) distset = '--define "dist .{}"'.format(tgtattrs[0])
elif tgtattrs[1] in ["6", "7", "8"]: elif tgtattrs[1] in ["6", "7", "8"]:
distset = '--define "dist .el{0}"'.format(tgtattrs[1]) distset = '--define "dist .el{}"'.format(tgtattrs[1])
else: else:
distset = "" distset = ""
return distset return distset
def _get_deps(deps, tree_base, saltenv="base"): def _get_deps(deps, tree_base, saltenv="base"):
""" """
Get include string for list of dependent rpms to build package Get include string for list of dependent rpms to build package
""" """
deps_list = "" deps_list = ""
skipping to change at line 165 skipping to change at line 156
) )
for deprpm in deps: for deprpm in deps:
parsed = _urlparse(deprpm) parsed = _urlparse(deprpm)
depbase = os.path.basename(deprpm) depbase = os.path.basename(deprpm)
dest = os.path.join(tree_base, depbase) dest = os.path.join(tree_base, depbase)
if parsed.scheme: if parsed.scheme:
__salt__["cp.get_url"](deprpm, dest, saltenv=saltenv) __salt__["cp.get_url"](deprpm, dest, saltenv=saltenv)
else: else:
shutil.copy(deprpm, dest) shutil.copy(deprpm, dest)
deps_list += " {0}".format(dest) deps_list += " {}".format(dest)
return deps_list return deps_list
def _check_repo_gpg_phrase_utils(): def _check_repo_gpg_phrase_utils():
""" """
Check for /usr/libexec/gpg-preset-passphrase is installed Check for /usr/libexec/gpg-preset-passphrase is installed
""" """
util_name = "/usr/libexec/gpg-preset-passphrase" util_name = "/usr/libexec/gpg-preset-passphrase"
if __salt__["file.file_exists"](util_name): if __salt__["file.file_exists"](util_name):
return True return True
else: else:
raise CommandExecutionError( raise CommandExecutionError(
"utility '{0}' needs to be installed".format(util_name) "utility '{}' needs to be installed".format(util_name)
) )
def _get_gpg_key_resources(keyid, env, use_passphrase, gnupghome, runas): def _get_gpg_key_resources(keyid, env, use_passphrase, gnupghome, runas):
""" """
Obtain gpg key resource infomation to sign repo files with Obtain gpg key resource infomation to sign repo files with
keyid keyid
Optional Key ID to use in signing packages and repository. Optional Key ID to use in signing packages and repository.
Utilizes Public and Private keys associated with keyid which have Utilizes Public and Private keys associated with keyid which have
skipping to change at line 240 skipping to change at line 231
use_gpg_agent = False use_gpg_agent = False
if ( if (
__grains__.get("os_family") == "RedHat" __grains__.get("os_family") == "RedHat"
and __grains__.get("osmajorrelease") >= 8 and __grains__.get("osmajorrelease") >= 8
): ):
use_gpg_agent = True use_gpg_agent = True
if keyid is not None: if keyid is not None:
# import_keys # import_keys
pkg_pub_key_file = "{0}/{1}".format( pkg_pub_key_file = "{}/{}".format(
gnupghome, __salt__["pillar.get"]("gpg_pkg_pub_keyname", None) gnupghome, __salt__["pillar.get"]("gpg_pkg_pub_keyname", None)
) )
pkg_priv_key_file = "{0}/{1}".format( pkg_priv_key_file = "{}/{}".format(
gnupghome, __salt__["pillar.get"]("gpg_pkg_priv_keyname", None) gnupghome, __salt__["pillar.get"]("gpg_pkg_priv_keyname", None)
) )
if pkg_pub_key_file is None or pkg_priv_key_file is None: if pkg_pub_key_file is None or pkg_priv_key_file is None:
raise SaltInvocationError( raise SaltInvocationError(
"Pillar data should contain Public and Private keys associated w ith 'keyid'" "Pillar data should contain Public and Private keys associated w ith 'keyid'"
) )
try: try:
__salt__["gpg.import_key"]( __salt__["gpg.import_key"](
user=runas, filename=pkg_pub_key_file, gnupghome=gnupghome user=runas, filename=pkg_pub_key_file, gnupghome=gnupghome
) )
__salt__["gpg.import_key"]( __salt__["gpg.import_key"](
user=runas, filename=pkg_priv_key_file, gnupghome=gnupghome user=runas, filename=pkg_priv_key_file, gnupghome=gnupghome
) )
except SaltInvocationError: except SaltInvocationError:
raise SaltInvocationError( raise SaltInvocationError(
"Public and Private key files associated with Pillar data and 'k eyid' " "Public and Private key files associated with Pillar data and 'k eyid' "
"{0} could not be found".format(keyid) "{} could not be found".format(keyid)
) )
# gpg keys should have been loaded as part of setup # gpg keys should have been loaded as part of setup
# retrieve specified key and preset passphrase # retrieve specified key and preset passphrase
local_keys = __salt__["gpg.list_keys"](user=runas, gnupghome=gnupghome) local_keys = __salt__["gpg.list_keys"](user=runas, gnupghome=gnupghome)
for gpg_key in local_keys: for gpg_key in local_keys:
if keyid == gpg_key["keyid"][8:]: if keyid == gpg_key["keyid"][8:]:
local_uids = gpg_key["uids"] local_uids = gpg_key["uids"]
local_keyid = gpg_key["keyid"] local_keyid = gpg_key["keyid"]
if use_gpg_agent: if use_gpg_agent:
skipping to change at line 291 skipping to change at line 282
try: try:
for line in local_keys2: for line in local_keys2:
if line.startswith("sec"): if line.startswith("sec"):
line_fingerprint = next(local_keys2).lstrip().rstrip() line_fingerprint = next(local_keys2).lstrip().rstrip()
if local_key_fingerprint == line_fingerprint: if local_key_fingerprint == line_fingerprint:
lkeygrip = next(local_keys2).split("=") lkeygrip = next(local_keys2).split("=")
local_keygrip_to_use = lkeygrip[1].lstrip().rstrip() local_keygrip_to_use = lkeygrip[1].lstrip().rstrip()
break break
except StopIteration: except StopIteration:
raise SaltInvocationError( raise SaltInvocationError(
"unable to find keygrip associated with fingerprint '{0}' fo r keyid '{1}'".format( "unable to find keygrip associated with fingerprint '{}' for keyid '{}'".format(
local_key_fingerprint, local_keyid local_key_fingerprint, local_keyid
) )
) )
if local_keyid is None: if local_keyid is None:
raise SaltInvocationError( raise SaltInvocationError(
"The key ID '{0}' was not found in GnuPG keyring at '{1}'".forma t( "The key ID '{}' was not found in GnuPG keyring at '{}'".format(
keyid, gnupghome keyid, gnupghome
) )
) )
if use_passphrase: if use_passphrase:
phrase = __salt__["pillar.get"]("gpg_passphrase") phrase = __salt__["pillar.get"]("gpg_passphrase")
if use_gpg_agent: if use_gpg_agent:
_check_repo_gpg_phrase_utils() _check_repo_gpg_phrase_utils()
cmd = ( cmd = (
"/usr/libexec/gpg-preset-passphrase --verbose --preset " "/usr/libexec/gpg-preset-passphrase --verbose --preset "
'--passphrase "{0}" {1}'.format(phrase, local_keygrip_to_use ) '--passphrase "{}" {}'.format(phrase, local_keygrip_to_use)
) )
retrc = __salt__["cmd.retcode"](cmd, runas=runas, env=env) retrc = __salt__["cmd.retcode"](cmd, runas=runas, env=env)
if retrc != 0: if retrc != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Failed to preset passphrase, error {1}, " "Failed to preset passphrase, error {1}, "
"check logs for further details".format(retrc) "check logs for further details".format(retrc)
) )
if local_uids: if local_uids:
define_gpg_name = "--define='%_signature gpg' --define='%_gpg_name { 0}'".format( define_gpg_name = "--define='%_signature gpg' --define='%_gpg_name { }'".format(
local_uids[0] local_uids[0]
) )
# need to update rpm with public key # need to update rpm with public key
cmd = "rpm --import {0}".format(pkg_pub_key_file) cmd = "rpm --import {}".format(pkg_pub_key_file)
retrc = __salt__["cmd.retcode"](cmd, runas=runas, use_vt=True) retrc = __salt__["cmd.retcode"](cmd, runas=runas, use_vt=True)
if retrc != 0: if retrc != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Failed to import public key from file {0} with return " "Failed to import public key from file {} with return "
"error {1}, check logs for further details".format( "error {}, check logs for further details".format(
pkg_pub_key_file, retrc pkg_pub_key_file, retrc
) )
) )
return (use_gpg_agent, local_keyid, define_gpg_name, phrase) return (use_gpg_agent, local_keyid, define_gpg_name, phrase)
def _sign_file(runas, define_gpg_name, phrase, abs_file, timeout): def _sign_file(runas, define_gpg_name, phrase, abs_file, timeout):
""" """
Sign file with provided key and definition Sign file with provided key and definition
""" """
SIGN_PROMPT_RE = re.compile(r"Enter pass phrase: ", re.M) SIGN_PROMPT_RE = re.compile(r"Enter pass phrase: ", re.M)
# interval of 0.125 is really too fast on some systems # interval of 0.125 is really too fast on some systems
interval = 0.5 interval = 0.5
number_retries = timeout / interval number_retries = timeout / interval
times_looped = 0 times_looped = 0
error_msg = "Failed to sign file {0}".format(abs_file) error_msg = "Failed to sign file {}".format(abs_file)
cmd = "rpm {0} --addsign {1}".format(define_gpg_name, abs_file) cmd = "rpm {} --addsign {}".format(define_gpg_name, abs_file)
preexec_fn = functools.partial(salt.utils.user.chugid_and_umask, runas, None ) preexec_fn = functools.partial(salt.utils.user.chugid_and_umask, runas, None )
try: try:
stdout, stderr = None, None stdout, stderr = None, None
proc = salt.utils.vt.Terminal( proc = salt.utils.vt.Terminal(
cmd, cmd,
shell=True, shell=True,
preexec_fn=preexec_fn, preexec_fn=preexec_fn,
stream_stdout=True, stream_stdout=True,
stream_stderr=True, stream_stderr=True,
) )
while proc.has_unread_data: while proc.has_unread_data:
stdout, stderr = proc.recv() stdout, stderr = proc.recv()
if stdout and SIGN_PROMPT_RE.search(stdout): if stdout and SIGN_PROMPT_RE.search(stdout):
# have the prompt for inputting the passphrase # have the prompt for inputting the passphrase
proc.sendline(phrase) proc.sendline(phrase)
else: else:
times_looped += 1 times_looped += 1
if times_looped > number_retries: if times_looped > number_retries:
raise SaltInvocationError( raise SaltInvocationError(
"Attemping to sign file {0} failed, timed out after {1} seco nds".format( "Attemping to sign file {} failed, timed out after {} second s".format(
abs_file, int(times_looped * interval) abs_file, int(times_looped * interval)
) )
) )
time.sleep(interval) time.sleep(interval)
proc_exitstatus = proc.exitstatus proc_exitstatus = proc.exitstatus
if proc_exitstatus != 0: if proc_exitstatus != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Signing file {0} failed with proc.status {1}".format( "Signing file {} failed with proc.status {}".format(
abs_file, proc_exitstatus abs_file, proc_exitstatus
) )
) )
except salt.utils.vt.TerminalException as err: except salt.utils.vt.TerminalException as err:
trace = traceback.format_exc() trace = traceback.format_exc()
log.error(error_msg, err, trace) log.error(error_msg, err, trace)
finally: finally:
proc.close(terminate=True, kill=True) proc.close(terminate=True, kill=True)
def _sign_files_with_gpg_agent(runas, local_keyid, abs_file, repodir, env, timeo ut): def _sign_files_with_gpg_agent(runas, local_keyid, abs_file, repodir, env, timeo ut):
""" """
Sign file with provided key utilizing gpg-agent Sign file with provided key utilizing gpg-agent
""" """
cmd = "rpmsign --verbose --key-id={0} --addsign {1}".format(local_keyid, ab s_file) cmd = "rpmsign --verbose --key-id={} --addsign {}".format(local_keyid, abs_ file)
retrc = __salt__["cmd.retcode"](cmd, runas=runas, cwd=repodir, use_vt=True, env=env) retrc = __salt__["cmd.retcode"](cmd, runas=runas, cwd=repodir, use_vt=True, env=env)
if retrc != 0: if retrc != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Signing encountered errors for command '{0}', " "Signing encountered errors for command '{}', "
"return error {1}, check logs for further details".format(cmd, retrc "return error {}, check logs for further details".format(cmd, retrc)
)
) )
def make_src_pkg( def make_src_pkg(
dest_dir, spec, sources, env=None, template=None, saltenv="base", runas="roo t" dest_dir, spec, sources, env=None, template=None, saltenv="base", runas="roo t"
): ):
""" """
Create a source rpm from the given spec file and sources Create a source rpm from the given spec file and sources
CLI Example: CLI Example:
skipping to change at line 455 skipping to change at line 446
using SHA256 as digest and minimum level dist el6 using SHA256 as digest and minimum level dist el6
""" """
_create_rpmmacros(runas) _create_rpmmacros(runas)
tree_base = _mk_tree(runas) tree_base = _mk_tree(runas)
spec_path = _get_spec(tree_base, spec, template, saltenv) spec_path = _get_spec(tree_base, spec, template, saltenv)
__salt__["file.chown"](path=spec_path, user=runas, group="mock") __salt__["file.chown"](path=spec_path, user=runas, group="mock")
__salt__["file.chown"](path=tree_base, user=runas, group="mock") __salt__["file.chown"](path=tree_base, user=runas, group="mock")
if isinstance(sources, six.string_types): if isinstance(sources, str):
sources = sources.split(",") sources = sources.split(",")
for src in sources: for src in sources:
_get_src(tree_base, src, saltenv, runas) _get_src(tree_base, src, saltenv, runas)
# make source rpms for dist el6 with SHA256, usable with mock on other dists # make source rpms for dist el6 with SHA256, usable with mock on other dists
cmd = 'rpmbuild --verbose --define "_topdir {0}" -bs --define "dist .el6" {1 }'.format( cmd = 'rpmbuild --verbose --define "_topdir {}" -bs --define "dist .el6" {}' .format(
tree_base, spec_path tree_base, spec_path
) )
retrc = __salt__["cmd.retcode"](cmd, runas=runas) retrc = __salt__["cmd.retcode"](cmd, runas=runas)
if retrc != 0: if retrc != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Make source package for destination directory {0}, spec {1}, source "Make source package for destination directory {}, spec {}, sources
s {2}, failed " {}, failed "
"with return error {3}, check logs for further details".format( "with return error {}, check logs for further details".format(
dest_dir, spec, sources, retrc dest_dir, spec, sources, retrc
) )
) )
srpms = os.path.join(tree_base, "SRPMS") srpms = os.path.join(tree_base, "SRPMS")
ret = [] ret = []
if not os.path.isdir(dest_dir): if not os.path.isdir(dest_dir):
__salt__["file.makedirs_perms"](name=dest_dir, user=runas, group="mock") __salt__["file.makedirs_perms"](name=dest_dir, user=runas, group="mock")
for fn_ in os.listdir(srpms): for fn_ in os.listdir(srpms):
full = os.path.join(srpms, fn_) full = os.path.join(srpms, fn_)
skipping to change at line 541 skipping to change at line 532
deps_dir = tempfile.mkdtemp() deps_dir = tempfile.mkdtemp()
deps_list = _get_deps(deps, deps_dir, saltenv) deps_list = _get_deps(deps, deps_dir, saltenv)
retrc = 0 retrc = 0
for srpm in srpms: for srpm in srpms:
dbase = os.path.dirname(srpm) dbase = os.path.dirname(srpm)
results_dir = tempfile.mkdtemp() results_dir = tempfile.mkdtemp()
try: try:
__salt__["file.chown"](path=dbase, user=runas, group="mock") __salt__["file.chown"](path=dbase, user=runas, group="mock")
__salt__["file.chown"](path=results_dir, user=runas, group="mock") __salt__["file.chown"](path=results_dir, user=runas, group="mock")
cmd = "mock --root={0} --resultdir={1} --init".format(tgt, results_d ir) cmd = "mock --root={} --resultdir={} --init".format(tgt, results_dir )
retrc |= __salt__["cmd.retcode"](cmd, runas=runas) retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
if deps_list and not deps_list.isspace(): if deps_list and not deps_list.isspace():
cmd = "mock --root={0} --resultdir={1} --install {2} {3}".format ( cmd = "mock --root={} --resultdir={} --install {} {}".format(
tgt, results_dir, deps_list, noclean tgt, results_dir, deps_list, noclean
) )
retrc |= __salt__["cmd.retcode"](cmd, runas=runas) retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
noclean += " --no-clean" noclean += " --no-clean"
cmd = "mock --root={0} --resultdir={1} {2} {3} {4}".format( cmd = "mock --root={} --resultdir={} {} {} {}".format(
tgt, results_dir, distset, noclean, srpm tgt, results_dir, distset, noclean, srpm
) )
retrc |= __salt__["cmd.retcode"](cmd, runas=runas) retrc |= __salt__["cmd.retcode"](cmd, runas=runas)
cmdlist = [ cmdlist = [
"rpm", "rpm",
"-qp", "-qp",
"--queryformat", "--queryformat",
"{0}/%{{name}}/%{{version}}-%{{release}}".format(log_dir), "{0}/%{{name}}/%{{version}}-%{{release}}".format(log_dir),
srpm, srpm,
] ]
skipping to change at line 596 skipping to change at line 587
if exc.errno != errno.EEXIST: if exc.errno != errno.EEXIST:
raise raise
shutil.copy(full, log_file) shutil.copy(full, log_file)
ret.setdefault("Log Files", []).append(log_file) ret.setdefault("Log Files", []).append(log_file)
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
log.error("Error building from %s: %s", srpm, exc) log.error("Error building from %s: %s", srpm, exc)
finally: finally:
shutil.rmtree(results_dir) shutil.rmtree(results_dir)
if retrc != 0: if retrc != 0:
raise SaltInvocationError( raise SaltInvocationError(
"Building packages for destination directory {0}, spec {1}, sources "Building packages for destination directory {}, spec {}, sources {}
{2}, failed " , failed "
"with return error {3}, check logs for further details".format( "with return error {}, check logs for further details".format(
dest_dir, spec, sources, retrc dest_dir, spec, sources, retrc
) )
) )
shutil.rmtree(deps_dir) shutil.rmtree(deps_dir)
shutil.rmtree(srpm_build_dir) shutil.rmtree(srpm_build_dir)
return ret return ret
def make_repo( def make_repo(
repodir, repodir,
keyid=None, keyid=None,
skipping to change at line 745 skipping to change at line 736
for fileused in os.listdir(repodir): for fileused in os.listdir(repodir):
if fileused.endswith(".rpm"): if fileused.endswith(".rpm"):
abs_file = os.path.join(repodir, fileused) abs_file = os.path.join(repodir, fileused)
if use_gpg_agent: if use_gpg_agent:
_sign_files_with_gpg_agent( _sign_files_with_gpg_agent(
runas, local_keyid, abs_file, repodir, env, timeout runas, local_keyid, abs_file, repodir, env, timeout
) )
else: else:
_sign_file(runas, define_gpg_name, phrase, abs_file, timeout) _sign_file(runas, define_gpg_name, phrase, abs_file, timeout)
cmd = "createrepo --update {0}".format(repodir) cmd = "createrepo --update {}".format(repodir)
retrc = __salt__["cmd.run_all"](cmd, runas=runas) retrc = __salt__["cmd.run_all"](cmd, runas=runas)
return retrc return retrc
 End of changes. 32 change blocks. 
44 lines changed or deleted 34 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)