snippets
genpasswd
LC_CTYPE=C.UTF-8 tr -dc 'A-Za-z0-9=_-' < /dev/urandom | head -c 32 | xargs
ssh
ssh proxy in one port
ssh -L $LOCALPORT:$REMOTEADDR:$REMOTEPORT $JUMPHOST
ssh socks proxy
ssh -D 8080 $JUMPHOST
After that use socks-proxy config in browser to localhost:8080
ssh gen pub key
ssh-keygen -f ~/.ssh/id_rsa -y > ~/.ssh/id_rsa.pub
sed remove all comments from file
sed -e '/^[[:blank:]]*[#;]/d;s/#.*//' -e '/^[[:space:]]*$/d' $file
hex to base64
echo '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37' | sed 's?0x??' | tr '[[:upper:]]' '[[:lower:]]' | xxd -ps -r | base64
regex fqdn
^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$
network
get all open ports on server devided with ,
ss -tulpn | awk '{print $5}' | awk -F: '{print $NF}' | grep -v Local | sort | uniq | paste -d, -s
get host's ip addr
hostname -I # all addresses
curl ifconfig.me # outer ip address
nmap to check open ports
nmap -Pn $ADDR -p $PORTS
vim
set mouse-=a
from root
:! echo 'set mouse-=a' >> $VIMRUNTIME/defaults.vim
add lines to file without editor
tee -a ~/.ssh/config << END
Host localhost
ForwardAgent yes
END
curl
curl write out format
{"http_code": %{http_code}, "time_namelookup": %{time_namelookup}, "time_connect": %{time_connect}, "time_appconnect": %{time_appconnect}, "time_pretransfer": %{time_pretransfer}, "time_redirect": %{time_redirect}, "time_starttransfer": %{time_starttransfer}, "time_total": %{time_total} }\n
or
curl -w @/Users/booger/occamfi/notes/_usefull/curl_write_out_format.txt -s -o/dev/null http://aeza.boogerman.xyz/joshuto.sh
arguments to script from curl
curl http://example.com/script.sh | bash -s -- arg1 arg2
bash
export .env
linux
export $(grep -v '^#' .env | xargs -d '\n')
macos
export $(grep -v '^#' .env | xargs -0)
bash locale problem
localedef -i en_US -f UTF-8 en_US.UTF-8
locale -a | grep UTF-8
locale-gen en_US.UTF-8
update-locale LANG=en_US.UTF-8
echo 'export LC_ALL=en_US.UTF-8' >> ~/.bashrc
echo 'export LANG=en_US.UTF-8' >> ~/.bashrc
source ~/.bashrc
Probably need to add to ssh config
Host your_remote_server
SendEnv LANG LC_*
for loop
https://www.cyberciti.biz/faq/bash-for-loop-array/
prep.sh
curl https://aeza.boogerman.xyz/prep.sh | bash
swap file
dd if=/dev/zero of=/swapfile bs=1024M count=4 && \
chmod 600 /swapfile && \
mkswap /swapfile && \
swapon /swapfile && \
swapon -s && \
echo '/swapfile swap swap defaults 0 0' | tee -a /etc/fstab
ranger / joshuto
install joshito
RELEASE_VER='v0.9.8' INSTALL_PREFIX="/usr/local/bin" bash <(curl -s https://raw.githubusercontent.com/kamiyaa/joshuto/master/utils/install.sh)
or
curl -L https://github.com/kamiyaa/joshuto/releases/download/v0.9.8/joshuto-v0.9.8-x86_64-unknown-linux-musl.tar.gz | tar zx --strip-components=1 -C /usr/local/bin/
or
curl https://aeza.boogerman.xyz/joshuto.sh | bash
config for joshuto
##TODO: make safe for that
du ncdu dust
curl -L https://github.com/bootandy/dust/releases/download/v1.1.1/du-dust_1.1.1-1_amd64.deb -O && dpkg -i du-dust_1.1.1-1_amd64.deb && rm du-dust_1.1.1-1_amd64.deb
tar zstd
tar c /home | nice -n1 zstd --long --adapt -T0 --auto-threads=logical -c > /srv/home.tar.zst
k8s
get all pod with namespaces
k get pod -A -o go-template='{{ range $depl := .items }}{{ .metadata.namespace }}/{{.metadata.name }};{{end}}' | awk -F';' '{for(i=1; i<=NF; i++) print $i}'
get all depl with namespaces
k get deployments.apps -A -o go-template='{{ range $depl := .items }}{{ .metadata.namespace }}/{{.metadata.name }};{{end}}' | awk -F';' '{for(i=1; i<=NF; i++) print $i}'
get all ingresses with namespaces
k get ingresses -A -o go-template='{{ range $depl := .items }}{{ .metadata.namespace }}/{{.metadata.name }};{{end}}' | awk -F';' '{for(i=1; i<=NF; i++) print $i}'
helm: delete resources which generated via helm chart
helm template rel-Name path/to/chart --namespace ns | kubectl delete -f -
helm template from unified
helm template --debug -n default app oci://registry.fulgur.tech/library/chart
kubectl start debian bash
c
kubectl delete pod handbash
apt update
apt install -y curl dnsutils netcat-openbsd traceroute
kubectl add tls cert
kubectl get secrets --field-selector=type=kubernetes.io/tls
kubectl create secret tls fulgur.io --cert=path/to/tls.crt --key=path/to/tls.key
kubectl move from old cluster
kubectl --context old_context -n namespace get secret some_secret -oyaml | grep -vE 'creationTimestamp|namespace|resourceVersion|uid' | kubectl --context new_context apply -f -
check later
https://github.com/stakater/Reloader
yq
yq install
export VERSION=v4.45.4 BINARY=yq_$( uname | tr '[[:upper:]]' '[[:lower:]]' )_amd64
curl -L https://github.com/mikefarah/yq/releases/download/$VERSION/$BINARY.tar.gz | tar xz && mv $BINARY /usr/bin/yq
yq delete comments
cat sample.yaml | yq eval '... comments=""'
jq
cat docker.images.json | jq -r '.[] | [.ID,.Repository] | @tsv'
jq try to parse
cat $file | jq -R '. as $line | try (fromjson) catch $line'
graphql request
curl -g \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 8c7dbd270cb98e83f9d8d57fb8a2ab7bac9d7501905fb013c69995ebf1b2a719" \
-d '{"query":"query{showCollection {items { title firstEpisodeDate lastEpisodeDate henshinMp4 { url }}}}"}' \
https://graphql.contentful.com/content/v1/spaces/mt0pmhki5db7
make
make help
help: ## Show this help
@printf "\033[33m%s:\033[0m\n" 'Available commands'
@awk 'BEGIN {FS = ":.*?## "} /^[[:alpha:][:punct:]]+:.*?## / {printf " \033[32m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
make list
do not insert it to makefile - cause recursiveness
make -npq : 2> /dev/null | awk -v RS= -F: '$1 ~ /^[^.#%]+$/ { print $1 }' | xargs
rust
rust minimal install
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain stable
cargo clean cache
cargo install cargo-cache && cargo cache -a
tracing in rust
rustfmt.toml
tab_spaces = 4
hard_tabs = true
fn_params_layout = "Tall"
match_block_trailing_comma = true
optimizations
[profile.release]
opt-level = 3 # Highest optimization for speed (default in release, but explicit is fine).
lto = "fat" # Enables full link-time optimization for better inlining and dead code elimination across crates.
codegen-units = 1 # Reduces parallelism in codegen to allow more aggressive optimizations (increases compile time).
panic = "abort" # Aborts on panic instead of unwinding, reducing overhead and binary size.
strip = "symbols" # Strips debug symbols and unused code for a leaner binary.
bash
export .env with spaces
export $(grep -v '^#' .env | xargs -d '\n')
#!/bin/sh
## Usage:
## . ./export-env.sh ; $COMMAND
## . ./export-env.sh ; echo ${MINIENTREGA_FECHALIMITE}
unamestr=$(uname)
if [ "$unamestr" = 'Linux' ]; then
export $(grep -v '^#' .env | xargs -d '\n')
elif [ "$unamestr" = 'FreeBSD' ] || [ "$unamestr" = 'Darwin' ]; then
export $(grep -v '^#' .env | xargs -0)
fi
date with most universe format
date '+%FT%T.%N%:z'
bash history keep more
# ~/.bashrc
export HISTTIMEFORMAT="%h %d %H:%M:%S "
export HISTSIZE=5000
export HISTFILESIZE=5000
shopt -s histappend
export PROMPT_COMMAND='history -a'
golnag
golang install
export VERSION=1.24.1 && mkdir -p /opt/go/$VERSION | curl -L https://go.dev/dl/go${VERSION}.linux-amd64.tar.gz | tar -xz -C /opt/go/${VERSION} && echo "export PATH=$PATH:/opt/go/$VERSION/go/bin" >> ~/.bashrc && source ~/.bashrc && go version
nginx
nginx basic auth
location / {
auth_basic "Restricted Content";
auth_basic_user_file /etc/nginx/.htpasswd;
}
ingress controller logs parser in loki
nginx log regex https://regex101.com/r/eDdwzW/1
^(?P<client_ip>\S+) - (?P<remote_user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<request>(?P<request_method>\S+) (?P<request_uri>\S+) (?P<request_httpv>\S+))?" (?P<status>\d+) (?P<body_bytes_sent>\d+) "(?P<http_referer>[^"]+)" "(?P<http_user_agent>[^"]+)" (?P<request_length>\d+) (?P<request_time>\d+\.\d+) \[(?P<proxy_upstream_name>\S+)?\] \[(?P<proxy_alternative_upstream_name>\S+)?\] (?P<upstream_addr>\S+(\-)?) ((?P<upstream_response_length>\d+)|-) ((?P<upstream_response_time>\d+\.\d+)|\-) ((?P<upstream_status>\d+)|-) (?P<req_id>\S+)
nginx conditional logging
map $status $loggable {
~^[23] 0;
default 1;
}
access_log /path/to/access.log main if=$loggable;
nginx logrotate
/var/log/nginx/*.log {
hourly
maxsize 500M
dateext
missingok
rotate 72
compress
#delaycompress
notifempty
create 0640 root root
sharedscripts
compresscmd /usr/bin/zstd
uncompresscmd /usr/bin/unzstd
compressoptions -9 --long -T0
compressext .zst
postrotate
/usr/bin/docker exec nginx-balancer nginx -s reload
endscript
}
openssl htaccess
printf "sbt-indexer:$(openssl passwd -apr1 cBGuVZz3rmGegcfjVPMkSu8F)\n" | tee -a /etc/nginx/.htpasswd
git
git archive
git archive --format tar.gz -o archive.tar.gz HEAD
git tags
git tag --sort=committerdate -l 'v*'
git ls-remote --tags origin
git checkout tag
git fetch --all --tags
git checkout tags/v1.0 -b v1.0-branch
git keep credentials to https
git config --global credential.helper store
git fetch
git latest tag
git tag --sort=committerdate | grep -E '[0-9]' | tail -1
git largest objects in history
git rev-list --objects --all --missing=print |
git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
sed -n 's/^blob //p' |
sort --numeric-sort --key=2 |
cut -c 1-12,41- |
$(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest |
tail -30
.git/info/exclude
The TL;DR answer is that neither .git/info/exclude nor .gitignore have any effect on a tracked file. Using git update-index --skip-worktree (or --assume-unchanged) just make Git stop comparing the index version to the work-tree version.
nice every process for user
/etc/security/limits.conf
user hard priority 1
ipv6
preffer ipv4 addresses
resolve problem when curl or another http client trying to resolve name to ip and get ipv6 that can be not accessable
/etc/gai.conf
label ::1/128 0
label ::/0 1
label 2002::/16 2
label ::/96 3
label ::ffff:0:0/96 4
precedence ::1/128 50
precedence ::/0 40
precedence 2002::/16 30
precedence ::/96 20
precedence ::ffff:0:0/96 100
docker
restarted runners
docker ps | grep runner | grep Rest | cut -d' ' -f1 | xargs -L1 docker inspect | jq -r '.[]| .Config.Env[]' | grep REPO | tee >(wc -l)
docker compose resources limits
version: "3.8"
services:
redis:
image: redis:alpine
deploy:
resources:
limits:
cpus: '0.01'
memory: 1000M
docker manifest
check that image exists on registry before build (from)
docker manifest inspect registry.domain.com/repo/image:tag
nodejs
start node js container in app dir
docker run -v "$(pwd)":/app -w /app -it --rm --entrypoint /bin/bash node:22.14
npm install -g pnpm@10.3.0 turbo@2.5.0
nodejs memory issue
export NODE_OPTIONS=--max-old-space-size=8192
# retry
python
python parse http response date
#datetime.datetime.strptime('Wed, 23 Sep 2009 22:15:29 GMT', '%a, %d %b %Y %H:%M:%S GMT')
resp = requests.get(URL)
date_str = resp.headers["Date"]
_dt = datetime.strptime(self.lastBlockTime_str, '%a, %d %b %Y %H:%M:%S GMT')
prom_timestamp = _dt.timestamp() * 1000
ip addresses pool
>>> import ipaddress
>>> [str(ip) for ip in ipaddress.IPv4Network('192.0.2.0/28')]
['192.0.2.0', '192.0.2.1', '192.0.2.2',
'192.0.2.3', '192.0.2.4', '192.0.2.5',
'192.0.2.6', '192.0.2.7', '192.0.2.8',
'192.0.2.9', '192.0.2.10', '192.0.2.11',
'192.0.2.12', '192.0.2.13', '192.0.2.14',
'192.0.2.15']
grafana
loki
count_over_time({cluster="proheku"}[1m])
github
clean workflow dir
- uses: eviden-actions/clean-self-hosted-runner@v1
if: ${{ always() }}
aws
ecr
export REGION=eu-central-1
export ACCOUNT=`aws sts get-caller-identity | jq -r .Account`
docker build -t ${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${REPO} .
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com
docker push ${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${REPO}
s3 find by name in bucket
aws s3api list-objects --bucket $BUCKET_NAME --query "Contents[?contains(Key, '$FILENAME')]"
Find files modified on a given date
aws s3api list-objects --bucket $BUCKET_NAME --query "Contents[?contains(LastModified) < '2023-06-12')]"
Find files modified between given times
aws s3api list-objects --bucket $BUCKET_NAME --query "Contents[?LastModified > '2017-08-03T23' && LastModified < '2017-08-03T23:15']"
debian
noninteractive apt
export DEBIAN_FRONTEND=noninteractive && apt update && apt install -y --no-install-recommends pv file curl vim make jq ripgrep
apt mark do not update
apt-mark hold locales && apt update && apt install -y --no-install-recommends build-essential && apt-mark unhold locales
Force Apt-Get to IPv4 or IPv6 on Ubuntu or Debian
apt-get -o Acquire::ForceIPv4=true update
apt-get -o Acquire::ForceIPv6=true update
Persistent option
sudoedit /etc/apt/apt.conf.d/99force-ipv4
# Put the following contents in it:
Acquire::ForceIPv4 "true";
Vim
vim as default editor
sudo update-alternatives --set editor /usr/bin/vim.basic
some usefull vim config
echo 'syntax on\nfiletype plugin indent on\nset mouse-=a\nautocmd FileType yaml setlocal ts=2 sts=2 sw=2 expandtab' > /etc/vim/vimrc.local
problems with virtio_net
/etc/initramfs-tools/modules
virtio_pci
virtio_blk
virtio_net
then running sudo update-initramfs -u
then rebooting
ubuntu
delete snap
snap list
### delete all snaps with
sudo snap remove --purge {}
sudo snap remove --purge bare
sudo snap remove --purge core20
sudo snap remove --purge snapd
sudo apt remove --autoremove snapd
sudo tee -a /etc/apt/preferences.d/nosnap.pref <<EOF
Package: snapd
Pin: release a=*
Pin-Priority: -10
EOF
dmesg from journalctl
journalctl -b
mysql
install on debian
export DEBIAN_FRONTEND=noninteractive \
&& apt update && apt install -y wget gnupg lsb-release && wget https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb && dpkg -i ./mysql-apt-config_0.8.29-1_all.deb && apt update && apt install -y mysql-client
get size of databases
SELECT table_schema "DB Name",
ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) "DB Size in MB"
FROM information_schema.tables
GROUP BY table_schema;
show tables
show tables;
select to kill client connections
SELECT
CONCAT('KILL ', id, ';')
FROM INFORMATION_SCHEMA.PROCESSLIST
WHERE `User` = 'some_user'
AND `Host` = '192.168.1.1'
AND `db` = 'my_db';
redis
install redis-cli on debian
sudo apt-get install redis-tools
hetzner
use nat to access ipv4 egress
xml format
cat $some_xml_file | xmllint --format -
clickhouse
select size of tables/databases
select
parts.*,
columns.compressed_size,
columns.uncompressed_size,
columns.ratio
from (
select database,
table,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed_size,
formatReadableSize(sum(data_compressed_bytes)) AS compressed_size,
sum(data_compressed_bytes) / sum(data_uncompressed_bytes) AS ratio
from system.columns
group by database, table
) columns right join (
select database,
table,
sum(rows) as rows,
max(modification_time) as latest_modification,
formatReadableSize(sum(bytes)) as disk_size,
formatReadableSize(sum(primary_key_bytes_in_memory)) as primary_keys_size,
any(engine) as engine,
sum(bytes) as bytes_size
from system.parts
where active
group by database, table
) parts on ( columns.database = parts.database and columns.table = parts.table )
order by parts.bytes_size desc;
fail2ban
$ sudo cat /etc/fail2ban/jail.local
[sshd]
+ backend=systemd
enabled = true
mdbook
docs about book - https://rust-lang.github.io/mdBook/index.html
mermaid
flowchart LR
classDef done fill:#2f2,stroke:#111,color:#111,stroke-width:4px;
classDef inwork fill:#882,stroke:#111,color:#111,stroke-width:4px;
classDef conf fill:#55f,stroke:#111,color:#111,stroke-width:4px;
undone -->
conf:::conf -->
serv:::inwork -->
done:::done
ansible
adhoc copy file
ansible all --module-name copy --args "src=/tmp/foo.txt dest=/tmp/foo.txt remote_src=true"
macos
sudo powermetrics | grep "CPU Average frequency as fraction of nominal"
postgres
some output formatting options
postgresql pager off
\pset pager 0
postgres psql print only data
\t
postgres aling off
\a 0
postgresql pg_stat_statements longest query
SELECT query, total_time, calls, rows FROM pg_stat_statements ORDER BY total_time DESC LIMIT 1;
postgres connections
SELECT pid, datname, usename, application_name,client_hostname, client_port, backend_start, query_start,query, state FROM pg_stat_activity
-- WHERE state = 'active'
;
install client on debian
export DEBIAN_FRONTEND=noninteractive && apt update && apt -y --no-install-recommends install postgresql-client
systemd
user`s service
# enable user`s systemd units on boot (lingering)
export NONROOTUSER=galactica
loginctl enable-linger "$NONROOTUSER"
# check
loginctl list-users
# force to edit systemd unit file for user` service
su "$NONROOTUSER"
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$UID/bus"
export XDG_RUNTIME_DIR=/run/user/$(id -u)
echo 'export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$UID/bus"' >> ~/.bashrc
echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
systemctl edit --user --force --full some-service.service
# enter systemd unit file
systemctl --user start some-service.service
systemctl --user enable some-service.service
[Unit]
Description=My Custom Service
After=default.target
Wants=mnt-kingston.automount
[Service]
Type=simple
WorkingDirectory=/mnt/kingston/influxdb-1.8.10-1
ExecStart=/mnt/kingston/influxdb-1.8.10-1/usr/bin/influxd run -config /mnt/kingston/influxdb-1.8.10-1/etc/influxdb/influxdb.conf
Restart=always
[Install]
WantedBy=default.target
systemctl --user start some-service.service
in some cases
unit for github runner under non root user
[Unit]
Description=Github runner for galactica ci tests in repo https://github.com/boogeroccam/galactica/settings/actions/runners
After=default.target
[Service]
Type=simple
WorkingDirectory=/home/devops
ExecStart=/home/devops/run.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
action runner
[Unit]
Description=github runner service
After=default.target
[Service]
Type=simple
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/chat/.local/bin"
Environment=XDG_RUNTIME_DIR=/run/user/30001
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/30001/bus
Environment=HOME=/home/chat
WorkingDirectory= /home/deploy/actions-runner/
ExecStart= /home/chat/actions-runner/run.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
systemd automount
sudo mkfs.ext4 /dev/xvdh
sudo mkdir /data
tee -a ~/mnt-kingston.mount <<EOT
[Unit]
Description=Mount partition
[Mount]
What=/dev/sdb1
Where=/mnt/kingston
Type=ext4
Options=rw,noatime
[Install]
WantedBy=multi-user.target
EOT
sudo mv ~/mnt-kingston.mount /etc/systemd/system/
tee -a ~/mnt-kingston.automount <<EOT
[Unit]
Description=Automount partition
ConditionPathExists=/mnt/kingston
[Automount]
Where=/mnt/kingston
TimeoutIdleSec=10
[Install]
WantedBy=multi-user.target
EOT
sudo mv ~/mnt-kingston.mount /etc/systemd/system/
sudo chown root:root /etc/systemd/system/mnt-kingston.mount /etc/systemd/system/mnt-kingston.automount
sudo chmod 755 /etc/systemd/system/mnt-kingston.mount /etc/systemd/system/mnt-kingston.automount
sudo systemctl daemon-reload
sudo systemctl enable --now data.automount
automount exfat
#!/bin/bash
path=/home/booger/subdisk
disk=/dev/nvme0n1p3
unit_filename=`echo $path | tr '/' '-' | cut -c 2-`
tee -a $unit_filename.mount <<EOT
[Unit]
Description=Mount partition
[Mount]
What=$disk
Where=$path
Type=exfat
Options=defaults,uid=1000,gid=1000,umask=002
[Install]
WantedBy=multi-user.target
EOT
sudo mv $unit_filename.mount /etc/systemd/system/
tee -a $unit_filename.automount <<EOT
[Unit]
Description=Automount partition
ConditionPathExists=$path
[Automount]
Where=$path
TimeoutIdleSec=10
[Install]
WantedBy=multi-user.target
EOT
sudo mv $unit_filename.automount /etc/systemd/system/
sudo chown root:root /etc/systemd/system/$unit_filename.mount /etc/systemd/system/$unit_filename.automount
sudo chmod 755 /etc/systemd/system/$unit_filename.mount /etc/systemd/system/$unit_filename.automount
sudo systemctl daemon-reload
sudo systemctl enable --now $unit_filename.automount
systemd
some adv unit for service
[Unit]
Description=pg-cache-sync service
After=network.target
[Service]
Type=simple
WorkingDirectory=/srv/nodesale-app/packages/pg-cache-sync/
ExecStart=/srv/nodesale-app/packages/pg-cache-sync/pg-cache-sync
Environment=DB_URL="postgresql://nsale:pass@10.24.1.7:5433/nsale"
Environment=HOST=0.0.0.0
Environment=PORT=8080
Environment=LOG_LEVEL=debug
Environment=LOG_FORMAT=json
Environment=SECRET_KEY=key
Environment=NODE_OPTIONS=--max-old-space-size=2048
Restart=always
RestartSec=5
LimitNOFILE=65535
TimeoutStopSec=30
; ProtectSystem=full
; ProtectHome=yes
PrivateTmp=true
NoNewPrivileges=true
SyslogIdentifier=pg-cache-sync
StandardOutput=syslog
StandardError=syslog
; MemoryLimit=500M
; CPUQuota=50%
; ExecStartPre=/usr/bin/bash -c "until nc -z -v -w30 10.24.1.7 5433; do echo 'Waiting for DB...'; sleep 5; done"
[Install]
WantedBy=multi-user.target
SOPS Usage Guide
Setup
Installation
You can install SOPS either by downloading the binary directly or using a package manager.
From Repository or Binary
- Visit the SOPS GitHub Repository.
- Download the binary (ensure you select the appropriate version for your OS and architecture):
curl -LO https://github.com/getsops/sops/releases/download/v3.9.2/sops-v3.9.2.linux.amd64 - Move the binary to your
PATH:mv sops-v3.9.2.linux.amd64 /usr/local/bin/sops - Make the binary executable:
chmod +x /usr/local/bin/sops
tldr:
curl -LO https://github.com/getsops/sops/releases/download/v3.9.2/sops-v3.9.2.linux.amd64 && mv sops-v3.9.2.linux.amd64 /usr/local/bin/sops && chmod +x /usr/local/bin/sops
From a Package Manager (macOS Example)
Install SOPS using Homebrew:
brew install sops
GPG Keys Setup
Check Existing GPG Keys
To list your GPG keys:
- Private keys:
gpg -K - Public keys:
gpg -k
Create a New GPG Key
Follow the GitHub guide for generating GPG keys. Key generation steps include:
- Generate a new key:
gpg --default-new-key-algo ed25519 --full-generate-key - List the public key IDs:
gpg --list-secret-keys --keyid-format=long - Export your public key:
gpg --armor --export ${YOUR_KEY_ID}
Add Your Public Key to GitHub
- Copy your GPG public key. It starts with:
And ends with:-----BEGIN PGP PUBLIC KEY BLOCK----------END PGP PUBLIC KEY BLOCK----- - Follow GitHub's guide to add the key.
Working with Encrypted Files
Basic Operations
Decrypting Files
To decrypt a file, you must have a private key listed in the sops metadata of the encrypted file.
Encrypting Files
To encrypt a file, you need all public keys that will be used for encryption. Add your public key to GitHub so others can import it from your profile.
For advanced use cases (e.g., encrypting with AWS KMS or Age), refer to the SOPS repository documentation.
Editing an Encrypted File
- Edit an encrypted file with your default editor:
sops file.yaml - To use a specific editor:
EDITOR='code --wait' sops file.yaml - Commit changes:
- Add the modified lines to the git index.
Note: If changes are made outside SOPS, the file won't decrypt properly. Use the
--ignore-macoption if needed.
Encrypting a File
To encrypt a file with a GPG key:
sops -e -i --pgp "${GPG_KEY_ID}"
# Options:
# -e: Encrypt
# -i: In-place encryption
# --pgp: Specify public GPG keys for encryption
# "${GPG_KEY_ID}" -- Public key ids separated with comma
Decrypting a File
- Decrypt to stdout:
sops -d file.yaml - Decrypt in place:
sops -i -d file
gpg tty on macos
- Проверить что
which pinentry-macчто-то отдаст иначе установить черезbrew isntall pinentry-mac - Добавить в файл конфигурации
~/.gnupg/gpg-agent.conf
default-cache-ttl 34560000
max-cache-ttl 34560000
max-cache-ttl-ssh 34560000
default-cache-ttl-ssh 34560000
pinentry-program /usr/local/bin/pinentry-mac
export GPG_TTY=$(tty)и добавить эту строку в~/.zshrc- Перезагрузить gpg-agent
gpgconf --kill gpg-agent
Case Setting Up a New Repository for SOPS
Example .sops.yaml
Create a .sops.yaml file in the repository root. This file defines keys and rules for encryption.
# YAML Formatting:
stores:
yaml:
indent: 2
# Encryption Rules:
creation_rules:
- pgp: >- ## List of GPG key IDs for encryption
14E89E005155B68088D72693D7D61759A1EEEB5D,
CD10A854ADDD2402BE1EF5AD075D1B59D139D890,
5EF7079DE682A4BA200A834726EA0666E4FE5472,
713429BA80BCB22675989D3AD1C9A13C324ECE5E
## <devops@occam.fi>,<mikhail@occam.fi>,<ilya@occam.fi>,<yakudgm@gmail.com>
# encrypted_regex: ^SecretEnv ## (Optional) Regex for encrypting specific YAML keys inside files
# path_regex: values.*.yaml$ ## (Optional) Regex to target specific files
Encrupt files with sops
sops -e -i ops/helm-values/dev/app.sops.yaml
Case: decrypt in github actions
- setup secret variable to get ci gpg key into action
export REPO=owner/repo
export VALUE=$(gpg --export-secret-keys 14E89E005155B68088D72693D7D61759A1EEEB5D | base64)
# with a github cli
gh secret set SOPS_CI_KEY --repo ${REPO} --body "$VALUE"
## gh secret set SOPS_CI_KEY --repo https://github.com/badaitech/bad-connector --body "$(gpg --export-secret-keys 14E89E005155B68088D72693D7D61759A1EEEB5D | base64)"
# or in settings of the repo
- make sure that gpg available in github runner
jobs:
job:
steps:
- uses: mdgreenwald/mozilla-sops-action@v1.6.0
- name: import gpg ci key
run: |
echo ${{ secrets.SOPS_CI_KEY }} | base64 -d | gpg --import
# ... do stuff
- name: deploy
run: |
sops -d config.sops.yaml > config.yaml
- name: delete ci gpg key
if: always()
run: |
gpg -K --with-colons | awk -F: '/^(fpr)/ {print $10; exit}' | xargs gpg --delete-secret-and-public-keys --yes --batch
Case: replace one key with another
export oldkey=9585F123A7E98549AD7E88FA300C05FE86177A5C
export newkey=96F339BEC4A967D173406DE0E189CDDBCAF01351
rg "fp: $oldkey" -l | xargs -n1 sops -r -i --add-pgp=$newkey --rm-pgp=$oldkey
Advanced case: with ansible
https://docs.ansible.com/ansible/latest/collections/community/sops/load_vars_module.html
- name: Configure PostgreSQL Role and Database
hosts: psql-prod
become: true
vars:
db_environment: bad_connector_stage
pre_tasks:
- name: Include variables of secrets.sops.yaml into the variable
community.sops.load_vars:
file: ./../secret.sops.yaml
name: secret
- name: Set variables from secret
set_fact:
db_user: "{{ secret['db_environment'][db_environment]['db_user'] }}"
db_password: "{{ secret['db_environment'][db_environment]['db_password'] }}"
db_database: "{{ secret['db_environment'][db_environment]['db_database']}}"
tasks:
hetzner rescue
installimage
by default hetzner offers rescue os whem you add server
From start you should check hardware (disks,cpu,etc)
After that cmd installimage
Input
DRIVE1 /dev/sda
DRIVE2 /dev/sdb
FORMATDRIVE1 1
FORMATDRIVE2 1
SWRAID 0
SWRAIDLEVEL 0
PART /boot/efi esp 256M
PART /boot ext3 1024M
PART lvm vg0 all
LV vg0 root / ext4 all
BOOTLOADER grub
HOSTNAME machine
#IPV4_ONLY yes
#IMAGE /root/images/Ubuntu-2204-jammy-amd64-base.tar.gz
Galactica node rescue config
echo <<ECHO | tee -a install-config
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme1n1
DRIVE3 /dev/nvme2n1
FORMATDRIVE1 1
FORMATDRIVE2 1
FORMATDRIVE3 1
SWRAID 0
SWRAIDLEVEL 0
BOOTLOADER grub
HOSTNAME gala-reti-bm-node02
PART /boot/efi esp 256M
PART /boot ext3 512M
PART / ext4 50G
PART lvm vg0 all
LV vg0 root /galactica ext4 all
IMAGE /root/images/Debian-bookworm-latest-amd64-base.tar.gz
ECHO
installimage -c install-config -a
In guest os to add lvm:
pvcreate /dev/nvme1n1
vgextend vg0 /dev/nvme1n1
lvextend -l +100%FREE /dev/vg0/root
resize2fs /dev/vg0/root
or
for disk in nvme0n1 nvme1n1 ; do echo $disk ; pvcreate /dev/$disk && vgextend vg0 /dev/$disk && lvextend -l +100%FREE /dev/vg0/root ; done && resize2fs /dev/vg0/root
in vm
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
ufw allow 22
ufw enable
# apt install fail2ban
# systemctl status fail2ban.service
# vim /etc/fail2ban/jail.local
crowdsec / fail2ban
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
sudo apt-get update
sudo apt-get install crowdsec
[INCLUDES]
before = paths-debian.conf
[DEFAULT]
ignorecommand =
bantime = 10m
findtime = 10m
maxretry = 5
maxmatches = %(maxretry)s
backend = auto
usedns = warn
logencoding = auto
enabled = false
mode = normal
filter = %(__name__)s[mode=%(mode)s]
destemail = root@localhost
sender = root@<fq-hostname>
mta = sendmail
protocol = tcp
chain = <known/chain>
port = 0:65535
fail2ban_agent = Fail2Ban/%(fail2ban_version)s
banaction = iptables-multiport
banaction_allports = iptables-allports
action_ = %(banaction)s[port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(action_)s
%(mta)s-whois[sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mwl = %(action_)s
%(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
action_xarf = %(action_)s
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
%(mta)s-whois-lines[sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
action_abuseipdb = abuseipdb
action = %(action_)s
[sshd]
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
[dropbear]
port = ssh
logpath = %(dropbear_log)s
backend = %(dropbear_backend)s
[selinux-ssh]
port = ssh
logpath = %(auditd_log)s
systemctl restart fail2ban
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme0n1
FORMATDRIVE1 1
FORMATDRIVE2 1
SWRAID 0
SWRAIDLEVEL 0
PART /boot/efi esp 256M
PART /boot ext3 1024M
PART btrfs.1 btrfs all
SUBVOL btrfs.1 @ /
SUBVOL btrfs.1 @root /root
SUBVOL btrfs.1 @log /var/log
BOOTLOADER grub
HOSTNAME cl-0.infra.badai.io
#IPV4_ONLY yes
IMAGE /root/images/Debian-trixie-latest-amd64-base.tar.gz
nginx
get source ip from cloudflare
The problem - we log only cf addresses if we not configure nginx properly
curl -s https://www.cloudflare.com/ips-v4/ | awk '{print "set_real_ip_from "$1 ";"}'
curl -s https://www.cloudflare.com/ips-v6/ | awk '{print "set_real_ip_from "$1 ";"}'
http {
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;
}
basic-auth
read -p "Enter username: " username && read -sp "Enter password: " password && echo "$username:$(openssl passwd -apr1 $password)" | sudo tee -a /etc/nginx/.htaccess && echo -e "\nCredentials added successfully."
location / {
# Basic authentication setup
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htaccess ;
}
fix upstream sent too big header
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
nginx ingress cotroller config snippet
https://ellie.wtf/notes/ingress-nginx-risky-annotations
Today I found myself needing to configure ingress-nginx. I needed to write a bit of nginx config to rewrite status codes for certain routes.
Something like
nginx.ingress.kubernetes.io/configuration-snippet: |-
location /metrics {
return 404;
}
I’ve done this many times in the past, but today I received the following error
Error: UPGRADE FAILED: cannot patch "xyz" with kind Ingress: admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: annotation group ConfigurationSnippet contains risky annotation based on ingress configuration
I already had
allowSnippetAnnotations: true
set, so this was confusing!
It turns out, in a recent release (controller 1.12), annotations are flagged by risk. There’s a table here
You now need to specify
annotations-risk-level: Critical
in the configmap. If you’re using the helm chart, it can be added like so
controller:
config:
annotations-risk-level: Critical
Note that this change is a reaction to a security issue. This is mostly an issue if you’re using a multi-tenant cluster.
webdav setup with nginx
install nginx
With webdav module
apt install libxslt-dev --no-install-recommends
make -f nginxbuild.mk install DAV=true
add /etc/nginx/webdav.passwd
read -p "Enter username: " username && read -sp "Enter password: " password && echo "$username:$(openssl passwd -apr1 $password)" | sudo tee -a /etc/nginx/webdav.passwd && echo -e "\nCredentials added successfully."
usermod -aG booger nginx
usermod -aG nginx booger
chmod g+rwx /srv/webdav/
nginx server conf
server {
#listen 80 ;
listen 443 ssl ;
http2 on;
server_name webdav.aeza.boogerman.xyz ;
ssl_certificate /home/booger/.acme.sh/aeza.boogerman.xyz_ecc/fullchain.cer;
ssl_certificate_key /home/booger/.acme.sh/aeza.boogerman.xyz_ecc/aeza.boogerman.xyz.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /srv/webdav ;
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location / {
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS;
dav_access user:rw group:rw;
client_max_body_size 0;
create_full_put_path on;
client_body_temp_path /tmp/;
# Basic authentication setup
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/webdav.passwd;
# Deny all access unless authenticated
satisfy all;
allow all; # This allows all authenticated users
deny all; # This denies all other users
# Deny all access except zelenika
#allow 62.4.35.1 ;
#deny all ;
}
}
Pulumi
- setup
- example new project
- tips
install & initial setup
brew install pulumi
pulumi login s3://occam-tf-state/pulumi?region=eu-central-1
export PULUMI_CONFIG_PASSPHRASE=""
echo '\nexport PULUMI_CONFIG_PASSPHRASE=""\n' >> ~/.zshrc
python - uv
Потому что в качестве мэнеджера пакетов для питона я использую его
curl -LsSf https://astral.sh/uv/install.sh | sh
Example project
create project
mkdir pulumi-example
cd $_
pulumi new python
# enter name
# enter description
# use uv
# enter stack name ususally main
add modules
google pulumi <something> - goes to module and get module for python
cloudflare
uv add pulumi-cloudflare
# Add config for module, usually api keys to access provider (cloudflare api key with access to account zones read and dns in zone read/write)
pulumi config set --secret cloudflare:apiToken ${TOKEN}
import zone or use its string values
hetzner
uv add pulumi-hcloud
# Add config for module, usually api keys to access provider (hetzner project api key)
pulumi config set hcloud:token --secret ${API_KEY}
import network and import subnet and after that use attach to apply propper ip
main cli operations
preview (plan)
pulumi preview
up (apply)
pulumi up
show resources with urn
pulumi stack --show-urns
get updates
if some resources updated mannually
pulumi state upgrade
delete resources with urn
in cases where import was success but some variables was renamed/replaced
pulumi state delete urn:pulumi:main::pulumi-cloudflare::cloudflare:index/zone:Zone::thearchai --force
destroy without protected objects
pulumi destroy --exclude-protected
tips
hetzner network
import pulumi
import pulumi_hcloud as hcloud
import variables
reticulum_network = hcloud.Network(
"reticulum_network",
ip_range="10.67.0.0/16",
labels={
"Name": "Galactica-reticulum",
},
name="reticulum",
opts=pulumi.ResourceOptions(protect=True),
)
reticulum_network_subnet = hcloud.NetworkSubnet(
"reticulum_network_subnet",
ip_range="10.67.0.0/24",
network_id=reticulum_network.id,
network_zone="eu-central",
type="cloud",
opts=pulumi.ResourceOptions(protect=True),
)
safe_server = hcloud.Server(
"safe_server",
name="gala-reti-safe",
image="debian-12",
server_type="cpx31",
public_nets=[
{
"ipv4_enabled": True,
"ipv6_enabled": True,
}
],
location="fsn1",
ssh_keys=variables.common_ssh_keys,
labels=variables.common_labels,
)
net_attach = hcloud.ServerNetwork(
"safe-server-netattach",
server_id=safe_server.id,
network_id=reticulum_network.id,
ip="10.67.0.202",
)
Using SOPS with Pulumi
-
safe PULUMI_CONFIG_PASSPHRASE to sops file
- encrypt
PULUMI_CONFIG_PASSPHRASEwith sops into file - sops exec-env encrypted-config-file.json 'pulumi preview'
- encrypt
-
safe config to sops file
sops exec-file sops.yaml 'pulumi up --config-file {}' -
load config from file as resource
https://sarg.org.ru/blog/pulumi-sops/ -- ts implementation
outputs TBD
Table of Contents
- Install libvirt
- Install Pulumi
- Setup Pulumi project and Dev Stack
- Create a VM
- Create filesystem volume
- Fix libvirt permission errors
- Attach virtual console to VM
- Use cloud-init to setup ubuntu user
- Setup network so we can SSH into VM
- Add Pulumi outputs
- Enable autostart
- Use Pulumi provider and config to support multiple stacks
Pulumi is an Infrastructure as Code (IaC) tool that supports using Go, .Net, Python, and TypeScript/JavaScript. Libvirt is a tool for managing virtual machines (VM). Typically, teams use Pulumi with different cloud providers, but we can leverage libvirt to manage virtual machines on bare-metal servers, perfect for a homelab.
We’ll go through the steps of setting up libvirt and Pulumi to run against our local machine to create a VM running Ubuntu 20.04. This post caters to folks that have never used libvirt or Pulumi.
I’m running this on Ubuntu 21.10 x64. We’ll use Pulumi v3.22.1 with the Go SDK and Go v1.17.6.
Note: If you just want to see the code, visit pulumi-libvirt-ubuntu-example.
Install libvirt
First, install libvirt via:
sudo apt install qemu-kvm libvirt-daemon-system
This will create a systemd service and automatically start running libvirt.
By default, our regular user can’t interact with libvirt without running as root. Fortunately, we can add ourselves to the libvirt group.
sudo adduser $USER libvirt
You’ll need to log out and back in for this to take effect.
Then verify the user can interact with libvirt by running:
virsh list
and you should see the following output:
Id Name State
--------------------
This command shows us the list of domains running.
Libvirt calls virtual machines domains. I’ll be using these terms interchangeably throughout this post.
Install Pulumi
Install Pulumi v3.22.1 for Linux x64 by running the following commands, which will download, extract, and move the required binaries to /usr/local/bin/.
cd ~
wget https://get.pulumi.com/releases/sdk/pulumi-v3.22.1-linux-x64.tar.gz
tar \
--extract \
--file pulumi-v3.22.1-linux-x64.tar.gz \
--gzip
sudo mv ~/pulumi/pulumi /usr/local/bin/
sudo mv ~/pulumi/pulumi-language-go /usr/local/bin/
This will move the pulumi and pulumi-language-go binaries to /usr/local/bin to make them available in our $PATH.
pulumi version
should output:
v3.22.1
Since we’re using Go in this post, we’ve only copied the pulumi-language-go binary. For other languages, copy the respective language binaries.
Setup Pulumi project and Dev Stack
Now that we’ve installed libvirt and Pulumi, we can begin creating our Pulumi project.
Create and navigate to a new directory at ~/pulumi-libvirt-ubuntu by running:
mkdir ~/pulumi-libvirt-ubuntu
cd ~/pulumi-libvirt-ubuntu
Before we can begin using Pulumi we need to specify a backend to save our infrastructure state. Pulumi supports multiple backends such as S3 and their own hosted service. For convenience, we’ll use our local filesystem to store our state.
Let’s create a directory to hold our project’s state and then log in using the newly created directory:
mkdir ~/pulumi-libvirt-ubuntu-state
pulumi login file://~/pulumi-libvirt-ubuntu-state
NOTE:
pulumi loginsupports a--localoption, defaulting to using~/to save state. This causes issues when dealing with multiple Pulumi projects because they’ll start sharing state. So better to create separate directories for each project.
Let’s create a new Pulumi project setup for Go via:
pulumi new go \
--description "Creates a Ubuntu 20.04 VM via libvirt" \
--name pulumi-libvirt-ubuntu \
--stack dev
After running this, Pulumi will prompt asking for a passphrase for our dev stack. Provide one, re-enter it, and enter it again to finish setting up the Pulumi project and our dev stack.
Note: you can think of a stack as an environment. Later, we’ll learn how to create another stack such as
prod.
The above command scaffolds out a Pulumi project that looks like this:
.
├── go.mod
├── go.sum
├── main.go
├── Pulumi.dev.yaml
└── Pulumi.yaml
go.modandgo.sumare created for us with dependencies needed by Pulumi.main.gocontains our actual code for creating our infrastructure, which we’ll be focused on in this post.Pulumi.yamlis info about the Project such as name, description, and the runtime (go in this case).Pulumi.dev.yamlis configuration for ourdevstack. You can think of a stack as an environment. So we can eventually deploy our VM to our dev stack, and we could create a new stack namedprod.
Create a VM
We’ll need to install the Pulumi libvirt provider before creating VMs.
To install this provider, run:
go get github.com/pulumi/pulumi-libvirt/sdk@v0.2.1
Update main.go to look like:
package main
import (
"github.com/pulumi/pulumi-libvirt/sdk/go/libvirt"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// create a provider, this isn't required, but will make it easier to configure
// a libvirt_uri, which we'll discuss in a bit
provider, err := libvirt.NewProvider(ctx, "provider", &libvirt.ProviderArgs{})
if err != nil {
return err
}
// create a VM that has a name starting with ubuntu
_, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{}, pulumi.Provider(provider))
if err != nil {
return err
}
return nil
})
}
Create an environment variable named PULUMI_CONFIG_PASSPHRASE, so the Pulumi CLI can decrypt our stack by running:
export PULUMI_CONFIG_PASSPHRASE=password
where password is the passphrase used when creating the dev stack.
Next, run the following command to create our domain:
pulumi up
Pulumi will install dependencies and then display a preview such as:
Previewing update (dev):
Type Name Plan
+ pulumi:pulumi:Stack pulumi-libvirt-ubuntu-dev create
+ └─ libvirt:index:Domain ubuntu create
Select “yes” to make the domain.
Afterward, Pulumi will output that it created two resources (the stack and the domain).
We can verify the libvirt VM exists by running:
virsh list
which will output
Id Name State
--------------------------------
1 ubuntu-0b14e16 running
The ubuntu-0b14e16 VM isn’t doing much for now. Let’s work on creating a volume and giving our VM a filesystem.
Create filesystem volume
We can install Ubuntu on our domain by creating a new volume from a Ubuntu ISO, and then creating another volume to act as the actual filesystem for the VM to use based on the Ubuntu volume.
Modify main.go to create a storage pool for our volumes and two volumes:
import (
"github.com/pulumi/pulumi-libvirt/sdk/go/libvirt"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// create a provider, this isn't required, but will make it easier to configure
// a libvirt_uri, which we'll discuss in a bit
provider, err := libvirt.NewProvider(ctx, "provider", &libvirt.ProviderArgs{})
if err != nil {
return err
}
+ // `pool` is a storage pool that can be used to create volumes
+ // the `dir` type uses a directory to manage files
+ // `Path` maps to a directory on the host filesystem, so we'll be able to
+ // volume contents in `/pool/cluster_storage/`
+ pool, err := libvirt.NewPool(ctx, "cluster", &libvirt.PoolArgs{
+ Type: pulumi.String("dir"),
+ Path: pulumi.String("/pool/cluster_storage"),
+ }, pulumi.Provider(provider))
+ if err != nil {
+ return err
+ }
+
+ // create a volume with the contents being a Ubuntu 20.04 server image
+ ubuntu, err := libvirt.NewVolume(ctx, "ubuntu", &libvirt.VolumeArgs{
+ Pool: pool.Name,
+ Source: pulumi.String("https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img"),
+ }, pulumi.Provider(provider))
+ if err != nil {
+ return err
+ }
+
+ // create a filesystem volume for our VM
+ // This filesystem will be based on the `ubuntu` volume above
+ // we'll use a size of 10GB
+ filesystem, err := libvirt.NewVolume(ctx, "filesystem", &libvirt.VolumeArgs{
+ BaseVolumeId: ubuntu.ID(),
+ Pool: pool.Name,
+ Size: pulumi.Int(10000000000),
+ }, pulumi.Provider(provider))
+ if err != nil {
+ return err
+ }
+
// create a VM that has a name starting with ubuntu
- _, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{}, pulumi.Provider(provider))
+ _, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
+ Disks: libvirt.DomainDiskArray{
+ libvirt.DomainDiskArgs{
+ VolumeId: filesystem.ID(),
+ },
+ },
+ }, pulumi.Provider(provider))
if err != nil {
return err
}
return nil
})
}
Then run
pulumi up
Select yes after viewing the preview. Pulumi will create a storage pool, download the Ubuntu image, and then hit a Permission denied error when attempting to use the image like:
error: error creating libvirt domain: internal error: process exited while connecting to monitor: 2022-01-12T01:36:34.005110Z qemu-system-x86_64: -blockdev {"driver":"file","filename":"/pool/cluster_storage/ubuntu-552ab14","node-name":"libvirt-2-storage","auto-read-only":true,"discard":"unmap"}: Could not open '/pool/cluster_storage/ubuntu-552ab14': Permission denied
Fix libvirt permission errors
There are a few ways to handle this, but the easiest solution is disabling SELinux.
To disable SELinux, modify /etc/libvirt/qemu.conf:
sudo sed --in-place 's/#security_driver = "selinux"/security_driver = "none"/' /etc/libvirt/qemu.conf
Then restart libvirtd for this config change to take effect.
sudo systemctl restart libvirtd
And finally, re-try running:
pulumi up
It’ll be successful this time. We can then run the following commands to see the impact on libvirt.
virsh list
will output a new domain:
Id Name State
--------------------------------
2 ubuntu-e629e71 running
virsh pool-list
will show our storage pool:
Name State Autostart
---------------------------------------
cluster-1d3f78e active yes
and we can view our volumes by running:
virsh vol-list cluster-1d3f78e
will show our volumes in our pool:
Name Path
----------------------------------------------------------------
filesystem-103d88a /pool/cluster_storage/filesystem-103d88a
ubuntu-552ab14 /pool/cluster_storage/ubuntu-552ab14
So now we have a VM with Ubuntu 20.04 running, but we cannot interact with it just yet.
Attach virtual console to VM
We can attach a virtual console to our VM so we can login from a terminal.
Modify main.go so the domain has a console attached like:
// create a VM that has a name starting with ubuntu
_, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
+ Consoles: libvirt.DomainConsoleArray{
+ // enables using `virsh console ...`
+ libvirt.DomainConsoleArgs{
+ Type: pulumi.String("pty"),
+ TargetPort: pulumi.String("0"),
+ TargetType: pulumi.String("serial"),
+ },
+ },
Disks: libvirt.DomainDiskArray{
libvirt.DomainDiskArgs{
VolumeId: filesystem.ID(),
},
},
- }, pulumi.Provider(provider))
+ // delete existing VM before creating replacement to avoid two VMs trying to use the same volume
+ }, pulumi.Provider(provider), pulumi.ReplaceOnChanges([]string{"*"}), pulumi.DeleteBeforeReplace(true))
if err != nil {
return err
}
Note: pay attention to the
ReplaceOnChangesandDeleteBeforeReplacegotcha. Since only a single domain can use our volumes at once, we need to delete the existing domain before creating a new one.
Run pulumi up again to create a new VM.
Get the name of the VM from virsh list, and then we can access the VM by running:
virsh console ubuntu-3c69e6a
Press enter to be access a username and password prompt. Unfortunately, there isn’t a default password for ubuntu, so we’re can’t login, yet.
virsh console ... can be great for debugging issues such as cloud-init, which we’ll do next.
Note: to exit the console, hold
CTRLand press].
Use cloud-init to setup ubuntu user
We can leverage cloud-init to create credentials for the ubuntu user, amongst other things.
Create a new file named cloud_init_user_data.yaml.
touch ~/pulumi-libvirt-ubuntu/cloud_init_user_data.yaml
with the following content:
#cloud-config
ssh_pwauth: True
chpasswd:
list: |
ubuntu:ubuntu
expire: False
Now update main.go so that we create a cloud-init resource and initialize the VM with the cloud-init mounted.
+ cloud_init_user_data, err := os.ReadFile("./cloud_init_user_data.yaml")
+ if err != nil {
+ return err
+ }
+
+ // create a cloud init disk that will setup the ubuntu credentials
+ cloud_init, err := libvirt.NewCloudInitDisk(ctx, "cloud-init", &libvirt.CloudInitDiskArgs{
+ MetaData: pulumi.String(string(cloud_init_user_data)),
+ Pool: pool.Name,
+ UserData: pulumi.String(string(cloud_init_user_data)),
+ }, pulumi.Provider(provider))
+ if err != nil {
+ return err
+ }
+
// create a VM that has a name starting with ubuntu
_, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
+ Cloudinit: cloud_init.ID(),
Consoles: libvirt.DomainConsoleArray{
// enables using `virsh console ...`
libvirt.DomainConsoleArgs{
Type: pulumi.String("pty"),
TargetPort: pulumi.String("0"),
TargetType: pulumi.String("serial"),
},
},
Disks: libvirt.DomainDiskArray{
libvirt.DomainDiskArgs{
VolumeId: filesystem.ID(),
},
},
// delete existing VM before creating replacement to avoid two VMs trying to use the same volume
}, pulumi.Provider(provider), pulumi.ReplaceOnChanges([]string{"*"}), pulumi.DeleteBeforeReplace(true))
if err != nil {
return err
}
Run pulumi up again.
Now, get the name of the VM again with virsh list and execute virsh console NAME_OF_VM.
Press enter, and you can then log in with the ubuntu username and ubuntu password. You may need to wait a few minutes for cloud-init to complete before these credentials are valid.
This is great, but most of the time we’ll want to SSH instead. Let’s get that working.
Setup network so we can SSH into VM
Currently, our VM doesn’t have an IP address that we can connect to for SSH. We’ll need a libvirt network to attach our VM to and configure our VM to get an IP address from libvirt’s DHCP server automatically.
Create a new file named cloud_init_network_config.yaml
touch ~/pulumi-libvirt-ubuntu/cloud_init_network_config.yaml
with the following content:
version: 2
ethernets:
ens3:
dhcp4: true
We’ll add this to our cloud-init, so the VM will attempt to get an IP address assigned at boot up.
Update main.go to add this network config to cloud-init and create a libvirt network.
+ cloud_init_network_config, err := os.ReadFile("./cloud_init_network_config.yaml")
+ if err != nil {
+ return err
+ }
+
- // create a cloud init disk that will setup the ubuntu credentials
+ // create a cloud init disk that will setup the ubuntu credentials and enable dhcp
cloud_init, err := libvirt.NewCloudInitDisk(ctx, "cloud-init", &libvirt.CloudInitDiskArgs{
MetaData: pulumi.String(string(cloud_init_user_data)),
+ NetworkConfig: pulumi.String(string(cloud_init_network_config)),
Pool: pool.Name,
UserData: pulumi.String(string(cloud_init_user_data)),
}, pulumi.Provider(provider))
if err != nil {
return err
}
+ // create NAT network using 192.168.10/24 CIDR
+ network, err := libvirt.NewNetwork(ctx, "network", &libvirt.NetworkArgs{
+ Addresses: pulumi.StringArray{pulumi.String("192.168.10.0/24")},
+ Mode: pulumi.String("nat"),
+ }, pulumi.Provider(provider))
+ if err != nil {
+ return err
+ }
+
// create a VM that has a name starting with ubuntu
_, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
Cloudinit: cloud_init.ID(),
Consoles: libvirt.DomainConsoleArray{
// enables using `virsh console ...`
libvirt.DomainConsoleArgs{
Type: pulumi.String("pty"),
TargetPort: pulumi.String("0"),
TargetType: pulumi.String("serial"),
},
},
Disks: libvirt.DomainDiskArray{
libvirt.DomainDiskArgs{
VolumeId: filesystem.ID(),
},
},
+ NetworkInterfaces: libvirt.DomainNetworkInterfaceArray{
+ libvirt.DomainNetworkInterfaceArgs{
+ NetworkId: network.ID(),
+ WaitForLease: pulumi.Bool(true),
+ },
+ },
// delete existing VM before creating replacement to avoid two VMs trying to use the same volume
}, pulumi.Provider(provider), pulumi.ReplaceOnChanges([]string{"*"}), pulumi.DeleteBeforeReplace(true))
if err != nil {
return err
}
Once again, run:
pulumi up
We can see a newly created network by running:
virsh net-list
which will output something similar to:
Name State Autostart Persistent
----------------------------------------------------
default active yes yes
network-171e7af active no yes
To find the IP address of the VM, we can look at the leases by running:
virsh net-dhcp-leases network-171e7af
to see
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
----------------------------------------------------------------------------------------------------------------------------------------------------
2022-01-11 21:40:37 52:54:00:ca:f2:e4 ipv4 192.168.10.52/24 ubuntu-af93b6f ff:b5:5e:67:ff:00:02:00:00:ab:11:f3:04:a5:1b:1a:65:18:76
We can finally SSH by running:
ssh ubuntu@192.168.10.52
and logging in with the ubuntu password again.
Note: This network is uses NAT, so it will only be reachable from the host that libvirt is running by default.
Add Pulumi outputs
Throughout this post, we’ve had to use virsh to find the VM name and IP address, but we can actually use Pulumi Outputs. Pulumi will then automatically retrieve these values and display them after provisioning resources.
We can define an IP Address and VM Name output by modifying main.go again:
// create a VM that has a name starting with ubuntu
- _, err = libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
+ domain, err := libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
Cloudinit: cloud_init.ID(),
Consoles: libvirt.DomainConsoleArray{
// enables using `virsh console ...`
libvirt.DomainConsoleArgs{
Type: pulumi.String("pty"),
TargetPort: pulumi.String("0"),
TargetType: pulumi.String("serial"),
},
},
Disks: libvirt.DomainDiskArray{
libvirt.DomainDiskArgs{
VolumeId: filesystem.ID(),
},
},
NetworkInterfaces: libvirt.DomainNetworkInterfaceArray{
libvirt.DomainNetworkInterfaceArgs{
NetworkId: network.ID(),
WaitForLease: pulumi.Bool(true),
},
},
// delete existing VM before creating replacement to avoid two VMs trying to use the same volume
}, pulumi.Provider(provider), pulumi.ReplaceOnChanges([]string{"*"}), pulumi.DeleteBeforeReplace(true))
if err != nil {
return err
}
+ ctx.Export("IP Address", domain.NetworkInterfaces.Index(pulumi.Int(0)).Addresses().Index(pulumi.Int(0)))
+ ctx.Export("VM name", domain.Name)
Run pulumi up again, and we’ll now see the following output:
Outputs:
+ IP Address: "192.168.10.52"
+ VM name : "ubuntu-af93b6f"
We can also retrieve these by running pulumi stack output to list all outputs. To get a specific output, run pulumi stack output "IP Address", which can be usefull in shell scripts.
Enable autostart
If we restart the computer, libvirt will not automatically start our network and domain. Fortunately, there’s an option to handle that.
Modify main.go:
// create NAT network using 192.168.10/24 CIDR
network, err := libvirt.NewNetwork(ctx, "network", &libvirt.NetworkArgs{
Addresses: pulumi.StringArray{pulumi.String("192.168.10.0/24")},
+ Autostart: pulumi.Bool(true),
Mode: pulumi.String("nat"),
}, pulumi.Provider(provider))
if err != nil {
return err
}
// create a VM that has a name starting with ubuntu
domain, err := libvirt.NewDomain(ctx, "ubuntu", &libvirt.DomainArgs{
+ Autostart: pulumi.Bool(true),
Cloudinit: cloud_init.ID(),
Consoles: libvirt.DomainConsoleArray{
// enables using `virsh console ...`
libvirt.DomainConsoleArgs{
Type: pulumi.String("pty"),
TargetPort: pulumi.String("0"),
TargetType: pulumi.String("serial"),
},
},
Disks: libvirt.DomainDiskArray{
libvirt.DomainDiskArgs{
VolumeId: filesystem.ID(),
},
},
NetworkInterfaces: libvirt.DomainNetworkInterfaceArray{
libvirt.DomainNetworkInterfaceArgs{
NetworkId: network.ID(),
WaitForLease: pulumi.Bool(true),
},
},
// delete existing VM before creating replacement to avoid two VMs trying to use the same volume
}, pulumi.Provider(provider), pulumi.ReplaceOnChanges([]string{"*"}), pulumi.DeleteBeforeReplace(true))
if err != nil {
return err
}
Run pulumi up again to create a new network and domain that will autostart on reboot.
Use Pulumi provider and config to support multiple stacks
So far, our main.go works great to create a VM on the same machine we’re running Pulumi. This is nice for a dev environment, but what about a staging or production environment where we want to run Pulumi against a remote libvirt instance?
By default, the libvirt provider uses a libvirt URI found in the LIBVIRT_DEFAULT_URI environment variable. If that isn’t defined, then it assumes qemu:///system for the libvirt URI.
Try it out by running virsh --connect qemu:///system list to see the same output as virsh list.
We could know to specify a LIBVIRT_DEFAULT_URI each time we run Pulumi, or we could leverage configuring a libvirt provider, so each environment will provide its own libvirt URI.
Install the Pulumi config package by running:
go get github.com/pulumi/pulumi/sdk/v3/go/pulumi/config@v3.19.0
and add as an import in main.go:
import (
"os"
"github.com/pulumi/pulumi-libvirt/sdk/go/libvirt"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
+ "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
Modify main.go to require libvirt_uri to be defined by the stack and pass the value to the provider:
+ conf := config.New(ctx, "")
+
+ // require each stack to specify a libvirt_uri
+ libvirt_uri := conf.Require("libvirt_uri")
+
// create a provider, this isn't required, but will make it easier to configure
// a libvirt_uri, which we'll discuss in a bit
- provider, err := libvirt.NewProvider(ctx, "provider", &libvirt.ProviderArgs{})
+ provider, err := libvirt.NewProvider(ctx, "provider", &libvirt.ProviderArgs{
+ Uri: pulumi.String(libvirt_uri),
+ })
if err != nil {
return err
}
Now run pulumi up and we’ll see the following error message:
panic: fatal: A failure has occurred: missing required configuration variable 'pulumi-libvirt-ubuntu:libvirt_uri'; run `pulumi config` to set
To define libvirt_uri for our dev stack, run:
pulumi config set libvirt_uri qemu:///system
Note: this will also update
Pulumi.dev.yaml.
and now pulumi up will run successfully.
If we wanted to create a production stack, for example, we could run the following commands:
pulumi stack init prod
pulumi config set libvirt_uri qemu://PROD_IP_ADDRESS/system
pulumi up
Note: you can switch to another stack by running
pulumi stack select dev.
And then, we can use the same code for different environments!
smartctl exporter
obtain metrics from disks
install and check
export DEBIAN_FRONTEND=noninteractive && apt update && apt install smartmontools
curl -L https://github.com/prometheus-community/smartctl_exporter/releases/download/v0.14.0/smartctl_exporter-0.14.0.linux-amd64.tar.gz | tar xz -C /tmp/
timeout 5 --preserve-status -s SIGTERM /tmp/smartctl_exporter-0.14.0.linux-amd64/smartctl_exporter --web.listen-address=localhost:9633 --smartctl.rescan=60m --smartctl.interval=15s ; mv /tmp/smartctl_exporter-0.14.0.linux-amd64/smartctl_exporter /usr/local/bin/smartctl_exporter
systemd unit file
#!/bin/bash
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit
fi
tee -a /etc/systemd/system/smartctl_exporter.service <<ECHO
[Unit]
Description=Smartctl Exporter Service
After=network.target
[Service]
ExecStart=/usr/local/bin/smartctl_exporter --web.listen-address=localhost:9633 --smartctl.rescan=60m --smartctl.interval=15s
Restart=on-failure
[Install]
WantedBy=multi-user.target
ECHO
systemctl daemon-reload && systemctl enable --now smartctl_exporter.service
On reload/edit sysd-unit:
systemctl daemon-reload && systemctl restart smartctl_exporter.service
vector.dev config
vim /etc/vector/vector.yaml
sources:
smartctl:
type: prometheus_scrape
scrape_interval_secs: 15
honor_labels: true
endpoints:
- http://localhost:9633/metrics
prometheus metrics to check
sum by (instance) (smartctl_device_critical_warning > 0)
Answer as briefly as possible, without explanations and unnecessary text. Only the necessary code or commands.
Gathered from phi4 llm To achieve your goal of creating a Git bare repository on a Linux server with a
post-receivehook that triggers a CI build, follow these steps:
Step 1: Create the Bare Repository
-
SSH into your Linux server.
-
Create the directory for the bare repository:
mkdir -p /path/to/repo.git
cd /path/to/repo.git
- Initialize the bare repository:
git init --bare
Step 2: Create the post-receive Hook
- Navigate to the hooks directory:
cd hooks
- Create a new
post-receivehook script:
touch post-receive
chmod +x post-receive
-
Edit the
post-receivefile with your desired script:Here's an example script that reads the
new_ref, exports a variable for the CI archive, and executesmake ciwithnew_refas an argument:
#!/bin/bash
prj=basename $(pwd) | sed 's?.git??'
while read oldrev newrev refname; do
# Extract branch name from refname (e.g., refs/heads/main -> main)
branch=$(echo $refname | sed 's|refs/heads/||')
# Define the filename for CI archive based on the branch and commit hash
ci_filename="$prj-$branch-${newrev:0:7}.tar.gz"
echo "Branch '$branch' updated to ${newrev:0:7}."
# Export the filename variable (for use in other scripts orcommands)
export CI_FILENAME=$ci_filename
# Execute 'make ci' with the new ref as an argument
echo "Running make ci for branch '$branch' at commit '${newrev:0:7}'..."
if ! make ci REF=$refname; then
echo "CI build failed for branch '$branch'."
exit 1
fi
echo "CI build succeeded for branch '$branch'. Archive: $ci_filename"
done
Step 3: Configure Developer Repositories to Use the CI Remote
Each developer needs to add a new remote called ci pointing to your bare
repository. This can be done with the following steps:
- On each developer's machine, navigate to their local Git repository:
cd /path/to/local/repo
- Add the CI remote:
git remote add ci ssh://username@server_ip:/path/to/repo.git
Replace username, server_ip, and /path/to/repo.git with your
server's SSH username, IP address, and the path to the bare repository.
- Push changes to trigger CI: When a developer wants to push their changes and trigger the CI process, they can use:
git push ci main # or replace 'main' with any branch name as needed
Notes
- Ensure that
makeand any other dependencies required by yourmake cicommand are installed on the server. - The script assumes you have a Makefile with a target named
ciwhich accepts aREFargument. Adjust this part if necessary. - Consider adding error handling or logging to better track CI builds in production environments.
By following these steps, you should be able to set up your Git bare
repository on the server and configure it to trigger CI builds using a
post-receive hook script.
#!/bin/bash
prj=$(basename $(pwd) | sed 's?.git??')
bare_dir="$(pwd)"
function ci() {
make ci REF=$refname CI_FILENAME=$1
}
while read oldrev newrev refname; do
# Extract branch name from refname (e.g., refs/heads/main -> main)
branch=$(echo $refname | sed 's|refs/heads/||')
echo "Branch '$branch' updated to ${newrev:0:7}."
# Define the filename for CI archive based on the branch and commit hash
ci_filename="$prj-$branch-${newrev:0:7}.tar.gz"
# Define directory for the worktree
worktree_dir="$HOME/$prj-$branch-worktree"
# Check if worktree exists
if git --git-dir="$bare_dir" worktree list | grep -q "$worktree_dir"; then
echo "Worktree for '$branch' already exists at $worktree_dir"
else
echo "Creating new worktree for '$branch'"
git --git-dir="$bare_dir" worktree add -f "$worktree_dir" "$branch" || {
echo "Failed to create worktree for branch '$branch'."
exit 1
}
fi
# Change to the worktree directory
cd "$worktree_dir" || {
echo "Failed to change to worktree directory."
exit 1
}
# Update the worktree directly to the pushed commit
echo "Updating worktree to latest commit ${newrev:0:7}"
# Stash any local changes
echo "Stashing local changes if any exist"
git --git-dir="$bare_dir" --work-tree="$worktree_dir" stash push -m "Auto-stashed before updating to ${newrev:0:7}" || {
echo "Note: No local changes to stash or stash failed."
# Continue even if stash fails (could be no changes)
}
# Skip checkout and directly reset to the new commit
# Using -f to force the update even if the branch is already checked out
echo "Resetting to commit ${newrev:0:7}"
git --git-dir="$bare_dir" --work-tree="$worktree_dir" reset --hard "$newrev" || {
echo "Failed to reset to commit ${newrev:0:7}."
exit 1
}
# Pop the stash if there are any stashed changes
if git --git-dir="$bare_dir" --work-tree="$worktree_dir" stash list | grep -q "Auto-stashed before updating to ${newrev:0:7}"; then
echo "Restoring local changes from stash"
git --git-dir="$bare_dir" --work-tree="$worktree_dir" stash pop || {
echo "Warning: Stash pop had conflicts. Please check the worktree."
# Continue execution even if there are conflicts
}
fi
# Export the filename variable (for use in other scripts or commands)
export CI_FILENAME=$ci_filename
if ! ci "$CI_FILENAME"; then
echo "CI build failed for branch '$branch'."
cd - >/dev/null || cd "$HOME"
exit 1
else
echo "make ci was successful for branch '$branch'."
fi
# Return to the original directory
cd "$bare_dir" || cd "$HOME"
done