Erste Schritte Rund um Ansible

Bild: Ansible Logo

Nachdem wir uns bereits eingehend mit den Grundlagen und auch schon das benötigte Programmpaket auf unserer Admin-Workstation zur Orchestrierung installiert haben, machen wir uns nun an die Konfiguration von Ansible und wagen uns an die ersten Playbooks heran.

Einer der wesentlichen Vorteil von Ansible ist, dass hierzu kein separater eigener Server von Nöten ist, sondern dass im Grunde ein (Client-/Admin-)Workstation ausreicht, auf dem die Playbooks zur Verfügung stehen. Gleichwohl wird man in größeren Installationsumgebungen in aller Regel einen dedizierten Server zur Orchestrierung mit Ansible vorhalten. Dieser Rechner muss dann lediglich die Server, die automatisiert verwaltet und konfiguriert werden sollen, mit Hilfe der SSH erreichen können, denn Ansible arbeitet im Push-Verfahren und benötigt neben SSH und Python keine weitere Installation auf den einzelnen Systemen.

Wichtige Hinweise zu Sicherheitsthemen rund um SSH finden sich im Chapter 1. Using secure communications between two systems with OpenSSH aus dem Red Hat Enterprise Securing networks Guide.

Damit sich nun unser Ansible-Admin-Host auch Verbindungen mit Hilfe der SSH1) aufbauen kann, benötigen wir natürlich entsprechendes Schlüsselmaterial, abhängig vom jeweiligen Zielsystem bzw. den kryptographischen Eigenschaften.

RSA-Schlüssel

Im ersten Fall erstellen wir uns einen RSA-Schlüssel (-t), mit einer Schlüssellänge von 4096 Bit (-b). Dabei verwenden wir als Beschreibung Ansible Systemuser (-C) und als Ziel-/Speicherort ~/.ssh/id_rsa_ansible (-f)

 $ ssh-keygen -b 4096 -t rsa -C 'Ansible Systemuser' -f ~/.ssh/id_rsa_ansible
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/django/.ssh/id_rsa_ansible.
Your public key has been saved in /home/django/.ssh/id_rsa_ansible.pub.
The key fingerprint is:
SHA256:0LJeZ5yvooIjAM0ctSCheyqOLfIMzWU/YZ3BLAgsQkc Ansible Systemuser
The key's randomart image is:
+---[RSA 4096]----+
|+++E.            |
|+.+o o +         |
|o= .o + =        |
|..+    * + .     |
|o . o + S =      |
|.= o + o o .     |
|= o.  +     .    |
|B+o .  ..  .     |
|o=+. ... ..      |
+----[SHA256]-----+

ED25519-Schlüssel

Ob man in Zeiten von Überwachungsphantasten in Unternehmen und vor allem auch bei einer NSA oder BND, noch solchen RSA-Schlüssel einsetzen kann und mag, muss natürlich jeder Admin für sich selbst entscheiden.

Der Sicherheitsguru Bruce Schneier hat in seinem Blog hierzu eine eindeutige Aussage getätigt:

„On the crypto bits in your guardian piece, I found especially interesting that you suggest classic discrete log crypto over ecc. I want to ask if you could elaborate more on that.“ I no longer trust the constants. I believe the NSA has manipulated them through their relationships with industry.

Auf diese RSA-Schlüssel muss man aber nicht mehr zwingend zurückgreifen, stehen doch aktuellere und zeitgemässere Cipher, MACs, Schlüssel Typen und Key Exchange Algorithmen zur Verfügung. Als Alternative zu einem RSA-Keys wollen wir nun nun einen ed25519 Schlüssel erzeugen. Ed25519 ist ein Elliptic Curve Signature Schema, welches beste Sicherheit bei vertretbaren Aufwand verspricht, als ECDSA oder DSA dies der Fall ist. Zur Auswahl sicherer kryptografischer Kurven bei der Elliptic-Curve Cryptography findet man auf der Seite hier hilfreiche Erklärungen und eine Gegenüberstellung der möglichen verschiedenen Alternativen.

Im zweiten Fall erstellen wir uns nun einen ED25519-Schlüssel (-t), mit einer festen Schlüssellänge. Der Parameter (-a) beschreibt dabei die Anzahl der KDF-Schlüsselableitfunktion (siehe manpage von ssh-keygen). Wir verwenden wieder als Beschreibung Ansible Systemuser (-C) und als Ziel-/Speicherort ~/.ssh/id_ed25519_ansible (-f).

  $ ssh-keygen -t ed25519 -a 100 -C 'Ansible Systemuser' -f ~/.ssh/id_ed25519_ansible
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/django/.ssh/id_ed25519_ansible.
Your public key has been saved in /home/django/.ssh/id_ed25519_ansible.pub.
The key fingerprint is:
SHA256:jTZQUDbCqZaV648fKVBfx3L4+tBMWL+z+iUCBY3kKMQ Ansible Systemuser
The key's randomart image is:
+--[ED25519 256]--+
|     o+==.oo     |
|     .E+ +.+.    |
|     ++.. = *    |
|    +..+ + O .   |
|   ...  S + o .  |
|     ... o *   . |
|      .oo o + + .|
|      .... o . = |
|       ..   ooo  |
+----[SHA256]-----

Hat man die Struktur, wie i.d.R. öffentliche Schlüssel auf einem Zielsystem bereits verinnerlicht, kann man den kurzen erklärenden Teil natürlich überspringen und direkt zum Abschnitt ssh-copy-id springen.

Auf dem den Zielrechnern legen wir nun das Verzeichnis .ssh an und schützen es entsprechend.

[django@zielhost django]$ (umask 077 ; mkdir -p $HOME/.ssh)

Den öffentlichen Schlüssel kopieren wir dann wie folgt auf das Zielsystem; hatten wir uns einen RSA-key erstellt verwenden wir folgenden Aufruf:

 $  scp /home/django/.ssh/id_rsa_ansible.pub zielhost:/home/django/.ssh/key.pub

bzw. bei einem ed25519 Schlüssel:

 $  scp /home/django/.ssh/id_ed25519_ansible.pub zielhost:/home/django/.ssh/key.pub

Anschliessend wird der Schlüssel in die Datei authorized_keys kopiert. Diese Datei kann mehrere Schlüssel enthalten, daher ist das doppelte Umleitungszeichen wichtig, um eine evt. existierende Datei nicht versehentlich zu überschreiben. Somit wird der neue Schlüssel in die Datei hinzugefügt:

 $ cat key.pub >> authorized_keys

Zu guter Letzt passen wir noch die Berechtigungen an und löschen die nicht mehr benötigten Public-key-Datei.

 $ chmod 600 authorized_keys
 $ rm key.pub

ssh-copy-id  
Das Kopieren des Public-Keys auf unseren Zielhost mit Anpassen der Dateiberechtigungen kann man natürlich auch einfacher vornehmen. Man benutzt hierzu einfach den Befehl ssh-copy-id aus dem Paket openssh-clients.

  • RSA-Key
     $ ssh-copy-id -i ~/.ssh/id_rsa_ansible.pub zielhost.dmz.nausch.org
  • ed25519-Key
     $ ssh-copy-id -i ~/.ssh/id_ed25519_ansible.pub zielhost.intra.nausch.org

Mit der Angabe ~/.ssh/id_rsa4096_dmz bzw. ~/.ssh/id_ed25519_dmz kopiert dann der Befehl ssh-copy-id den zugehörigen öffentlichen Schlüssel auf den Zielhost zielhost.intra.nausch.org.

Nun können wir eine erste Testverbindung zu unserem Zielsystem aufbauen.

 $ ssh -l ansible -i ~/.ssh/id_ed25519_ansible 10.0.0.190
##############################################################################
#                                                                            #
#     ╭∩╮( ͡° ل͟ ͡° )╭∩╮   This is not your server!   ╭∩╮( ͡° ل͟ ͡° )╭∩╮       #
#                                                                            #
#             Unauthorized access to this system is prohibited !             #
#                                                                            #
#    This system is actively monitored and all connections may be logged.    #
#         By accessing this system, you consent to this monitoring.          #
#                                                                            #
##############################################################################
Activate the web console with: systemctl enable --now cockpit.socket

Last login: Mon Feb 30 21:43:35 2019

Die Konfigurationsdateien rund um Ansible finden sich im Verzeichnis /etc/ansible.

/etc/ansible/
├── ansible.cfg
├── hosts
└── roles

1 directory, 2 files

Fast alle Parameter können in einem Ansible-playbook oder mit Kommandozeilenflags überschrieben werden.

Bei der Ermittlung der Konfigurationsdatei - ansible.cfg sucht Ansible die Ansible Konfigurationseinstellungen nacheinander in folgenden Verzeichnissen:

  1. Erst durch die Umgebungsvariable ANSIBLE_CONFIG definierten individuellen Konfigurationsdatei,
  2. dann die Datei ./ansible.cfg im aktuellen Verzeichnis und
  3. anschließend ~/.ansible.cfg im $HOME Verzeichnis des verwendeten Benutzers und
  4. zum Schluss in der Konfigurationsdatei /etc/ansible/ansible.cfg.
 # vim /etc/ansible/ansible.cfg
/etc/ansible/ansible.cfg
# config file for ansible -- https://ansible.com/
# ===============================================
 
# nearly all parameters can be overridden in ansible-playbook
# or with command line flags. ansible will read ANSIBLE_CONFIG,
# ansible.cfg in the current working directory, .ansible.cfg in
# the home directory or /etc/ansible/ansible.cfg, whichever it
# finds first
 
[defaults]
 
# some basic default values...
 
#inventory      = /etc/ansible/hosts
#library        = /usr/share/my_modules/
#module_utils   = /usr/share/my_module_utils/
#remote_tmp     = ~/.ansible/tmp
#local_tmp      = ~/.ansible/tmp
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks          = 5
#poll_interval  = 15
#sudo_user      = root
#ask_sudo_pass = True
#ask_pass      = True
#transport      = smart
#remote_port    = 22
#module_lang    = C
#module_set_locale = False
 
# plays will gather facts by default, which contain information about
# the remote system.
#
# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
#gathering = implicit
 
# This only affects the gathering done by a play's gather_facts directive,
# by default gathering retrieves all facts subsets
# all - gather all subsets
# network - gather min and network facts
# hardware - gather hardware facts (longest facts to retrieve)
# virtual - gather min and virtual facts
# facter - import facts from facter
# ohai - import facts from ohai
# You can combine them using comma (ex: network,virtual)
# You can negate them using ! (ex: !hardware,!facter,!ohai)
# A minimal set of facts is always gathered.
#gather_subset = all
 
# some hardware related facts are collected
# with a maximum timeout of 10 seconds. This
# option lets you increase or decrease that
# timeout to something more suitable for the
# environment.
# gather_timeout = 10
 
# Ansible facts are available inside the ansible_facts.* dictionary
# namespace. This setting maintains the behaviour which was the default prior
# to 2.5, duplicating these variables into the main namespace, each with a
# prefix of 'ansible_'.
# This variable is set to True by default for backwards compatibility. It
# will be changed to a default of 'False' in a future release.
# ansible_facts.
# inject_facts_as_vars = True
 
# additional paths to search for roles in, colon separated
#roles_path    = /etc/ansible/roles
 
# uncomment this to disable SSH key host checking
#host_key_checking = False
 
# change the default callback, you can only have one 'stdout' type  enabled at a time.
#stdout_callback = skippy
 
 
## Ansible ships with some plugins that require whitelisting,
## this is done to avoid running all of a type by default.
## These setting lists those that you want enabled for your system.
## Custom plugins should not need this unless plugin author specifies it.
 
# enable callback plugins, they can output to stdout but cannot be 'stdout' type.
#callback_whitelist = timer, mail
 
# Determine whether includes in tasks and handlers are "static" by
# default. As of 2.0, includes are dynamic by default. Setting these
# values to True will make includes behave more like they did in the
# 1.x versions.
#task_includes_static = False
#handler_includes_static = False
 
# Controls if a missing handler for a notification event is an error or a warning
#error_on_missing_handler = True
 
# change this for alternative sudo implementations
#sudo_exe = sudo
 
# What flags to pass to sudo
# WARNING: leaving out the defaults might create unexpected behaviours
#sudo_flags = -H -S -n
 
# SSH timeout
#timeout = 10
 
# default user to use for playbooks if user is not specified
# (/usr/bin/ansible will use current user as default)
#remote_user = root
 
# logging is off by default unless this path is defined
# if so defined, consider logrotate
#log_path = /var/log/ansible.log
 
# default module name for /usr/bin/ansible
#module_name = command
 
# use this shell for commands executed under sudo
# you may need to change this to bin/bash in rare instances
# if sudo is constrained
#executable = /bin/sh
 
# if inventory variables overlap, does the higher precedence one win
# or are hash values merged together?  The default is 'replace' but
# this can also be set to 'merge'.
#hash_behaviour = replace
 
# by default, variables from roles will be visible in the global variable
# scope. To prevent this, the following option can be enabled, and only
# tasks and handlers within the role will see the variables there
#private_role_vars = yes
 
# list any Jinja2 extensions to enable here:
#jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n
 
# if set, always use this private key file for authentication, same as
# if passing --private-key to ansible or ansible-playbook
#private_key_file = /path/to/file
 
# If set, configures the path to the Vault password file as an alternative to
# specifying --vault-password-file on the command line.
#vault_password_file = /path/to/vault_password_file
 
# format of string {{ ansible_managed }} available within Jinja2
# templates indicates to users editing templates files will be replaced.
# replacing {file}, {host} and {uid} and strftime codes with proper values.
#ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
# {file}, {host}, {uid}, and the timestamp can all interfere with idempotence
# in some situations so the default is a static string:
#ansible_managed = Ansible managed
 
# by default, ansible-playbook will display "Skipping [host]" if it determines a task
# should not be run on a host.  Set this to "False" if you don't want to see these "Skipping"
# messages. NOTE: the task header will still be shown regardless of whether or not the
# task is skipped.
#display_skipped_hosts = True
 
# by default, if a task in a playbook does not include a name: field then
# ansible-playbook will construct a header that includes the task's action but
# not the task's args.  This is a security feature because ansible cannot know
# if the *module* considers an argument to be no_log at the time that the
# header is printed.  If your environment doesn't have a problem securing
# stdout from ansible-playbook (or you have manually specified no_log in your
# playbook on all of the tasks where you have secret information) then you can
# safely set this to True to get more informative messages.
#display_args_to_stdout = False
 
# by default (as of 1.3), Ansible will raise errors when attempting to dereference
# Jinja2 variables that are not set in templates or action lines. Uncomment this line
# to revert the behavior to pre-1.3.
#error_on_undefined_vars = False
 
# by default (as of 1.6), Ansible may display warnings based on the configuration of the
# system running ansible itself. This may include warnings about 3rd party packages or
# other conditions that should be resolved if possible.
# to disable these warnings, set the following value to False:
#system_warnings = True
 
# by default (as of 1.4), Ansible may display deprecation warnings for language
# features that should no longer be used and will be removed in future versions.
# to disable these warnings, set the following value to False:
#deprecation_warnings = True
 
# (as of 1.8), Ansible can optionally warn when usage of the shell and
# command module appear to be simplified by using a default Ansible module
# instead.  These warnings can be silenced by adjusting the following
# setting or adding warn=yes or warn=no to the end of the command line
# parameter string.  This will for example suggest using the git module
# instead of shelling out to the git command.
# command_warnings = False
 
 
# set plugin path directories here, separate with colons
#action_plugins     = /usr/share/ansible/plugins/action
#become_plugins     = /usr/share/ansible/plugins/become
#cache_plugins      = /usr/share/ansible/plugins/cache
#callback_plugins   = /usr/share/ansible/plugins/callback
#connection_plugins = /usr/share/ansible/plugins/connection
#lookup_plugins     = /usr/share/ansible/plugins/lookup
#inventory_plugins  = /usr/share/ansible/plugins/inventory
#vars_plugins       = /usr/share/ansible/plugins/vars
#filter_plugins     = /usr/share/ansible/plugins/filter
#test_plugins       = /usr/share/ansible/plugins/test
#terminal_plugins   = /usr/share/ansible/plugins/terminal
#strategy_plugins   = /usr/share/ansible/plugins/strategy
 
 
# by default, ansible will use the 'linear' strategy but you may want to try
# another one
#strategy = free
 
# by default callbacks are not loaded for /bin/ansible, enable this if you
# want, for example, a notification or logging callback to also apply to
# /bin/ansible runs
#bin_ansible_callbacks = False
 
 
# don't like cows?  that's unfortunate.
# set to 1 if you don't want cowsay support or export ANSIBLE_NOCOWS=1
#nocows = 1
 
# set which cowsay stencil you'd like to use by default. When set to 'random',
# a random stencil will be selected for each task. The selection will be filtered
# against the `cow_whitelist` option below.
#cow_selection = default
#cow_selection = random
 
# when using the 'random' option for cowsay, stencils will be restricted to this list.
# it should be formatted as a comma-separated list with no spaces between names.
# NOTE: line continuations here are for formatting purposes only, as the INI parser
#       in python does not support them.
#cow_whitelist=bud-frogs,bunny,cheese,daemon,default,dragon,elephant-in-snake,elephant,eyes,\
#              hellokitty,kitty,luke-koala,meow,milk,moofasa,moose,ren,sheep,small,stegosaurus,\
#              stimpy,supermilker,three-eyes,turkey,turtle,tux,udder,vader-koala,vader,www
 
# don't like colors either?
# set to 1 if you don't want colors, or export ANSIBLE_NOCOLOR=1
#nocolor = 1
 
# if set to a persistent type (not 'memory', for example 'redis') fact values
# from previous runs in Ansible will be stored.  This may be useful when
# wanting to use, for example, IP information from one group of servers
# without having to talk to them in the same playbook run to get their
# current IP information.
#fact_caching = memory
 
#This option tells Ansible where to cache facts. The value is plugin dependent.
#For the jsonfile plugin, it should be a path to a local directory.
#For the redis plugin, the value is a host:port:database triplet: fact_caching_connection = localhost:6379:0
 
#fact_caching_connection=/tmp
 
 
 
# retry files
# When a playbook fails a .retry file can be created that will be placed in ~/
# You can enable this feature by setting retry_files_enabled to True
# and you can change the location of the files by setting retry_files_save_path
 
#retry_files_enabled = False
#retry_files_save_path = ~/.ansible-retry
 
# squash actions
# Ansible can optimise actions that call modules with list parameters
# when looping. Instead of calling the module once per with_ item, the
# module is called once with all items at once. Currently this only works
# under limited circumstances, and only with parameters named 'name'.
#squash_actions = apk,apt,dnf,homebrew,pacman,pkgng,yum,zypper
 
# prevents logging of task data, off by default
#no_log = False
 
# prevents logging of tasks, but only on the targets, data is still logged on the master/controller
#no_target_syslog = False
 
# controls whether Ansible will raise an error or warning if a task has no
# choice but to create world readable temporary files to execute a module on
# the remote machine.  This option is False by default for security.  Users may
# turn this on to have behaviour more like Ansible prior to 2.1.x.  See
# https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user
# for more secure ways to fix this than enabling this option.
#allow_world_readable_tmpfiles = False
 
# controls the compression level of variables sent to
# worker processes. At the default of 0, no compression
# is used. This value must be an integer from 0 to 9.
#var_compression_level = 9
 
# controls what compression method is used for new-style ansible modules when
# they are sent to the remote system.  The compression types depend on having
# support compiled into both the controller's python and the client's python.
# The names should match with the python Zipfile compression types:
# * ZIP_STORED (no compression. available everywhere)
# * ZIP_DEFLATED (uses zlib, the default)
# These values may be set per host via the ansible_module_compression inventory
# variable
#module_compression = 'ZIP_DEFLATED'
 
# This controls the cutoff point (in bytes) on --diff for files
# set to 0 for unlimited (RAM may suffer!).
#max_diff_size = 1048576
 
# This controls how ansible handles multiple --tags and --skip-tags arguments
# on the CLI.  If this is True then multiple arguments are merged together.  If
# it is False, then the last specified argument is used and the others are ignored.
# This option will be removed in 2.8.
#merge_multiple_cli_flags = True
 
# Controls showing custom stats at the end, off by default
#show_custom_stats = True
 
# Controls which files to ignore when using a directory as inventory with
# possibly multiple sources (both static and dynamic)
#inventory_ignore_extensions = ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo
 
# This family of modules use an alternative execution path optimized for network appliances
# only update this setting if you know how this works, otherwise it can break module execution
#network_group_modules=eos, nxos, ios, iosxr, junos, vyos
 
# When enabled, this option allows lookups (via variables like {{lookup('foo')}} or when used as
# a loop with `with_foo`) to return data that is not marked "unsafe". This means the data may contain
# jinja2 templating language which will be run through the templating engine.
# ENABLING THIS COULD BE A SECURITY RISK
#allow_unsafe_lookups = False
 
# set default errors for all plays
#any_errors_fatal = False
 
[inventory]
# enable inventory plugins, default: 'host_list', 'script', 'auto', 'yaml', 'ini', 'toml'
#enable_plugins = host_list, virtualbox, yaml, constructed
 
# ignore these extensions when parsing a directory as inventory source
#ignore_extensions = .pyc, .pyo, .swp, .bak, ~, .rpm, .md, .txt, ~, .orig, .ini, .cfg, .retry
 
# ignore files matching these patterns when parsing a directory as inventory source
#ignore_patterns=
 
# If 'true' unparsed inventory sources become fatal errors, they are warnings otherwise.
#unparsed_is_failed=False
 
[privilege_escalation]
#become=True
#become_method=sudo
#become_user=root
#become_ask_pass=False
 
[paramiko_connection]
 
# uncomment this line to cause the paramiko connection plugin to not record new host
# keys encountered.  Increases performance on new host additions.  Setting works independently of the
# host key checking setting above.
#record_host_keys=False
 
# by default, Ansible requests a pseudo-terminal for commands executed under sudo. Uncomment this
# line to disable this behaviour.
#pty=False
 
# paramiko will default to looking for SSH keys initially when trying to
# authenticate to remote devices.  This is a problem for some network devices
# that close the connection after a key failure.  Uncomment this line to
# disable the Paramiko look for keys function
#look_for_keys = False
 
# When using persistent connections with Paramiko, the connection runs in a
# background process.  If the host doesn't already have a valid SSH key, by
# default Ansible will prompt to add the host key.  This will cause connections
# running in background processes to fail.  Uncomment this line to have
# Paramiko automatically add host keys.
#host_key_auto_add = True
 
[ssh_connection]
 
# ssh arguments to use
# Leaving off ControlPersist will result in poor performance, so use
# paramiko on older platforms rather than removing it, -C controls compression use
#ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
 
# The base directory for the ControlPath sockets.
# This is the "%(directory)s" in the control_path option
#
# Example:
# control_path_dir = /tmp/.ansible/cp
#control_path_dir = ~/.ansible/cp
 
# The path to use for the ControlPath sockets. This defaults to a hashed string of the hostname,
# port and username (empty string in the config). The hash mitigates a common problem users
# found with long hostnames and the conventional %(directory)s/ansible-ssh-%%h-%%p-%%r format.
# In those cases, a "too long for Unix domain socket" ssh error would occur.
#
# Example:
# control_path = %(directory)s/%%h-%%r
#control_path =
 
# Enabling pipelining reduces the number of SSH operations required to
# execute a module on the remote server. This can result in a significant
# performance improvement when enabled, however when using "sudo:" you must
# first disable 'requiretty' in /etc/sudoers
#
# By default, this option is disabled to preserve compatibility with
# sudoers configurations that have requiretty (the default on many distros).
#
#pipelining = False
 
# Control the mechanism for transferring files (old)
#   * smart = try sftp and then try scp [default]
#   * True = use scp only
#   * False = use sftp only
#scp_if_ssh = smart
 
# Control the mechanism for transferring files (new)
# If set, this will override the scp_if_ssh option
#   * sftp  = use sftp to transfer files
#   * scp   = use scp to transfer files
#   * piped = use 'dd' over SSH to transfer files
#   * smart = try sftp, scp, and piped, in that order [default]
#transfer_method = smart
 
# if False, sftp will not use batch mode to transfer files. This may cause some
# types of file transfer failures impossible to catch however, and should
# only be disabled if your sftp version has problems with batch mode
#sftp_batch_mode = False
 
# The -tt argument is passed to ssh when pipelining is not enabled because sudo 
# requires a tty by default. 
#usetty = True
 
# Number of times to retry an SSH connection to a host, in case of UNREACHABLE.
# For each retry attempt, there is an exponential backoff,
# so after the first attempt there is 1s wait, then 2s, 4s etc. up to 30s (max).
#retries = 3
 
[persistent_connection]
 
# Configures the persistent connection timeout value in seconds.  This value is
# how long the persistent connection will remain idle before it is destroyed.
# If the connection doesn't receive a request before the timeout value
# expires, the connection is shutdown. The default value is 30 seconds.
#connect_timeout = 30
 
# The command timeout value defines the amount of time to wait for a command
# or RPC call before timing out. The value for the command timeout must
# be less than the value of the persistent connection idle timeout (connect_timeout)
# The default value is 30 second.
#command_timeout = 30
 
[accelerate]
#accelerate_port = 5099
#accelerate_timeout = 30
#accelerate_connect_timeout = 5.0
 
# The daemon timeout is measured in minutes. This time is measured
# from the last activity to the accelerate daemon.
#accelerate_daemon_timeout = 30
 
# If set to yes, accelerate_multi_key will allow multiple
# private keys to be uploaded to it, though each user must
# have access to the system via SSH to add a new key. The default
# is "no".
#accelerate_multi_key = yes
 
[selinux]
# file systems that require special treatment when dealing with security context
# the default behaviour that copies the existing context or uses the user default
# needs to be changed to use the file system dependent context.
#special_context_filesystems=nfs,vboxsf,fuse,ramfs,9p,vfat
 
# Set this to yes to allow libvirt_lxc connections to work without SELinux.
#libvirt_lxc_noseclabel = yes
 
[colors]
#highlight = white
#verbose = blue
#warn = bright purple
#error = red
#debug = dark gray
#deprecate = purple
#skip = cyan
#unreachable = red
#ok = green
#changed = yellow
#diff_add = green
#diff_remove = red
#diff_lines = cyan
 
 
[diff]
# Always print diff when running ( same as always running with -D/--diff )
# always = no
 
# Set how many context lines to show in diff
# context = 3

In der Konfigurationsdatei /etc/ansible/hosts werden alle Hosts und Hostgruppen definiert, über die Ansible bei der Orchestrierung mit Hilfe der SSH zentral administriert werden sollen.

In dieser Konfigurationsdatei gelten folgende Rahmenparameter:

  • Kommentare beginnen mit dem Zeichen '#'.
  • Leere Zeilen werden ignoriert
  • Gruppen von Hosts werden durch [header]-Elemente abgegrenzt
  • Es können Hostnamen oder IP-Adressen verwendet werden
  • Ein Hostname/IP-Adresse kann Mitglied mehrerer Gruppen sein
 # vim /etc/ansible/hosts
/etc/ansible/hosts
#
# It should live in /etc/ansible/hosts
#
#   - Comments begin with the '#' character
#   - Blank lines are ignored
#   - Groups of hosts are delimited by [header] elements
#   - You can enter hostnames or ip addresses
#   - A hostname/ip can be a member of multiple groups
 
# Ex 1: Ungrouped hosts, specify before any group headers.
 
## green.example.com
## blue.example.com
## 192.168.100.1
## 192.168.100.10
 
# Ex 2: A collection of hosts belonging to the 'webservers' group
 
## [webservers]
## alpha.example.org
## beta.example.org
## 192.168.1.100
## 192.168.1.110
 
# If you have multiple hosts following a pattern you can specify
# them like this:
 
## www[001:006].example.com
 
# Ex 3: A collection of database servers in the 'dbservers' group
 
## [dbservers]
## 
## db01.intranet.mydomain.net
## db02.intranet.mydomain.net
## 10.25.1.56
## 10.25.1.57
 
# Here's another example of host ranges, this time there are no
# leading 0s:
 
## db-[99:101]-node.example.com

Gemäß Ansible's Best Practices gibt es durchaus einige Möglichkeiten wie man seine Playbooks organisiert. In aller Regel wird man hier die eigenen individuellen Bedürfnisse (seiner Unternehmung) voran stellen.

Jedoch empfiehlt es sich durchaus auf Empfohlenes zurückzugreifen! So empfiehlt es sich zum Beispiel auch, Rollen anstelle von Aufgaben zu verwenden, da dies wesentlich bei der Flexibilität und besseren Organisation der eigenen Playbooks/Codes helfen!

Ansible bietet zwei Beispiele für Verzeichnis-Layouts. Wir werden im folgen Beispiel uns eine Umgebung aufbauen, in der unser Inventory, also unsere Hosts und Nodes, in mehreren groups und childs aufteilen.

Mit dieser Struktur sind wir dann in der Lage jede Inventardatei mit ihrer group_vars und host_vars in ein separates Verzeichnis zu packen. Fernen können wir so einfach neue Rollen roleserzeugen, in dem wir dann einfach die bereits vorgefertigte Rollenvorlage common kopieren.

Werfen wir also einfach mal auf die beschrieben Verzeichnisstruktur einen genaueren Blick. Die entsprechende Verwendung der einzelnen Verzeichnisse und Dateien ist in der Aufstellung entsprechend angegeben.

ansible/
├── filter_plugins                 # (optionales) Verzeichnis für individuelle filter plugins
├── library                        # (optionales) Verzeichnis für benutzerdefinierte Module
├── module_utils                   # (optionales) Verzeichnis für benutzerdefinierte module_utils zur Unterstützung von Modulen
│
├── inventories                    # Verzeichnis für die einzelnen (unterschiedlichen) Invenory
│   ├── production                 # Verzeichnis für die Hosts aus der Gruppe production
│   │   ├── hosts.yml              # YML-Datei mit den Host-Definitionen aus der Gruppe production
│   │   ├── group_vars             # Verzeichnis für die Gruppenspezifischen Variablen der Gruppe production
│   │   └── host_vars              # Verzeichnis für die Hostspezifischen Variablen der Gruppe production
│   │    
│   └── staging                    # Verzeichnis für die Hosts aus der Gruppe staging
│       ├── hosts.yml              # YML-Datei mit den Host-Definitionen aus der Gruppe production
│       ├── group_vars             # Verzeichnis für die Gruppenspezifischen Variablen der Gruppe 
│       └── host_vars              # Verzeichnis für die Hostspezifischen Variablen der Gruppe production
│
├── roles                          # Verzeichnis für die einzelnen (unterschiedlichen) Rollen
│   └── common                     # Verzeichnis "role" common mit seinen entsprechenden Definitionen - Vorlage zum Kopieren
│       ├── defaults               # Verzeichnis "defaults"
│       │   └── main.yml           # Standardvariablen mit niedrigerer Priorität für diese Rolle
│       ├── files                  # Verzeichnis "files"
│       │   └── main.yml           # (Skript-)Dateien zur Verwendung als Kopier- bzw. Script-Rressource
│       ├── handlers               # Verzeichnis "handlers"
│       │   └── main.yml           # Datei mit den Definitionen zu den rollenspezifischen "handlers"
│       ├── library                # Verzeichnis für benutzerdefinierte Module einer der Rolle (role) "common"
│       ├── lookup_plugin          # Verzeichnis für weitere Arten von Plugins, wie in diesem Fall "lookup"
│       ├── meta                   # Verzeichnis "meta" für Rollenspezifische Definitionen/Abhängigkeiten
│       │   └── main.yml           # Datei mit Rollenspezifischen Definitionen
│       ├── module_utils           # Verzeichnis "module_utils", die benutzerdefinierte module_utils der Rollen enthalten könnte
│       ├── tasks                  # Verzeichnis "tasks" für kleinere Aufgabendateien
│       │   └── main.yml           # Datei für kleinere Aufgaben, falls diese benötigt werden würden 
│       ├── templates              # Verzeichnis mit den Templates
│       │   └── main.j2            # Template-Datei mit dem Dateisuffix/-Ende .j2
│       └── vars                   # Verzeichnis "vars", mit den zu dieser Rolle zugeordneten Variablen
│           └── main.yml           # Datei mit den Rollenspezifischen Variablen
│
└── site.yml                       # master playbook

Um dieses Verzeichnis-Layout einfach und schnell auf den Weg zu bringen, verwenden wir die nachfolgend gezeigten zwei Befehle bzw. genauer gesagt die beiden Befehlskette:

 $ mkdir -p ~/ansible/inventories/{production,staging}/{group_vars,host_vars} \
            ~/ansible/{library,module_utils,filter_plugins} \
            ~/ansible/roles/common/{tasks,handlers,templates,files,vars,defaults,meta,library,module_utils,lookup_plugin}
 $ touch    ~/ansible/inventories/{production,staging}/hosts.yml \
            ~/ansible/site.yml \
            ~/ansible/roles/common/{tasks,handlers,templates,files,vars,defaults,meta}/main.yml

Wir werden uns nun im Detail die wichtigsten Optionen ansehen und dabei deren Auswirkung genauer ansehen.

In unserer Produktionsumgebung werden wir aber keineswegs für jeden Admin die Konfiguration manuell vornehmen zu müssen. Vielmehr werden wir Ansible mit Hilfe eines Ansible-Playbooks reproduzierbar grundkonfigurieren. Im Kapitel Ansible - erweiterte Konfigurationsbeispiele: Ansible mit Hilfe von Ansible einrichten werden wir das später genauer betrachten!

ansible.cfg

Wie schon zuvor angemerkt sucht Ansible die Ansible Konfigurationseinstellungen bei der Ermittlung der Konfigurationseinstellungen nacheinander in folgenden Verzeichnissen:

  1. Erst durch die Umgebungsvariable ANSIBLE_CONFIG definierten individuellen Konfigurationsdatei,
  2. dann die Datei ./ansible.cfg im aktuellen Verzeichnis und
  3. anschließend ~/.ansible.cfg im $HOME Verzeichnis des verwendeten Benutzers und
  4. zum Schluss in der Konfigurationsdatei /etc/ansible/ansible.cfg.

Wir werden nun unsere Host- und Admin-spezifische Konfiguration also in der datei .ansible.cfg im ~-/Homeverzeichnis unseres Adminbenutzers vorhalten. Dazu kopieren wir uns die aus dem RPM stammende Musterdatei in unser home-Verzeichnis.

 # cp /etc/ansible/ansible.cfg ~/.ansible.cfg

Zuvor haten wir bereits die Datei ~/ansible/inventories/production/hosts.yml für die Definition der Hosts (Inventory), bei der Verzeichnisanlage unserer Musterinstallation angelegt. Damit diese auch verwendet werden kann werden wir als erstes in der Konfigurationsdatei .ansible.cfg hierzu die Betreffende Option inventory in der Sektion [defaults] setzen.

 # vim /etc/ansible/ansible.cfg
/etc/ansible/ansible.cfg
# config file for ansible -- https://ansible.com/
# ===============================================
 
# nearly all parameters can be overridden in ansible-playbook
# or with command line flags. ansible will read ANSIBLE_CONFIG,
# ansible.cfg in the current working directory, .ansible.cfg in
# the home directory or /etc/ansible/ansible.cfg, whichever it
# finds first
 
[defaults]
 
# some basic default values...
 
# Django : 2020-06-19
# default: #inventory      = /etc/ansible/hosts
inventory       = ~/ansible/inventories/production/hosts.yml
#library        = /usr/share/my_modules/
#module_utils   = /usr/share/my_module_utils/
#remote_tmp     = ~/.ansible/tmp
#local_tmp      = ~/.ansible/tmp
#plugin_filters_cfg = /etc/ansible/plugin_filters.yml
#forks          = 5
#poll_interval  = 15
#sudo_user      = root
#ask_sudo_pass = True
#ask_pass      = True
#transport      = smart
#remote_port    = 22
#module_lang    = C
#module_set_locale = False
 
# plays will gather facts by default, which contain information about
# the remote system.
#
# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
#gathering = implicit
 
# This only affects the gathering done by a play's gather_facts directive,
# by default gathering retrieves all facts subsets
# all - gather all subsets
# network - gather min and network facts
# hardware - gather hardware facts (longest facts to retrieve)
# virtual - gather min and virtual facts
# facter - import facts from facter
# ohai - import facts from ohai
# You can combine them using comma (ex: network,virtual)
# You can negate them using ! (ex: !hardware,!facter,!ohai)
# A minimal set of facts is always gathered.
#gather_subset = all
 
# some hardware related facts are collected
# with a maximum timeout of 10 seconds. This
# option lets you increase or decrease that
# timeout to something more suitable for the
# environment.
# gather_timeout = 10
 
# Ansible facts are available inside the ansible_facts.* dictionary
# namespace. This setting maintains the behaviour which was the default prior
# to 2.5, duplicating these variables into the main namespace, each with a
# prefix of 'ansible_'.
# This variable is set to True by default for backwards compatibility. It
# will be changed to a default of 'False' in a future release.
# ansible_facts.
# inject_facts_as_vars = True
 
# additional paths to search for roles in, colon separated
#roles_path    = /etc/ansible/roles
 
# uncomment this to disable SSH key host checking
#host_key_checking = False
 
# change the default callback, you can only have one 'stdout' type  enabled at a time.
#stdout_callback = skippy
 
 
## Ansible ships with some plugins that require whitelisting,
## this is done to avoid running all of a type by default.
## These setting lists those that you want enabled for your system.
## Custom plugins should not need this unless plugin author specifies it.
 
# enable callback plugins, they can output to stdout but cannot be 'stdout' type.
#callback_whitelist = timer, mail
 
# Determine whether includes in tasks and handlers are "static" by
# default. As of 2.0, includes are dynamic by default. Setting these
# values to True will make includes behave more like they did in the
# 1.x versions.
#task_includes_static = False
#handler_includes_static = False
 
# Controls if a missing handler for a notification event is an error or a warning
#error_on_missing_handler = True
 
# change this for alternative sudo implementations
#sudo_exe = sudo
 
# What flags to pass to sudo
# WARNING: leaving out the defaults might create unexpected behaviours
#sudo_flags = -H -S -n
 
# SSH timeout
#timeout = 10
 
# default user to use for playbooks if user is not specified
# (/usr/bin/ansible will use current user as default)
#remote_user = root
 
# logging is off by default unless this path is defined
# if so defined, consider logrotate
#log_path = /var/log/ansible.log
 
# default module name for /usr/bin/ansible
#module_name = command
 
# use this shell for commands executed under sudo
# you may need to change this to bin/bash in rare instances
# if sudo is constrained
#executable = /bin/sh
 
# if inventory variables overlap, does the higher precedence one win
# or are hash values merged together?  The default is 'replace' but
# this can also be set to 'merge'.
#hash_behaviour = replace
 
# by default, variables from roles will be visible in the global variable
# scope. To prevent this, the following option can be enabled, and only
# tasks and handlers within the role will see the variables there
#private_role_vars = yes
 
# list any Jinja2 extensions to enable here:
#jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n
 
# if set, always use this private key file for authentication, same as
# if passing --private-key to ansible or ansible-playbook
#private_key_file = /path/to/file
 
# If set, configures the path to the Vault password file as an alternative to
# specifying --vault-password-file on the command line.
#vault_password_file = /path/to/vault_password_file
 
# format of string {{ ansible_managed }} available within Jinja2
# templates indicates to users editing templates files will be replaced.
# replacing {file}, {host} and {uid} and strftime codes with proper values.
#ansible_managed = Ansible managed: {file} modified on %Y-%m-%d %H:%M:%S by {uid} on {host}
# {file}, {host}, {uid}, and the timestamp can all interfere with idempotence
# in some situations so the default is a static string:
#ansible_managed = Ansible managed
 
# by default, ansible-playbook will display "Skipping [host]" if it determines a task
# should not be run on a host.  Set this to "False" if you don't want to see these "Skipping"
# messages. NOTE: the task header will still be shown regardless of whether or not the
# task is skipped.
#display_skipped_hosts = True
 
# by default, if a task in a playbook does not include a name: field then
# ansible-playbook will construct a header that includes the task's action but
# not the task's args.  This is a security feature because ansible cannot know
# if the *module* considers an argument to be no_log at the time that the
# header is printed.  If your environment doesn't have a problem securing
# stdout from ansible-playbook (or you have manually specified no_log in your
# playbook on all of the tasks where you have secret information) then you can
# safely set this to True to get more informative messages.
#display_args_to_stdout = False
 
# by default (as of 1.3), Ansible will raise errors when attempting to dereference
# Jinja2 variables that are not set in templates or action lines. Uncomment this line
# to revert the behavior to pre-1.3.
#error_on_undefined_vars = False
 
# by default (as of 1.6), Ansible may display warnings based on the configuration of the
# system running ansible itself. This may include warnings about 3rd party packages or
# other conditions that should be resolved if possible.
# to disable these warnings, set the following value to False:
#system_warnings = True
 
# by default (as of 1.4), Ansible may display deprecation warnings for language
# features that should no longer be used and will be removed in future versions.
# to disable these warnings, set the following value to False:
#deprecation_warnings = True
 
# (as of 1.8), Ansible can optionally warn when usage of the shell and
# command module appear to be simplified by using a default Ansible module
# instead.  These warnings can be silenced by adjusting the following
# setting or adding warn=yes or warn=no to the end of the command line
# parameter string.  This will for example suggest using the git module
# instead of shelling out to the git command.
# command_warnings = False
 
 
# set plugin path directories here, separate with colons
#action_plugins     = /usr/share/ansible/plugins/action
#become_plugins     = /usr/share/ansible/plugins/become
#cache_plugins      = /usr/share/ansible/plugins/cache
#callback_plugins   = /usr/share/ansible/plugins/callback
#connection_plugins = /usr/share/ansible/plugins/connection
#lookup_plugins     = /usr/share/ansible/plugins/lookup
#inventory_plugins  = /usr/share/ansible/plugins/inventory
#vars_plugins       = /usr/share/ansible/plugins/vars
#filter_plugins     = /usr/share/ansible/plugins/filter
#test_plugins       = /usr/share/ansible/plugins/test
#terminal_plugins   = /usr/share/ansible/plugins/terminal
#strategy_plugins   = /usr/share/ansible/plugins/strategy
 
 
# by default, ansible will use the 'linear' strategy but you may want to try
# another one
#strategy = free
 
# by default callbacks are not loaded for /bin/ansible, enable this if you
# want, for example, a notification or logging callback to also apply to
# /bin/ansible runs
#bin_ansible_callbacks = False
 
 
# don't like cows?  that's unfortunate.
# set to 1 if you don't want cowsay support or export ANSIBLE_NOCOWS=1
#nocows = 1
 
# set which cowsay stencil you'd like to use by default. When set to 'random',
# a random stencil will be selected for each task. The selection will be filtered
# against the `cow_whitelist` option below.
#cow_selection = default
#cow_selection = random
 
# when using the 'random' option for cowsay, stencils will be restricted to this list.
# it should be formatted as a comma-separated list with no spaces between names.
# NOTE: line continuations here are for formatting purposes only, as the INI parser
#       in python does not support them.
#cow_whitelist=bud-frogs,bunny,cheese,daemon,default,dragon,elephant-in-snake,elephant,eyes,\
#              hellokitty,kitty,luke-koala,meow,milk,moofasa,moose,ren,sheep,small,stegosaurus,\
#              stimpy,supermilker,three-eyes,turkey,turtle,tux,udder,vader-koala,vader,www
 
# don't like colors either?
# set to 1 if you don't want colors, or export ANSIBLE_NOCOLOR=1
#nocolor = 1
 
# if set to a persistent type (not 'memory', for example 'redis') fact values
# from previous runs in Ansible will be stored.  This may be useful when
# wanting to use, for example, IP information from one group of servers
# without having to talk to them in the same playbook run to get their
# current IP information.
#fact_caching = memory
 
#This option tells Ansible where to cache facts. The value is plugin dependent.
#For the jsonfile plugin, it should be a path to a local directory.
#For the redis plugin, the value is a host:port:database triplet: fact_caching_connection = localhost:6379:0
 
#fact_caching_connection=/tmp
 
 
 
# retry files
# When a playbook fails a .retry file can be created that will be placed in ~/
# You can enable this feature by setting retry_files_enabled to True
# and you can change the location of the files by setting retry_files_save_path
 
#retry_files_enabled = False
#retry_files_save_path = ~/.ansible-retry
 
# squash actions
# Ansible can optimise actions that call modules with list parameters
# when looping. Instead of calling the module once per with_ item, the
# module is called once with all items at once. Currently this only works
# under limited circumstances, and only with parameters named 'name'.
#squash_actions = apk,apt,dnf,homebrew,pacman,pkgng,yum,zypper
 
# prevents logging of task data, off by default
#no_log = False
 
# prevents logging of tasks, but only on the targets, data is still logged on the master/controller
#no_target_syslog = False
 
# controls whether Ansible will raise an error or warning if a task has no
# choice but to create world readable temporary files to execute a module on
# the remote machine.  This option is False by default for security.  Users may
# turn this on to have behaviour more like Ansible prior to 2.1.x.  See
# https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user
# for more secure ways to fix this than enabling this option.
#allow_world_readable_tmpfiles = False
 
# controls the compression level of variables sent to
# worker processes. At the default of 0, no compression
# is used. This value must be an integer from 0 to 9.
#var_compression_level = 9
 
# controls what compression method is used for new-style ansible modules when
# they are sent to the remote system.  The compression types depend on having
# support compiled into both the controller's python and the client's python.
# The names should match with the python Zipfile compression types:
# * ZIP_STORED (no compression. available everywhere)
# * ZIP_DEFLATED (uses zlib, the default)
# These values may be set per host via the ansible_module_compression inventory
# variable
#module_compression = 'ZIP_DEFLATED'
 
# This controls the cutoff point (in bytes) on --diff for files
# set to 0 for unlimited (RAM may suffer!).
#max_diff_size = 1048576
 
# This controls how ansible handles multiple --tags and --skip-tags arguments
# on the CLI.  If this is True then multiple arguments are merged together.  If
# it is False, then the last specified argument is used and the others are ignored.
# This option will be removed in 2.8.
#merge_multiple_cli_flags = True
 
# Controls showing custom stats at the end, off by default
#show_custom_stats = True
 
# Controls which files to ignore when using a directory as inventory with
# possibly multiple sources (both static and dynamic)
#inventory_ignore_extensions = ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo
 
# This family of modules use an alternative execution path optimized for network appliances
# only update this setting if you know how this works, otherwise it can break module execution
#network_group_modules=eos, nxos, ios, iosxr, junos, vyos
 
# When enabled, this option allows lookups (via variables like {{lookup('foo')}} or when used as
# a loop with `with_foo`) to return data that is not marked "unsafe". This means the data may contain
# jinja2 templating language which will be run through the templating engine.
# ENABLING THIS COULD BE A SECURITY RISK
#allow_unsafe_lookups = False
 
# set default errors for all plays
#any_errors_fatal = False
 
[inventory]
# enable inventory plugins, default: 'host_list', 'script', 'auto', 'yaml', 'ini', 'toml'
#enable_plugins = host_list, virtualbox, yaml, constructed
 
# ignore these extensions when parsing a directory as inventory source
#ignore_extensions = .pyc, .pyo, .swp, .bak, ~, .rpm, .md, .txt, ~, .orig, .ini, .cfg, .retry
 
# ignore files matching these patterns when parsing a directory as inventory source
#ignore_patterns=
 
# If 'true' unparsed inventory sources become fatal errors, they are warnings otherwise.
#unparsed_is_failed=False
 
[privilege_escalation]
#become=True
#become_method=sudo
#become_user=root
#become_ask_pass=False
 
[paramiko_connection]
 
# uncomment this line to cause the paramiko connection plugin to not record new host
# keys encountered.  Increases performance on new host additions.  Setting works independently of the
# host key checking setting above.
#record_host_keys=False
 
# by default, Ansible requests a pseudo-terminal for commands executed under sudo. Uncomment this
# line to disable this behaviour.
#pty=False
 
# paramiko will default to looking for SSH keys initially when trying to
# authenticate to remote devices.  This is a problem for some network devices
# that close the connection after a key failure.  Uncomment this line to
# disable the Paramiko look for keys function
#look_for_keys = False
 
# When using persistent connections with Paramiko, the connection runs in a
# background process.  If the host doesn't already have a valid SSH key, by
# default Ansible will prompt to add the host key.  This will cause connections
# running in background processes to fail.  Uncomment this line to have
# Paramiko automatically add host keys.
#host_key_auto_add = True
 
[ssh_connection]
 
# ssh arguments to use
# Leaving off ControlPersist will result in poor performance, so use
# paramiko on older platforms rather than removing it, -C controls compression use
#ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
 
# The base directory for the ControlPath sockets.
# This is the "%(directory)s" in the control_path option
#
# Example:
# control_path_dir = /tmp/.ansible/cp
#control_path_dir = ~/.ansible/cp
 
# The path to use for the ControlPath sockets. This defaults to a hashed string of the hostname,
# port and username (empty string in the config). The hash mitigates a common problem users
# found with long hostnames and the conventional %(directory)s/ansible-ssh-%%h-%%p-%%r format.
# In those cases, a "too long for Unix domain socket" ssh error would occur.
#
# Example:
# control_path = %(directory)s/%%h-%%r
#control_path =
 
# Enabling pipelining reduces the number of SSH operations required to
# execute a module on the remote server. This can result in a significant
# performance improvement when enabled, however when using "sudo:" you must
# first disable 'requiretty' in /etc/sudoers
#
# By default, this option is disabled to preserve compatibility with
# sudoers configurations that have requiretty (the default on many distros).
#
#pipelining = False
 
# Control the mechanism for transferring files (old)
#   * smart = try sftp and then try scp [default]
#   * True = use scp only
#   * False = use sftp only
#scp_if_ssh = smart
 
# Control the mechanism for transferring files (new)
# If set, this will override the scp_if_ssh option
#   * sftp  = use sftp to transfer files
#   * scp   = use scp to transfer files
#   * piped = use 'dd' over SSH to transfer files
#   * smart = try sftp, scp, and piped, in that order [default]
#transfer_method = smart
 
# if False, sftp will not use batch mode to transfer files. This may cause some
# types of file transfer failures impossible to catch however, and should
# only be disabled if your sftp version has problems with batch mode
#sftp_batch_mode = False
 
# The -tt argument is passed to ssh when pipelining is not enabled because sudo 
# requires a tty by default. 
#usetty = True
 
# Number of times to retry an SSH connection to a host, in case of UNREACHABLE.
# For each retry attempt, there is an exponential backoff,
# so after the first attempt there is 1s wait, then 2s, 4s etc. up to 30s (max).
#retries = 3
 
[persistent_connection]
 
# Configures the persistent connection timeout value in seconds.  This value is
# how long the persistent connection will remain idle before it is destroyed.
# If the connection doesn't receive a request before the timeout value
# expires, the connection is shutdown. The default value is 30 seconds.
#connect_timeout = 30
 
# The command timeout value defines the amount of time to wait for a command
# or RPC call before timing out. The value for the command timeout must
# be less than the value of the persistent connection idle timeout (connect_timeout)
# The default value is 30 second.
#command_timeout = 30
 
[accelerate]
#accelerate_port = 5099
#accelerate_timeout = 30
#accelerate_connect_timeout = 5.0
 
# The daemon timeout is measured in minutes. This time is measured
# from the last activity to the accelerate daemon.
#accelerate_daemon_timeout = 30
 
# If set to yes, accelerate_multi_key will allow multiple
# private keys to be uploaded to it, though each user must
# have access to the system via SSH to add a new key. The default
# is "no".
#accelerate_multi_key = yes
 
[selinux]
# file systems that require special treatment when dealing with security context
# the default behaviour that copies the existing context or uses the user default
# needs to be changed to use the file system dependent context.
#special_context_filesystems=nfs,vboxsf,fuse,ramfs,9p,vfat
 
# Set this to yes to allow libvirt_lxc connections to work without SELinux.
#libvirt_lxc_noseclabel = yes
 
[colors]
#highlight = white
#verbose = blue
#warn = bright purple
#error = red
#debug = dark gray
#deprecate = purple
#skip = cyan
#unreachable = red
#ok = green
#changed = yellow
#diff_add = green
#diff_remove = red
#diff_lines = cyan
 
 
[diff]
# Always print diff when running ( same as always running with -D/--diff )
# always = no
 
# Set how many context lines to show in diff
# context = 3

hosts.yml

Im Abschnitt /etc/ansible/hosts hatten wir bereits einen ersten Blick auf die Inventory-Definitionsvorlage aus dem RPM geworfen. In der Ansible-Konfigurationsdatei .ansible.cfg hatten wir definiert, dass unsere Hosts aus dem Produktionsumfeld in der Datei ~/ansible/inventories/production/hosts.yml definiert werden.

Wir befüllen nun also die entsprechende Datei mit den Daten unserer Produktions-/INstallationsumgebung.

 $ vim ~/ansible/inventories/production/hosts.yml
~/ansible/inventories/production/hosts.yml
--- #YAML start syntax (optional) 
centos7:
  hosts:
    bh7: 
      ansible_ssh_host: bh7.dmz.nausch.org
 
centos8:
  hosts:
    ansible:
      ansible_ssh_host: 10.0.0.40
    demo:
      ansible_ssh_host: 10.0.0.190
      ansible_ssh_port: 10022
... #YAML ende syntax (optional)

Wir definieren damit folgende Hostgruppen:

  • centos7 mit dem Host bh7, definiert über dessen Hostname bh7.dmz.nausch.org
  • centos8
    • mit dem Host ansible, definiert über dessen IP-Adresse 10.0.0.40 und
    • mit dem Host demo, definiert über dessen IP-Adresse 10.0.0.190 und einem anderen SSH-Port, nämlich

Mit dem Befehl ansible und der Option –list-hosts können wir nun abfragen welche Hoste in den entsprechendne Hostgruppen enthalten sind.

 # ansible --list-hosts centos7
  hosts (1):
    bh7
 # ansible --list-hosts centos8
  hosts (2):
    ansible
    demo

Das Bearbeiten von YAML_Dateien kann mit dem Standard-Editor der Wahl vim zuweilen recht anstrengend werden, da zum einen das Syntax-highlighting nicht immer passt und die automatischen Einrückungen nicht immer wunschgemäß funktioniert. Hierzu wollen wir nun, wie im Buch Ansible - Das Praxisbuxch für Administratoren und DevOps-Teams von Axel Miesen auf Seite 64 ff. gezeigt, unseren Standard-Editor vim mit dem Plugin ansible-vim von Dave Honeffer aus.

Hierzu legen wir uns im Home-Verzeichnis unseres Admins ein zugehöriges Verzeichnis an.

 $ mkdir -p ~/.vim/pack/vendor/start

Anschließend klonen wir das GitHub-Projekt direkt in dieses Verzeichnis.

 $ git clone --depth 1 https://github.com/pearofducks/ansible-vim.git ~/.vim/pack/vendor/start/ansible-vim

Abschließend legen wir uns noch die für den Admin gehörige persönliche vim-Einstellungsdatei mit nachfolgendem Inhalt an.

 $ vim ~/.vimrc
~/.vimrc
filetype plugin indent on
syntax on
au BufRead,BufNewFile */playbooks/*.yml set filetype=yaml.ansible

Nachdem wir im Abschnitt Voraussetzung: SSH-Schlüssel bereits unsere Schlüssel erstellt und verteilt hatten, können wir nun ein paar kleine Test fahren. Im ersten Schritt wollen wir uns das HOME-Verzeichnis unseres Ansibe-Systemusers ansible anzeigen lassen.

Dies können wir mit Hilfe des Folgenden SSH-Aufrufes bewerkstelligen:

 $ ssh -l ansible -i ~/.ssh/id_ed25519_ansible 10.0.0.190 "ls -alF ~/"
##############################################################################
#                                                                            #
#     ╭∩╮( ͡° ل͟ ͡° )╭∩╮   This is not your server!   ╭∩╮( ͡° ل͟ ͡° )╭∩╮       #
#                                                                            #
#             Unauthorized access to this system is prohibited !             #
#                                                                            #
#    This system is actively monitored and all connections may be logged.    #
#         By accessing this system, you consent to this monitoring.          #
#                                                                            #
##############################################################################
total 16
drwx------. 3 ansible ansible  90 Dec 30 21:47 ./
drwxr-xr-x. 4 root    root     35 Dec 30 21:43 ../
-rw-r--r--. 1 ansible ansible  18 Oct  1 15:26 .bash_logout
-rw-r--r--. 1 ansible ansible 141 Oct  1 15:26 .bash_profile
-rw-r--r--. 1 ansible ansible 312 Oct  1 15:26 .bashrc
drwx------. 2 ansible ansible  29 Dec 30 21:47 .ssh/
-rw-------. 1 ansible ansible 793 Dec 30 21:47 .viminfo

Im Abschnitt adhoc - Befehle des Kapitels Ansible - Grundlagen sind wir bereits schon mal kurz in die Möglichkeit eingestiegen, mit der wir Befehle auf einem Zielsystem ausführen können, in dem wir den Befehl ansible hierzu verwenden.

 $ ansible 10.0.0.190 -u ansible --private-key /home/django/.ssh/id_ed25519_ansible -m shell -a "/usr/bin/ls -alF" 

10.0.0.190 | CHANGED | rc=0 >>
total 20
drwx------. 4 ansible ansible 127 Dec 30 22:26 ./
drwxr-xr-x. 4 root    root     35 Dec 30 21:43 ../
drwx------. 3 ansible ansible  17 Dec 30 22:26 .ansible/
-rw-------. 1 ansible ansible  93 Dec 30 22:15 .bash_history
-rw-r--r--. 1 ansible ansible  18 Oct  1 15:26 .bash_logout
-rw-r--r--. 1 ansible ansible 141 Oct  1 15:26 .bash_profile
-rw-r--r--. 1 ansible ansible 312 Oct  1 15:26 .bashrc
drwx------. 2 ansible ansible  29 Dec 30 21:47 .ssh/
-rw-------. 1 ansible ansible 793 Dec 30 21:47 .viminfo

Alternative dazu können wir natürlich auch den Host oder auch die definierten Hostgruppen aus unserer Inventory-Konfigurationsdatei ~/ansible/inventories/production/hosts.yml ansprechen. In nachfolgendem Beispiel wollen wir unsere Anfrage zur Hostgruppe centos8 schicken.

 $ ansible centos8 -u ansible --private-key /home/django/.ssh/id_ed25519_ansible -m shell -a "/usr/bin/ls -alF" 

10.0.0.90 | CHANGED | rc=0 >>
total 20
drwx------. 4 ansible ansible 127 Dec 30 22:26 ./
drwxr-xr-x. 4 root    root     35 Dec 30 21:43 ../
drwx------. 3 ansible ansible  17 Dec 30 22:26 .ansible/
-rw-------. 1 ansible ansible  93 Dec 30 22:15 .bash_history
-rw-r--r--. 1 ansible ansible  18 Oct  1 15:26 .bash_logout
-rw-r--r--. 1 ansible ansible 141 Oct  1 15:26 .bash_profile
-rw-r--r--. 1 ansible ansible 312 Oct  1 15:26 .bashrc
drwx------. 2 ansible ansible  29 Dec 30 21:47 .ssh/
-rw-------. 1 ansible ansible 793 Dec 30 21:47 .viminfo

Möchten wir testen, ob all unsere Hosts erreichbar sind, können wir z.B. nachfolgenden adhoc-Befehl verwenden:

 $ ansible all -u ansible --private-key /home/django/.ssh/id_ed25519_ansible -m ping

bh7.dmz.nausch.org | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname bh7.dmz.nausch.org: Name or service not known",
    "unreachable": true
}
10.0.0.190 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}

Im gezeigten Fall ist der Host bh7.dmz.nausch.org aktuell nicht erreichbar und entsprechend rot gekennzeichnet. Der zweite grün markierte Host mit der IP-Adresse 10.0.0.90 hingegen ist erreichbar uns mit dem Zusatz SUCCESS versehen.

Um nun nicht bei jedem Aufruf den Remouteuser ansible und dessen Keyfile ~/.ssh/id_ed25519_ansible explizit angeben zu müssen, erweitern wir unser Konfigurationsdatei ~/.ansible.cfg nun entsprechend.

Wie öffnen also unsere Konfigurationsdatei und ergänzen nachfolgende Abschnitte.

 # vim ~/.ansible.cfg

remote_user

# default user to use for playbooks if user is not specified
# (/usr/bin/ansible will use current user as default)
#remote_user = root
# Django : 2019-12-30
remote_user = ansible

private_key_file

# if set, always use this private key file for authentication, same as
# if passing --private-key to ansible or ansible-playbook
#private_key_file = /path/to/file
# Django : 2019-12-30
private_key_file = ~/.ssh/id_ed25519_ansible

(Verbindungs-)Tests

Nun können wir schon mal viel einfacher ein adhoc-Kommando absetzen, um z.B. den im Einsatz befindlichen CentOS-Release abzufragen.

 $ ansible centos7 -m shell -a "/usr/bin/cat /etc/redhat-release"

10.0.0.90 | CHANGED | rc=0 >>
CentOS Linux release 7.8.2003 (Core)

Das vorgenannte Beispiel funktioniert natürlich nur, wenn wir uns in einem homogenen Systemumfeld befinden, in dem alle Zielsysteme z.B. den gleichen Schlüsseltyp verwenden. Haben wir aber Systeme, die z.B. nur RSA-Schlüssel verwenden wie z.B. CentOS 6 Systeme oder Knoten eines Freifunk Netzes, so müssen wir natürlich eine Möglichkeit schaffen, für unterschiedliche Hosts auch unterschiedliche Schlüsseldateien und/oder sogar unterschiedliche User angeben zu können.

Wir verändern nun also unser Konfigurationsdatei ~/.ansible.cfg entsprechend.

Wie öffnen also unsere Konfigurationsdatei und ergänzen nachfolgende Abschnitte.

 # vim ~/.ansible.cfg

remote_user

Für unseren CentOS basierenden Hosts, bei denen wir den User ansible verwenden wollen/können belassen wir den zuvor eingestellten Eintrag remote_user = ansible. So brauchen wir nicht explizit bei jedem Host später den Unser anzugeben.

# default user to use for playbooks if user is not specified
# (/usr/bin/ansible will use current user as default)
#remote_user = root
# Django : 2019-12-30
remote_user = ansible

private_key_file

Den Eintrag für das keyfile für unseren vorherigen Test entfernen wir nun wieder, da wir die unterschiedlichen key-Files im Inventory-File anlegen werden.

# if set, always use this private key file for authentication, same as
# if passing --private-key to ansible or ansible-playbook
#private_key_file = /path/to/file

inventory

Zuvor hatten wir bereits die Datei ~/ansible/inventories/production/hosts.yml für die Definition der Hosts (Inventory), bei der Verzeichnisanlage unserer Musterinstallation angelegt.

In der Konfigurationsdatei ~/.ansible.cfg finden wir ganz am Anfang ein paar Angaben zu default Werten. Wir hatten bereits zuvor in der Sektion [defaults] definiert, dass unsere Inventory-/Host-Definitionen aus der Datei ~/ansible/inventories/production/hosts.ymlgeladen werden sollen.

hosts.yml

Unsere Hosts packen wir nun in der YAML-Notation in die Inventory-Konfigurationsdatei vim ~/ansible/inventories/production/hosts.yml

 # vim ~/ansible/inventories/production/hosts.yml
~/ansible/inventories/production/hosts.yml
--- #YAML start syntax (optional) 
 
centos8:
    hosts:
       www8.dmz.nausch.org:
           ansible_ssh_host: 10.0.0.90
           ansible_ssh_port: 22
           #ansible_ssh_user: ansible
           ansible_ssh_private_key_file: /home/django/.ssh/id_ed25519_ansible
 
centos7:
    hosts:
       www7.dmz.nausch.org:
           ansible_ssh_host: 10.0.0.97
           ansible_ssh_port: 22
           #ansible_ssh_user: ansible
           ansible_ssh_private_key_file: /home/django/.ssh/id_rsa_ansible
 
... #YAML end syntax

(Verbindungs-)Tests

Nun können wir erneut ein adhoc-Kommando zu den beiden Hosts absetzen, um z.B. den im Einsatz befindlichen CentOS-Relase abzufragen.

Zunächst checken wir unser CentOS 7 System:

  $ ansible centos7 -m shell -a "/usr/bin/cat /etc/redhat-release"

www7.dmz.nausch.org | CHANGED | rc=0 >>
CentOS Linux release 7.7.1908 (Core)

Anschließend überprüfen wir nun noch das CentOS 8 System:

 $ ansible centos8 -m shell -a "/usr/bin/cat /etc/redhat-release"

www8.dmz.nausch.org | CHANGED | rc=0 >>
CentOS Linux release 8.0.1905 (Core)

Wollen wir aber z.B. das Verzeichnis /root abfragen wird dies natürlich fehl schlagen, da unser remote-user ansible keine ausreichenden Rechte für den Zugriff hat.

 $ ansible centos_8 -m shell -a "/usr/bin/ls -alF /root/"

10.0.0.90 | FAILED | rc=2 >>
/usr/bin/ls: cannot open directory '/root/': Permission deniednon-zero return code

ohne Passwortabfrage

Wir müssen also nun dafür sorgen, dass die Befehle auf den Zielsystem mit den richtigen Rechten ausgeführt werden können. Hierzu passen wir nun die Konfigurationsdatei /etc/ansible/ansible.cfg an.

[privilege_escalation]
# Django : 2019-12-30
# default: unset
#become=True
#become_method=sudo
#become_user=root
#become_ask_pass=False
become=True
become_method=sudo
become_user=root
become_ask_pass=False

Werden nun root-Rechte bei der Ausführung auf dem Remotesystem benötigt, werden diese

  • become=True : automatisch zugewiesen,
  • become_method=sudo : mit welchem Befehl die Rechteerweiterung, erlangt werden soll,
  • become_user=root : welcher User bzw. dessen Rechte erlangt und
  • become_ask_pass=False : und ob dazu eine Passwortabfrage durchgeführt werden soll.

WICHTIG :

Auf einem Benutzerendgerät (Laptop, Desktop-Rechner) mag der geneigte sicherheitsbewusste Admin ja noch mit solch einer Rollen und Rechte-/Zuordnung ohne Passwortabfrage einverstanden sein. Nicht umsonst unterbinden wir den SSH-login für den User root mit der Option PermitRootLogin no in unserer SSH-Konfiguration. Ob man nun so eine Konfiguration auf einem Server, der unter Umständen noch dazu von mehreren Usern auf der Konsole genutzt werden kann, haben will, ist durchaus stark diskussionswürdig!

Sicher wird man da später bei der zentralen Konfiguration und Orchestrierung auf eine bessere Lösung zurückgreifen!

Da unser Ansible-Systemuser ansible in der Gruppe wheel und in der Konfigurationsdatei /etc/sudoers

%wheel  ALL=(ALL)       NOPASSWD: ALL

die Passwortabfrage für die Gruppe wheel deaktiviert ist, klappt nun auch der nachfolgende Versuch den Inhalt des HOME-Verzeichnisses des Benutzers root einsehen.

 $ ansible centos_8 -m shell -a "/usr/bin/ls -alF /root/"

BECOME password:
10.0.0.90 | CHANGED | rc=0 >>
total 44
dr-xr-x---.  5 root root  212 Dec 31 13:33 ./
dr-xr-xr-x. 17 root root  224 Oct 21 14:12 ../
-rw-------.  1 root root 2417 Dec 30 22:15 .bash_history
-rw-r--r--.  1 root root   18 May 11  2019 .bash_logout
-rw-r--r--.  1 root root  176 May 11  2019 .bash_profile
-rw-r--r--.  1 root root  176 May 11  2019 .bashrc
drwx------.  3 root root   16 Oct 21 15:37 .cache/
drwx------.  3 root root   16 Oct 21 15:37 .config/
-rw-r--r--.  1 root root  100 May 11  2019 .cshrc
drwx------.  3 root root   19 Oct 21 15:37 .local/
-rw-r--r--.  1 root root  129 May 11  2019 .tcshrc
-rw-------.  1 root root 9306 Dec 31 13:33 .viminfo
-rw-r--r--.  1 root root  209 Nov  7 09:25 .wget-hsts
-rw-------.  1 root root 2226 Oct 21 14:19 anaconda-ks.cfg

mit Abfrage eines Passwortes

Da eine Rechteerweiterung ohne Abfrage eines Passwortes nicht unbedingt erstrebenswert ist, wollen wir uns nun daran setzen unsere Konfiguration etwas optimieren. Aus Sicherheitsgründen wird ein user, sobald er mittels sudo su - versucht root-Rechte zu erlangen gebeten sein Userpasswort einzugeben. Genau diesen Mechanismus wollen wir nun auch bei unseren Ansible-Aufgaben nutzen.

Unser Nutzer Ansible, den wir benutzen um per SSH auf die Zielsysteme zu gelangen, haben wir ja bereits in die Gruppe wheel gepackt.

 # grep ansible /etc/group
wheel:x:10:ansible
ansible:x:500:

Der Eintrag %wheel ALL=(ALL) ALL in der Konfigurationsdatei /etc/sudoers sorgt dafür, dass jedesmal wenn der Benutzer ansible versucht root-Rechte zu erlangen, sein Passwort eingeben muss.

 # egrep -v '(^.*#|^$)' /etc/sudoers | grep wheel
%wheel ALL=(ALL)	ALL

Nun passen wir nun die Konfigurationsdatei /etc/ansible/ansible.cfg an, damit bei der Rechteerweiterung nach einem Passwort gefragt werden soll.

[privilege_escalation]
# Django : 2020-01-04
# default: unset
#become=True
#become_method=sudo
#become_user=root
#become_ask_pass=False
become=True
become_method=sudo
become_user=root
become_ask_pass=True

Werden nun root-Rechte bei der Ausführung auf dem Remotesystem benötigt, werden diese

  • become=True : automatisch zugewiesen,
  • become_method=sudo : mit welchem Befehl die Rechteerweiterung, erlangt werden soll,
  • become_user=root : welcher User bzw. dessen Rechte erlangt und
  • become_ask_pass=true : beim Aufruf des Ansible-playbooks einen Abfrage des Userpasswortes für den Systemnutzer ansible durchgeführt werden soll.

Ohne die Anpassung müssten wir jedesmal beim Aufrudf des Befehls ansible-playbook die Option --ask-become-pass angeben.

Da unser Ansible-Systemuser ansible in der Gruppe wheel und in der Konfigurationsdatei /etc/sudoers

%wheel  ALL=(ALL)       NOPASSWD: ALL

die Passwortabfrage für die Gruppe wheel deaktiviert ist, klappt nun auch der nachfolgende Versuch den Inhalt des HOME-Verzeichnisses des Benutzers root einsehen.

 $ ansible centos_8 -m shell -a "/usr/bin/ls -alF /root/"

BECOME password:
10.0.0.90 | CHANGED | rc=0 >>
total 44
dr-xr-x---.  5 root root  212 Dec 31 13:33 ./
dr-xr-xr-x. 17 root root  224 Oct 21 14:12 ../
-rw-------.  1 root root 2417 Dec 30 22:15 .bash_history
-rw-r--r--.  1 root root   18 May 11  2019 .bash_logout
-rw-r--r--.  1 root root  176 May 11  2019 .bash_profile
-rw-r--r--.  1 root root  176 May 11  2019 .bashrc
drwx------.  3 root root   16 Oct 21 15:37 .cache/
drwx------.  3 root root   16 Oct 21 15:37 .config/
-rw-r--r--.  1 root root  100 May 11  2019 .cshrc
drwx------.  3 root root   19 Oct 21 15:37 .local/
-rw-r--r--.  1 root root  129 May 11  2019 .tcshrc
-rw-------.  1 root root 9306 Dec 31 13:33 .viminfo
-rw-r--r--.  1 root root  209 Nov  7 09:25 .wget-hsts
-rw-------.  1 root root 2226 Oct 21 14:19 anaconda-ks.cfg


Neben der Absicherung unseres SSH-Schlüssels mittel Passphrase haben wir auch unser Zielsystem nicht unnötig aufgeweicht. Und die einmalige Eingabe eines Passworts beim Befehl ansible-playbook ist auch soweit gerechtfertigt und eine unnötige Rechteerweiterung auf den Zielsystemen ist somit nicht notwendig! :OK:

Links


1)
Secure SHell
Diese Website verwendet Cookies. Durch die Nutzung der Website stimmen Sie dem Speichern von Cookies auf Ihrem Computer zu. Außerdem bestätigen Sie, dass Sie unsere Datenschutzbestimmungen gelesen und verstanden haben. Wenn Sie nicht einverstanden sind, verlassen Sie die Website.Weitere Information
  • linux/ansible/first.txt
  • Zuletzt geändert: 22.09.2022 08:29.
  • von django