Write about Xonsh and VPN.
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user