diff --git a/content/posts/xonsh-vpn.md b/content/posts/xonsh-vpn.md new file mode 100644 index 0000000..5f1a366 --- /dev/null +++ b/content/posts/xonsh-vpn.md @@ -0,0 +1,243 @@ +--- +title: "Xonsh Vpn" +date: 2024-07-18T10:11:22-05:00 +draft: false +--- + +I know picking a shell in Unix is, by no means, a simple thing. People have very strong +opinions about the shells that they want to use. Beyond using their favorite terminal program, +the shell a developer, engineer, or administrator uses is something that is a highly personal +choice. Most of us who have been working with a particular shell for a while have +probably developed deeply ingrained habits and personalization choices. + +Personally, I have never really become deeply enamored with highly customized prompts, +status lines, or such. My prompt is a slightly modified version of the standard Ubuntu +prompt with green and blue colors for unprivileged users and red for when I am running +with root privileges. I keep four main pieces of information in my prompt: username, +current working directory, and (if applicable) current git branch and Python virtualenv. +For my professional work I do a significant amount of writing Python code, so adding in +the Python virtualenv information is a nod to that bit which is often relevant to my +work. + +## My Shell Choice: Xonsh + +However, being that I am very comfortable with Python, I have made a choice to go away +from the standard POSIX shells for my preferred choice. Of course the most popular +Linux shell choices out there are Bash, zsh, and fish. However, a few years ago I +struck on [xonsh](https://xon.sh). The name is pronounced similarly to the conch shell +of ocean creature fame, which inspires Xonsh's logo and branding. Yes, it is a young and +relatively new addititon to the Shell world, but I have quickly grown to appreciate it. + +Bowing to the power of POSIX shells, Xonsh supports a wide variety of sh/Bash style +syntax. It supports pipes, redirects, and sub-shell captures (that is, the command style +of `foo=$(cat bar.txt)` to capture the output of a subcommand) using the same basic +syntax as the standard shells you might be used to. You can also reference environment +variables using mostly the same syntax such as `$PATH` to fetch its values. + +Where Xonsh deviates from other shells is that it uses Python syntax for all of its +control structures and for more advanced features. In fact, and this is something that +can take a while to remember when interacting with it, Xonsh syntax is a strict superset +of Python! So anything that is valid Python is valid Xonsh, with a few nice features +added on that make the shell experience more friendly. + +### Examples: Shell Escaping + +For some things, the Python syntax in the shell can be very handy if you don't mind +the feature and are comfortable with Python. One of my favorite shortcuts that the +syntax of Xonsh allows me is simplifying the pesky "shell expansion" behavior in +Bash. Take for example this basic Bash script where you need to use quotes to avoid +shell problems: + +```bash +name="Greg Hellings" +login_script --name "${name}" +``` + +With Xonsh, I do not need to quote the value when I refer to it. Xonsh will pass the value +of the variable as a single value into the command. Note that I do have to use a different +syntax to refer to the variable, because it is a local variable and not an environment +variable. + +```xonsh +name="Greg Hellings" +login_script --name @(name) +``` + +### Examples: Arrays and Loops + +Similarly to above, when using a list or array, I do not need to worry about spaces and +values being interpreted incorrectly because Xonsh uses standard Python primitives for +this. Similarly for appending to a list, popping from a list, etc. Xonsh handles all the +spaces and so forth. + +```xonsh +mylist=["Greg Hellings", "Bugs Bunny", "Captain Kirk"] +mylist.append("George Washington") +for name in mylist: + login_script --name @(name) +``` + +There you can see that my looping must be handled with standard Python syntax, but +I do not have issues with lists that contain spaces, and I don't have issues with +a regular variable that has spaces being interpreted as a list, etc. + +Please know that I am not saying that these things cannot be addressed in Bash or similar +shells. I am, however, saying that in Xonsh these things are very straightforward and +are handled by the Python portion of the syntax. + +### Examples: Aliases and Functions + +Because Xonsh uses a full Python syntax and all valid Python is also valid Xonsh, I +can write or call any Python code I want directly from my Xonsh RC file or on the +command line. Xonsh uses this to enhance the idea of alias commands. You can use +regular single-line alias commands like you are probably used to but with a slightly +different syntax. In my xonshrc file I have numerous such simple aliases that look +something like `aliases['work'] = 'cd ~/work'`. And these work exactly like you +might expect. + +But then I also have some slightly more complicated ones. I use the following alias +on my NixOS and MacOS systems so I can update my Nix environment without having to +remember the exact command for each one: + +```xonsh +def _rebuild(args): + from os import uname + system = uname() + if system.sysname == 'Darwin': + darwin-rebuild --flake ~/.config/darwin switch + else: + sudo nixos-rebuild switch +aliases['rebuild'] = _rebuild +``` + +Here you can see how a more copmlex alias can be created. These are similar to the +bash `function` and are, really, just a Python function. I could call them directly +with `_rebuild("blah")`, but it's probably better for me to call them the way +that Xonsh expects... as if they were shell commands. In this case I can just +referenc `rebuild` at my prompt, and it will run the Xonsh function listed. + +Hopefully you can also see that the Python code lives perfectly happily with the +Xonsh shell commands. Only the lines with the `darwin-rebuild` and `nixos-rebuild` +commands in the above function are Xonsh specific. Everything else is just plain +Python. + +### Examples: Aliases with arguments + +In the above example the function `_rebuild` accepts an argument `args`. This is +required for any of these more complex aliases. `args` will be an array of any +arguments passed to the alias on the command line. Here is an example of an alias +where I accept an argument: + +```xonsh +def _unknown_host(args): + sed -i -e @(args[0])d ~/.ssh/known_hosts +aliases['unknown_host'] +``` + +This alias allows me to selectively delete any specific line from my known hosts +file when its SSH key has changed. As you see, I use the standard Xonsh syntax +to reference a Python variable, then invoke the 0th element of the array. Will +this result in a problem if I don't pass one to the alias? Yes. Could I use +something more heavy handed like argparse to parse the list of arguments? Also +yes. Do I want to go through all of that pain and misery for a simple shell +alias? Nope! Just let the Python line error and then I'll hopefully do it right + the next time. + + ### Examples: Aliases with function calls + + Like anything else in Python or other languages, I can write and call functions + on my command line or from aliases. So here's an example where I do that. In this + example, I have a function that ensures I have unlocked my BitWarden CLI. Then + another function will log into a given VPN using the password and OTP stored + in BitWarden. And finally my shell alias calls the VPN login function with + arguments to specifically log into a particular VPN with a particular set of + crecentials. + + ```xonsh + def bw_unlock(): + if "BW_SESSION" in ${...}: # This checks if the BW_SESSION environment variable is set, using Xonsh's env dictionary + return $BW_SESSION # Return the variable value of the token + result = $(bw unlock) # Sub-shell syntax to capture the stdout of the command + while "BW_SESSION" not in result: + result = $(bw unlock) # Loop until I don't fat-finger my master password + lines = result.split("\n") + l = [k for k in lines if 'BW_SESSION="' in k][0] # Find the first line where the session key is output + left, right = l.split("=", 1) # The key can include this charater, so only split on the first one + token = right[1:-1] # The output of bwunlock wraps the value in quotation marks + $BW_SESSION = token # When Xonsh sees a variable assignment that begins with $, it sets that variable as an environment variable automatically + return token + +def vpn(con, bwname): + bw_unlock() + base=$(bw get password @(bwname)) + secret=$(bw get totp @(bwname)) + + from tempfile import NamedTemporaryFile + # delete_on_close is a new option in Python 3.12 + with NameTemporaryFile(delete_on_close=False) as fp: + secret = f"vpn.secrets.password:{base}{secret}" + fp.write(secret.encode("utf-8")) + fp.close() + nmcli c up @(con) passwd-file @(fp.name) + +def _workvpn(args): + vpn("Work VPN", "Work Account") +aliases['workvpn'] = _workvpn +``` + +Now, from my shell, I can just invoke the command `workvpn` and my system will +automatically log into my work VPN. If I have not previously unlocked my BitWarden +first, it will ask me to unlock that. Hopefully, with the extra comments I have +added to the code above, you can see how seamlessly Xonsh and Python are interacting +here and how quickly and smoothly I can move between them. + +### Example: Probably don't do this - but you can! + +Here is a fun little alias that I use sometimes. More rarely since I moved to +Nix where I basically already have everything that I could want but I still +occasionally use it on other systems: + +```xonsh +from pathlib import Path +from os import getcwd +def _rundock(args): + if Path('/usr/bin/podman').exists(): + e = 'podman' + else: + e = 'docker' + @(e) exec -ti @(args[0]) /bin/bash +aliases['rundock'] = _rundock + +def _newdock(args): + if Path('/usr/bin/podman').exists() + e = 'podmand' + else: + e = 'docker' + @(e) run -P --privileged=true -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -v @(getcwd()):/dmnt -v /etc/pki:/etc/pki:ro -d --name @(args[1]) @(args[0]) /sbin/iniu + rundock @(args[1]) +aliases['newdock'] = _newdock +``` + +The alias `newdock debian:12 deb` will create a new OCI container running in +Podamn, if it is installed locally, or Docker otherwise. This container will be +named `deb` and will be created from the `debian:12` image. I use volume mounting +to pull in a few values that I want - namely the X11 socket and to mount the current +directory under the folder `/dmnt`. The newdock function then calls the rundock +function which puts the user into a bash shell in the running container. + +Obviously there are quite a few assumptions being made here, and it definitely does +not work all the time. But I have frequently used these commands when I want to check +on if something works in a given environment or otherwise quickly jump myself into +a container. I used this before tools like Distrobox or Toolbox standardized the way +that these alternatives were made available. I keep it around, though, because +I like to sometimes remind myself that even the command portion of a line of Xonsh +can be dynamic just like it can in other shells. + +## Caveats + +While it is true that nothing you can do in Xonsh you can't do in raw Python, +the shell-friendly syntax sure that Xonsh brings to the Python standard library +makes the whole process so much smoother than invoking subcommand processes in +raw Python. Now, I'm not advocating you use Xonsh for developing whole applications. +But I am saying that you could easily do so without losing any of the power of +Python while simultaneously grabbing easier access to shell calls and subprocesses.