Skip to content

Blog

Installing Python on WSL2 (Ubuntu)

Install build tools and required Python libraries

Install build tools and required Python libraries
sudo apt update && sudo apt install -y build-essential git libexpat1-dev libssl-dev zlib1g-dev \
libncurses5-dev libbz2-dev liblzma-dev \
libsqlite3-dev libffi-dev tcl-dev linux-headers-generic libgdbm-dev \
libreadline-dev tk tk-dev

Install pyenv

Install pyenv
curl https://pyenv.run | bash

Update ~/.bashrc, ~/.profile, ~/.bash_profile (read instructions from previous output)

~/.bashrc
...

# Load pyenv automatically by adding
# the following to ~/.bashrc:

export PATH="/home/{USERNAME}/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
~/.profile
# Load pyenv automatically by appending
# the following to
# ~/.bash_profile if it exists, otherwise ~/.profile (for login shells)
# and ~/.bashrc (for interactive shells) :

export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"

Install Python "common" libraries

Install needed version(s) ... can install multiple versions
# list available python versions
# pyenv install --list
pyenv install 3.14.2

Install / Upgrade base libraries for installed Python version(s)

Install / Upgrade base libraries into installed version(s) of Python
pyenv global 3.14.2
pip install pip --upgrade
pip install pipenv

Creating Cloud Image Template on Proxmox

  • Follow commands below for Rocky Linux and Ubuntu cloud images.
  • Once template is created, VMs can be cloned from the template.
1. Preparations
Connect to PVE
ssh pve

List storage pools - we will be using vm_ct
pvesm status
output
Name                        Type     Status     Total (KiB)      Used (KiB) Available (KiB)        %
local                        dir     active        70892712        10550868        56694972   14.88%
local-lvm                lvmthin     active       147714048               0       147714048    0.00%
vm_ct                    lvmthin     active       957100032       471754605       485345426   49.29%

List configured networks - we will be using vmbr0
brctl show
output
bridge name     bridge id               STP enabled     interfaces
fwbr30040i0     8000.7ef38b20b3aa       no              fwln30040i0
                                                        tap30040i0
fwbr30080i0     8000.2ed716ece7b9       no              fwln30080i0
                                                        tap30080i0
vmbr0           8000.705a0f4343e0       no              fwpr30040p0
                                                        fwpr30080p0
                                                        nic0
2. Get Cloud Image
Latest images at https://dl.rockylinux.org/pub/rocky
wget https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2
Latest images at https://cloud-images.ubuntu.com/releases
wget https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-amd64.img
3. Create new VM with 4 GB (4096 MB) RAM on storage vm_ct with vmbr0 bridge
VM ID: 8001, name: rocky-cloud; CPU: host (did not work with default kvm64)
qm create 8001 --memory 4096 --name rocky-cloud --cores 2 --sockets 2 --cpu host --net0 virtio,bridge=vmbr0
VM ID: 8002, name: ubuntu-cloud, CPU: can leave default kvm64
qm create 8002 --memory 4096 --name ubuntu-cloud --cores 2 --sockets 2 --cpu kvm64 --net0 virtio,bridge=vmbr0
4. Import downloaded image to newly created VM
VM ID: 8001, Downloaded disk: Rocky-10-GenericCloud-Base.latest.x86_64.qcow2, Storage: vm_ct
qm importdisk 8001 Rocky-10-GenericCloud-Base.latest.x86_64.qcow2 vm_ct
output
... successfully imported disk 'vm_ct:vm-8001-disk-0'

VM ID: 8002, Downloaded disk: ubuntu-24.04-server-cloudimg-amd64.img, Storage: vm_ct
qm importdisk 8002 ubuntu-24.04-server-cloudimg-amd64.img vm_ct
output
... successfully imported disk 'vm_ct:vm-8002-disk-0'

5. Attach imprted disk to VM
VM ID: 8001, Storage: vm_ct, Disk: vm-8001-disk-0
qm set 8001 --scsihw virtio-scsi-pci --scsi0 vm_ct:vm-8001-disk-0
output
update VM 8001: -scsi0 vm_ct:vm-8001-disk-0 -scsihw virtio-scsi-pci
VM ID: 8002, Storage: vm_ct, Disk: vm-8002-disk-0
qm set 8001 --scsihw virtio-scsi-pci --scsi0 vm_ct:vm-8002-disk-0
output
update VM 8002: -scsi0 vm_ct:vm-8002-disk-0 -scsihw virtio-scsi-pci
6. Attach logical volume cloudinit drive to the new VM
VM ID: 8001, Storage: vm_ct, Disk: cloudinit
qm set 8001 --ide2 vm_ct:cloudinit
output
update VM 8001: -ide2 vm_ct:cloudinit
  Logical volume "vm-8001-cloudinit" created.
  Logical volume vm_ct/vm-8001-cloudinit changed.
ide2: successfully created disk 'vm_ct:vm-8001-cloudinit,media=cdrom'
generating cloud-init ISO
VM ID: 8002, Storage: vm_ct, Disk: cloudinit
qm set 8002 --ide2 vm_ct:cloudinit
output
update VM 8002: -ide2 vm_ct:cloudinit
  Logical volume "vm-8002-cloudinit" created.
  Logical volume vm_ct/vm-8002-cloudinit changed.
ide2: successfully created disk 'vm_ct:vm-8002-cloudinit,media=cdrom'
generating cloud-init ISO
7. Set to Boot from cloudinit
VM ID: 8001
qm set 8001 --boot c --bootdisk scsi0
output
update VM 8001: -boot c -bootdisk scsi0
VM ID: 8002
qm set 8002 --boot c --bootdisk scsi0
output
update VM 8002: -boot c -bootdisk scsi0
8. Enable serial console for use with Web VNC within Proxmox
VM ID: 8001
qm set 8001 --serial0 socket --vga serial0
output
output: update VM 8001: -serial0 socket -vga serial0
VM ID: 8002
qm set 8002 --serial0 socket --vga serial0
output
output: update VM 8002: -serial0 socket -vga serial0
9. Set defaults from Proxmox (PVE) GUI
  • User:
  • Password:
  • DNS domain: (uses host settings by default)
  • DNS servers: (uses host settings by default)
  • SSH public key:
  • IP Config (net0): (make sure to select DHCP for IPv4 or provide Static value)

10. DO NOT START THIS VM to preserve clean machine id, etc.

11. At this point, if needed, Processors and Memory can be changed as needed.

12. Right-Click and Convert to Template

Ms Word Merge Print Preview Problem

Word for Windows with Merge Fields and problem during Print Preview

Word for Windows with Merge Fields and problem during Print Preview

ISSUE:

Document merges normally and shows all the fields. When CTRL-P is pressed for Print Preview, the footer fields are shown as Merge Fields (not replaced).

FIX:

  • Edit Word Template
  • Select all footer fields
  • Press CTRL-F11 to lock them

This seems to be fixing the issue.

MORE INFO:

Word Shortcuts

Ms Excel Column Name to Number Converter

Formula for converting Excel Column Number to Column Name (Column Number specified in B1):

=SUBSTITUTE(ADDRESS(2, B1, 4), "2", "")

Formula for converting Excel Column Name to Column Number (Column Name specified in B2):

=COLUMN(INDIRECT(B2&"1"))

Screenshot

Vim Useful Commands List - 1

Vim Useful Commands List - 1

Command Description
i insert - insert new text before cursor
x delete current character
X backspace delete previous character
i insert - insert new text before cursor
x delete current character
X backspace delete previous character
u undo
Escape change to command mode
o new line below
O new line above
dd cut current line
dw cut current word
p paste before
P paste after
/[search_string] find [search_string]
n next result
N previous result
/( find next opening parentesis
% jump to matching parentesis
ci( change all text inside brackets ()
di( delete all text inside brackets ()
w next word
Ctrl-D move down a page (around 15 lines)
Ctrl-U move up a page (around 15 lines)
:new [filename] open a new [filename] to edit (can {tab} to see list)
Ctrl-w w switch down between windows / buffers
Ctrl-w W switch up between windows / buffers
Ctrl-w N Convert terminal into a "normal mode" buffer
:ls list opened buffers / files opened for edit
:wa write all - save all opened files
:bd buffer delete - to close current buffer
:term opens terminal in separate window
yw yank / copy word
yy yank / copy line

Application specific password google

Create Application specific password for Google

Starting 2022-05-30 Google is no longer supporting less secure apps.

Please use below steps to turn on App password for your google account.

  1. Login to your Google email at https://gmail.google.com.
  2. Navigate to https://myaccount.google.com/signinoptions/two-step-verification.
  3. Enable "two step verification".
  4. Create application-specific password via https://security.google.com/settings/security/apppasswords.
  5. Save a 16 digits App password that can be used by specific app using your Google email address.

Bad Owner Permissions Ssh Config Win

Fix Bad Owner / Permissions for SSH Config in Windows 10

Error:

  • Bad Owner or Permissions on SSH Config

Resolution:

  • Folder should only be accessible to SYSTEM and your logged in user

Steps:

  • Navigate to %USERPROFILE% in Windows Explorer
  • Right-Click on .ssh folder and choose Properties
  • Click on Security tab. It should only show your account and SYSTEM under "Group or user names". If not, here are the steps to take:
  • Click Advanced button
  • Click Disable inheritance button and Convert inherited permissions into explicit ...
  • Select and Remove any Principal in the list EXCEPT FOR your account name and SYSTEM
  • Click OK and then OK on the Properties window
  • You might perform similar steps on the all of the files in %UserProfile%\.ssh folder
  • If you run OpenSSH SSH Server, you might need to restart it in the services.msc

RPi (Raspberry Pi) Headless Dynamic IP Change Notification

Raspberry Pi Headless Server from Scratch

WHY

Since ISP (Internet Service Provider) started to change public IPs at will, I wanted to have sort of a control center network appliance that would monitor things like Public IP changes and possibly other things (like speed tests, checking if certain external sites are up, etc.) utilizing existing, unused, and, probably soon, outdated hardware.

WHAT

  • Raspberry Pi 1 Model B+ Board
  • Clear acrylic case for RPi Model B+
  • 8GB SD card
  • Old iPhone 1A adapter (works fine for Model 1b)
  • Micro USB to USB Cable
  • RJ45 Ethernet cable

HOW

Prepare RPi SD Card
  • Download Raspberry Pi OS Lite (at the time of writing Kernel version 5.00 released on March 4th 2021)
  • Download Balena Etcher or similar software to copy image to SD card (Raspberry Pi Imager did not work for me but was my first choice)
  • Plug SD Card into your Windows / OSX / Linux host machine and copy downloaded image to SD Card
  • Navigate to the SD Card (you might have more than one drive mounted / available under windows) and create an empty file named ssh (this will ensure that SSH server on headless Raspberry Pi (RPi) will start on the first boot)
First Time Setup
  • On windows I have download and installed wonderful Cygwin that provides a large collection of GNU and Open Source tools to provide functionality similar to a Linux distribution on windows. If using Windows 10, you might want to look into much heavier but "out of box" option called WSL2.
  • Unmount SD Card and plug it into RPi (via SD adapter if required), plug in RJ45 for wired internet connection (other side should go to your router), and then plug in Power cable.
  • RPi lights will be lit.
  • Identify an IP address of newly built RPi using either your Router's DHCP list of IPs -- RPi MAC addresses would start with B8-27-EB or use one of the utilities like Adafruit Pi Finder, nmap, Angry IP Scanner (try older version with a single executable ipscan.exe), or just type in something like "ping -4 raspberrypi.local" at the command prompt.
Sample Naming
  • RPi hostname: raspberry ==> headless_horseman
  • RPi IP Address: 192.168.1.123
  • SSH Port: 22 ==> 7522
  • Sample Sudo User / Pass: mysudouser / Pwd.926!
  • Sample SSH User / Pass: mysshuser / T3n.F0ur!
  • SSH Public key file on Host machine: ~/.ssh/id_rsa-headless_horseman.pub
First Time Login
  • Using Cygwin or WSL2 (from windows) or Terminal (on OSX / Linux), connect to RPi (default user "pi" with default password "raspberry"): user@host $ ssh pi@192.168.1.123

  • To enable SSH for the future boots one can use "sudo raspi-config" or via command line: pi@192.168.1.123 $ sudo systemctl enable ssh # enable ssh

  • Change default "pi" password (optionally - user "pi" can be removed in the future if another user is created): pi@192.168.1.123 $ passwd

  • Change default hostname: pi@192.168.1.123 $ sudo vi /etc/hostname

  • To add users:

pi@192.168.1.123 $ sudo adduser mysshuser #add user for non-sudo SSH access

pi@192.168.1.123 $ sudo adduser mysudouser sudo #add user for sudo non-SSH access

pi@192.168.1.123 $ sudo usermod -aG sudo mysudouser #OPTIONAL: add user to sudo group (covered by adduser with 'sudo' group name at the end)

Remove ability for ssh user to browse sudo user folder:

pi@192.168.1.123 $ sudo chmod 0750 /home/mysudouser

  • Modify SSH Config to Allow / deny certain users and change default port (append at the end):

pi@192.168.1.123 $ sudo vi /etc/ssh/sshd_config

Port 7522 # Change Port to 7522 from default 22
AllowUsers mysshuser # allow SSH
DenyUsers mysudouser # sudo user to be denied SSH
PermitRootLogin=no # do not allow root login via ssh
 SSH Server needs to be restarted for changes to take effect. Status can be queried as well:

pi@192.168.1.123 $ sudo systemctl restart ssh

pi@192.168.1.123 $ sudo systemctl status ssh

Setup Host machine for Passwordless SSH login
  • In terminal in the host machine (cygwin, wsl2, terminal, etc.) key needs to be available or generated (accept default empty value for passphrase by pressing ENTER):

user@host $ mkdir -p ~/.ssh && cd ~/.ssh

user@host $ ssh-keygen -f id_rsa-headless_horseman

  • Copy public key to RPi (target is ~/.ssh/authorized_keys):

user@host $ ssh-copy-id -i ~/.ssh/id_rsa-headless_horseman -p 7522 mysshuser@192.168.1.123

  • Create configuration file for SSH to avoid supplying parameters every time:

user@host $ vi ~/.ssh/config

Host headless_horseman
  HostName 192.168.1.123
  User mysshuser
  Port 7522
  IdentityFile ~/.ssh/id_rsa-headless_horseman
Connect to RPi:

user@host $ ssh headless_horseman

mysshuser@headless_horseman $ su - mysudouser

Delete pi user

mysudouser@headless_horseman $ sudo userdel -r pi

Create scripts folder:

mysudouser@headless_horseman $ mkdir -p ~/scripts

Install fail2ban

mysudouser@headless_horseman $ sudo apt-get update

mysudouser@headless_horseman $ sudo apt-get -y

install fail2ban

mysudouser@headless_horseman $ sudo vi /etc/fail2ban/jail.local

[ssh]
enabled = true
port = 7522
filter = sshd
logpath = /var/log/auth.log
bantime = 900
banaction = iptables-allports
findtime = 900
maxretry = 3

mysudouser@headless_horseman $ sudo systemctl restart fail2ban

mysudouser@headless_horseman $ sudo iptables -L -n --line #check banned addresses

mysudouser@headless_horseman $ sudo iptables -D fail2ban-ssh 1 #unban line number 1

mysudouser@headless_horseman $ ln -s -T /var/log/fail2ban.log ~/scripts/fail2ban.log

Modify prompt and ls alias

mysudouser@headless_horseman $ vi ~/.profile

export PS1="\[\033[0;33m\][\D{%Y-%m-%d}][\t]\[\033[0;36m\] [\u\[\033[0;37m\]@\[\033[0;36m\]\h:\[\033[0;32m\]\w]\[\033[0m\] \n$ "
alias ls='ls -laFh --color=auto'

Script: update.sh

mysudouser@headless_horseman $ vi ~/scripts/update.sh

#!/bin/bash
sudo apt update;
sudo apt -y upgrade;
sudo apt -y autoclean;
sudo apt -y autoremove;
sudo shutdown -r

mysudouser@headless_horseman $ chmod +x ~/scripts/update.sh

mysudouser@headless_horseman $ sudo crontab -e

30 23  *  * WED /bin/bash /home/mysudouser/scripts/update.sh
Script: myutil.py

mysudouser@headless_horseman $ vi ~/scripts/myutil.py && chmod +x ~/scripts/myutil.py

#!/usr/bin/env python3

def getip(networkCardName):
    import subprocess
    return subprocess.getoutput("ip addr show " + networkCardName + " | grep -Po 'inet \K[\d.]+'")

def getips():
    import re
    import subprocess
    # get ip(s) from ifconfig
    found_ips = []
    ips = re.findall( r'[0-9]+(?:\.[0-9]+){3}', subprocess.getoutput("/sbin/ifconfig"))
    for ip in ips:
        if ip.startswith("255") or ip.startswith("127") or ip.endswith("255"):
            continue
        found_ips.append(ip)
    return ", ".join(found_ips)

def emailsend(subjectPart, messagePart):
    import time
    import smtplib
    import socket

    from datetime import datetime
    current_utc = datetime.utcnow().isoformat() + 'Z'

    ####--[CONFIGURATION]
    server = 'smtp.gmail.com'
    server_port = '587'
    username = 'MYEMAIL@gmail.com'
    password = 'MYGMAILPA$$'

    fromaddr = 'DEVOPS <MYEMAIL@gmail.com>'
    toaddr = 'TARGET_EMAIL@domain.com'
    subject = 'DEVOPS | ' + socket.gethostname() + ' | ' + subjectPart
    message = 'DEVOPS | ' + socket.gethostname() + ' | <br>' + messagePart + '<br><br><br>Email Generated On: ' + current_utc
    ####--[/CONFIGURATION]
    headers = [
        "Subject: " + subject,
        "From: " + fromaddr,
        "To: " + toaddr,
        "MIME-Version: 1.0",
        "Content-Type: text/html"
        ]
    headers = "\r\n".join(headers)
    server = smtplib.SMTP(server + ":" + server_port)
    server.ehlo()
    server.starttls()
    server.ehlo()
    server.login(username,password)
    server.sendmail(fromaddr, toaddr, headers + "\r\n\r\n" + message)
    server.quit()

def get_script_path():
    import os
    import sys
    return os.path.dirname(os.path.realpath(sys.argv[0]))
Script: ipcheck.py

mysudouser@headless_horseman $ vi ~/scripts/ipcheck.py && chmod +x ~/scripts/ipcheck.py

#!/usr/bin/env python3

import json, os
import urllib.request
import myutil
from datetime import date

today = date.today()

formatted_date = today.strftime("%Y%m%d")

script_dir = myutil.get_script_path()

ipcheck_url = "https://mydomain.com/ipcheck.php"
ipcheck_log = script_dir + "/ipcheck_changes-py.log"
lastip_filename = script_dir + "/ipcheck_lastip-py.txt"
lastip = "unknown"

if os.path.isfile(lastip_filename):
    with open(lastip_filename, 'r') as f:
        lastip = f.read()

data = json.loads(urllib.request.urlopen(ipcheck_url).read())
currentip = data["ip"]

if lastip != currentip:
    with open(lastip_filename, 'w') as f:
        f.write(currentip)
    with open(ipcheck_log, 'a') as f:
        f.write(formatted_date + " " + currentip + '\n')
    subject = "Public IP address change"
    message = "Old IP: " + lastip + " / New IP: " + currentip
    myutil.emailsend(subject, message)
Create a script on external web site (mydomain.com) named ipcheck.php as follows
<?php
header('Content-Type: application/json');
$ip=$_SERVER["REMOTE_ADDR"];
$arr=array( "ip" => $ip );
echo json_encode($arr);
?>

mysudouser@headless_horseman $ sudo crontab -e

*/1  *  *  *   * /usr/bin/python3 /home/mysudouser/scripts/ipcheck.py
Script: i_am_alive_email.py

mysudouser@headless_horseman $ vi ~/scripts/i_am_alive_email.py && chmod +x ~/scripts/i_am_alive_email.py

#!/usr/bin/env python3
import time
import myutil
# wait for interface to initialize before parsing ifconfig
time.sleep(1)
subject = "IP address(es)"
# ifconfig will show networkCardName
networkCardName = "eth0"
message = myutil.getip(networkCardName)
myutil.emailsend(subject, message)

mysudouser@headless_horseman $ sudo crontab -e

33 03  *  *   * /usr/bin/python3 /home/mysudouser/scripts/i_am_alive_email.py