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

Create a dedicated script for removing TinyPilot auto-generated changes #1710

Closed
mtlynch opened this issue Dec 19, 2023 · 7 comments · Fixed by #1722
Closed

Create a dedicated script for removing TinyPilot auto-generated changes #1710

mtlynch opened this issue Dec 19, 2023 · 7 comments · Fixed by #1722
Assignees
Labels
enhancement New feature or request small

Comments

@mtlynch
Copy link
Contributor

mtlynch commented Dec 19, 2023

When TinyPilot modifies config files owned by other services, we sometimes add custom markers that allow us to back out our changes later:

  • # Retrieve original `/etc/hosts`.
    # - Read file line by line, make sure to preserve all whitespace.
    # - Remove all marker sections.
    etc_hosts_original=()
    is_in_marker_section='false'
    while IFS= read -r line; do
    if "${is_in_marker_section}" && [[ "${line}" == "${MARKER_END}" ]]; then
    is_in_marker_section='false'
    continue
    fi
    if "${is_in_marker_section}" || [[ "${line}" == "${MARKER_START}" ]]; then
    is_in_marker_section='true'
    continue
    fi
    etc_hosts_original+=("${line}")
    done < /etc/hosts
    if "${is_in_marker_section}"; then
    echo 'Unclosed marker section' >&2
    exit 1
    fi
    # Populate new hostname to `/etc/hostname`.
    # Note that the value must be newline-terminated, see:
    # https://manpages.debian.org/stretch/systemd/hostname.5.en.html
    printf "%s\n" "${NEW_HOSTNAME}" > /etc/hostname
  • # Remove any existing auto-generated configuration for the interface.
    dhcpcd_original=()
    section=()
    interface_matched="false"
    while IFS= read -r line; do
    if [[ "${#section[@]}" -ne 0 ]] && [[ "${line}" == "${MARKER_END}" ]]; then
    section+=("${line}")
    if [[ "${interface_matched}" == "false" ]] ; then
    dhcpcd_original=("${dhcpcd_original[@]}" "${section[@]}")
    fi
    section=()
    interface_matched="false"
    continue
    fi
    if [[ "${#section[@]}" -ne 0 ]] || [[ "${line}" == "${MARKER_START}" ]]; then
    if [[ "${line}" == "interface ${INTERFACE}" ]] ; then
    interface_matched="true"
    fi
    section+=("${line}")
    continue
    fi
    dhcpcd_original+=("${line}")
    done < "${CONFIG_FILE}"
    if [[ "${#section[@]}" -ne 0 ]] ; then
    echo "The contents of ${CONFIG_FILE} are not in the expected format." >&2
    echo "Auto-generated changes may have been manually edited." >&2
    echo "" >&2
    echo "Remove the relevant configuration manually to proceed." >&2
    exit 1
    fi
    # Convert array of lines to a single string.
    OLD_CONFIG_FILE=$(printf "%s\n" "${dhcpcd_original[@]}")
    readonly OLD_CONFIG_FILE
    # Write original file contents back to the file.
    echo "${OLD_CONFIG_FILE}" | sudo tee "${CONFIG_FILE}" > /dev/null

We're about to add a third incarnation of this code on the website repo:

We should refactor this logic into a script whose only job is to remove the TinyPilot-generated section. We can use this reusable script for removing lines and markers rather than duplicating the logic in multiple places.

Example

$ cat example.conf
foo=bar

# --- AUTOGENERATED BY TINYPILOT - START ---
tinypilot_timing=7
hdmi_output=3
# --- AUTOGENERATED BY TINYPILOT - END ---

baz=baa

$ /opt/tinypilot/scripts/remove-tinypilot-lines < example.conf
foo=bar

baz=baa
@jotaen4tinypilot
Copy link
Contributor

I’m not sure we can easily consolidate the instance in unset-static-ip: the code contains additional logic to account for the --interface flag, so it only removes marker sections that match the given network interface, and disregards any other marker section. For example, if multiple static IP configurations for different network interfaces had been added to dhcpcd.conf, then they are each wrapped into their own markers. You can then do e.g. unset-static-ip --interface eth0, to only remove the one particular marker section for the eth0 interface, while leaving all other marker sections untouched.

A potential solution that would come to my mind is to provide some sort of filter functionality in an otherwise generic remove-tinypilot-lines script, to check whether a marker section is eligible for dropping. However, I’m worried that this would complicate things instead of simplifying them, partly because it might be cumbersome to express such filter clauses with bash APIs.

We originally introduced the static IP CLI scripts a year ago. I don’t know whether we actually make use of the multi-interface functionality anywhere in practice right now, and whether it’s therefore safe or not to drop it:

  • We don’t need it for the static IP UI right now, though I don’t know whether we have plans for that. In the static IP UI, it’s currently not possible to specify the network interface, and it implicitly defaults to eth0. (I’m also actually not sure the UI can correctly deal with multiple static IP configs for different interfaces at all, or what would happen if they are present.)
  • We might have advertised CLI usage of the static IP scripts publicly, so some users might rely on it?

I think we could generally consider to establish an internal convention around these marker sections. My intuitive take is (was) that there should only be one marker section per file, and we must always be able to re-generate the section in its entirety. That at least would be the main benefit that I’d see in using the marker section approach over something more sophisticated. Otherwise, we have to do (conditional) parsing work to inspect the contents of the file, and that is actually what the marker section approach tries to avoid.

@jotaen4tinypilot
Copy link
Contributor

That being said, my question is how we’d proceed from here best? E.g.,

  • We could still create a remove-tinypilot-lines script, but only use it in change-hostname and the FAQ, and not in unset-static-ip.
  • We could look into refactoring the static IP logic, and trying to unify the multiple markers sections into a single one.
  • We could try to find a remove-tinypilot-lines abstractions that somehow works for both use cases.
  • We do re-consider the issue altogether.

@jotaen4tinypilot
Copy link
Contributor

(WIP branch, including bats tests; unfortunately, I didn’t notice this problem earlier / right away.)

@mtlynch
Copy link
Contributor Author

mtlynch commented Jan 10, 2024

I think we can just drop the --interface flag from unset-static-ip. In other words, unset-static-ip removes all TinyPilot-assigned static IPs from all interfaces.

We should make it error out if the user passes an --interface flag so that we don't silently break old behavior if anyone has scripts relying on this (unlikely, but possible).

For this to break a use-case, we'd need to have a user go through a flow like this, correct?

  1. Assign a static IP to eth0
  2. Assign a static IP to wlan0
  3. Remove a static IP from eth0 only

I think the number of users who go through a flow like that is vanishingly small, probably zero.

I don't recall anyone ever requesting support for this. I think we just added it because "why not?" and we didn't think through the maintenance burden.

@jotaen4tinypilot
Copy link
Contributor

jotaen4tinypilot commented Jan 11, 2024

I think we can just drop the --interface flag from unset-static-ip. In other words, unset-static-ip removes all TinyPilot-assigned static IPs from all interfaces.

Okay cool, that simplifies things a lot.

For this to break a use-case, we'd need to have a user go through a flow like this, correct?

Correct. In this case two things could happen when they call the unset-static-ip script:

  • They don’t specify an --interface flag: the script would previously have defaulted to deleting the eth0 section. Then, it would delete all sections/interfaces.
  • They specify an --interface flag, e.g. --interface wlan0: the script would previously have deleted only the wlan0 section. Then, it would fail with our about-to-be-added error message.

I’d do this now:

  1. Remove --interface flag from unset-static-ip API as discussed, to make unset-static-ip on par with change-hostname.
  2. Introduce unified remove-tinypilot-lines script
  3. Introduce bats tests
  4. Switch unset-static-ip and change-hostname to delegate to remove-tinypilot-lines.

@jotaen4tinypilot
Copy link
Contributor

One addendum, just for the records: set-static-ip invokes unset-static-ip. So users will effectively only be able to configure one interface at a time, because trying to set a second one would clear any previously set configuration.

So regarding the scenario above:

  1. Assign a static IP to eth0
  2. Assign a static IP to wlan0 ← This clears the assignment from step 1, only leaving wlan0

@mtlynch
Copy link
Contributor Author

mtlynch commented Jan 12, 2024

So users will effectively only be able to configure one interface at a time, because trying to set a second one would clear any previously set configuration.

Ah, I hadn't considered that. Still, I think it's probably a rare scenario to need static IP for both WiFi and Ethernet. If we get requests for that, it shouldn't be too hard for us to revisit the feature and figure out a way to support it later.

jotaen4tinypilot added a commit that referenced this issue Jan 15, 2024
Related #1710.

[As discussed in the
issue](#1710 (comment)),
this PR drops support for the `--interface` flag of the
`unset-static-ip` script. Instead of allowing to selectively unset
interface configurations, the `unset-static-ip` script now only allows
to remove all/any custom IP configs. We didn’t find to have a need for
the selective unsetting, and can therefore spare the complexity.

## Notes

- I noticed we accidentally forgot to parse the `--help` flag, and we
also forgot to document the `--help` and `--quiet` flags in the usage
description. I’ve fixed both.
- I dropped the “Examples” section from the help output, because without
the `--interface` flag the usage seems trivial enough to me.
- Using the `--interface` flag now produces an error, just in case
someone relied on it.
- The code for eliminating the marker sections is now the same as [the
code in the `change-hostname` privileged
script](https://github.com/tiny-pilot/tinypilot/blob/8e99d5a666df12d851b14dd4276e8b95ab958493/debian-pkg/opt/tinypilot-privileged/scripts/change-hostname#L62-L79)
– except for the `dhcpcd_original` variable name. Both instances will
later be replaced by a call to the [upcoming unified
script](#1710).
- Heads-up: this PR will (probably) conflict with
tiny-pilot/tinypilot-pro#1175, as both touch the
tail of the `unset-static-ip` script. It should be trivial to resolve
this, though.
<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1719"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <jan@jotaen.net>
jotaen4tinypilot added a commit that referenced this issue Jan 17, 2024
Related #1710.

This PR adds a unified script to remove TinyPilot marker sections from
files. We will [use this script in a subsequent
PR](#1722) to simplify the
corresponding instances in the
[unset-static-ip](https://github.com/tiny-pilot/tinypilot/blob/2e211199a21a2a6c285c5d1d2ff60eff6afa77d4/debian-pkg/opt/tinypilot-privileged/scripts/unset-static-ip#L64-L81)
and
[change-hostname](https://github.com/tiny-pilot/tinypilot/blob/8e99d5a666df12d851b14dd4276e8b95ab958493/debian-pkg/opt/tinypilot-privileged/scripts/change-hostname#L62-L79)
privileged scripts.

## Notes

- Unless someone feels strongly about the [originally mentioned
`remove-tinypilot-lines` script
name](https://github.com/tiny-pilot/tinypilot/issues/1710#:~:text=opt/tinypilot/scripts/-,remove%2Dtinypilot%2Dlines,-%3C%20example.conf)
(or any other name), I thought that `strip-marker-sections` sounded most
expressive.
- I added a comment to the `lib/markers.sh` file, to explain the marker
idea in a bit more detail.
- There will be automated bash tests for this in [a subsequent
PR](#1721).


<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1720"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <jan@jotaen.net>
jotaen4tinypilot added a commit that referenced this issue Jan 17, 2024
Resolves #1714.
Related #1710.
Stacked onto #1720.

This PR sets up bash tests with
[bats](https://bats-core.readthedocs.io/en/stable/), and adds a first
test suite for [the `strip-marker-sections`
script](#1720).

## Notes
- Similar to how we do it with other test files, I put the `.bats` test
file next to the script under test. The directive in `.dockerignore`
[removes it from the bundle / `.deb`
file](https://github.com/tiny-pilot/tinypilot/assets/83721279/7f52ca3e-17d9-486a-83a5-ea1c6132eb36).
- The `build_bash` dev script is called “build” for consistency with
`build_python` and `build_javascript`, even though we technically don’t
build something. I’ve created
#1716 for us to
potentially reconsider this overall.
- In `build_bash`, it somehow felt reasonable to me to search all places
that are likely to contain `.bats` files, even though we currently only
have them in `/opt/tinypilot-privileged/scripts/`. I don’t feel strongly
about this, though.
- For running the tests, there theoretically would be [an official bats
Docker
image](https://bats-core.readthedocs.io/en/stable/installation.html#running-bats-in-docker),
however that doesn’t play nicely with our bash files: the docker image
doesn’t have a symlink at `/bin/bash`, so it fails to execute any of our
bash scripts that have a `#!/bin/bash` shebang (which is effectively all
of them). We instead would either have to change all our shebangs to the
(most portable) `#!/usr/bin/env bash` shebang, or we just install bats
manually like done here (in the Circle conf).
<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1721"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <jan@jotaen.net>
jotaen4tinypilot added a commit that referenced this issue Jan 17, 2024
Resolves #1710.
Stacked on #1720.

Based on the [simplifications in
`unset-static-ip`](#1719),
this PR removes the code redundancies in the `unset-static-ip` and
`change-hostname` privileged scripts, and calls the new, unified
`strip-marker-sections` script instead.

## Notes

- `change-hostname`:
- Naively speaking, it would seem simpler to *append* the new entry to
the file. I remembered, however, that the prepending was done on
purpose, because in `/etc/hosts` the first matching entry takes
precedence. I captured that as a comment therefore.
- I slightly reordered the code so that all `/etc/hosts`-related logic
is close together now (i.e., clearing the markers, then rewriting the
file).
- `unset-static-ip`
- ~I couldn’t figure out why we have been using `tee` previously to
write to the config file, because we discarded `tee`’s stdout output by
redirecting to `/dev/null`. I’m also not sure why we used `sudo` for
invoking `tee` – [we already execute the script with `root`
privileges](https://github.com/tiny-pilot/tinypilot-pro/blob/cfb99f88a3039d587a9108d427e7ecfa81b7d190/app/static_ip.py#L128).
It’s a bit odd, because I cannot imagine we did that without good
reason~ 🤔 (We [had been using `sudo tee` to avoid permission
issues](#1722 (comment))
when writing the file.)

<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1722"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <jan@jotaen.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request small
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants