commit 765f59831360dbdfdce1cd34844dade233ef943739ac9f6c7e90c6ff1bbd4f33 Author: Bryan Roessler Date: Thu Dec 4 23:23:42 2025 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac07246 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ansible +testing/ \ No newline at end of file diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..1f0632a --- /dev/null +++ b/bootstrap @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Bootstrap Ansible environment +# Usage: ./bootstrap [--force] + +pip install --upgrade pip "$@" +pip install --upgrade --requirement pip-requirements "$@" +ansible-galaxy install --role-file galaxy-requirements.yml "$@" \ No newline at end of file diff --git a/deploy-vm b/deploy-vm new file mode 100755 index 0000000..c85ff9e --- /dev/null +++ b/deploy-vm @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# ansible-playbook -i inventories/remote-hosts.yml playbooks/deploy.yml -l workstation --ask-become-pass + +ansible-playbook -i inventories/remotehosts playbook.yml -l testing --ask-vault-pass --ask-become-pass --diff "$@" diff --git a/deploy-workstation b/deploy-workstation new file mode 100755 index 0000000..0ad1823 --- /dev/null +++ b/deploy-workstation @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +# ansible-playbook -i inventories/localhosts playbook.yml -l workstation --ask-vault-pass --ask-become-pass --check --diff + +ansible-playbook -i inventories/localhosts playbook.yml -l workstation --ask-vault-pass --ask-become-pass --diff "$@" \ No newline at end of file diff --git a/deploy.code-workspace b/deploy.code-workspace new file mode 100644 index 0000000..4534fc1 --- /dev/null +++ b/deploy.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "ansible.python.interpreterPath": "/home/bryan/.virtualenvs/deploy/bin/python" + } +} \ No newline at end of file diff --git a/dotfiles/common/.config/aichat/config.yaml.j2 b/dotfiles/common/.config/aichat/config.yaml.j2 new file mode 100644 index 0000000..9e97c8f --- /dev/null +++ b/dotfiles/common/.config/aichat/config.yaml.j2 @@ -0,0 +1,7 @@ +# see https://github.com/sigoden/aichat/blob/main/config.example.yaml + +model: claude:claude-haiku-4-5-20251001 + +clients: + - type: claude + api_key: {{ ANTHROPIC_API_KEY }} diff --git a/dotfiles/common/.env.j2 b/dotfiles/common/.env.j2 new file mode 100644 index 0000000..3a30b4f --- /dev/null +++ b/dotfiles/common/.env.j2 @@ -0,0 +1,2 @@ +OPENAI_API_KEY="{{ OPENAI_API_KEY }}" +ANTHROPIC_API_KEY="{{ ANTHROPIC_API_KEY }}" \ No newline at end of file diff --git a/dotfiles/common/.gitconfig b/dotfiles/common/.gitconfig new file mode 100644 index 0000000..a4f2ac7 --- /dev/null +++ b/dotfiles/common/.gitconfig @@ -0,0 +1,10 @@ +# This is Git's per-user configuration file. +[user] + email = bryanroessler@gmail.com + name = Bryan Roessler +[color] + ui = auto +[credential] + helper = cache +[init] + defaultBranch = main diff --git a/dotfiles/common/.local/share/nautilus/scripts/share-link b/dotfiles/common/.local/share/nautilus/scripts/share-link new file mode 100755 index 0000000..e423bd7 --- /dev/null +++ b/dotfiles/common/.local/share/nautilus/scripts/share-link @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Nautilus script for creating one or more shared links +# Requires wl-clipboard and notify-send + +ssh_server="bryanroessler.com" +ssh_files_path="/var/www/repos.bryanroessler.com/files" +www_files_path="https://repos.bryanroessler.com/files" + +if [[ "$#" -lt 1 ]]; then + echo "You must provide at least one argument" + exit 1 +fi + +hash wl-copy &>/dev/null || { echo "Please install wl-copy"; exit 1; } +hash rsync &>/dev/null || { echo "Please install rsync"; exit 1; } + +if [[ -v NAUTILUS_SCRIPT_SELECTED_URIS ]]; then + readarray -t files <<< "$NAUTILUS_SCRIPT_SELECTED_URIS" + for f in "${files[@]}"; do + f="${f#file://}" + f="${f//\%20/ }" + fixed_files+=("$f") + done +else + fixed_files=("$@") +fi + +links_array=() +for f in "${fixed_files[@]}"; do + [[ "$f" == "" ]] && continue + fname="${f##*/}" + random64=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1) + nohup rsync -a "$f" "${ssh_server}:${ssh_files_path}/${random64}/" & + links_array+=("$www_files_path/${random64}/${fname// /%20}") +done + +if [[ "${#links_array[@]}" == 1 ]]; then + printf '%s' "${links_array[@]}" | wl-copy +else + printf '%s\n' "${links_array[@]}" | wl-copy +fi + +hash notify-send &>/dev/null && + notify-send -t 3000 -i face-smile "share-link" "File(s) uploaded and link copied to clipboard" + +exit 0 \ No newline at end of file diff --git a/dotfiles/common/.ssh/authorized_keys.j2 b/dotfiles/common/.ssh/authorized_keys.j2 new file mode 100644 index 0000000..958e1aa --- /dev/null +++ b/dotfiles/common/.ssh/authorized_keys.j2 @@ -0,0 +1,3 @@ +{% for key in SSH_AUTHORIZED_KEYS %} +{{ key }} +{% endfor %} \ No newline at end of file diff --git a/dotfiles/common/.ssh/config b/dotfiles/common/.ssh/config new file mode 100644 index 0000000..3179679 --- /dev/null +++ b/dotfiles/common/.ssh/config @@ -0,0 +1,42 @@ +HashKnownHosts yes +AddressFamily inet + +Host * + StrictHostKeyChecking no + +Host uv.asc.edu + User uabbcr + ProxyJump roessler@hartmanlab.genetics.uab.edu + +Host router + Hostname router.lan + User root + ServerAliveCountMax 10 + ServerAliveInterval 120 + +Host home-router + Hostname home-router + User root + +Host ax6000 + Hostname ax6000.lan + User root + +Host bryanroessler.com + Hostname 23.94.201.46 + +Host hartmanlab + Hostname hartmanlab.genetics.uab.edu + User roessler + +Host workstation + Hostname workstation + +Host laptop + Hostname laptop + +Host vm-fedora42 + Hostname vm-fedora42 + +Host vm-alma9 + Hostname 192.168.122.235 diff --git a/dotfiles/common/.tmux.conf b/dotfiles/common/.tmux.conf new file mode 100644 index 0000000..bc00764 --- /dev/null +++ b/dotfiles/common/.tmux.conf @@ -0,0 +1,32 @@ +# Synchronize windows shortcut +unbind a +bind a set-window-option synchronize-panes + +# Use | and - to split a window vertically and horizontally instead of " and % respoectively +unbind '"' +unbind % +bind | split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" + +# Automatically set window title +set-window-option -g automatic-rename on +set-option -g set-titles on + +# Alias (Ctrl-b + k) to kill the current session +bind-key k kill-session +unbind r +bind r \ + source-file ~/.tmux.conf \;\ + display 'Reloaded tmux config' + +# Set the history limit so we get lots of scrollback. +setw -g history-limit 50000000 + +# switch panes using Alt-arrow without prefix +bind -n M-Left select-pane -L +bind -n M-Right select-pane -R +bind -n M-Up select-pane -U +bind -n M-Down select-pane -D + +# Enable mouse mode +set -g mouse on diff --git a/dotfiles/common/.vimrc b/dotfiles/common/.vimrc new file mode 100644 index 0000000..248ae36 --- /dev/null +++ b/dotfiles/common/.vimrc @@ -0,0 +1,169 @@ +" File: .vimrc +" Author: Jake Zimmerman +" +" How I configure Vim :P +" + +" Gotta be first +set nocompatible + +filetype off + +set rtp+=~/.vim/bundle/Vundle.vim +call vundle#begin() + +Plugin 'VundleVim/Vundle.vim' + +" ----- Making Vim look good ------------------------------------------ +Plugin 'altercation/vim-colors-solarized' +Plugin 'tomasr/molokai' +Plugin 'vim-airline/vim-airline' +Plugin 'vim-airline/vim-airline-themes' + +" ----- Vim as a programmer's text editor ----------------------------- +Plugin 'scrooloose/nerdtree' +Plugin 'jistr/vim-nerdtree-tabs' +Plugin 'vim-syntastic/syntastic' +Plugin 'xolox/vim-misc' +Plugin 'xolox/vim-easytags' +Plugin 'majutsushi/tagbar' +Plugin 'ctrlpvim/ctrlp.vim' +Plugin 'vim-scripts/a.vim' + +" ----- Working with Git ---------------------------------------------- +Plugin 'airblade/vim-gitgutter' +Plugin 'tpope/vim-fugitive' + +" ----- Other text editing features ----------------------------------- +Plugin 'Raimondi/delimitMate' + +" ----- man pages, tmux ----------------------------------------------- +Plugin 'jez/vim-superman' +Plugin 'christoomey/vim-tmux-navigator' + +" ----- Syntax plugins ------------------------------------------------ +Plugin 'jez/vim-c0' +Plugin 'jez/vim-ispc' +Plugin 'kchmck/vim-coffee-script' + +" ---- Extras/Advanced plugins ---------------------------------------- +" Highlight and strip trailing whitespace +"Plugin 'ntpeters/vim-better-whitespace' +" Easily surround chunks of text +"Plugin 'tpope/vim-surround' +" Align CSV files at commas, align Markdown tables, and more +"Plugin 'godlygeek/tabular' +" Automaticall insert the closing HTML tag +"Plugin 'HTML-AutoCloseTag' +" Make tmux look like vim-airline (read README for extra instructions) +"Plugin 'edkolev/tmuxline.vim' +" All the other syntax plugins I use +"Plugin 'ekalinin/Dockerfile.vim' +"Plugin 'digitaltoad/vim-jade' +"Plugin 'tpope/vim-liquid' +"Plugin 'cakebaker/scss-syntax.vim' + +call vundle#end() + +filetype plugin indent on + +" --- General settings --- +set backspace=indent,eol,start +set ruler +set number +set showcmd +set incsearch +set hlsearch + +syntax on + +set mouse=a + +" We need this for plugins like Syntastic and vim-gitgutter which put symbols +" in the sign column. +hi clear SignColumn + +" ----- Plugin-Specific Settings -------------------------------------- + +" ----- altercation/vim-colors-solarized settings ----- +" Toggle this to "light" for light colorscheme +set background=dark + +" Uncomment the next line if your terminal is not configured for solarized +let g:solarized_termcolors=256 + +" Set the colorscheme +colorscheme solarized + + +" ----- bling/vim-airline settings ----- +" Always show statusbar +set laststatus=2 + +" Fancy arrow symbols, requires a patched font +" To install a patched font, run over to +" https://github.com/abertsch/Menlo-for-Powerline +" download all the .ttf files, double-click on them and click "Install" +" Finally, uncomment the next line +"let g:airline_powerline_fonts = 1 + +" Show PASTE if in paste mode +let g:airline_detect_paste=1 + +" Show airline for tabs too +let g:airline#extensions#tabline#enabled = 1 + +" Use the solarized theme for the Airline status bar +let g:airline_theme='solarized' + +" ----- jistr/vim-nerdtree-tabs ----- +" Open/close NERDTree Tabs with \t +nmap t :NERDTreeTabsToggle +" To have NERDTree always open on startup +let g:nerdtree_tabs_open_on_console_startup = 1 + +" ----- scrooloose/syntastic settings ----- +let g:syntastic_error_symbol = '✘' +let g:syntastic_warning_symbol = "▲" +augroup mySyntastic + au! + au FileType tex let b:syntastic_mode = "passive" +augroup END + + +" ----- xolox/vim-easytags settings ----- +" Where to look for tags files +set tags=./tags;,~/.vimtags +" Sensible defaults +let g:easytags_events = ['BufReadPost', 'BufWritePost'] +let g:easytags_async = 1 +let g:easytags_dynamic_files = 2 +let g:easytags_resolve_links = 1 +let g:easytags_suppress_ctags_warning = 1 + +" ----- majutsushi/tagbar settings ----- +" Open/close tagbar with \b +nmap b :TagbarToggle +" Uncomment to open tagbar automatically whenever possible +"autocmd BufEnter * nested :call tagbar#autoopen(0) + + +" ----- airblade/vim-gitgutter settings ----- +" In vim-airline, only display "hunks" if the diff is non-zero +let g:airline#extensions#hunks#non_zero_only = 1 + + +" ----- Raimondi/delimitMate settings ----- +let delimitMate_expand_cr = 1 +augroup mydelimitMate + au! + au FileType markdown let b:delimitMate_nesting_quotes = ["`"] + au FileType tex let b:delimitMate_quotes = "" + au FileType tex let b:delimitMate_matchpairs = "(:),[:],{:},`:'" + au FileType python let b:delimitMate_nesting_quotes = ['"', "'"] +augroup END + +" ----- jez/vim-superman settings ----- +" better man page support +noremap K :SuperMan + diff --git a/dotfiles/common/.zshrc b/dotfiles/common/.zshrc new file mode 100644 index 0000000..a7a58db --- /dev/null +++ b/dotfiles/common/.zshrc @@ -0,0 +1,143 @@ +# Shell options +setopt autocd menucomplete correct globdots extendedglob nomatch notify \ + share_history inc_append_history hist_expire_dups_first hist_reduce_blanks \ + hist_find_no_dups hist_verify extended_history auto_pushd pushd_ignore_dups \ + prompt_subst +unsetopt beep +bindkey -e + +# Load secrets +if [[ -f $HOME/.env ]]; then + set -a # automatically export all variables + source "$HOME/.env" + set +a +fi + +# Completions +local compdump=${XDG_CACHE_HOME:-$HOME/.cache}/zsh/zcompdump-${HOST}-${ZSH_VERSION} +[[ -d ${compdump:h} ]] || mkdir -p ${compdump:h} +zstyle ':completion:*' menu select +zstyle ':completion:*' gain-privileges 1 +zstyle ':completion:*:descriptions' format '%U%B%d%b%u' +zmodload zsh/complist +autoload -Uz compinit && compinit -d "$compdump" + +# History +HISTFILE=${XDG_STATE_HOME:-$HOME}/.histfile +[[ -d $HISTFILE:h ]] || mkdir -p $HISTFILE:h +HISTSIZE=1000000 +SAVEHIST=1000000 +autoload -Uz up-line-or-beginning-search down-line-or-beginning-search +zle -N up-line-or-beginning-search +zle -N down-line-or-beginning-search + +# Colors +autoload -Uz colors && colors + +# Prompt +if [[ $EUID -eq 0 ]]; then + user_color=red +else + user_color=white +fi + +# Assign colors based on the hostname +# https://www.ditig.com/256-colors-cheat-sheet +if [[ -v TOOLBOX_PATH ]]; then + host_color=magenta +elif [[ -v DISTROBOX_ENTER_PATH ]]; then + host_color=15 +else + case $HOSTNAME in + laptop) host_color=green ;; + workstation) host_color=red ;; + bryan-pc) host_color=cyan ;; + time4vps) host_color=blue ;; + racknerd) host_color=yellow ;; + htpc) host_color=214 ;; + hartmanlab) host_color=magenta ;; + router) host_color=blue ;; + ax6000) host_color=87 ;; + home-router) host_color=218 ;; + vm-fedora*) host_color=57 ;; + vm-alma*) host_color=214 ;; + *) host_color=white ;; + esac +fi + +_git_prompt() { + local br + if br=$(git symbolic-ref --short HEAD 2>/dev/null); then + print -n " %F{242}($br)%f" + fi +} + +PROMPT='[%F{'$user_color'}%n%f@%F{'$host_color'}%m%f]%~$(_git_prompt)%(!.#.$) ' +# RPROMPT='%*' # display clock to right of screen +precmd() { print -Pn "\e]0;%n@%m: ${PWD/#$HOME/~}\a" ; } + +# Set hostname on OpenWRT +[[ -z $HOSTNAME ]] && HOSTNAME=$(noglob uci get system.@system[0].hostname 2>/dev/null) + +# Paths +typeset -U path PATH +path=( + $HOME/bin + $HOME/.local/bin + $HOME/documents/develop/scripts/shell/.local/bin + $HOME/.cargo/bin + $path +) +export PATH +export R_LIBS_USER="$HOME/R/qhtcp-workflow" + +# Keybindings +typeset -g -A key +for k v in \ + Home khome End kend Insert kich1 Backspace kbs Delete kdch1 \ + Up kcuu1 Down kcud1 Left kcub1 Right kcuf1 PageUp kpp PageDown knp ShiftTab kcbt; do + [[ -n ${terminfo[$v]} ]] && key[$k]=${terminfo[$v]} +done +bindkey -- ${key[Home]-} beginning-of-line +bindkey -- ${key[End]-} end-of-line +bindkey -- ${key[Insert]-} overwrite-mode +bindkey -- ${key[Backspace]-} backward-delete-char +bindkey -- ${key[Delete]-} delete-char +bindkey -- ${key[Left]-} backward-char +bindkey -- ${key[Right]-} forward-char +bindkey -- ${key[PageUp]-} beginning-of-buffer-or-history +bindkey -- ${key[PageDown]-} end-of-buffer-or-history +bindkey -- ${key[ShiftTab]-} reverse-menu-complete +bindkey -- ${key[Up]-} up-line-or-beginning-search +bindkey -- ${key[Down]-} down-line-or-beginning-search + +if (( ${+terminfo[smkx]} && ${+terminfo[rmkx]} )); then + autoload -Uz add-zle-hook-widget + zle_app_start() { echoti smkx; } + zle_app_finish() { echoti rmkx; } + add-zle-hook-widget zle-line-init zle_app_start + add-zle-hook-widget zle-line-finish zle_app_finish +fi + +# Aliases and one-liners +alias ll='ls -lh' +alias la='ls -A' +alias vmd='vmd -nt' +alias dnf-list-files='dnf repoquery -l' +alias gedit='gnome-text-editor' +alias xclip='xclip -selection c' +alias pdoman='podman' +alias git-list='git ls-tree -r HEAD --name-only' +alias chatgpt='aichat' +alias chromium='chromium --disable-features=GlobalShortcutsPortal' + +podman-pull-all() { + for image in $(podman images --format "{{.Repository}}:{{.Tag}}"); do + podman pull "$image" + done +} + +buildah-prune() { buildah rm --all; } + +export EDITOR="code --wait" + diff --git a/dotfiles/laptop/.config/btrbk/btrbk.conf b/dotfiles/laptop/.config/btrbk/btrbk.conf new file mode 100644 index 0000000..988b57f --- /dev/null +++ b/dotfiles/laptop/.config/btrbk/btrbk.conf @@ -0,0 +1,176 @@ +# +# Example btrbk configuration file +# +# +# Please refer to the btrbk.conf(5) man-page for a complete +# description of all configuration options. +# +# Note that the options can be overridden per volume/subvolume/target +# in the corresponding sections. +# +# Enable transaction log +transaction_log /var/log/btrbk.log + +# Enable stream buffer. Adding a buffer between the sending and +# receiving side is generally a good idea. +# NOTE: If enabled, make sure the "mbuffer" package is installed on +# the target host! +#stream_buffer 512m + +# Directory in which the btrfs snapshots are created. Relative to +# of the volume section. +# If not set, the snapshots are created in . +# +# If you want to set a custom name for the snapshot (and backups), +# use the "snapshot_name" option within the subvolume section. +# +# NOTE: btrbk does not autmatically create this directory, and the +# snapshot creation will fail if it is not present. +# +snapshot_dir .snapshots + +# Always create snapshots. Set this to "ondemand" to only create +# snapshots if the target volume is reachable. Set this to "no" if +# snapshot creation is done by another instance of btrbk. +snapshot_create ondemand + +# Perform incremental backups (set to "strict" if you want to prevent +# creation of non-incremental backups if no parent is found). +#incremental yes + +# Specify after what time (in full hours after midnight) backups/ +# snapshots are considered as a daily backup/snapshot +#preserve_hour_of_day 0 + +# Specify on which day of week weekly/monthly backups are to be +# preserved. +#preserve_day_of_week sunday + +# Preserve all snapshots for a minimum period of time. +#snapshot_preserve_min 1d + +# Retention policy for the source snapshots. +#snapshot_preserve h d w m y + +# Preserve all backup targets for a minimum period of time. +#target_preserve_min no + +# Retention policy for backup targets: +#target_preserve h d w m y + +# Retention policy for archives ("btrbk archive" command): +#archive_preserve_min no +#archive_preserve h d w m y + +# Specify SSH private key for "ssh://" volumes / targets: +#ssh_identity /etc/btrbk/ssh/id_ed25519 +ssh_identity /root/.ssh/id_ed25519 +ssh_user root +ssh_compression no +#ssh_cipher_spec default + +compat_remote busybox +send_protocol 2 + +# Enable compression for remote btrfs send/receive operations: +stream_compress no +#stream_compress_level default +#stream_compress_threads default + +# Enable lock file support: Ensures that only one instance of btrbk +# can be run at a time. +lockfile /var/lock/btrbk.lock + +# Don't wait for transaction commit on deletion. Set this to "after" +# or "each" to make sure the deletion of subvolumes is committed to +# disk when btrbk terminates. +#btrfs_commit_delete no +# +# Volume section: "volume " +# +# Directory of a btrfs volume (or subvolume) +# containing the subvolume to be backuped +# (usually the mount-point of a btrfs filesystem +# mounted with subvolid=5 option) +# +# Subvolume section: "subvolume " +# +# Subvolume to be backuped, relative to +# in volume section. +# +# Target section: "target " +# +# (optional) type, defaults to "send-receive". +# Directory of a btrfs volume (or subvolume) +# receiving the backups. +# +# NOTE: The parser does not care about indentation, this is only for +# human readability. The options always apply to the last section +# encountered, overriding the corresponding option of the upper +# section. This means that the global options must be set before any +# "volume" section. +# +# +# Example configuration: +# +# Backup to external disk mounted on /mnt/btr_backup +#volume /mnt/btr_pool + # no action if external disk is not attached +# snapshot_create ondemand + + # propagates to all subvolume sections: +# target /mnt/btr_backup/_btrbk + +# subvolume root_gentoo +# subvolume kvm + # use different retention policy for kvm backups +# target_preserve 7d 4w + + +# Backup to external disk as well as some remote host +#volume /mnt/btr_data +# subvolume home + # always create snapshot, even if targets are unreachable +# snapshot_create always +# target /mnt/btr_backup/_btrbk +# target ssh://backup.my-remote-host.com/mnt/btr_backup + + +# Backup from remote host, with different naming +#volume ssh://my-remote-host.com/mnt/btr_pool +# subvolume data_0 +# snapshot_dir snapshots/btrbk +# snapshot_name data_main +# target /mnt/btr_backup/_btrbk/my-remote-host.com + + +# Resume backups from remote host which runs its own btrbk instance +# creating snapshots for "home" in "/mnt/btr_pool/btrbk_snapshots". +#volume ssh://my-remote-host.com/mnt/btr_pool +# snapshot_dir btrbk_snapshots +# snapshot_create no +# snapshot_preserve_min all +# subvolume home +# target /mnt/btr_backup/_btrbk/my-remote-host.com + +snapshot_preserve_min 2d +snapshot_preserve 14d + +target_preserve_min no +target_preserve 14d 10w *m + +archive_preserve_min latest +archive_preserve 12m 10y + +subvolume / + target_preserve 14d 10w 6m + snapshot_dir /.snapshots + target ssh://workstation/mnt/backup/laptop/root + # target ssh://router.lan/mnt/backup/laptop/root + +volume /home + subvolume bryan + # target /mnt/backup/laptop/home + target ssh://workstation/mnt/backup/laptop/home + # target ssh://router.lan/mnt/backup/laptop/home + diff --git a/dotfiles/workstation/.config/btrbk/btrbk.conf b/dotfiles/workstation/.config/btrbk/btrbk.conf new file mode 100644 index 0000000..eca11a2 --- /dev/null +++ b/dotfiles/workstation/.config/btrbk/btrbk.conf @@ -0,0 +1,253 @@ +# +# Example btrbk configuration file +# +# +# Please refer to the btrbk.conf(5) man-page for a complete +# description of all configuration options. +# For more examples, see README.md included with this package. +# +# btrbk.conf(5): +# README.md: +# +# Note that the options can be overridden per volume/subvolume/target +# in the corresponding sections. +# + + +# Enable transaction log +transaction_log /var/log/btrbk.log + +# Specify SSH private key for remote connections +ssh_identity /home/bryan/.config/btrbk/id_ed25519 +ssh_user root + +# Use sudo if btrbk or lsbtr is run by regular user +backend_local_user btrfs-progs-sudo + +# Enable stream buffer. Adding a buffer between the sending and +# receiving side is generally a good idea. +# NOTE: If enabled, make sure to install the "mbuffer" package! +stream_buffer 1g + +# Directory in which the btrfs snapshots are created. Relative to +# of the volume section. +# If not set, the snapshots are created in . +# +# If you want to set a custom name for the snapshot (and backups), +# use the "snapshot_name" option within the subvolume section. +# +# NOTE: btrbk does not automatically create this directory, and the +# snapshot creation will fail if it is not present. +# +snapshot_dir .snapshots + +# Always create snapshots. Set this to "ondemand" to only create +# snapshots if the target volume is reachable. Set this to "no" if +# snapshot creation is done by another instance of btrbk. +snapshot_create onchange + +# Perform incremental backups (set to "strict" if you want to prevent +# creation of non-incremental backups if no parent is found). +#incremental yes + +# Specify after what time (in full hours after midnight) backups/ +# snapshots are considered as a daily backup/snapshot +#preserve_hour_of_day 0 + +# Specify on which day of week weekly/monthly backups are to be +# preserved. +#preserve_day_of_week sunday + +# Preserve all snapshots for a minimum period of time. +#snapshot_preserve_min 1d + +# Retention policy for the source snapshots. +#snapshot_preserve h d w m y + +# Preserve all backup targets for a minimum period of time. +#target_preserve_min no + +# Retention policy for backup targets: +#target_preserve h d w m y + +# Retention policy for archives ("btrbk archive" command): +#archive_preserve_min no +#archive_preserve h d w m y + +# Enable compression for remote btrfs send/receive operations: +#stream_compress no +#stream_compress_level default +#stream_compress_threads default + +# Enable lock file support: Ensures that only one instance of btrbk +# can be run at a time. +lockfile /var/lock/btrbk.lock + +# Don't wait for transaction commit on deletion. Enable this to make +# sure the deletion of subvolumes is committed to disk when btrbk +# terminates. +#btrfs_commit_delete no + + +# +# Volume section (optional): "volume " +# +# Base path within a btrfs filesystem +# containing the subvolumes to be backuped +# (usually the mount-point of a btrfs filesystem +# mounted with subvolid=5 option). +# +# Subvolume section: "subvolume " +# +# Subvolume to be backuped, relative to +# in volume section. +# +# Target section: "target " +# +# (optional) type, defaults to "send-receive". +# Directory within a btrfs filesystem +# receiving the backups. +# +# NOTE: The parser does not care about indentation, this is only for +# human readability. All options apply to the last section +# encountered, overriding the corresponding option of the upper +# section. This means that the global options must be set on top, +# before any "volume", "subvolume" or "target section. +# + +# +# Example retention policy: +# +# snapshot_preserve_min 2d +# snapshot_preserve 14d +# target_preserve_min no +# target_preserve 20d 10w *m + +snapshot_preserve_min 2d +snapshot_preserve 14d + +target_preserve_min no +target_preserve 14d 10w *m + +archive_preserve_min latest +archive_preserve 12m 10y + +# Global settings +compat_remote busybox +send_protocol 2 + +# Root backup workaround, omit the volume section +subvolume / + target_preserve 14d 10w 6m + snapshot_dir /.snapshots # Absolute path to snapshots dir + target /mnt/backup/workstation/root + # target ssh://router.lan/mnt/backup/workstation/root +# target /run/media/bryan/backup/workstation/root +# target ssh://home-router/mnt/backup/workstation/root + +volume /home + subvolume bryan + target /mnt/backup/workstation/home + # target ssh://router.lan/mnt/backup/workstation/home + target_preserve 14d 10w 6m + # target ssh://home-router/mnt/backup/workstation/home + # target /run/media/bryan/backup/workstation/home + +volume /mnt/downloads + subvolume downloads + target /mnt/backup/workstation/downloads + # target /run/media/bryan/backup/workstation/downloads + +volume / + subvolume /mnt/ebooks + target /mnt/backup/media + subvolume /mnt/cover-art + target /mnt/backup/media + # target ssh://router.lan/mnt/backup/media + # target ssh://home-router/mnt/backup/media + +volume /mnt/array/media + target /mnt/backup/media + # target ssh://router.lan/mnt/backup/media + # target ssh://home-router/mnt/backup/media + subvolume pictures + subvolume music + target_preserve_min all # for home-router to keep samba share (and safer overall) + + +# # +# # Simple setup: Backup root and home to external disk +# # +# snapshot_dir /btrbk_snapshots +# target /mnt/btr_backup +# subvolume / +# subvolume /home + + +# # +# # Complex setup +# # +# # In order to keep things organized, it is recommended to use "volume" +# # sections and mount the top-level subvolume (subvolid=5): +# # +# # $ mount -o subvolid=5 /dev/sda1 /mnt/btr_pool +# # +# # Backup to external disk mounted on /mnt/btr_backup +# volume /mnt/btr_pool +# # Create snapshots in /mnt/btr_pool/btrbk_snapshots +# snapshot_dir btrbk_snapshots + +# # Target for all subvolume sections: +# target /mnt/btr_backup + +# # Some default btrfs installations (e.g. Ubuntu) use "@" for rootfs +# # (mounted at "/") and "@home" (mounted at "/home"). Note that this +# # is only a naming convention. +# #subvolume @ +# subvolume root +# subvolume home +# subvolume kvm +# # Use different retention policy for kvm backups: +# target_preserve 7d 4w + + +# # Backup data to external disk as well as remote host +# volume /mnt/btr_data +# subvolume data +# # Always create snapshot, even if targets are unreachable +# snapshot_create always +# target /mnt/btr_backup +# target ssh://backup.my-remote-host.com/mnt/btr_backup + + +# # Backup from remote host, with different naming +# volume ssh://my-remote-host.com/mnt/btr_pool +# subvolume data_0 +# snapshot_dir snapshots/btrbk +# snapshot_name data_main +# target /mnt/btr_backup/my-remote-host.com + + +# # Backup on demand (noauto) to remote host running busybox, login as +# # regular user using ssh-agent with current user name (ssh_user no) +# # and default credentials (ssh_identity no). +# volume /home +# noauto yes +# compat busybox +# backend_remote btrfs-progs-sudo +# ssh_user no +# ssh_identity no + +# target ssh://my-user-host.com/mnt/btr_backup/home +# subvolume alice +# subvolume bob + + +# # Resume backups from remote host which runs its own btrbk instance +# # creating snapshots for "home" in "/mnt/btr_pool/btrbk_snapshots". +# volume ssh://my-remote-host.com/mnt/btr_pool +# snapshot_dir btrbk_snapshots +# snapshot_create no +# snapshot_preserve_min all +# subvolume home +# target /mnt/btr_backup/my-remote-host.com diff --git a/galaxy-requirements.yml b/galaxy-requirements.yml new file mode 100644 index 0000000..96a1ac6 --- /dev/null +++ b/galaxy-requirements.yml @@ -0,0 +1,6 @@ +--- +# roles: +# - linux-system-roles.systemd + +collections: + - name: ansible.posix \ No newline at end of file diff --git a/group_vars/all/filesystems.yml b/group_vars/all/filesystems.yml new file mode 100644 index 0000000..4d25a4b --- /dev/null +++ b/group_vars/all/filesystems.yml @@ -0,0 +1,4 @@ +--- +directories: + - path: "{{ ansible_facts['env']['HOME'] }}/.local/bin" + mode: '0755' diff --git a/group_vars/all/services.yml b/group_vars/all/services.yml new file mode 100644 index 0000000..f5b2045 --- /dev/null +++ b/group_vars/all/services.yml @@ -0,0 +1,6 @@ +--- +services_system: + - dnf-automatic.timer + +services_user: + - psd diff --git a/group_vars/all/software.yml b/group_vars/all/software.yml new file mode 100644 index 0000000..1df1c34 --- /dev/null +++ b/group_vars/all/software.yml @@ -0,0 +1,67 @@ +--- +dnf_add_repositories: + - name: zsh-completions + description: zsh-completions from openSUSE + baseurl: https://download.opensuse.org/repositories/shells:zsh-users:zsh-completions/Fedora_Rawhide/ + gpgkey: https://download.opensuse.org/repositories/shells:zsh-users:zsh-completions/Fedora_Rawhide/repodata/repomd.xml.key + - name: code + description: Visual Studio Code + baseurl: https://packages.microsoft.com/yumrepos/vscode + gpgkey: https://packages.microsoft.com/keys/microsoft.asc + +dnf_remove: + - abrt + - rhythmbox + - gnome-software + - open-vm-tools-desktop + - orca + - anaconda-live + - gnome-initial-setup + +dnf_install: + - rpmfusion-free-release + - zsh + - zsh-completions + - ShellCheck + - btrbk + - btrfsmaintenance + - vim + - htop + - remmina + - calibre + - pinta + - toolbox + - code + - gnome-tweaks + - wl-clipboard + - syncthing + - profile-sync-daemon + - python3-virtualenv + - python3-virtualenvwrapper + - nautilus-python + - gettext + - setroubleshoot + - cargo + - flatpak + - snapd + - tailscale + - dnf5-plugin-automatic + +# Cargo packages to install +cargo_packages: + - aichat + +# Git repositories to clone +git_repos: + - repo: https://git.bryanroessler.com/bryan/installJRMC.git + dest: "{{ ansible_facts['env']['HOME'] }}/.local/bin/installJRMC" + version: dev + - repo: https://git.bryanroessler.com/bryan/openwrtbuilder.git + dest: "{{ ansible_facts['env']['HOME'] }}/.local/bin/openwrtbuilder" + version: dev + # - repo: https://git.bryanroessler.com/bryan/deployer.git + # dest: "{{ ansible_facts['env']['HOME'] }}/.local/bin/deployer" + # version: dev + # - repo: https://git.bryanroessler.com/bryan/deploy.git + # dest: "{{ ansible_facts['env']['HOME'] }}/.local/bin/deploy" + # version: dev diff --git a/group_vars/all/sysconfig.yml b/group_vars/all/sysconfig.yml new file mode 100644 index 0000000..65828b5 --- /dev/null +++ b/group_vars/all/sysconfig.yml @@ -0,0 +1,21 @@ +--- +# GNOME settings via gsettings +sysconfig_gsettings: + - schema: org.gnome.nautilus.preferences + key: always-use-location-entry + value: "true" + +# Sysctl configurations +sysconfig_sysctl: + - name: fs.inotify.max_user_watches + value: 524288 + file: /etc/sysctl.d/local.conf + +# Sudoers configuration - commands that can run without password +sysconfig_sudoers_nopasswd_commands: + - /usr/bin/psd-overlay-helper + - /usr/sbin/btrfs + - /usr/bin/journalctl + - /usr/bin/dnf + - /usr/bin/fwupdmgr + - /usr/bin/dmesg diff --git a/group_vars/all/users.yml b/group_vars/all/users.yml new file mode 100644 index 0000000..fd8b3a5 --- /dev/null +++ b/group_vars/all/users.yml @@ -0,0 +1,4 @@ +--- +users_configure: + - name: bryan + shell: /usr/bin/zsh diff --git a/group_vars/all/vault.yml b/group_vars/all/vault.yml new file mode 100644 index 0000000..84257bb --- /dev/null +++ b/group_vars/all/vault.yml @@ -0,0 +1,36 @@ +$ANSIBLE_VAULT;1.1;AES256 +32626332653961623932383137363730363737346333353432653563366161316332343261643463 +3735336466356330313662356639323964613264626634310a316134363162383362636365616562 +31363662373232323865346138623163363564353066393538643032333738353038333263623537 +3630656531313632390a303730626633636530376166663936313632373737636463336265613437 +31356332306665386263393865303431386334386365646562343465346663346363636233623262 +32656231366565383561663565663739633632363033633930366533643836316436333965643033 +66323639623964386263326634336534363865373238306435353331616461333836373331323034 +37653433643433346262313166666634303366663466643732323134346265626337376536336162 +35656430663233653864326265313637373062386232373733623139646430313765353733633462 +31666631663562303832626165376262356632326566353366313566323730366265633333643531 +32613166306164313338633438633762396530343636643937646131343837393965363063643161 +65633330353162333465656133633765633039613431663838636362623666383332643138363337 +66623137653432613634613534373264383139303462303138323232363962383236646138646465 +31636262313462643536643730643939313766323737383830396462346665616439336162613837 +64666364306234613761376462303362363334386635353639663534666232623165343661393435 +66356263323736626266663662636338636535353639306265303965303636313939613564346164 +38633964666634303761333432346632333635663931366663306633656138353532323764616465 +32383336663436386435343037396131663932353836336266316539623939343563653339353932 +38636266636162363339316461616531353031323161313834623131313336633762396661353437 +37336537383531356332353132313562333736343261616236316133626530393934623437366130 +66353634386538363530613131666465346463366232656364366336323861633834636461313562 +35376263353632666438356139303835323030363035373134303937323433303834303030613838 +62653835623639623164386130666363393734313662653231613531326431633239346661623434 +38396136396131643931643663613538666633323239303837656264396636383531646435363837 +34303233616134343332353664653265613135353732336238613232343431623965383939623261 +64663330336637303361343032616533393236336532343432306164353436633535356535363530 +64313065373939653039343632303632356136333138353336363636343035356530333230663461 +38343862336163306632373863306337353466663364313437376165313762376533363039346664 +39663039613534393731643335353763383265386630656163396631353761383732666533303239 +34383464666364313965313938633435333534636330323939353339666433356131323539306435 +30386633316337356534326135356563313939643463626235336237376162616438343534626364 +32353062613538333336353935336563613166373361663334643766396162383562323264336362 +63656233393961653065663966353036376136616135656637376334613964386535643935336339 +35643933666663623463393634623235366465396233346434336238326632653865646665336438 +613961303139323263653965366632333231 diff --git a/group_vars/laptop/filesystems.yml b/group_vars/laptop/filesystems.yml new file mode 100644 index 0000000..3f18c80 --- /dev/null +++ b/group_vars/laptop/filesystems.yml @@ -0,0 +1,11 @@ +--- +mounts: + - path: /home + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=home,compress=zstd:1,defaults + state: mounted + owner: root + group: root + mode: '0755' + create_dir: false diff --git a/group_vars/workstation/filesystems.yml b/group_vars/workstation/filesystems.yml new file mode 100644 index 0000000..4a0428c --- /dev/null +++ b/group_vars/workstation/filesystems.yml @@ -0,0 +1,131 @@ +--- +mounts: + - path: /home + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=home,compress=zstd:1,defaults + state: mounted + owner: root + group: root + mode: '0755' + create_dir: false + + - path: /mnt/array + src: /dev/disk/by-uuid/36fe5749-800a-4ab5-a89a-6ad343f5d42f + fstype: btrfs + opts: defaults,compress=zstd:1,x-systemd.device-timeout=180s + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /mnt/backup + src: /dev/disk/by-uuid/64cc836d-e55f-4c34-83db-01c9b43c218a + fstype: btrfs + opts: defaults,compress=zstd:1,x-systemd.device-timeout=180s,nofail + state: mounted + owner: root + group: root + mode: '0755' + create_dir: false + + - path: /mnt/downloads + src: /dev/disk/by-uuid/ee6247ed-5bcf-481e-802e-74efbc02eb45 + fstype: btrfs + opts: defaults,compress=zstd:1 + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/downloads + src: /dev/disk/by-uuid/ee6247ed-5bcf-481e-802e-74efbc02eb45 + fstype: btrfs + opts: subvol=downloads,defaults,compress=zstd:1,x-systemd.requires=home.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/media + src: /dev/disk/by-uuid/36fe5749-800a-4ab5-a89a-6ad343f5d42f + fstype: btrfs + opts: subvol=media,defaults,compress=zstd:1,x-systemd.requires=home.mount,x-systemd.device-timeout=180s,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /mnt/array/media/cover-art + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/cover-art,defaults,compress=zstd:1,x-systemd.requires=mnt-array.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/media/cover-art + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/cover-art,defaults,compress=zstd:1,x-systemd.requires=home-bryan-media.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /mnt/array/media/ebooks + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/ebooks,defaults,compress=zstd:1,x-systemd.requires=mnt-array.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/media/ebooks + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/ebooks,defaults,compress=zstd:1,x-systemd.requires=home-bryan-media.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /mnt/array/media/pictures/Screenshots + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/screenshots,defaults,compress=zstd:1,x-systemd.requires=mnt-array.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/media/pictures/Screenshots + src: /dev/disk/by-uuid/42f5911d-d634-4f92-9561-c7e20ca66c83 + fstype: btrfs + opts: subvol=root/mnt/screenshots,defaults,compress=zstd:1,x-systemd.requires=home-bryan-media.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false + + - path: /home/bryan/devices/laptop/music + src: /dev/disk/by-uuid/d0ed963e-aaa0-4dcc-9ece-4ea8fe7fcea2 + fstype: btrfs + opts: defaults,compress=zstd:1,x-systemd.requires=home.mount,x-gvfs-hide + state: mounted + owner: bryan + group: bryan + mode: '0755' + create_dir: false diff --git a/inventories/localhosts b/inventories/localhosts new file mode 100644 index 0000000..98c5c05 --- /dev/null +++ b/inventories/localhosts @@ -0,0 +1,11 @@ +[all] +localhost ansible_connection=local ansible_user=bryan + +[htpc] +localhost ansible_connection=local ansible_user=bryan + +[workstation] +localhost ansible_connection=local ansible_user=bryan + +[laptop] +localhost ansible_connection=local ansible_user=bryan \ No newline at end of file diff --git a/inventories/remotehosts b/inventories/remotehosts new file mode 100644 index 0000000..04fed51 --- /dev/null +++ b/inventories/remotehosts @@ -0,0 +1,16 @@ +[all] + +[server] +bryanroessler.com ansible_user=bryan + +[testing] +192.168.122.205 ansible_user=bryan + +[htpc] +htpc ansible_user=bryan + +[workstation] +workstation ansible_user=bryan + +[laptop] +laptop ansible_user=bryan \ No newline at end of file diff --git a/pip-requirements b/pip-requirements new file mode 100644 index 0000000..c5e1d58 --- /dev/null +++ b/pip-requirements @@ -0,0 +1,3 @@ +ansible +ansible-core +ansible-lint diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..9d52fe5 --- /dev/null +++ b/playbook.yml @@ -0,0 +1,35 @@ +--- +- name: Sync filesystems + hosts: all + roles: + - filesystems + +- name: Sync scripts + hosts: all + roles: + - scripts + +- name: Sync dotfiles + hosts: all + roles: + - dotfiles + +- name: Sync software + hosts: all + roles: + - software + +- name: Sync services + hosts: all + roles: + - services + +- name: Sync users + hosts: all + roles: + - users + +- name: Sync sysconfig + hosts: all + roles: + - sysconfig diff --git a/roles/dotfiles/tasks/main.yml b/roles/dotfiles/tasks/main.yml new file mode 100644 index 0000000..31d3f85 --- /dev/null +++ b/roles/dotfiles/tasks/main.yml @@ -0,0 +1,96 @@ +--- +- name: Find common dotfiles (excluding templates) + ansible.builtin.find: + paths: "{{ playbook_dir }}/dotfiles/common" + recurse: true + file_type: file + hidden: true + excludes: "*.j2" + delegate_to: localhost + register: dotfiles_common_files + run_once: true + +- name: Find group dotfiles (excluding templates) + ansible.builtin.find: + paths: "{{ playbook_dir }}/dotfiles/{{ item }}" + recurse: true + file_type: file + hidden: true + excludes: "*.j2" + loop: "{{ group_names | default([]) }}" + delegate_to: localhost + register: dotfiles_group_files + run_once: true + ignore_errors: true + +- name: Deploy common dotfiles (remote) + ansible.builtin.copy: + src: "{{ item.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.path | replace(playbook_dir + '/dotfiles/common/', '') }}" + mode: preserve + loop: "{{ dotfiles_common_files.files }}" + when: ansible_connection not in ['local', 'localhost'] + +- name: Deploy group dotfiles (remote) + ansible.builtin.copy: + src: "{{ item.1.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.1.path | replace(playbook_dir + '/dotfiles/' + item.0.item + '/', '') }}" + mode: preserve + loop: "{{ dotfiles_group_files.results | subelements('files', skip_missing=True) }}" + when: ansible_connection not in ['local', 'localhost'] + +- name: Symlink common dotfiles (local) + ansible.builtin.file: + src: "{{ item.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.path | replace((playbook_dir + '/dotfiles/common/'), '') }}" + state: link + force: true + loop: "{{ dotfiles_common_files.files }}" + when: ansible_connection in ['local', 'localhost'] + +- name: Symlink group dotfiles (local) + ansible.builtin.file: + src: "{{ item.1.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.1.path | replace((playbook_dir + '/dotfiles/' + item.0.item + '/'), '') }}" + state: link + force: true + loop: "{{ dotfiles_group_files.results | subelements('files') }}" + when: ansible_connection in ['local', 'localhost'] + +- name: Find template files in common dotfiles + ansible.builtin.find: + paths: "{{ playbook_dir }}/dotfiles/common" + recurse: true + file_type: file + hidden: true + patterns: "*.j2" + delegate_to: localhost + register: dotfiles_common_templates + run_once: true + +- name: Find template files in group dotfiles + ansible.builtin.find: + paths: "{{ playbook_dir }}/dotfiles/{{ item }}" + recurse: true + file_type: file + hidden: true + patterns: "*.j2" + loop: "{{ group_names | default([]) }}" + delegate_to: localhost + register: dotfiles_group_templates + run_once: true + ignore_errors: true + +- name: Template common dotfiles + ansible.builtin.template: + src: "{{ item.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.path | replace(playbook_dir + '/dotfiles/common/', '') | replace('.j2', '') }}" + mode: '0600' + loop: "{{ dotfiles_common_templates.files }}" + +- name: Template group dotfiles + ansible.builtin.template: + src: "{{ item.1.path }}" + dest: "{{ ansible_facts['env']['HOME'] }}/{{ item.1.path | replace(playbook_dir + '/dotfiles/' + item.0.item + '/', '') | replace('.j2', '') }}" + mode: '0600' + loop: "{{ dotfiles_group_templates.results | subelements('files', skip_missing=True) }}" diff --git a/roles/filesystems/tasks/main.yml b/roles/filesystems/tasks/main.yml new file mode 100644 index 0000000..05635fc --- /dev/null +++ b/roles/filesystems/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Ensure mount points exist when create_dir is true + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner | default('root') }}" + group: "{{ item.group | default('root') }}" + mode: "{{ item.mode | default('0755') }}" + loop: "{{ mounts | default([]) }}" + become: true + when: item.create_dir | default(false) | bool + +- name: Verify mount points exist + ansible.builtin.stat: + path: "{{ item.path }}" + register: filesystems_mounts_stat + changed_when: false + loop: "{{ mounts | default([]) }}" + +- name: Assert mount points exist + ansible.builtin.assert: + that: + - (item.stat.exists | default(false)) + fail_msg: "Mount point {{ item.item.path }} does not exist" + loop: "{{ filesystems_mounts_stat.results }}" + +- name: Manage fstab entries and mount + ansible.posix.mount: + path: "{{ item.path }}" + src: "{{ item.src }}" + fstype: "{{ item.fstype }}" + opts: "{{ item.opts | default('defaults') }}" + state: "{{ item.state | default('mounted') }}" + backup: true + loop: "{{ mounts | default([]) }}" + become: true + +- name: Ensure directories exist + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + mode: "{{ item.mode | default('0755') }}" + loop: "{{ directories | default([]) }}" diff --git a/roles/scripts/tasks/main.yml b/roles/scripts/tasks/main.yml new file mode 100644 index 0000000..e0f0574 --- /dev/null +++ b/roles/scripts/tasks/main.yml @@ -0,0 +1,25 @@ +--- + +- name: Copy repo scripts to local bin (for remote hosts) + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ local_bin_dir | default(ansible_facts['env']['HOME'] ~ '/.local/bin') }}/{{ item | basename }}" + mode: "0755" + owner: "{{ local_bin_owner | default(ansible_facts['user_id']) }}" + group: "{{ local_bin_group | default(ansible_facts['user_gid']) }}" + with_fileglob: + - "{{ scripts_src_glob | default(playbook_dir + '/scripts/*') }}" + when: ansible_connection not in ['local', 'localhost'] and item is file + +- name: Symlink repo scripts into local bin (stow-like, for local hosts) + ansible.builtin.file: + src: "{{ item }}" + dest: "{{ local_bin_dir | default(ansible_facts['env']['HOME'] ~ '/.local/bin') }}/{{ item | basename }}" + state: link + force: true + owner: "{{ local_bin_owner | default(ansible_facts['user_id']) }}" + group: "{{ local_bin_group | default(ansible_facts['user_gid']) }}" + follow: false + with_fileglob: + - "{{ scripts_src_glob | default(playbook_dir + '/scripts/*') }}" + when: ansible_connection in ['local', 'localhost'] and item is file diff --git a/roles/services/tasks/main.yml b/roles/services/tasks/main.yml new file mode 100644 index 0000000..09b8ccb --- /dev/null +++ b/roles/services/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: Enable and start system services + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + scope: system + loop: "{{ services_system }}" + become: true + when: services_system is defined and services_system | length > 0 + +- name: Enable and start user services + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + scope: user + loop: "{{ services_user }}" + when: services_user is defined and services_user | length > 0 diff --git a/roles/software/tasks/main.yml b/roles/software/tasks/main.yml new file mode 100644 index 0000000..817497e --- /dev/null +++ b/roles/software/tasks/main.yml @@ -0,0 +1,46 @@ +--- + +- name: Add DNF repositories + ansible.builtin.yum_repository: + name: "{{ item.name }}" + description: "{{ item.description }}" + baseurl: "{{ item.baseurl }}" + enabled: true + gpgcheck: true + gpgkey: "{{ item.gpgkey }}" + loop: "{{ dnf_add_repositories }}" + become: true + when: dnf_add_repositories is defined and dnf_add_repositories | length > 0 + +- name: Remove unwanted packages + ansible.builtin.dnf: + name: "{{ dnf_remove }}" + state: absent + become: true + when: dnf_remove is defined and dnf_remove | length > 0 + failed_when: false + +- name: Install DNF packages + ansible.builtin.dnf: + name: "{{ dnf_install }}" + state: present + become: true + when: dnf_install is defined and dnf_install | length > 0 + +- name: Install cargo packages + ansible.builtin.command: + cmd: "cargo install {{ item }}" + loop: "{{ cargo_packages }}" + when: cargo_packages is defined and cargo_packages | length > 0 + register: software_cargo_install_result + changed_when: "'Installing' in software_cargo_install_result.stderr or 'Compiling' in software_cargo_install_result.stderr" + failed_when: software_cargo_install_result.rc != 0 and 'already exists' not in software_cargo_install_result.stderr + +- name: Clone git repositories + ansible.builtin.git: + repo: "{{ item.repo }}" + dest: "{{ item.dest }}" + version: "{{ item.version }}" + update: true + loop: "{{ git_repos }}" + when: git_repos is defined and git_repos | length > 0 diff --git a/roles/sysconfig/tasks/main.yml b/roles/sysconfig/tasks/main.yml new file mode 100644 index 0000000..4d431da --- /dev/null +++ b/roles/sysconfig/tasks/main.yml @@ -0,0 +1,29 @@ +--- + +- name: Configure sysctl parameters + ansible.posix.sysctl: + name: "{{ item.name }}" + value: "{{ item.value }}" + sysctl_file: "{{ item.file }}" + state: present + reload: true + loop: "{{ sysconfig_sysctl }}" + become: true + when: sysconfig_sysctl is defined and sysconfig_sysctl | length > 0 + +- name: Configure GNOME settings + community.general.dconf: + key: "/{{ item.schema | replace('.', '/') }}/{{ item.key }}" + value: "{{ item.value }}" + state: present + loop: "{{ sysconfig_gsettings }}" + when: sysconfig_gsettings is defined and sysconfig_gsettings | length > 0 + +- name: Configure sudoers for passwordless commands + ansible.builtin.lineinfile: + path: /etc/sudoers + line: "{{ ansible_facts['user_id'] }} ALL=(ALL) NOPASSWD: {{ sysconfig_sudoers_nopasswd_commands | join(', ') }}" + state: present + validate: /usr/sbin/visudo -cf %s + become: true + when: sysconfig_sudoers_nopasswd_commands is defined and sysconfig_sudoers_nopasswd_commands | length > 0 diff --git a/roles/users/tasks/main.yml b/roles/users/tasks/main.yml new file mode 100644 index 0000000..155869b --- /dev/null +++ b/roles/users/tasks/main.yml @@ -0,0 +1,6 @@ +- name: Set user shell + ansible.builtin.user: + name: "{{ item.name }}" + shell: "{{ item.shell }}" + loop: "{{ users_configure }}" + become: true diff --git a/scripts/btrfs-convert b/scripts/btrfs-convert new file mode 100755 index 0000000..be50ee3 --- /dev/null +++ b/scripts/btrfs-convert @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Convert directories to btrfs subvolumes + +usage() { + echo "Usage: $0 [directory2 ...]" + echo "Converts each directory to a btrfs subvolume." +} + +btrfs_convert() { + local d dir tmp_dir + for d in "$@"; do + dir="$d" + tmp_dir="$dir.tmp" + if [[ ! -d "$dir" ]]; then + echo "[ERROR] Directory '$dir' does not exist. Skipping." + continue + fi + if [[ -d "$tmp_dir" ]]; then + echo "[ERROR] Temporary directory '$tmp_dir' already exists. Skipping '$dir'." + continue + fi + echo "[INFO] Creating btrfs subvolume: '$tmp_dir'..." + if ! btrfs subvolume create "$tmp_dir" &>/dev/null; then + echo "[ERROR] Failed to create btrfs subvolume '$tmp_dir'. Skipping '$dir'." + continue + fi + echo "[INFO] Moving contents from '$dir' to '$tmp_dir'..." + if ! mv "$dir"/* "$tmp_dir"/ 2>/dev/null; then + echo "[ERROR] Failed to move contents from '$dir' to '$tmp_dir'. Cleaning up." + rm -rf "$tmp_dir" + continue + fi + echo "[INFO] Removing original directory '$dir'..." + if ! rm -rf "$dir"; then + echo "[ERROR] Failed to remove '$dir'. Manual cleanup may be required." + continue + fi + echo "[INFO] Renaming '$tmp_dir' to '$dir'..." + if ! mv "$tmp_dir" "$dir"; then + echo "[ERROR] Failed to rename '$tmp_dir' to '$dir'. Manual cleanup may be required." + continue + fi + echo "[SUCCESS] Converted '$dir' to a btrfs subvolume." + done +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +btrfs_convert "$@" \ No newline at end of file diff --git a/scripts/chroot-rescue b/scripts/chroot-rescue new file mode 100755 index 0000000..bb106c1 --- /dev/null +++ b/scripts/chroot-rescue @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Mount and chroot a linux system + +set -euo pipefail + +lsblk + +# Set defaults +RP="${ROOT_PART:-/dev/nvme0n0p3}" +BP="${BOOT_PART:-/dev/nvme0n0p1}" + +read -r -p "Root partition [$RP]: " input_rp +RP="${input_rp:-$RP}" +read -r -p "Boot partition [$BP]: " input_bp +BP="${input_bp:-$BP}" +MD="${MOUNT_DIR:-/mnt/${RP##*/}}" +[[ -d "$MD" ]] && MD="$MD-$RANDOM" +read -r -p "Mount directory [$MD]: " input_md +MD="${input_md:-$MD}" + +if [[ ! -e "$RP" || ! -e "$BP" ]]; then + echo "[ERROR] Root or boot partition does not exist." + exit 1 +fi + +# Mount and enter the chroot +echo "[INFO] Mounting and entering chroot..." +sudo mkdir -p "$MD" +sudo mount "$RP" "$MD" +for i in proc sys dev; do + sudo mount --bind "/$i" "$MD/$i" +done +sudo mount "$BP" "$MD/boot/efi" +sudo chroot "$MD" + +# After chroot +echo "[INFO] Exiting and unmounting chroot..." +sudo umount "$MD/boot/efi" +for i in proc sys dev; do + sudo umount "$MD/$i" +done +sudo umount "$MD" + +exit 0 \ No newline at end of file diff --git a/scripts/container-home-assistant b/scripts/container-home-assistant new file mode 100755 index 0000000..066aad8 --- /dev/null +++ b/scripts/container-home-assistant @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# One-liner to deploy containerized home-assistant + +podman run -d --name="home-assistant" -v ~/.config/home-assistant:/config -v /etc/localtime:/etc/localtime:ro --net=host docker.io/homeassistant/home-assistant:stable && +podman generate systemd --name "home-assistant" --container-prefix "" --separator "" > ~/.config/systemd/user/home-assistant.service && +systemctl --user daemon-reload && +systemctl --user enable --now home-assistant diff --git a/scripts/drive-info b/scripts/drive-info new file mode 100755 index 0000000..afe762e --- /dev/null +++ b/scripts/drive-info @@ -0,0 +1,193 @@ +#!/usr/bin/env bash +# Gathers disk info including: +# Hardware info +# Filesystem data +# Btrfs array membership +# LUKS encryption +# SMART status +# +# Usage: sudo ./drive-info.sh +# +# Requires root privileges for complete information access + +# Check for root privileges +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root for complete information" + exit 1 +fi + +# Get list of block devices (excluding loop devices, partitions, and virtual devices) +devices=$(lsblk -dn -o NAME,TYPE | grep disk | awk '{print $1}') + +for device in $devices; do + dev_path="/dev/$device" + + # Get basic size info + size=$(lsblk -dno SIZE "$dev_path" 2>/dev/null) + + # Get comprehensive SMART information + smart_info=$(smartctl -i "$dev_path" 2>/dev/null) + smart_health=$(smartctl -H "$dev_path" 2>/dev/null | grep "SMART overall-health" | awk '{print $NF}') + smart_all=$(smartctl -A "$dev_path" 2>/dev/null) + + # Extract model + model=$(echo "$smart_info" | grep "Device Model:" | cut -d: -f2- | xargs) + [[ -z "$model" ]] && model=$(echo "$smart_info" | grep "Model Number:" | cut -d: -f2- | xargs) + [[ -z "$model" ]] && model=$(cat "/sys/block/$device/device/model" 2>/dev/null | xargs) + + # Extract serial number + serial=$(echo "$smart_info" | grep "Serial Number:" | cut -d: -f2- | xargs) + [[ -z "$serial" ]] && serial=$(lsblk -dno SERIAL "$dev_path" 2>/dev/null) + + # Extract WWN + wwn=$(echo "$smart_info" | grep "LU WWN Device Id:" | cut -d: -f2- | xargs | sed 's/ //g') + [[ -z "$wwn" ]] && wwn=$(lsblk -dno WWN "$dev_path" 2>/dev/null) + + # Extract rotation rate + rpm=$(echo "$smart_info" | grep "Rotation Rate:" | cut -d: -f2- | xargs) + if [[ -z "$rpm" || "$rpm" == "Solid State Device" ]]; then + rot=$(cat "/sys/block/$device/queue/rotational" 2>/dev/null) + [[ "$rot" == "0" ]] && rpm="SSD" + fi + + # Extract form factor + form_factor=$(echo "$smart_info" | grep "Form Factor:" | cut -d: -f2- | xargs) + + # Extract interface and link speed + interface=$(echo "$smart_info" | grep "SATA Version" | cut -d: -f2- | xargs) + [[ -z "$interface" ]] && interface=$(echo "$smart_info" | grep "Transport protocol:" | cut -d: -f2- | xargs) + [[ -z "$interface" ]] && interface=$(echo "$smart_info" | grep "NVMe Version:" | cut -d: -f2- | xargs | awk '{print "NVMe " $1}') + + # Extract temperature + temp=$(echo "$smart_all" | grep -i "Temperature" | head -1 | awk '{print $10}') + [[ -n "$temp" ]] && temp="${temp}°C" + + # Extract power-on hours + power_hours=$(echo "$smart_all" | grep "Power_On_Hours" | awk '{print $10}') + [[ -z "$power_hours" ]] && power_hours=$(echo "$smart_all" | grep "Power On Hours" | awk '{print $3}') + + # Get disk ID + disk_id="" + for f in /dev/disk/by-id/*; do + [[ -e "$f" ]] || continue + # resolve symlink target and compare basename to device, skip entries that reference partitions + target=$(readlink -f "$f" 2>/dev/null) || continue + if [[ "$(basename "$target")" == "$device" && "$f" != *part* ]]; then + disk_id=$(basename "$f") + break + fi + done + + # Get filesystem UUID + uuid=$(lsblk -no UUID "$dev_path" 2>/dev/null | head -1) + [[ -z "$uuid" ]] && uuid=$(lsblk -no UUID "${dev_path}1" 2>/dev/null) + + # Check for LUKS encryption + luks_uuid="" + luks_mapper="" + if cryptsetup isLuks "$dev_path" 2>/dev/null; then + luks_uuid=$(cryptsetup luksUUID "$dev_path" 2>/dev/null) + for mapper in /dev/mapper/luks-*; do + [[ -e "$mapper" ]] || continue + mapper_name=$(basename "$mapper") + [[ "$mapper_name" == "luks-$luks_uuid" ]] && luks_mapper="$mapper_name" && break + done + fi + + # Get partition table type + ptable=$(blkid -p -s PTTYPE "$dev_path" 2>/dev/null | grep -oP 'PTTYPE="\K[^"]+') + [[ -z "$ptable" ]] && ptable="none" + + # Get initial mount point + mount_point=$(findmnt -no TARGET "$dev_path" 2>/dev/null | head -1) + if [[ -z "$mount_point" && -n "$luks_mapper" ]]; then + mount_point=$(findmnt -no TARGET "/dev/mapper/$luks_mapper" 2>/dev/null | head -1) + fi + + # Get HBA information + hba_info="" + if [[ -L "/sys/block/$device" ]]; then + dev_path_sys=$(readlink -f "/sys/block/$device") + + # Exclude USB, virtual, and NVMe devices from HBA detection + if [[ ! "$dev_path_sys" =~ (usb|virtual|nvme) ]]; then + phy=$(echo "$dev_path_sys" | grep -oP 'phy-\K[0-9]+' | head -1) + port=$(echo "$dev_path_sys" | grep -oP 'port-\K[0-9]+' | head -1) + target=$(echo "$dev_path_sys" | grep -oP 'target\K[0-9:]+' | head -1) + + # Find the actual storage controller in the PCI chain + mapfile -t pci_addrs < <(echo "$dev_path_sys" | grep -oP '\d+:\d+:\d+\.\d+') + for addr in "${pci_addrs[@]}"; do + desc=$(lspci -s "$addr" 2>/dev/null | cut -d: -f3-) + if [[ "$desc" =~ (SAS|SATA|RAID|HBA|LSI|Adaptec|AHCI) ]]; then + pci_addr="$addr" + pci_desc="$desc" + break + fi + done + + # Build HBA info string + if [[ -n "$pci_addr" ]]; then + hba_info="PCI: $pci_addr ($pci_desc)" + [[ -n "$phy" ]] && hba_info="$hba_info | PHY: $phy" + [[ -n "$port" ]] && hba_info="$hba_info | Port: $port" + [[ -n "$target" ]] && hba_info="$hba_info | Target: $target" + fi + fi + fi + + # Get Btrfs information + btrfs_label="" + btrfs_uuid="" + btrfs_devid="" + + # Check device or its LUKS mapper for btrfs + check_dev="$dev_path" + if [[ -n "$luks_mapper" ]]; then + check_dev="/dev/mapper/$luks_mapper" + fi + + btrfs_show=$(btrfs filesystem show "$check_dev" 2>/dev/null) + if btrfs filesystem show "$check_dev" &>/dev/null; then + btrfs_label=$(echo "$btrfs_show" | head -1 | grep -oP "Label: '\K[^']+") + btrfs_uuid=$(echo "$btrfs_show" | head -1 | grep -oP "uuid: \K[a-f0-9-]+") + btrfs_devid=$(echo "$btrfs_show" | grep -E "(${check_dev}|${dev_path})" | grep -oP "devid\s+\K[0-9]+" | head -1) + + # If not mounted, check if any other device in the btrfs array is mounted + if [[ -z "$mount_point" && -n "$btrfs_uuid" ]]; then + all_devs=$(echo "$btrfs_show" | grep "path" | grep -oP "path \K[^ ]+") + for btrfs_dev in $all_devs; do + mount_point=$(findmnt -no TARGET "$btrfs_dev" 2>/dev/null | head -1) + [[ -n "$mount_point" ]] && break + done + fi + fi + + # Default mount point if still empty + [[ -z "$mount_point" ]] && mount_point="Not mounted" + + # Text output + echo "╔════════════════════════════════════════╗" + echo "║ Device: $dev_path" + echo "╚════════════════════════════════════════╝" + echo "Model: $model" + echo "Serial: $serial" + echo "WWN: $wwn" + echo "Size: $size" + echo "Rotation: $rpm" + echo "Form Factor: $form_factor" + echo "Interface: $interface" + echo "Disk ID: $disk_id" + echo "Filesystem UUID: $uuid" + echo "LUKS UUID: $luks_uuid" + echo "Partition Table: $ptable" + echo "Mount: $mount_point" + echo "HBA Info: $hba_info" + echo "SMART Health: $smart_health" + echo "Temperature: $temp" + echo "Power On Hours: $power_hours" + echo "Btrfs Label: $btrfs_label" + echo "Btrfs UUID: $btrfs_uuid" + echo "Btrfs devid: $btrfs_devid" + echo +done \ No newline at end of file diff --git a/scripts/estimate-musicdir b/scripts/estimate-musicdir new file mode 100755 index 0000000..303f3f2 --- /dev/null +++ b/scripts/estimate-musicdir @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Estimate the size of a music directory if FLACs are lossy compressed + +MUSIC_DIR="${1:-/home/bryan/media/music}" + +# Sum existing MP3s +MP3_BYTES=$(find "$MUSIC_DIR" -type f -iname "*.mp3" -exec du -b {} + | awk '{sum+=$1} END{print sum}') + +# Sum FLACs +FLAC_BYTES=$(find "$MUSIC_DIR" -type f -iname "*.flac" -exec du -b {} + | awk '{sum+=$1} END{print sum}') + +# Estimate FLACs as 160k Ogg (roughly 1/8 size of FLAC) +EST_FLAC_OGG=$(( FLAC_BYTES / 8 )) + +# Total estimated size +TOTAL_EST=$(( MP3_BYTES + EST_FLAC_OGG )) + +# Human-readable +EST_HR=$(numfmt --to=iec-i --suffix=B "$TOTAL_EST") +echo "Estimated total size (MP3 + FLAC → 160k Ogg): $EST_HR" + diff --git a/scripts/extract b/scripts/extract new file mode 100755 index 0000000..b4e1a0c --- /dev/null +++ b/scripts/extract @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Automatically decompresses most filetypes + +extract() { + local a + [[ $# -eq 0 ]] && { echo "usage: extract " >&2; return 1; } + for a in "$@"; do + [[ ! -f $a ]] && { echo "$a: not a file" >&2; continue; } + case $a in + *.tar.*|*.tgz|*.tbz2) tar xvf "$a" --auto-compress ;; + *.tar) tar xvf "$a" ;; + *.gz) gunzip "$a" ;; + *.bz2) bunzip2 "$a" ;; + *.xz) unxz "$a" ;; + *.zst) unzstd "$a" ;; + *.zip) unzip "$a" ;; + *.rar) unrar x "$a" ;; + *.7z) 7z x "$a" ;; + *.Z) uncompress "$a" ;; + *) echo "$a: cannot extract" ;; + esac + done +} + +# Allow script to be safely sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + extract "$@" + exit +fi diff --git a/scripts/history-clean b/scripts/history-clean new file mode 100755 index 0000000..883c64d --- /dev/null +++ b/scripts/history-clean @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Cleans the history file of PGP messages and keys + +histfile="${1:-$HISTFILE:-$HOME/.histfile}" +cp -a "$histfile" "/tmp/$histfile.bak" +sed --in-place '/gpg/d' "$histfile" +sed --in-place '/-----BEGIN PGP MESSAGE-----/,/-----END PGP MESSAGE-----/d' "$histfile" diff --git a/scripts/iso-to-mkv b/scripts/iso-to-mkv new file mode 100755 index 0000000..24a738f --- /dev/null +++ b/scripts/iso-to-mkv @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Convert ISO files to MKV format with automatic season/episode naming + +set -euo pipefail + +SEARCH_DIR="${1:-$(pwd)}" +OUT_DIR="${2:-$SEARCH_DIR/out}" + +if [[ ! -d "$SEARCH_DIR" ]]; then + echo "[ERROR] Search directory '$SEARCH_DIR' does not exist." + exit 1 +fi + +mkdir -p "$OUT_DIR" +prev_season="" +ep=1 + +echo "[INFO] Searching for ISO files in '$SEARCH_DIR'..." + +find "$SEARCH_DIR" -type f -iname '*.iso' | sort | while read -r iso; do + echo "[INFO] Processing: $iso" + parent=$(basename "$(dirname "$iso")") + + if [[ ! $parent =~ S([0-9]+) ]]; then + echo "[WARN] Skipping '$iso' - parent directory doesn't match season pattern." + continue + fi + + season=$(printf "%02d" "${BASH_REMATCH[1]}") + + if [[ "$season" != "$prev_season" ]]; then + ep=1 + prev_season="$season" + fi + + ripdir="$OUT_DIR/temp/$parent" + mkdir -p "$ripdir" "$OUT_DIR/Season $season" + + echo "[INFO] Ripping ISO with MakeMKV..." + if ! snap run makemkv.makemkvcon -r mkv --minlength=1800 iso:"$iso" all "$ripdir"; then + echo "[ERROR] Failed to rip '$iso'. Skipping." + continue + fi + + for mkv in "$ripdir"/*.mkv; do + [[ -e "$mkv" ]] || continue + out="$OUT_DIR/Season $season/S${season}E$(printf "%02d" "$ep").mkv" + echo "[INFO] Converting to: $out" + if ffmpeg -nostdin -hide_banner -loglevel error -i "$mkv" \ + -map 0:v -map 0:a:m:language:eng -map 0:s:m:language:eng \ + -c copy "$out"; then + rm "$mkv" + ((ep++)) + else + echo "[ERROR] FFmpeg conversion failed for '$mkv'." + fi + done +done + +echo "[INFO] Conversion complete. Output in '$OUT_DIR'." diff --git a/scripts/jriver-exclusions.ps1 b/scripts/jriver-exclusions.ps1 new file mode 100644 index 0000000..d69a85c --- /dev/null +++ b/scripts/jriver-exclusions.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Adds JRiver Media Center folders & processes to Windows Defender exclusions + +.DESCRIPTION + powershell -ExecutionPolicy Bypass -File .\jriver-exclusions.ps1 +#> + +function Add-ItemExclusion { + param( + [string]$Item, + [ValidateSet('Path','Process')]$Type + ) + try { + if ($Type -eq 'Path') { + Add-MpPreference -ExclusionPath $Item -ErrorAction Stop + } else { + Add-MpPreference -ExclusionProcess $Item -ErrorAction Stop + } + Write-Host "Added ${Type}: ${Item}" + } + catch { + Write-Warning "Skipped/failed ${Type}: ${Item} - $_" + } +} + +Write-Host "Configuring JRiver Media Center (folders via wildcards, processes version 30–50)" + +# Folder exclusions (wildcards cover all files inside) +$folders = @( + 'C:\Program Files\J River', + 'C:\Program Files\J River\Media Center *', + "$Env:APPDATA\J River", + "$Env:APPDATA\J River\Media Center *" +) + +# Process exclusions (explicit versions 30–50) +$processes = @() +for ($v = 30; $v -le 50; $v++) { + $processes += "MC$v.exe" + $processes += "Media Center $v.exe" +} +# Add static processes that are version-independent +$processes += @('JRService.exe','JRWorker.exe','JRWeb.exe') + +# Add exclusions +Write-Host "=== Adding folder exclusions ===" +$folders | ForEach-Object { Add-ItemExclusion -Item $_ -Type Path } + +Write-Host "=== Adding process exclusions ===" +$processes | Sort-Object -Unique | ForEach-Object { Add-ItemExclusion -Item $_ -Type Process } + +# Validation step +$pref = Get-MpPreference +Write-Host '' +Write-Host "=== Current Defender exclusions ===" +Write-Host "Paths:" +$pref.ExclusionPath | ForEach-Object { Write-Host " $_" } +Write-Host '' +Write-Host "Processes:" +$pref.ExclusionProcess | ForEach-Object { Write-Host " $_" } +Write-Host '' +Write-Host "=== Validation complete ===" diff --git a/scripts/jriver-expressions.txt b/scripts/jriver-expressions.txt new file mode 100644 index 0000000..e4168ca --- /dev/null +++ b/scripts/jriver-expressions.txt @@ -0,0 +1,25 @@ +IfElse( + IsEqual([Media Type], Audio), + If(IsEqual([Media Sub Type], Podcast), + podcasts/Clean([Album],3), + music/Clean([Album Artist (auto)],3)/[[Year]] Clean([Album],3)), + IsEqual([Media Sub Type], Movie), + movies/Clean([Name], 3), + IsEqual([Media Sub Type], TV Show), + tv/Clean([Series],3)/Season PadNumber([Season], 2) +) + +IfElse( + IsEqual([Media Type], Audio), + If(IsEmpty([Disc #],1), + 1[Track #], + [Disc #][Track #]) - Clean([Artist] - [Name],3), + IsEqual([Media Sub Type], Movie), + Clean([Name],3) [[Year]], + IsEqual([Media Sub Type], TV Show), + Clean([Series] - S[Season]E[Episode] - [Name],3) +) + +IfElse(IsEqual([Media Type], Audio), If(IsEqual([Media Sub Type], Podcast), podcasts/Clean([Album],3), music/RemoveCharacters(Clean([Album Artist (auto)],3),.,2)/[[Year]] RemoveCharacters(Clean([Album],3),.,3)), IsEqual([Media Sub Type], Movie), movies/Clean(RemoveCharacters([Name],:), 3) [[Year]], IsEqual([Media Sub Type], TV Show), tv/Clean([Series],3)/Season PadNumber([Season], 2)) + +IfElse(IsEqual([Media Type], Audio), If(IsEmpty([Disc #],1), 1[Track #], [Disc #][Track #]) - RemoveCharacters(Clean([Artist] - [Name],3),.,3), IsEqual([Media Sub Type], Movie), Clean(RemoveCharacters([Name],:),3) [[Year]], IsEqual([Media Sub Type], TV Show), Clean([Series] - S[Season]E[Episode] - [Name],3)) diff --git a/scripts/jriver-fix-date-imported b/scripts/jriver-fix-date-imported new file mode 100755 index 0000000..6f92210 --- /dev/null +++ b/scripts/jriver-fix-date-imported @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +Fix JRiver date imported fields to use the earliest date for each album. +""" + +import sys +from pathlib import Path, PureWindowsPath + + +def get_album_path(line: str) -> str: + """Extract and return the album directory path from a filename field.""" + filename = line.lstrip('').rstrip('\n') + path = PureWindowsPath(filename) + return str(path.parent) + + +def get_date(line: str) -> int: + """Extract and return the date imported value from a date field.""" + date = line.lstrip('').rstrip('\n') + return int(date) + + +def main() -> None: + """Main function to process JRiver library file.""" + if len(sys.argv) != 3: + print("Usage: jriver-fix-date-imported ", file=sys.stderr) + sys.exit(1) + + in_file = Path(sys.argv[1]) + out_file = Path(sys.argv[2]) + + if not in_file.exists(): + print(f"[ERROR] Input file '{in_file}' does not exist.", file=sys.stderr) + sys.exit(1) + + # Read input file + with open(in_file, "r", encoding="utf-8") as f: + lines = f.readlines() + + # Build album to tracks mapping + albums: dict[str, list[tuple[int, int]]] = {} + current_album: str | None = None + + for lnum, line in enumerate(lines): + if '' in line: + current_album = get_album_path(line) + elif '' in line: + date = get_date(line) + if current_album: + albums.setdefault(current_album, []).append((lnum, date)) + + # Update lines with earliest date for each album + for _, tracks in albums.items(): + earliest_date: int = min(tracks, key=lambda t: t[1])[1] + for lnum, _ in tracks: + lines[lnum] = f'{earliest_date}\n' + + # Write output file + with open(out_file, 'w', encoding="utf-8") as f: + f.writelines(lines) + + print(f"[SUCCESS] Processed {len(albums)} albums. Output written to '{out_file}'.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/jriver-replace-date-imported b/scripts/jriver-replace-date-imported new file mode 100755 index 0000000..4fdf852 --- /dev/null +++ b/scripts/jriver-replace-date-imported @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Replace JRiver date imported fields with date modified when the latter is earlier. +""" + +import sys +from pathlib import Path + + +def get_import_date(line: str) -> int: + """Extract and return the date imported value from a date imported field.""" + date = line.lstrip('').rstrip('\n') + return int(date) + + +def get_create_date(line: str) -> int: + """Extract and return the date modified value from a date modified field.""" + date = line.lstrip('').rstrip('\n') + return int(date) + + +def main() -> None: + """Main function to process JRiver library file.""" + if len(sys.argv) != 3: + print("Usage: jriver-replace-date-imported ", file=sys.stderr) + sys.exit(1) + + in_file = Path(sys.argv[1]) + out_file = Path(sys.argv[2]) + + if not in_file.exists(): + print(f"[ERROR] Input file '{in_file}' does not exist.", file=sys.stderr) + sys.exit(1) + + # Read input file + with open(in_file, "r", encoding="utf-8") as f: + lines = f.readlines() + + # Process lines and replace dates where appropriate + import_date: int = 0 + date_imported_line: int = 0 + replacements = 0 + + for lnum, line in enumerate(lines): + if '' in line: + import_date = get_import_date(line) + date_imported_line = lnum + elif '' in line: + create_date = get_create_date(line) + if create_date < import_date: + print(f"[INFO] Replacing {import_date} with {create_date} at line {date_imported_line}") + lines[date_imported_line] = f'{create_date}\n' + replacements += 1 + + # Write output file + with open(out_file, 'w', encoding="utf-8") as f: + f.writelines(lines) + + print(f"[SUCCESS] Made {replacements} replacements. Output written to '{out_file}'.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/prune-files b/scripts/prune-files new file mode 100755 index 0000000..cde92bf --- /dev/null +++ b/scripts/prune-files @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# Remove all but the latest N versions of files matching given prefixes +# Usage: prune-files -k 3 thisfileprefix [thatfileprefix] + +set -euo pipefail + +prune-files() { + local -a PREFIXES + local KEEP_INT=1 # default number of files to keep + local DRY_RUN=false + + printHelpAndExit() { + cat <<-'EOF' + USAGE: + prune-files -k 3 thisfileprefix [thatfileprefix] + + OPTIONS: + -k, --keep NUMBER + Keep NUMBER of the latest files that match each file prefix (Default: 1) + -n, --dry-run + Show what would be removed without actually deleting files + -h, --help + Print this help dialog and exit + EOF + [[ -z "$1" ]] && exit 0 || exit "$1" + } + + parseInput() { + local _input + if _input=$(getopt -o hk:n -l help,keep:,dry-run -- "$@"); then + eval set -- "$_input" + while true; do + case "$1" in + -k|--keep) shift; KEEP_INT="$1" ;; + -n|--dry-run) DRY_RUN=true ;; + -h|--help) printHelpAndExit 0 ;; + --) shift; break ;; + esac + shift + done + else + echo "[ERROR] Incorrect option(s) provided" >&2 + printHelpAndExit 1 + fi + + if [[ $# -eq 0 ]]; then + echo "[ERROR] At least one file prefix must be provided" >&2 + printHelpAndExit 1 + fi + + if ! [[ "$KEEP_INT" =~ ^[0-9]+$ ]] || [[ "$KEEP_INT" -lt 1 ]]; then + echo "[ERROR] --keep must be a positive integer" >&2 + exit 1 + fi + + PREFIXES=("$@") + } + + findAndRemove() { + local prefix file count + + for prefix in "${PREFIXES[@]}"; do + count=0 + echo "[INFO] Processing files with prefix: $prefix" + + # List files matching the prefix sorted by modification time (latest first), + # then remove all except the first KEEP_INT files. + while IFS= read -r file; do + if [[ "$DRY_RUN" == true ]]; then + echo "[DRY-RUN] Would remove: $file" + else + echo "[INFO] Removing: $file" + if ! rm -- "$file"; then + echo "[ERROR] Failed to remove: $file" >&2 + fi + fi + ((count++)) + done < <( + find . -maxdepth 1 -type f -name "${prefix}*" -printf '%T@ %p\n' 2>/dev/null | \ + sort -rn | \ + awk -v keep="$KEEP_INT" 'NR > keep {print $2}' + ) + + if [[ $count -eq 0 ]]; then + echo "[INFO] No files to remove for prefix: $prefix" + else + echo "[INFO] Processed $count file(s) for prefix: $prefix" + fi + done + } + + main() { + parseInput "$@" + findAndRemove + } + + main "$@" +} + +# Allow script to be safely sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + prune-files "$@" + exit $? +fi \ No newline at end of file diff --git a/scripts/random-words b/scripts/random-words new file mode 100755 index 0000000..a8f2dcd --- /dev/null +++ b/scripts/random-words @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# This function will create a random word pair with an underscore separator ex. turtle_ladder +# It accepts one optional argument (an integer) (the number of words to return) + +set -euo pipefail + +random_words() { + local num="${1:-2}" + local -a arr + local word + + # Validate input + if ! [[ "$num" =~ ^[0-9]+$ ]] || [[ "$num" -lt 1 ]]; then + echo "[ERROR] Argument must be a positive integer" >&2 + return 1 + fi + + # Check if dictionary file exists + if [[ ! -f /usr/share/dict/words ]]; then + echo "[ERROR] Dictionary file /usr/share/dict/words not found" >&2 + return 1 + fi + + for ((i=0; i&2 + usage + exit 1 + ;; + *) + break + ;; + esac +done + +if [[ $# -lt 1 ]]; then + echo "[ERROR] You must provide a directory." >&2 + usage + exit 1 +fi + +dir="$1" +SIZE="${2:-1000}" + +if [[ ! -d "$dir" ]]; then + echo "[ERROR] Directory does not exist: $dir" >&2 + exit 1 +fi + +if ! [[ "$SIZE" =~ ^[0-9]+$ ]] || [[ "$SIZE" -lt 1 ]]; then + echo "[ERROR] SIZE_THRESHOLD must be a positive integer" >&2 + exit 1 +fi + +echo "[INFO] Searching for directories <= $SIZE KB in '$dir'..." + +# Find directories with size less or equal to SIZE +# Sort by depth (deepest first) to avoid removing parent before child +small_dirs=$(find "$dir" -mindepth 1 -type d -exec du -ks {} + | \ + awk -v size="$SIZE" '$1 <= size {print $2}' | \ + awk '{ print length, $0 }' | sort -rn | cut -d' ' -f2-) + +if [[ -z "$small_dirs" ]]; then + echo "[INFO] No directories with size <= $SIZE KB found in '$dir'." + exit 0 +fi + +echo "[INFO] Found $(echo "$small_dirs" | wc -l) directories to remove:" +echo "$small_dirs" + +if [[ "$DRY_RUN" == true ]]; then + echo "[DRY-RUN] Would remove the above directories." + exit 0 +fi + +read -r -p "Remove these directories? [y/N] " response +response="${response,,}" # Convert to lowercase + +if [[ ! "$response" =~ ^(yes|y)$ ]]; then + echo "[INFO] Exiting, no changes were made." + exit 0 +fi + +count=0 +while IFS= read -r small_dir; do + if [[ -d "$small_dir" ]]; then + echo "[INFO] Removing: $small_dir" + if rm -rf "$small_dir"; then + ((count++)) + else + echo "[ERROR] Failed to remove: $small_dir" >&2 + fi + fi +done <<< "$small_dirs" + +echo "[SUCCESS] Removed $count directories." \ No newline at end of file diff --git a/scripts/speedtest-compare b/scripts/speedtest-compare new file mode 100755 index 0000000..dc9d041 --- /dev/null +++ b/scripts/speedtest-compare @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# This script performs speedtests over Wireguard and native connections and prints their output + +set -euo pipefail + +usage() { + cat <<-EOF + Usage: speedtest-compare [OPTIONS] + + Compare network speed between Wireguard and native connections. + + OPTIONS: + -s, --server ID Specify server ID for native test (default: 17170) + -u, --upload Include upload speed test + -h, --help Display this help message + EOF +} + +# Parse options +UPLOAD_FLAG="--no-upload" +SERVER_ID="17170" + +while [[ $# -gt 0 ]]; do + case "$1" in + -s|--server) shift; SERVER_ID="$1"; shift ;; + -u|--upload) UPLOAD_FLAG=""; shift ;; + -h|--help) usage; exit 0 ;; + *) + echo "[ERROR] Unknown option: $1" >&2 + usage + exit 1 + ;; + esac +done + +# Check if speedtest-cli is installed +if ! command -v speedtest-cli &>/dev/null; then + echo "[ERROR] speedtest-cli is not installed. Please install it first." >&2 + exit 1 +fi + +run_test() { + local output pingBps pingPart bpsPart pingInt bpsInt mbpsInt + + # Run speedtest-cli and extract the 7th and 8th CSV fields + if ! output=$(speedtest-cli $UPLOAD_FLAG --csv "$@" 2>/dev/null); then + echo "[ERROR] Speedtest failed" >&2 + return 1 + fi + + pingBps=$(echo "$output" | cut -d"," -f7-8) + + # Extract ping value (as an integer) and bps (and convert to Mbps) + pingPart="${pingBps%,*}" + bpsPart="${pingBps#*,}" + pingInt="${pingPart%.*}" + bpsInt="${bpsPart%.*}" + mbpsInt=$(( bpsInt / 1000000 )) + + echo "$pingInt $mbpsInt" +} + +echo "[INFO] Running speedtest comparison..." +echo "" + +# Test Wireguard using automatic server selection +echo "Testing Wireguard connection..." +if output=$(run_test); then + read -r pingInt mbpsInt <<< "$output" + echo " Ping: ${pingInt}ms" + echo " Speed: ${mbpsInt}Mbps" +else + echo " [ERROR] Wireguard test failed" +fi + +echo "" + +# Test native connection to ISP +echo "Testing native connection (server: $SERVER_ID)..." +if output=$(run_test --server "$SERVER_ID"); then + read -r pingInt mbpsInt <<< "$output" + echo " Ping: ${pingInt}ms" + echo " Speed: ${mbpsInt}Mbps" +else + echo " [ERROR] Native test failed" +fi \ No newline at end of file diff --git a/scripts/ssh-wrap b/scripts/ssh-wrap new file mode 100755 index 0000000..c1ad450 --- /dev/null +++ b/scripts/ssh-wrap @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Usage: ssh-wrap user@host [ssh-options] +# Wrapper to handle SSH host key changes automatically + +set -uo pipefail + +if [[ $# -eq 0 ]]; then + echo "Usage: ssh-wrap user@host [ssh-options]" >&2 + exit 1 +fi + +# Capture SSH output +output=$(ssh "$@" 2>&1) +exit_code=$? + +# Print the SSH output so user sees what happened +echo "$output" + +# If SSH succeeded, we're done +if [[ $exit_code -eq 0 ]]; then + exit 0 +fi + +# Check if the known_hosts warning appears +if echo "$output" | grep -q "REMOTE HOST IDENTIFICATION HAS CHANGED"; then + echo "" + echo "[WARNING] Host key has changed - possible man-in-the-middle attack or host reinstall." + + # Extract the known_hosts file and line number from the "Offending RSA key in ..." line + # The line format typically is: "Offending RSA key in /path/to/known_hosts:line" + if offending_info=$(echo "$output" | grep "Offending.*key in"); then + KNOWN_HOSTS_FILE=$(echo "$offending_info" | awk '{print $5}' | cut -d: -f1) + LINE_NUMBER=$(echo "$offending_info" | awk -F: '{print $NF}') + + if [[ -z "$KNOWN_HOSTS_FILE" || -z "$LINE_NUMBER" || ! -f "$KNOWN_HOSTS_FILE" ]]; then + echo "[ERROR] Could not extract offending key information or file doesn't exist." >&2 + exit 1 + fi + + echo "[INFO] Offending key detected in: $KNOWN_HOSTS_FILE on line: $LINE_NUMBER" + read -rp "Remove offending key and retry SSH connection? [y/N]: " RESPONSE + + if [[ "$RESPONSE" =~ ^[Yy]$ ]]; then + # Backup known_hosts + if cp "$KNOWN_HOSTS_FILE" "$KNOWN_HOSTS_FILE.bak"; then + echo "[INFO] Backup created: $KNOWN_HOSTS_FILE.bak" + else + echo "[ERROR] Failed to create backup." >&2 + exit 1 + fi + + # Remove offending line + if sed -i "${LINE_NUMBER}d" "$KNOWN_HOSTS_FILE"; then + echo "[INFO] Offending key removed. Retrying SSH connection..." + ssh "$@" + else + echo "[ERROR] Failed to remove offending key." >&2 + exit 1 + fi + else + echo "[INFO] Key was not removed. Exiting." + exit 1 + fi + else + echo "[ERROR] Could not extract offending key information. Remove it manually if needed." >&2 + exit 1 + fi +else + # SSH failed for another reason + exit $exit_code +fi diff --git a/scripts/strip-exif b/scripts/strip-exif new file mode 100755 index 0000000..c4109d8 --- /dev/null +++ b/scripts/strip-exif @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Strips all EXIF data from images provided as arguments + +exiftool -all= "$@" diff --git a/scripts/sync-music b/scripts/sync-music new file mode 100755 index 0000000..0eba8ab --- /dev/null +++ b/scripts/sync-music @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Sync and transcode music files to a destination directory +set -e + +SRC="${1:?Source directory required}" +DST="${2:?Destination directory required}" +JOBS="${3:-12}" + +command -v opusenc >/dev/null || { echo "ERROR: opusenc not found" >&2; exit 1; } + +mkdir -p "$DST" + +echo "Syncing music from $SRC to $DST (using $JOBS parallel jobs)" + +# Process source files in parallel +process_file() { + local src="$1" + local rel="${src#"$SRC/"}" + local dst="$DST/$rel" + + case "${src,,}" in + *.flac) + dst="${dst%.*}.opus" + if [[ ! -f "$dst" || "$src" -nt "$dst" ]]; then + echo "Converting: $rel" + mkdir -p "$(dirname "$dst")" + opusenc --quiet --bitrate 160 --vbr "$src" "$dst" + fi + ;; + *.mp3) + if [[ ! -f "$dst" || "$src" -nt "$dst" ]]; then + echo "Copying: $rel" + mkdir -p "$(dirname "$dst")" + cp -p "$src" "$dst" + fi + ;; + esac +} + +export -f process_file +export SRC DST + +find -L "$SRC" -type f \( -iname "*.flac" -o -iname "*.mp3" \) -print0 | \ + xargs -0 -P "$JOBS" -I {} bash -c 'process_file "$@"' _ {} + +# Remove stray files +while IFS= read -r -d '' dst; do + rel="${dst#"$DST/"}" + base="${rel%.*}" + + case "${dst,,}" in + *.opus) + [[ -f "$SRC/$base.flac" || -f "$SRC/$base.FLAC" ]] && continue + echo "Removing: $rel" + rm -f "$dst" + ;; + *.mp3) + [[ -f "$SRC/$rel" ]] && continue + echo "Removing: $rel" + rm -f "$dst" + ;; + esac +done < <(find -L "$DST" -type f \( -iname "*.opus" -o -iname "*.mp3" \) -print0) + +# Clean empty directories +find "$DST" -type d -empty -delete 2>/dev/null || true + +echo "Done" \ No newline at end of file diff --git a/scripts/tmux-management b/scripts/tmux-management new file mode 100755 index 0000000..1c36d5d --- /dev/null +++ b/scripts/tmux-management @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Open a tiled tmux window with one pane per host each in its own tmux session. +# The local session is always the last (active) pane. + +set -euo pipefail + +# Configuration (override with env vars if desired) +HOSTS=(workstation laptop) # hosts in pane order +REMOTE_SESSION=${REMOTE_SESSION:-main} # tmux session on remotes +SYNCHRONIZE=${SYNCHRONIZE:-1} # 1 = broadcast keystrokes +INCLUDE_LOCAL=${INCLUDE_LOCAL:-1} # 0 = skip local host +LOCAL_SHELL_ONLY=${LOCAL_SHELL_ONLY:-0} # 1 = plain shell locally +DEBUG=${DEBUG:-0} + +debug() { if (( DEBUG )); then echo "Debug: $*"; fi; } + +# Returns 0 if $2 is found in nameref array $1 +array_contains() { + local -n arr=$1 + local needle=$2 + for element in "${arr[@]}"; do + [[ "$element" == "$needle" ]] && return 0 + done + return 1 +} + +LOCAL=$(hostname -s) + +# Build TARGETS list so that LOCAL is always last +TARGETS=() +for h in "${HOSTS[@]}"; do + [[ $h != "$LOCAL" ]] && TARGETS+=("$h") +done +if (( INCLUDE_LOCAL )); then + TARGETS+=("$LOCAL") +fi + +(( ${#TARGETS[@]} )) || { echo "No hosts to connect to."; exit 1; } + +SESSION=$(IFS=-; echo "${TARGETS[*]}") +debug "Session : $SESSION" +debug "Targets : ${TARGETS[*]}" + +# Re‑attach if session already exists +if tmux has-session -t "$SESSION" 2>/dev/null; then + exec tmux attach -t "$SESSION" +fi + +# Builds the command that will run inside a pane +open_cmd() { + local tgt=$1 + if [[ $tgt == "$LOCAL" ]]; then + if (( LOCAL_SHELL_ONLY )); then + printf '%q -l' "${SHELL:-bash}" + else + printf 'tmux -L %q new -A -s %q' "${SESSION}_local" "$REMOTE_SESSION" + fi + else + printf 'ssh -t %q tmux new -A -s %q' "$tgt" "$REMOTE_SESSION" + fi +} + +# Create the first pane +tmux new-session -d -s "$SESSION" -n "$SESSION" "$(open_cmd "${TARGETS[0]}")" + +# Create remaining panes +for tgt in "${TARGETS[@]:1}"; do + tmux split-window -t "$SESSION:0" -h "$(open_cmd "$tgt")" +done + +tmux select-layout -t "$SESSION:0" tiled +((SYNCHRONIZE)) && tmux setw -t "$SESSION:0" synchronize-panes on + +# Activate the last pane (local host) +local_index=$(( ${#TARGETS[@]} - 1 )) +tmux select-pane -t "$SESSION:0.$local_index" + +exec tmux attach -t "$SESSION" diff --git a/scripts/tree-to-markdown b/scripts/tree-to-markdown new file mode 100755 index 0000000..ac6f6ff --- /dev/null +++ b/scripts/tree-to-markdown @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Make a nice markdown file from a directory tree + +tree=$(tree -f --noreport --charset ascii "$1" | +sed -e 's/| \+/ /g' -e 's/[|`]-\+/ */g' -e 's:\(* \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g') +printf "# Code/Directory Structure:\n\n%s" "$tree" \ No newline at end of file diff --git a/scripts/update-git-hooks b/scripts/update-git-hooks new file mode 100755 index 0000000..db81b6e --- /dev/null +++ b/scripts/update-git-hooks @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Update the post-receive hooks of multiple bare git repos + +for i in /var/lib/git/gogs-repositories/bryan/*/hooks/post-receive; do + # Get repo name + rn="${i%/hooks/post-receive}" + rn="${rn##*/}" + + # Don't duplicate the line if it already exists + while IFS= read -r line; do + [[ "$line" == "git push --mirror git@github.com:cryobry/${rn}" ]] && continue + done < "$i" + + # Append the line + echo "git push --mirror git@github.com:cryobry/${rn}" >> "$i" +done \ No newline at end of file