Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ repos:
# false-positives on the project's own code style (`shell=dict(...)`
# in argument_spec triggers B604, the literal `'on_create'` sentinel
# triggers B105). Out of scope for in-tree review.
exclude: '^plugins/modules/ipa.*\.py$'
# `tests/` holds unit tests whose fixtures use throwaway passwords
# (B105/B106); scanning test fixtures for hardcoded secrets is noise.
exclude: '^(plugins/modules/ipa.*|tests/.*)\.py$'
types_or: ['python']

- repo: 'https://github.com/jendrikseipp/vulture'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* **plugin:bitwarden_item**: Fixed the lookup's documentation so `ansible-doc` renders it again.
* **plugin:combine_lod**: The `combine_lod` filter now reports an error when an item is missing part of a composite `unique_key` (a list of keys), instead of silently grouping such items together. Inventories with incomplete composite keys that previously merged by accident now fail loudly and must be corrected. Also fixed its documentation so `ansible-doc` renders it again.
* **role:kernel_settings**: The `systemd_cpu_affinity` setting is now actually applied. The value was computed and shown in the debug output but never passed to the underlying system role, so a configured CPU affinity had no effect.

Expand Down
15 changes: 14 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,19 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored

In-house plugins live under `plugins/` following the standard Ansible collection layout: `filter/`, `lookup/`, `modules/` and `module_utils/`. The `## Tasks` rules above (FQCN, meta modules, idempotency) are about role tasks; the points below are specific to writing the plugins themselves.

* Every in-house plugin starts with the standard file header, followed by `from __future__ import absolute_import, division, print_function` and `__metaclass__ = type`:

```python
#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author: Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
# https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.
```

* Use single quotes and f-strings consistently (vendored plugins keep their upstream style, see below).
* Every plugin carries `DOCUMENTATION` (and `RETURN` / `EXAMPLES` where applicable). Keep it valid YAML: in a `description` list, a bullet containing a colon followed by a space is parsed as a mapping and makes `ansible-doc` fail, so rephrase or quote such bullets. Verify with `ansible-doc -t <filter|lookup|module> linuxfabrik.lfops.<name>`.
* Set `version_added` to the LFOps release the plugin first shipped in, and never change it afterwards.
* `module_utils` holds code shared between plugins. Do not import the external Linuxfabrik Python Libraries (`lib`) into a plugin; copy what you need and note the origin in a comment.
Expand All @@ -767,7 +780,7 @@ In-house plugins live under `plugins/` following the standard Ansible collection

Unit tests are **mandatory** for every in-house plugin. Any pull request that adds or changes a plugin must add or update its test, and `git grep` should never find a plugin without one.

* **Where**: under `tests/unit/`, mirroring the plugin tree, named `test_<plugin>.py` (e.g. `tests/unit/plugins/filter/test_combine_lod.py`). Load the plugin by path (the plugins are not an importable package) and assert behavior, not implementation details.
* **Where**: under `tests/unit/`, mirroring the plugin tree, named `test_<plugin>.py` (e.g. `tests/unit/plugins/filter/test_combine_lod.py`). A plugin with no collection-qualified imports (e.g. the `combine_lod` filter) can be loaded by file path. A plugin that imports `ansible_collections.linuxfabrik.lfops...` (modules, or lookups pulling in a module_util) is imported through that path; `tests/conftest.py` makes this checkout importable as the collection so the imports resolve under plain pytest/tox. Same-named test files in different plugin-type directories are fine (`--import-mode=importlib`). Assert behavior, not implementation details.
* **Two tiers**, because plugins run in different environments:

* Controller plugins (`plugins/filter/`, `plugins/lookup/`) are evaluated on the Ansible controller and only ever see the controller's Python (>= 3.10). They run on the standard CI matrix.
Expand Down
53 changes: 15 additions & 38 deletions plugins/lookup/bitwarden_item.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2022, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch
# The Unlicense (see LICENSE or https://unlicense.org/)
#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author: Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
# https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

from __future__ import absolute_import, division, print_function

Expand All @@ -15,7 +17,7 @@

description:
- Looks up a password item in Bitwarden by name (and optional username, folder, collection and organization), or directly by Bitwarden item ID.
- If the lookup is by name and no matching item is found, a new login item is created on the fly. This makes the plugin idempotent for automation: the first run creates the secret, every subsequent run returns the same item.
- If the lookup is by name and no matching item is found, a new login item is created on the fly. This makes the plugin idempotent for automation. The first run creates the secret, every subsequent run returns the same item.
- If the lookup is by I(id) and the item is not in the local cache, the plugin falls back to a single API call. If the ID still does not resolve, the plugin fails - IDs are never auto-created.
- If a name-based search returns more than one match, the plugin fails because it cannot decide which item to use.
- On success, the plugin returns the full Bitwarden item object. C(username) and C(password) are additionally lifted to the top level so they can be addressed without going through the C(login) sub-dictionary.
Expand Down Expand Up @@ -284,7 +286,7 @@
from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import \
Bitwarden

display = Display() # lfbwlp = Linuxfabrik Bitwarden Lookup Plugin
display = Display() # log prefix "lfbwlp" = Linuxfabrik Bitwarden Lookup Plugin

# https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-lookup-plugins
# inspired by the lookup plugins lastpass (same topic) and redis (more modern)
Expand All @@ -302,7 +304,7 @@ def run(self, terms, variables=None, **kwargs):

ret = []
for term in terms:
display.vvv('lfbwlp - run - lookup term: {}'.format(term))
display.vvv(f'lfbwlp - run - lookup term: {term}')
try:
collection_id = term.get('collection_id', None)
folder_id = term.get('folder_id', None)
Expand All @@ -317,11 +319,11 @@ def run(self, terms, variables=None, **kwargs):
uris = term.get('uris', [])
username = term.get('username', None)
except Exception as e:
raise AnsibleError('Encountered exception while fetching {}: {}'.format(term, e))
raise AnsibleError(f'Encountered exception while fetching {term}: {e}')

if id_:
result = bw.get_item_by_id(id_)
display.vvv('lfbwlp - run - get item by id: {}'.format(id_))
display.vvv(f'lfbwlp - run - get item by id: {id_}')
if result:
# move username and password higher for easier access
result['username'] = result['login']['username']
Expand All @@ -330,10 +332,10 @@ def run(self, terms, variables=None, **kwargs):
continue # done here, go to next term
else:
# item not found by ID. if there is an ID given we expect it to exist
raise AnsibleError('Item with id {} not found.'.format(id_))
raise AnsibleError(f'Item with id {id_} not found.')

name = Bitwarden.get_pretty_name(name, hostname, purpose)
display.vvv('lfbwlp - run - get item: {}'.format(name))
display.vvv(f'lfbwlp - run - get item: {name}')
result = bw.get_items(name, username, folder_id, collection_id, organization_id)

if len(result) > 1:
Expand Down Expand Up @@ -380,30 +382,5 @@ def run(self, terms, variables=None, **kwargs):
out['password'] = out['login']['password']
ret.append(out)

# always returns a list of dicts
#
# example:
#
# - collectionIds:
# - 47b22450-fb65-4ad2-836a-03f25c982fb1
# favorite: false
# folderId: null
# id: 2656edf2-3600-4d8d-88e8-bcdda35d1ccf
# login:
# password: d2Dft5FqGK4yhzmsDcjWJD5LMAPGDsN8oZpXsxx6
# passwordRevisionDate: null
# totp: null
# uris:
# - match: null
# uri: https://www.example.com
# - match: null
# uri: https://git.example.com
# username: mariadb-admin
# name: app4711 - MariaDB
# notes: Automatically generated by Ansible.
# object: item
# organizationId: 5ae8f510-1f84-4243-8c35-bec35091706c
# reprompt: 0
# revisionDate: '2019-01-28T15:31:34.300Z'
## type: 1
# always returns a list of dicts; see the RETURN block above for the shape
return ret
Loading