oO.o's Neverending Tech Blog

Idempotent sed is always a fun challenge.

Adjust -i flag for the BSDs, but should work otherwise.

3 Likes

sed -i is such a good feature, kinda sad BSDs donā€™t have it. Having to pipe it into another temp file manually, then overwrite the original is cringe. Yet if I ever script stuff, I donā€™t use sed -i. If I work on a linux system and I donā€™t need to repeat the command (and if I do, I script it), then I just use -i.

1 Like

Hmmā€¦ ? (Theyā€™re not the same)
https://man.freebsd.org/cgi/man.cgi?query=sed&apropos=0&sektion=1&manpath=FreeBSD+15.0-CURRENT&arch=default&format=html
https://man.openbsd.org/sed.1
https://man.netbsd.org/sed.1

1 Like

BSD has -i but it works differently. To replicate Linux -i behavior, iirc, you would do sed -i '' ... but (also iirc) that will error out on linux, so thereā€™s no way to make a cross-platform sed command with edit in place functionality. I once tried to pipe sed to tee and then back into the file, but that also didnā€™t work in all cases (or any case? I donā€™t remember).

2 Likes

Tried too, didnā€™t work. What worked for me was something silly like:

sed whatever > /tmp/file.out
mv -f /tmp/file.out whatever

Horrible way of doing it, but cross-compatibleā€¦

1 Like

Yeah, same. So annoying.

1 Like

Just install gsed or possibly use awk instead?

1 Like

Of course, pragmatically, those are valid alternatives. I think @ThatGuyB and I like to understand the current-day behaviors of posixy sed, especially cross-platform, for scienceĀ® and in general to track the evolution of posix shell and utilities as they relate to real world use.

@ThatGuyB please confirm or clarify as I donā€™t mean to speak for youā€¦

My goal and part of the reason Iā€™m still on this forum is sharing knowledge. And I like keeping my scripts POSIX-compliant (or as much as Iā€™m aware of), because I can never know whoā€™ll try to run them and where. Unfortunately, the more scripts I make, the more and more incompatibilities I get thrown in my face.

Lately Iā€™ve only been scripting for linux, but if I get a chance to write something posix-compliant and I get hit with something like completely different interfaces, Iā€™ll most likely just split the code into modules and have an os detection function at the top. I havenā€™t thought of how Iā€™m going to do that (probably uname).

Unfortunately, Iā€™ve yet to release the (bare-basic and piss-poor) scripts I made to the world, Iā€™m still procrastinating. But at least Iā€™ve pretty much decided how my labā€™s going to look like (incus on iscsi), so I should be able to eventually release them on a gitea instance (Iā€™d love to go for stagit, but Iā€™m frankly not going to put much work into keeping everything static, maybe in the future). My skills are really rusty and I have other projects I need to spend time on (which Iā€™ll probably share details on my rants blog).

2 Likes

Do you know if . is POSIX for sourcing scripts? Itā€™s difficult to search for because itā€™s just a period and I havenā€™t gotten around to scanning the POSIX docs for it.

Not sure, I usually do not deal with importing env from external sources, except for a shell input prompt at most, if not already specified as flags in the command (the latter was particularly useful for nagios check scripts).

1 Like

These old docs probably answers you question easy-rsa/doc/README-2.0 at 2.2.2 Ā· OpenVPN/easy-rsa Ā· GitHub (yes, it works at least on FreeBSD too)

1 Like

According to these:

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#dot

Yes, it is.

2 Likes

Nice. It works in DASH which was a good sign but I wasnā€™t 100% sure.

1 Like

Super charge your first_found lookups in Ansible with this one weird trick:

---
# vars/main.yml

o0_net_os_short: "{{ ansible_network_os | regex_replace('^.*\\.') }}"

o0_ansible_system: >-
  {{ ansible_system | default(o0_raw_facts['uname']['os']) }}
o0_ansible_distribution: >-
  {{  ansible_distribution
      | default(o0_raw_facts['os_release']['NAME'] )
      | default(o0_raw_facts['uname']['os'])
      | regex_replace('^Arch Linux$', 'Archlinux')
      | regex_replace('^Darwin$', 'MacOSX') }}
o0_ansible_distribution_release: >-
  {{  ansible_distribution_release
      | default(o0_raw_facts['os_release']['VERSION_CODENAME'])
      | default(o0_raw_facts['uname']['release']) }}
o0_ansible_distribution_version: >-
  {{  ansible_distribution_version
      | default(o0_raw_facts['os_release']['VERSION_ID'])
      | default(o0_raw_facts['macos']['vers'])
      | default(o0_raw_facts['uname']['release']) }}
o0_ansible_architecture: >-
  {{  ansible_architecture
      | default(o0_raw_facts['uname']['arch']) }}

# For use with the first_found lookup
o0_ff_attrs: >-
  {{  ( [ [ ansible_distribution,
            ansible_distribution_version,
            o0_net_mgr
          ],
          [ansible_distribution, ansible_distribution_version, o0_mac],
          [o0_ansible_distribution, o0_ansible_distribution_version],
          [ ansible_distribution,
            ansible_distribution_major_version,
            o0_net_mgr
          ],
          [ ansible_distribution,
            ansible_distribution_major_version,
            o0_mac
          ],
          [ansible_distribution, ansible_distribution_major_version],
          [ansible_distribution, o0_net_mgr],
          [ansible_distribution, o0_mac],
          [o0_ansible_distribution],
          [ansible_os_family, o0_net_mgr],
          [ansible_os_family, o0_mac],
          [ansible_os_family],
          [ansible_system, o0_net_mgr],
          [ansible_system, o0_mac],
          [o0_ansible_system],
          [o0_net_mgr],
          [o0_mac],
          [o0_net_os_short],
          [ 'raw' if not o0_py_success | bool
            else ansible_connection | regex_replace('^local$', 'ssh') ] ]
        | map('map', 'default', '')
        | map('map', 'lower')
        | map('join', ff_delim_var)
        | reject('match', ff_delim_var | regex_escape + '$')
        | reject('match', '^' + ff_delim_var | regex_escape)
        | reject('match', ff_delim_var | regex_escape * 2)
        + [ ff_default_var | default('') ] )
      | reject('==', '') }}
o0_ff_files: >-
  {{  [ff_prefix_var]
      | product(o0_ff_attrs)
      | map('join')
      | product(  [ff_suffix_var]
                  | product(ff_exts_var)
                  | map('join', '.')
                  | default([ff_suffix_var], true)
                  | map('regex_replace', '\.$') )
      | map('join') }}

o0_ff:
  tasks:
    files: &files >-
      {{ o0_ff_files + ['/dev/null'] if ff_skip_var | bool else o0_ff_files }}
    paths: "{{ ansible_search_path | product(['/tasks']) | map('join') }}"
  vars:
    files: *files
    paths: "{{ ansible_search_path | product(['/vars']) | map('join') }}"
  template:
    files: *files
    paths: "{{ ansible_search_path | product(['/templates']) | map('join') }}"
- include_tasks: >-
    {{ lookup('first_found', o0_ff['tasks']) }}
  vars:
    ff_prefix_var: my_task_
3 Likes

@wendell what do you think about putting this on a t-shirt (tee-shirt)?

If itā€™s already been done, I didnā€™t find it, but ā€œman tee shirtā€ is a difficult search term for obvious reasons.

3 Likes

The APT configuration syntax is difficult to parse and convert in and out of structured variables, but after some effort I have something functional in Ansible that gives me APT facts and then can write from a provided configuration dictionary into /etc/apt/apt.conf.d/90ansibleoverrides.

Supporting lists in the config structure was the last hurdle.

APT facts come in like this (config is parsed out of apt-conf dump):

    "o0_apt_facts": {
        "cfg": {
            "APT": "",
            "APT::Architecture": "amd64",
            "APT::Architectures": [
                "amd64"
            ],
            "APT::Authentication": "",
            "APT::Authentication::TrustCDROM": "true",
            "APT::Build-Essential": [
                "build-essential"
            ],
            "APT::Compressor": "",
            "APT::Compressor::.": "",
            "APT::Compressor::.::Binary": "",
            "APT::Compressor::.::Cost": "0",
            "APT::Compressor::.::Extension": "",
            "APT::Compressor::.::Name": ".",
            "APT::Compressor::bzip2": "",
            "APT::Compressor::bzip2::Binary": "bzip2",
            "APT::Compressor::bzip2::CompressArg": [
                "-6"
            ],
            "APT::Compressor::bzip2::Cost": "300",
            "APT::Compressor::bzip2::Extension": ".bz2",
            "APT::Compressor::bzip2::Name": "bzip2",
            "APT::Compressor::bzip2::UncompressArg": [
                "-d"
            ],
            "APT::Compressor::gzip": "",
            "APT::Compressor::gzip::Binary": "gzip",
            "APT::Compressor::gzip::CompressArg": [
                "-6n"
            ],
            "APT::Compressor::gzip::Cost": "100",
            "APT::Compressor::gzip::Extension": ".gz",
            "APT::Compressor::gzip::Name": "gzip",
            "APT::Compressor::gzip::UncompressArg": [
                "-d"
            ],
            "APT::Compressor::lz4": "",
            "APT::Compressor::lz4::Binary": "false",
            "APT::Compressor::lz4::Cost": "50",
            "APT::Compressor::lz4::Extension": ".lz4",
            "APT::Compressor::lz4::Name": "lz4",
            "APT::Compressor::lzma": "",
            "APT::Compressor::lzma::Binary": "xz",
            "APT::Compressor::lzma::CompressArg": [
                "--format=lzma",
                "-6"
            ],
            "APT::Compressor::lzma::Cost": "400",
            "APT::Compressor::lzma::Extension": ".lzma",
            "APT::Compressor::lzma::Name": "lzma",
            "APT::Compressor::lzma::UncompressArg": [
                "--format=lzma",
                "-d"
            ],
            "APT::Compressor::xz": "",
            "APT::Compressor::xz::Binary": "xz",
            "APT::Compressor::xz::CompressArg": [
                "-6"
            ],
            "APT::Compressor::xz::Cost": "200",
            "APT::Compressor::xz::Extension": ".xz",
            "APT::Compressor::xz::Name": "xz",
            "APT::Compressor::xz::UncompressArg": [
                "-d"
            ],
            "APT::Compressor::zstd": "",
            "APT::Compressor::zstd::Binary": "zstd",
            "APT::Compressor::zstd::CompressArg": [
                "-19"
            ],
            "APT::Compressor::zstd::Cost": "60",
            "APT::Compressor::zstd::Extension": ".zst",
            "APT::Compressor::zstd::Name": "zstd",
            "APT::Compressor::zstd::UncompressArg": [
                "-d"
            ],
            "APT::Install-Recommends": "1",
            "APT::Install-Suggests": "0",
            "APT::Move-Autobit-Sections": [
                "oldlibs"
            ],
            "APT::Never-MarkAuto-Sections": [
                "metapackages",
                "tasks"
            ],
            "APT::NeverAutoRemove": [
                "^firmware-linux.*",
                "^linux-firmware$",
                "^linux-image-[a-z0-9]*$",
                "^linux-image-[a-z0-9]*-[a-z0-9]*$"
            ],
            "APT::Periodic": "",
            "APT::Periodic::Enable": "0",
            "APT::Sandbox": "",
            "APT::Sandbox::User": "_apt",
            "APT::VersionedKernelPackages": [
                "linux-.*",
                "kfreebsd-.*",
                "gnumach-.*",
                ".*-modules",
                ".*-kernel"
            ],
            "Acquire": "",
            "Acquire::AllowDowngradeToInsecureRepositories": "0",
            "Acquire::AllowInsecureRepositories": "0",
            "Acquire::AllowWeakRepositories": "0",
            "Acquire::Changelogs": "",
            "Acquire::Changelogs::AlwaysOnline": "",
            "Acquire::Changelogs::AlwaysOnline::Origin": "",
            "Acquire::Changelogs::AlwaysOnline::Origin::Ubuntu": "1",
            "Acquire::Changelogs::URI": "",
            "Acquire::Changelogs::URI::Origin": "",
            "Acquire::Changelogs::URI::Origin::Debian": "https://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog",
            "Acquire::Changelogs::URI::Origin::Ubuntu": "https://changelogs.ubuntu.com/changelogs/pool/@CHANGEPATH@/changelog",
            "Acquire::CompressionTypes": "",
            "Acquire::CompressionTypes::bz2": "bzip2",
            "Acquire::CompressionTypes::gz": "gzip",
            "Acquire::CompressionTypes::lz4": "lz4",
            "Acquire::CompressionTypes::lzma": "lzma",
            "Acquire::CompressionTypes::xz": "xz",
            "Acquire::CompressionTypes::zst": "zstd",
            "Acquire::IndexTargets": "",
            "Acquire::IndexTargets::deb": "",
            "Acquire::IndexTargets::deb-src": "",
            "Acquire::IndexTargets::deb-src::Sources": "",
            "Acquire::IndexTargets::deb-src::Sources::Description": "$(RELEASE)/$(COMPONENT) Sources",
            "Acquire::IndexTargets::deb-src::Sources::MetaKey": "$(COMPONENT)/source/Sources",
            "Acquire::IndexTargets::deb-src::Sources::Optional": "0",
            "Acquire::IndexTargets::deb-src::Sources::ShortDescription": "Sources",
            "Acquire::IndexTargets::deb-src::Sources::flatDescription": "$(RELEASE) Sources",
            "Acquire::IndexTargets::deb-src::Sources::flatMetaKey": "Sources",
            "Acquire::IndexTargets::deb::Packages": "",
            "Acquire::IndexTargets::deb::Packages::Description": "$(RELEASE)/$(COMPONENT) $(ARCHITECTURE) Packages",
            "Acquire::IndexTargets::deb::Packages::MetaKey": "$(COMPONENT)/binary-$(ARCHITECTURE)/Packages",
            "Acquire::IndexTargets::deb::Packages::Optional": "0",
            "Acquire::IndexTargets::deb::Packages::ShortDescription": "Packages",
            "Acquire::IndexTargets::deb::Packages::flatDescription": "$(RELEASE) Packages",
            "Acquire::IndexTargets::deb::Packages::flatMetaKey": "Packages",
            "Acquire::IndexTargets::deb::Translations": "",
            "Acquire::IndexTargets::deb::Translations::Description": "$(RELEASE)/$(COMPONENT) Translation-$(LANGUAGE)",
            "Acquire::IndexTargets::deb::Translations::MetaKey": "$(COMPONENT)/i18n/Translation-$(LANGUAGE)",
            "Acquire::IndexTargets::deb::Translations::ShortDescription": "Translation-$(LANGUAGE)",
            "Acquire::IndexTargets::deb::Translations::flatDescription": "$(RELEASE) Translation-$(LANGUAGE)",
            "Acquire::IndexTargets::deb::Translations::flatMetaKey": "$(LANGUAGE)",
            "Acquire::Languages": [
                "en_US",
                "en",
                "none"
            ],
            "Acquire::cdrom": "",
            "Acquire::cdrom::mount": "/media/cdrom",
            "Binary": "apt-config",
            "Binary::apt": "",
            "Binary::apt::APT": "",
            "Binary::apt::APT::Cache": "",
            "Binary::apt::APT::Cache::AllVersions": "0",
            "Binary::apt::APT::Cache::Search": "",
            "Binary::apt::APT::Cache::Search::Version": "2",
            "Binary::apt::APT::Cache::Show": "",
            "Binary::apt::APT::Cache::Show::Version": "2",
            "Binary::apt::APT::Cache::ShowDependencyType": "1",
            "Binary::apt::APT::Cache::ShowVersion": "1",
            "Binary::apt::APT::Cache::ShowVirtuals": "1",
            "Binary::apt::APT::Cmd": "",
            "Binary::apt::APT::Cmd::Pattern-Only": "1",
            "Binary::apt::APT::Cmd::Show-Update-Stats": "1",
            "Binary::apt::APT::Color": "1",
            "Binary::apt::APT::Get": "",
            "Binary::apt::APT::Get::Update": "",
            "Binary::apt::APT::Get::Update::InteractiveReleaseInfoChanges": "1",
            "Binary::apt::APT::Get::Upgrade-Allow-New": "1",
            "Binary::apt::APT::Keep-Downloaded-Packages": "0",
            "Binary::apt::DPkg": "",
            "Binary::apt::DPkg::Lock": "",
            "Binary::apt::DPkg::Lock::Timeout": "120",
            "Binary::apt::DPkg::Progress-Fancy": "1",
            "DPkg": "",
            "DPkg::Path": "/usr/sbin:/usr/bin:/sbin:/bin",
            "DPkg::Pre-Install-Pkgs": [
                "/usr/bin/apt-listchanges --apt || test $? -lt 10",
                "/usr/sbin/dpkg-preconfigure --apt || true"
            ],
            "DPkg::Tools": "",
            "DPkg::Tools::Options": "",
            "DPkg::Tools::Options::/usr/bin/apt-listchanges": "",
            "DPkg::Tools::Options::/usr/bin/apt-listchanges::InfoFD": "20",
            "DPkg::Tools::Options::/usr/bin/apt-listchanges::Version": "2",
            "Dir": "/",
            "Dir::Bin": "",
            "Dir::Bin::bzip2": "/bin/bzip2",
            "Dir::Bin::dpkg": "/usr/bin/dpkg",
            "Dir::Bin::gzip": "/bin/gzip",
            "Dir::Bin::lz4": "/usr/bin/lz4",
            "Dir::Bin::lzma": "/usr/bin/xz",
            "Dir::Bin::methods": "/usr/lib/apt/methods",
            "Dir::Bin::planners": [
                "/usr/lib/apt/planners"
            ],
            "Dir::Bin::solvers": [
                "/usr/lib/apt/solvers"
            ],
            "Dir::Bin::xz": "/usr/bin/xz",
            "Dir::Bin::zstd": "/usr/bin/zstd",
            "Dir::Cache": "var/cache/apt",
            "Dir::Cache::archives": "archives/",
            "Dir::Cache::pkgcache": "pkgcache.bin",
            "Dir::Cache::srcpkgcache": "srcpkgcache.bin",
            "Dir::Etc": "etc/apt",
            "Dir::Etc::apt-listchanges-main": "listchanges.conf",
            "Dir::Etc::apt-listchanges-parts": "listchanges.conf.d",
            "Dir::Etc::main": "apt.conf",
            "Dir::Etc::netrc": "auth.conf",
            "Dir::Etc::netrcparts": "auth.conf.d",
            "Dir::Etc::parts": "apt.conf.d",
            "Dir::Etc::preferences": "preferences",
            "Dir::Etc::preferencesparts": "preferences.d",
            "Dir::Etc::sourcelist": "sources.list",
            "Dir::Etc::sourceparts": "sources.list.d",
            "Dir::Etc::trusted": "trusted.gpg",
            "Dir::Etc::trustedparts": "trusted.gpg.d",
            "Dir::Ignore-Files-Silently": [
                "~$",
                "\\.disabled$",
                "\\.bak$",
                "\\.dpkg-[a-z]+$",
                "\\.ucf-[a-z]+$",
                "\\.save$",
                "\\.orig$",
                "\\.distUpgrade$"
            ],
            "Dir::Log": "var/log/apt",
            "Dir::Log::History": "history.log",
            "Dir::Log::Planner": "eipp.log.xz",
            "Dir::Log::Terminal": "term.log",
            "Dir::Media": "",
            "Dir::Media::MountPath": "/media/cdrom",
            "Dir::State": "var/lib/apt",
            "Dir::State::cdroms": "cdroms.list",
            "Dir::State::extended_states": "extended_states",
            "Dir::State::lists": "lists/",
            "Dir::State::status": "/var/lib/dpkg/status"
        },
        "repos": {
            "main": {
                "cfg_file": "/etc/apt/sources.list",
                "enabled": true,
                "src": {
                    "cfg_file": "/etc/apt/sources.list",
                    "enabled": true,
                    "url": "http://deb.debian.org/debian/"
                },
                "sub_repos": {
                    "security": {
                        "cfg_file": "/etc/apt/sources.list",
                        "enabled": true,
                        "src": {
                            "cfg_file": "/etc/apt/sources.list",
                            "enabled": true,
                            "url": "http://security.debian.org/debian-security"
                        },
                        "url": "http://security.debian.org/debian-security"
                    },
                    "updates": {
                        "cfg_file": "/etc/apt/sources.list",
                        "enabled": true,
                        "src": {
                            "cfg_file": "/etc/apt/sources.list",
                            "enabled": true,
                            "url": "http://deb.debian.org/debian/"
                        },
                        "url": "http://deb.debian.org/debian/"
                    }
                },
                "url": "http://deb.debian.org/debian/"
            },
            "non-free-firmware": {
                "cfg_file": "/etc/apt/sources.list",
                "enabled": true,
                "src": {
                    "cfg_file": "/etc/apt/sources.list",
                    "enabled": true,
                    "url": "http://deb.debian.org/debian/"
                },
                "sub_repos": {
                    "security": {
                        "cfg_file": "/etc/apt/sources.list",
                        "enabled": true,
                        "src": {
                            "cfg_file": "/etc/apt/sources.list",
                            "enabled": true,
                            "url": "http://security.debian.org/debian-security"
                        },
                        "url": "http://security.debian.org/debian-security"
                    },
                    "updates": {
                        "cfg_file": "/etc/apt/sources.list",
                        "enabled": true,
                        "src": {
                            "cfg_file": "/etc/apt/sources.list",
                            "enabled": true,
                            "url": "http://deb.debian.org/debian/"
                        },
                        "url": "http://deb.debian.org/debian/"
                    }
                },
                "url": "http://deb.debian.org/debian/"
            }
        }
    }

An arbitrary example of configuration from a host variable to actual config lines. Lines are only written if the values differ from those in the gathered facts.

o0_apt_cfg_add:
  'Acquire::AllowInsecureRepositories': 1
  'Binary::apt::APT::Cache::AllVersions': 1
  'APT::NeverAutoRemove':
    - '^mypkg$'
    - '^myotherpkg$'
Acquire::AllowInsecureRepositories "1";
Binary::apt::APT::Cache::AllVersions "1";
#clear APT::NeverAutoRemove; APT::NeverAutoRemove { "^mypkg$"; "^myotherpkg$"; };

Of course this is all idempotent as well.

I will do the same for other package managers. Thankfully, I had already done a lot of the work of parsing the repository configurations a couple years ago and the other package managers have friendlier configuration syntax, so Iā€™m glad to be done with APT.

2 Likes

I switched my tmux prefix to C-Space today and have never felt better. After using C-a for years now I donā€™t understand why itā€™s popular.

1 Like

Finally got around to trudging through custom Ansible modules and plugins. Ansible under the hood is kind of a mess but no going back for me at this point (sunk cost fallacy, I know).

Still things are beginning to work. My first task is to develop some facts modules. For my purposes, Ansible facts leave a lot to be desired. I have hacked together several roles that essentially function as fact gathering but in several instances, they have to iterate over long lists (users, repositories, DHCP leases) and itā€™s just gotten so time consuming itā€™s clearly a completely stupid way to do it when iterating over a list in an action module with Python is trivially quick and thereā€™s opportunity for async if it comes to that.

Anyway, Iā€™m wrapping up a users fact gathering module thatā€¦ you guessed itā€¦ enumerates the users on the system including, gecos, home, groups, etc. I have the basic work done but want to add authorized and public SSH key data as well. Shouldnā€™t be too heavy a lift now that I understand the general structure.

2 Likes

Today I wrote a custom command module for Ansible. It is a wrapper of the Ansibleā€™s command module but it will fall back to issuing a raw command if Python isnā€™t installed on the remote host. It supports all module arguments of Ansibleā€™s command module with the exception of expand_argument_vars which Iā€™m not sure how to replicate safely with _low_level_execute_command. It also doesnā€™t support free-form commands because Ansible appears to actively block that in custom modules.

This will replace a lot of complexity in my roles that need to run on hosts before Python can be installed (more of them than you might think).

# vim: ts=4:sw=4:sts=4:noet:ft=python
#
# Adapted from:
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or
# https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import annotations

import datetime
import shlex
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.common.collections import is_iterable
from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash


class ActionModule(ActionBase):
	"""
	Execute a command on the remote host and fallback to raw if necessary
	"""

	TRANSFERS_FILES = False

	def cmd(self, module_args=None):
		shell = module_args['_uses_shell']
		chdir = module_args['chdir']
		executable = module_args['executable']
		args = module_args['_raw_params']
		argv = module_args['argv']
		creates = module_args['creates']
		removes = module_args['removes']
		stdin = module_args['stdin']
		stdin_add_newline = module_args['stdin_add_newline']
		strip = module_args['strip_empty_ends']
		expand_argument_vars = module_args['expand_argument_vars']

		# we promised these in 'always' ( _lines get auto-added on action plugin)
		r = { 'changed': False, 'stdout': '', 'stderr': '', 'rc': None,
			'cmd': None, 'start': None, 'end': None, 'delta': None, 'msg': '' }

		if not shell and executable:
			self._display.warning(
				"As of Ansible 2.4, the parameter 'executable' is no longer" +
				"supported with the 'command' module. Not using '%s'."
				% executable)
			executable = None

		if (not args or args.strip() == '') and not argv:
			r['rc'] = 256
			r['msg'] = "no command given"

		if args and argv:
			r['rc'] = 256
			r['msg'] = "only command or argv can be given, not both"

		if not shell and args:
			args = shlex.split(args)

		args = args or argv
		# All args must be strings
		if is_iterable(args, include_strings=False):
			args = [
				shlex.quote(arg)
				for arg in args
			]

		r['cmd'] = args

		if chdir:
			chdir = shlex.quote(chdir)
			cd = self._low_level_execute_command(
				"cd %s" % chdir,
				executable=executable
			)
			if cd['rc'] != 0:
				raise Exception(
					'Unable to change directory before execution: %s'
					% chdir
				)

		# check_mode partial support, since it only really works in
		# checking creates/removes
		if self._task.check_mode:
			shoulda = "Would"
		else:
			shoulda = "Did"

		# special skips for idempotence if file exists (assumes command
		# creates)
		if creates:
			creates = shlex.quote(creates)
			cr = self._low_level_execute_command("[ -e %s ]" % creates)
			if cr['rc'] == 0:
				r['msg'] = (
					"%s not run command since '%s' exists"
					% (shoulda, creates)
				)
				# TODO: deprecate
				r['stdout'] = "skipped, since %s exists" % creates
				r['stdout_lines'] = [r['stdout']]
				r['stderr_lines'] = []
				r['rc'] = 0

		# special skips for idempotence if file does not exist (assumes
		# command removes)
		if not r['msg'] and removes:
			removes = shlex.quote(removes)
			rm = self._low_level_execute_command("[ -e %s ]" % removes)
			if rm['rc'] != 0:
				r['msg'] = (
					"%s not run command since '%s' does not exist"
					% (shoulda, removes)
				)
				# TODO: deprecate
				r['stdout'] = "skipped, since %s does not exist" % removes
				r['stdout_lines'] = [r['stdout']]
				r['stderr_lines'] = []
				r['rc'] = 0

		# actually executes command (or not ...)
		if not r['msg']:
			if not self._task.check_mode:
				r['changed'] = True
				r['start'] = datetime.datetime.now()
				r = merge_hash(r, self._low_level_execute_command(
						shlex.join(args),
						in_data=stdin,
						executable=executable,
						chdir=chdir
					)
				)
				r['end'] = datetime.datetime.now()
			else:
				# this is partial check_mode support, since we end up
				# skipping if we get here
				r['rc'] = 0
				r['msg'] = "Command would have run if not in check mode"
				if creates is None and removes is None:
					r['skipped'] = True
					# skipped=True and changed=True are mutually
					# exclusive
					r['changed'] = False

		# convert to text for jsonization and usability
		if r['start'] is not None and r['end'] is not None:
			# these are datetime objects, but need them as strings to
			# pass back
			r['delta'] = to_text(r['end'] - r['start'])
			r['end'] = to_text(r['end'])
			r['start'] = to_text(r['start'])

		if strip:
			r['stdout'] = to_text(r['stdout']).rstrip("\r\n")
			r['stderr'] = to_text(r['stderr']).rstrip("\r\n")

		if r['rc'] != 0:
			r['msg'] = 'non-zero return code'

		return r


	def run(self, tmp=None, task_vars=None):
		if task_vars is None:
			task_vars = dict()

		self._supports_async = True

		argument_spec = {
			'_raw_params': {},
			'_uses_shell': {
				'type': 'bool',
				'default': False
			},
			'argv': {
				'type': 'list',
				'elements': 'str'
			},
			'chdir': {'type': 'path'},
			'executable': {},
			'expand_argument_vars': {
				'type': 'bool',
				'default': True
			},
			'creates': {'type': 'path'},
			'removes': {'type': 'path'},
			'stdin': {'required': False},
			'stdin_add_newline': {
				'type': 'bool',
				'default': True
			},
			'strip_empty_ends': {
				'type': 'bool',
				'default': True
			}
		}
		validation_result, new_module_args = self.validate_argument_spec(
			argument_spec=argument_spec
		)

		results = super(ActionModule, self).run(tmp, task_vars)
		del tmp  # tmp no longer has any effect

		wrap_async = self._task.async_val and not self._connection.has_native_async

		# explicitly call `ansible.legacy.command` for backcompat to
		# allow library/ override of `command` while not allowing
		# collections search for an unqualified `command` module
		ansible_cmd_mod = self._execute_module(
			module_name='ansible.legacy.command',
			task_vars=task_vars,
			wrap_async=wrap_async
		)
		try:
			ansible_cmd_mod['failed']
			ansible_cmd_mod['ansible_facts']['discovered_interpreter_python']
			self._display.warning(
				'Ansible command module failed, falling back to raw. ' +
				'Variable expansion is not supported.'
			)
			results = merge_hash(results, self.cmd(module_args=new_module_args))
		except:
			results = merge_hash(results, ansible_cmd_mod)

		if not wrap_async:
			# remove a temporary path we created
			self._remove_tmp_path(self._connection._shell.tmpdir)

		return results

I would like to create similar raw fallback variants of slurp, template and lineinfile.

3 Likes