Write about Xonsh and VPN.

This commit is contained in:
Greg Hellings
2024-07-18 11:24:47 -05:00
parent bad6ddf447
commit c0f222e96b
+243
View File
@@ -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.