Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

files.Block: fixed couple of bugs, added try_prevent_shell_expansion and support for content: list[str] #1030

Merged
merged 6 commits into from
Jan 13, 2024
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
68 changes: 52 additions & 16 deletions pyinfra/operations/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
FileUploadCommand,
OperationError,
OperationTypeError,
OperationValueError,
QuoteString,
RsyncCommand,
StringCommand,
Expand Down Expand Up @@ -1614,6 +1615,7 @@ def block(
line=None,
backup=False,
escape_regex_characters=False,
try_prevent_shell_expansion=False,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1029
helps with mentioned issue

before=False,
after=False,
marker=None,
Expand All @@ -1631,6 +1633,7 @@ def block(
+ line: regex before or after which the content should be added if it doesn't exist.
+ backup: whether to backup the file (see ``files.line``). Default False.
+ escape_regex_characters: whether to escape regex characters from the matching line
+ try_prevent_shell_expansion: tries to prevent shell expanding by values like `$`
+ marker: the base string used to mark the text. Default is ``# {mark} PYINFRA BLOCK``
+ begin: the value for ``{mark}`` in the marker before the content. Default is ``BEGIN``
+ end: the value for ``{mark}`` in the marker after the content. Default is ``END``
Expand All @@ -1647,12 +1650,15 @@ def block(

Removal ignores ``content`` and ``line``

Preventing shell expansion works by wrapping the content in '`' before passing to `awk`.
WARNING: This will break if the content contains raw single quotes.

**Examples:**

.. code:: python

# add entry to /etc/host
files.marked_block(
files.block(
name="add IP address for red server",
path="/etc/hosts",
content="10.0.0.1 mars-one",
Expand All @@ -1661,7 +1667,7 @@ def block(
)

# have two entries in /etc/host
files.marked_block(
files.block(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name="add IP address for red server",
path="/etc/hosts",
content="10.0.0.1 mars-one\\n10.0.0.2 mars-two",
Expand All @@ -1670,21 +1676,29 @@ def block(
)

# remove marked entry from /etc/hosts
files.marked_block(
files.block(
name="remove all 10.* addresses from /etc/hosts",
path="/etc/hosts",
present=False
)

# add out of date warning to web page
files.marked_block(
files.block(
name="add out of date warning to web page",
path="/var/www/html/something.html",
content= "<p>Warning: this page is out of date.</p>",
regex=".*<body>.*",
after=True
marker="<!-- {mark} PYINFRA BLOCK -->",
)

# put complex alias into .zshrc
files.block(
path="/home/user/.zshrc",
content="eval $(thefuck -a)",
try_prevent_shell_expansion=True,
marker="## {mark} ALIASES ##"
)
"""

logger.warning("The `files.block` operation is currently in beta!")
Expand Down Expand Up @@ -1723,22 +1737,36 @@ def block(
cmd = None
if present:
if not content:
raise ValueError("'content' must be supplied when 'present' == True")
raise OperationValueError("'content' must be supplied when 'present' == True")
if line:
if before == after:
raise ValueError("only one of 'before' or 'after' used when 'line` is specified")
raise OperationValueError(
"only one of 'before' or 'after' used when 'line` is specified"
)
elif before != after:
raise ValueError("'line' must be supplied or 'before' and 'after' must be equal")
raise OperationValueError(
"'line' must be supplied or 'before' and 'after' must be equal"
)
if isinstance(content, str):
# convert string to list of lines
content = content.split("\n")
if try_prevent_shell_expansion and any("'" in line for line in content):
logger.warning("content contains single quotes, shell expansion prevention may fail")

the_block = "\n".join([mark_1, content, mark_2])
the_block = "\n".join([mark_1, *content, mark_2])

if (current is None) or ((current == []) and (before == after)):
# a) no file or b) file but no markers and we're adding at start or end. Both use 'cat'
redirect = ">" if (current is None) else ">>"
stdin = "- " if ((current == []) and before) else ""
# here = hex(random.randint(0, 2147483647))
here = "PYINFRAHERE"
cmd = StringCommand(f"cat {stdin}{redirect}", q_path, f"<<{here}\n{the_block}\n{here}")
cmd = StringCommand(
f"cat {stdin}{redirect}",
q_path,
f"<<{here}" if not try_prevent_shell_expansion else f"<<'{here}'",
f"\n{the_block}\n{here}",
)
elif current == []: # markers not found and have a pattern to match (not start or end)
regex = adjust_regex(line, escape_regex_characters)
print_before = "{ print }" if before else ""
Expand All @@ -1748,21 +1776,29 @@ def block(
f"{print_after} f!=1 && /{regex}/ {{ print x; f=1}} "
f"END {{if (f==0) print ARGV[2] }} {print_before}'"
)
cmd = StringCommand(out_prep, prog, q_path, f'$"{the_block}"', "> $OUT &&", real_out)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...f'$"{the_block}"'...
Removed $

cmd = StringCommand(
out_prep,
prog,
q_path,
f'"{the_block}"' if not try_prevent_shell_expansion else f"'{the_block}'",
"> $OUT &&",
real_out,
)
else:
pieces = content.split("\n")
if (len(current) != len(pieces)) or (
not all(lines[0] == lines[1] for lines in zip(pieces, current))
if (len(current) != len(content)) or (
not all(lines[0] == lines[1] for lines in zip(content, current))
): # marked_block found but text is different
prog = (
'awk \'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=""}}'
f"/{mark_1}/ {{print; print x; f=0}} /{mark_2}/ {{print; f=1}} f'"
f"/{mark_1}/ {{print; print x; f=0}} /{mark_2}/ {{print; f=1; next}} f'"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1031
Fixes this issue

)
cmd = StringCommand(
out_prep,
prog,
q_path,
'$"' + content + '"',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'$"' + content + '"',
Removed $

'"' + "\n".join(content) + '"'
if not try_prevent_shell_expansion
else "'" + "\n".join(content) + "'",
"> $OUT &&",
real_out,
)
Expand All @@ -1774,7 +1810,7 @@ def block(
host.create_fact(
Block,
kwargs={"path": path, "marker": marker, "begin": begin, "end": end},
data=content.split("\n"),
data=content,
)
else: # remove the marked_block
if content:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
}
},
"commands": [
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1} f' /home/someone/something $\"should be this\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something \"should be this\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
}
},
"commands": [
"cp /home/someone/something /home/someone/something.a-timestamp && OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1} f' /home/someone/something $\"should be this\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
"cp /home/someone/something /home/someone/something.a-timestamp && OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {{f=1; x=ARGV[2]; ARGV[2]=\"\"}}/# BEGIN PYINFRA BLOCK/ {print; print x; f=0} /# END PYINFRA BLOCK/ {print; f=1; next} f' /home/someone/something \"should be this\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
}
},
"exception": {
"names": ["ValueError"],
"names": ["OperationValueError"],
"message": "only one of 'before' or 'after' used when 'line` is specified"
}
}
2 changes: 1 addition & 1 deletion tests/operations/files.block/add_no_content_provided.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}
},
"exception": {
"names": ["ValueError"],
"names": ["OperationValueError"],
"message": "'content' must be supplied when 'present' == True"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
}
},
"commands": [
"cat > /home/someone/something <<PYINFRAHERE\n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE"
"cat > /home/someone/something <<PYINFRAHERE \n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
}
},
"commands": [
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this.*$/ { print x; f=1} END {if (f==0) print ARGV[2] } { print }' /home/someone/something $\"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this.*$/ { print x; f=1} END {if (f==0) print ARGV[2] } { print }' /home/someone/something \"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
}
},
"commands": [
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this \\*.*$/ { print x; f=1} END {if (f==0) print ARGV[2] } { print }' /home/someone/something $\"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
"OUT=\"$(TMPDIR=/tmp mktemp -t pyinfra.XXXXXX)\" && awk 'BEGIN {x=ARGV[2]; ARGV[2]=\"\"} f!=1 && /^.*before this \\*.*$/ { print x; f=1} END {if (f==0) print ARGV[2] } { print }' /home/someone/something \"# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\" > $OUT && chmod $(stat -c %a /home/someone/something 2>/dev/null || stat -f %Lp /home/someone/something ) $OUT && (chown $(stat -c \"%U:%G\" /home/someone/something 2>/dev/null) $OUT || chown -n $(stat -f \"%u:%g\" /home/someone/something ) $OUT) && mv \"$OUT\" /home/someone/something"
]
}
2 changes: 1 addition & 1 deletion tests/operations/files.block/add_no_existing_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
}
},
"commands": [
"cat > /home/someone/something <<PYINFRAHERE\n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE"
"cat > /home/someone/something <<PYINFRAHERE \n# BEGIN PYINFRA BLOCK\nplease add this\n# END PYINFRA BLOCK\nPYINFRAHERE"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}
},
"exception": {
"names": ["ValueError"],
"names": ["OperationValueError"],
"message": "'line' must be supplied or 'before' and 'after' must be equal"
}
}