pixel
Seite wählen

NETWAYS Blog

Python – Generator

Wer schon einmal eine etwas größere Datei, bspw. 1Gb, mit dem Editor VIM geöffnet hat, der weis, wie lange dies dauern kann. Das kommt daher, dass diese Datei zunächst komplett in den Arbeitsspeicher geladen werden muss. So ähnlich verhält es sich, wenn in Python eine Variable “befüllt” wird, welche anschließend Speicherplatz im Arbeitsspeicher belegt. Bei der heutigen Hardware stellen 1 Gb große Variablen kein Problem dar, aber was passiert wenn diese deutlich größer sind und zusätzlich eine gute Performance benötigt wird? In diesem Fall empfiehlt es sich, auf einen Generator zurückzugreifen. Ein Generator liefert lapidar gesagt die Ergebnisse einer Funktion “häppchenweise” und nicht als riesiges Stück zurück. Syntaktisch unterscheidet sich ein Generator von einer “normalen” Funktion nicht großartig, wie hier zu sehen:

def a_function():
  yield x
  yield y
  yield z

Bei der Funktionsdefinition wird lediglich das Schlüsselwort return durch yield ersetzt. Der große Unterschied zu einer “normalen” Funktion dabei ist, dass sich die Funktion mit yield nicht sofort beendet. Somit erhält man, vereinfacht gesagt, ein Objekt (Iterator) als Rückgabewert, über das man iterieren kann.

Zur besseren Veranschaulichung eines Generators werde ich eine exemplarische Datenbankabfrage aufzeigen:

Hinweis: Ich habe eine Beispieldatenbank genommen.

“normale” Funktion
SELECT-Statement und anschließende Rückgabe des gesamten Ergebnisses:

def select_list():
    result = []

    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor:
        result.append(row)

    return result

Die Funktion “select_list()” liefert eine Liste zurück:

print(select_list())

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

“generator” Funktion
Im Gegensatz zum obigen Beispiel wird keine Liste mit den Werten befüllt, sondern jeder Wert einzeln durch yield zurückgegeben:

def select_generator():
    db = mysql.connector.connect(host='127.0.0.1',
                                 user='thomas',
                                 password="4nD3r5on",
                                 db='employees')

    cursor = db.cursor()

    cursor.execute('select * from employees')

    for row in cursor.fetchmany(1000):
        yield row

Die Funktion “select_generator()” liefert keine Liste zurück, sondern einen Generator:

print(select_generator())

<generator object select_generator at 0x10ba165d0>

Durch anschließendes Iterieren des Generatorsobjektes werden die gewünschten Werte ausgegeben:

for row in select_generator():
        print(row)

[...]
(10016,'1961-05-02','Kazuhito','Cappelletti','M','1995-01-27'),
(10017,'1958-07-06','Cristinel','Bouloucos','F','1993-08-03'),
(10018,'1954-06-19','Kazuhide','Peha','F','1987-04-03'),
[...]

Wo liegt nun der Vorteil eines Generators? Das zeigt sich erst bei Benchmark-Tests, welche im folgenden Beispiel die Performance der jeweiligen Funktion aufzeigt. Die SELECT-Statements sind bei beiden Beispielen dieselbigen, werden aber nicht ausgegeben, es wird nur eine Variable initialisiert:
select_list()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))
    
t1_start = perf_counter()
test_list = select_list()
t1_stop = perf_counter()
    
print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_generator()

print('Memory (Before): {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

t1_start = perf_counter()
test_generator = select_generator()
t1_stop = perf_counter()

print('Memory (After) : {} Mb'.format(
    psutil.Process(os.getpid()).memory_info().rss / 1000000))

print("Elapsed time: {0} seconds".format(t1_stop-t1_start))

select_list():

Memory (Before): 11.776 Mb
Memory (After) : 124.960768 Mb
Elapsed time: 8.311030800000001 seconds

 

select_generator():

Memory (Before): 11.698176 Mb
Memory (After) : 11.718656 Mb
Elapsed time: 4.869999999934649e-07 seconds

Die Zahlen sprechen für sich. Beim Generator bleibt der Verbrauch des Arbeitsspeichers so gut wie unverändert, da dieser beim Ausführen des Codes nicht “alle Werte speichert”, sondern jeden einzelnen Wert ab dem Schlüsselwort yield zurückgibt. Dadurch ergibt sich auch die immense Geschwindigkeit.
An dieser Stelle ist noch zu erwähnen, dass alle Vorteile eines Generators verloren gehen, wenn man diesen in ein Liste umwandelt.

Quelle: https://media.giphy.com/media/yUrUb9fYz6x7a/giphy.gif

Philipp Dorschner
Philipp Dorschner
Developer

Philipp hat im Jahr 2017 die Ausbildung zum Fachinformatiker – Systemintegration bei NETWAYS Professional Services begonnen. Während der Ausbildung bekam er ein immer größeres Interesse am Programmieren. Das führte dazu, dass Philipp nach erfolgreich bestandener Ausbildung die Kolleg:innen aus Professional Services nicht nur als Consultant sondern auch als Entwickler tatkräftig unterstützt. Neben seinem Interesse an der Informationstechnologie, macht er Sport im Freien oder liest bei schlechtem Wetter auch gerne mal ein Buch zu Hause.

Ansible – How to create reusable tasks

Ansible is known for its simplicity, lightweight footprint and flexibility to configure nearly any device in your infrastructure. Therefore it’s used in large scale environments shared between teams or departments. Often tasks could be used in multiple playbooks to combine update routines, setting downtimes at an API or update data at the central asset management.

To use external tasks in Ansible we use the include_task module. This module dynamically includes the tasks from the given file. When used in a specific plays we would assign play specific variables to avoid confusion. For example:


vim tasks/get_ldap_user.yml

- name: get user from ldap
  register: users
  community.general.ldap_search:
    bind_pw: "{{ myplay_ad_bind_pw }}"
    bind_dn: "{{ myplay_ad_bind_dn }}"
    server_uri: "{{ myplay_ad_server }}"
    dn: "{{ myplay_ad_user_dn }}"
    filter: "(&(ObjectClass=user)(objectCategory=person)(mail={{ myplay_usermail }}))"
    scope: children
    attrs:
      - cn
      - mail
      - memberOf
      - distinguishedName

If this task should be used in another playbook to reduce the amount of code or is used again with other conditions or values. Therefore the variables need to be overwritten or if it is another playbook the variables are named wrong.

The solve this problem change the variables to unused generic variables. And assign your own variables in the include_task statement.


vim tasks/get_ldap_user.yml

- name: get user from ldap
  register: users
  community.general.ldap_search:
    bind_pw: "{{ _ad_bind_pw }}"
    bind_dn: "{{ _ad_bind_dn }}"
    server_uri: "{{ _ad_server }}"
    dn: "{{ _ad_user_dn }}"
    filter: "(&(ObjectClass=user)(objectCategory=person)(mail={{ _ad_usermail }}))"
    scope: children
    attrs:
      - cn
      - mail
      - memberOf
      - distinguishedName

The include_task vars parameter provides own variables to the tasks.


vim plays/user_management.yml
[...]
- name: check if user exists in ldap
  include_tasks:
    file: tasks/get_ldap_user.yml
  vars: 
    _ad_bind_pw: "{{ play_ad_pw }}"
    _ad_bind_dn: "{{ play_ad_user }}"
    _ad_server: "{{ play_ad_server }}"
    _ad_user_dn: "OU=users,DC=example,DC=de"
    _ad_usermail: "{{ play_usermail }}"

This can be easily combined with loops, to enhance the reusability of your tasks even more! Checkout this blogpost about looping multiple tasks. Ansible – Loop over multiple tasks

Check out our Blog for more awesome posts and if you need help with Ansible send us a message or sign up for one of our trainings!

Thilo Wening
Thilo Wening
Senior Consultant

Thilo hat bei NETWAYS mit der Ausbildung zum Fachinformatiker, Schwerpunkt Systemadministration begonnen und unterstützt nun nach erfolgreich bestandener Prüfung tatkräftig die Kollegen im Consulting. In seiner Freizeit ist er athletisch in der Senkrechten unterwegs und stählt seine Muskeln beim Bouldern. Als richtiger Profi macht er das natürlich am liebsten in der Natur und geht nur noch in Ausnahmefällen in die Kletterhalle.

Ansible – AWX|Tower State handling on Workflows

The Ansible Tower or its upstream AWX provides an easy to use GUI to handle Ansible tasks and schedules. Playbooks are configured as templates and as the name suggests, they can be modified to the needs, extended by variables, a survey or tags.

Furthermore those templates can be logically grouped, connected and visualised in Workflows.

The downside to those Workflows, all playbooks affected by this are executed separately and can’t access each others variables. On first glance we maybe only spot that we can define variables for the whole workflow but those are not changeable throughout the flow.

But there is a solution, which is the module set_stats. This module allows to save or accumulate variables and make them available for other playbooks within the workflow.

As an example we could use the monitoring environment when setting downtimes.

workflow

As a downtime is created before a maintenance and should be gone when the maintenance is done. This creates a dependency on the first task, which can be solved as we save the result of the first tasks with the set_stats module.


      - name: schedule downtimes
        icinga2_downtimes:
          state: "{{ downtime_icinga_state | default('present') }}"
          host: ***
          author: "{{ icinga2_downtimes_author | default('ansible_downtime') }}"
          comment: "{{ icinga2_downtimes_comment | default('Downtime scheduled by Ansible') }}"
          duration: "{{ icinga2_downtimes_duration | default(omit) }}"
        register: content
 
      - set_stats:
          data:
            downtime: "{{ content }}"

The content of the data will be now available to all playbooks included by the workflow. The variable is also shown as artefacts in the GUI.

artefacts

Keep in mind that the variable will be part of the extra variables for all other playbooks. As covered in the variable precedence it will overwrite any other variable named the same.

With this module you can reorganise your playbooks and connect them in workflows. This allows you to have a more flexible automation than before.

Check out our Blog for more awesome posts and if you need help with Ansible send us a message or sign up for one of our trainings!

Thilo Wening
Thilo Wening
Senior Consultant

Thilo hat bei NETWAYS mit der Ausbildung zum Fachinformatiker, Schwerpunkt Systemadministration begonnen und unterstützt nun nach erfolgreich bestandener Prüfung tatkräftig die Kollegen im Consulting. In seiner Freizeit ist er athletisch in der Senkrechten unterwegs und stählt seine Muskeln beim Bouldern. Als richtiger Profi macht er das natürlich am liebsten in der Natur und geht nur noch in Ausnahmefällen in die Kletterhalle.

Ansible – Loop over multiple tasks

ansible logo

The last time I wrote about Ansible and the possibility to use blocks to group multiple tasks. Which you can read here. Sadly this feature does not work with loop, so there is no clean way to loop over multiple tasks in a play without writing the same loop statement at tasks over and over.

But when we come across the need of tasks which depend on each other, for example, we execute a script with a certain parameter and its result is necessary for the upcoming tasks.

Let’s go through a common example, creating a site consists of a few steps. Creating the directory, creating the vhost and then enabling the site.


- name: "create {{ site }} directory"
  file:
    ensure: directory
    dest: "/var/www/{{ site }}"
    
- name: "create {{ site }}"
  template:
    src: vhost.j2
    dest: "/etc/apache2/sites-available/{{ site }}"
  register: vhost

- name: "enable {{ site }}"
  command: /usr/sbin/a2ensite "{{ site }}"
  register: result
  when: vhost.changed
  changed_when: "'Enabling site' in result.stdout"
  notify: apache_reload

We could use a loop for each tasks and afterwards find the right result for the next task to depend on. But the styleguide will warn you if you try to use Jinja2 syntax in when statements.

So the best solution to this is to use include_tasks, which can include a file with tasks. This task is allowed to have a loop directive and so we can include it multiple times.
Lets see how this would apply to our scenario:


- set_fact:
    sites:
      - default
      - icingaweb2

- name: create vhosts
  include_tasks: create-vhosts.yml
  loop: "{{ sites }}"
  loop_control:
    loop_var: site


In the Result we can see clearly that all tasks are applied for each element in the sites variable.


TASK [set_fact] *********************************************
ok: [localhost]

TASK [create vhosts] ****************************************
included: /Users/twening/Documents/netways/ansible_test20/create-vhosts.yml for localhost => (item=default)
included: /Users/twening/Documents/netways/ansible_test20/create-vhosts.yml for localhost => (item=icingaweb2)

TASK [create default directory] *****************************
ok: [localhost]

TASK [create default] ***************************************
ok: [localhost]

TASK [enable default] ***************************************
ok: [localhost]

TASK [create icingaweb2 directory] **************************
ok: [localhost]

TASK [create icingaweb2] ************************************
ok: [localhost]

TASK [enable icingaweb2] ************************************
ok: [localhost]

PLAY RECAP **************************************************
localhost                  : ok=10   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0


Check out our Blog for more awesome posts and if you need help with Ansible send us a message or sign up for one of our trainings!

Thilo Wening
Thilo Wening
Senior Consultant

Thilo hat bei NETWAYS mit der Ausbildung zum Fachinformatiker, Schwerpunkt Systemadministration begonnen und unterstützt nun nach erfolgreich bestandener Prüfung tatkräftig die Kollegen im Consulting. In seiner Freizeit ist er athletisch in der Senkrechten unterwegs und stählt seine Muskeln beim Bouldern. Als richtiger Profi macht er das natürlich am liebsten in der Natur und geht nur noch in Ausnahmefällen in die Kletterhalle.

Virtual Environments in Python

Viele Betriebssysteme liefern eine Python-Version mit, die sich aufgrund von weiteren Abhängigkeiten nicht so einfach wechseln oder entfernen lässt. Ein Beispiel dafür ist CentOS 7.7. Hier wird auch heute noch Python 2.7.5 standardmäßig mit ausgeliefert, aktuell ist 3.8.2. Mit Virtual Environments (Virtualenv) bietet Python ein Funktion, um trotzdem andere Versionen dort nutzen zu können und zwar dort, wo sie benötigt werden.

Die gewünschte Version muss natürlich trotzdem installiert werden, auf CentOS 7 geschieht das beispielsweise mit:

$ yum install python3

Anschließend wird das Virtual Environment initialisiert, dafür muss zuerst in ein Verzeichnis gewechselt werden, in dem zusätzliche Dateien abgelegt werden können (hier am Beispiel Graphite):

$ cd /opt/
$ python3 -m venv graphite

Danach wird das Virtual Environment aktiviert:

$ source graphite/bin/activate

Während man sich im Virtual Environment befindet, ändert sich der Bash-Prompt und sämtliche Python-Befehle werden auf die geänderte Python-Version angepasst:

(graphite)$ pip --version
(graphite)$ pip 9.0.3 from /opt/graphite/lib64/python3.6/site-packages (python 3.6)

Nun lassen sich die gewünschten Paketbhängigkeiten installieren oder Änderungen vornehmen. Und mit deactivate lässt sich das Virtual Environment wieder verlassen, bis es erneut aktiviert wird.

Wer trotzdem noch Unterstützung bei Linux oder vielleicht auch bei Graphite braucht, der kann sich natürlich gerne vertrauensvoll an uns wenden: clickhere

Markus Waldmüller
Markus Waldmüller
Lead Senior Consultant

Markus war bereits mehrere Jahre als Sysadmin in Neumarkt i.d.OPf. und Regensburg tätig. Nach Technikerschule und Selbständigkeit ist er nun Anfang 2013 bei NETWAYS als Lead Senior Consultant gelandet. Wenn er nicht gerade die Welt bereist, ist der sportbegeisterte Neumarkter mit an Sicherheit grenzender Wahrscheinlichkeit auf dem Mountainbike oder am Baggersee zu finden.