Idempotent sed is always a fun challenge.
Adjust -i flag for the BSDs, but should work otherwise.
Idempotent sed is always a fun challenge.
Adjust -i flag for the BSDs, but should work otherwise.
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
.
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
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).
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ā¦
Yeah, same. So annoying.
Just install gsed or possibly use awk instead?
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).
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).
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)
According to these:
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#dot
Yes, it is.
Nice. It works in DASH which was a good sign but I wasnāt 100% sure.
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_
@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.
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.
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.
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.
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.