244 lines
11 KiB
Markdown
244 lines
11 KiB
Markdown
---
|
|
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.
|