1
0
mirror of https://gitlab.alpinelinux.org/alpine/cloud/tiny-cloud.git synced 2026-02-04 04:22:43 +03:00

Introducing Tiny Cloud!

This commit is contained in:
Jake Buchholz Göktürk 2022-01-29 22:27:34 +00:00
parent 0bfdd16977
commit 8ffdca9786
28 changed files with 1132 additions and 169 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*~
*.bak
*.swp
.vscode/

View File

@ -1,4 +1,4 @@
Copyright (c) 2017-2020 Michael Crute
Copyright (c) 2017-2022 Jake Buchholz Göktürk, Michael Crute
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -1,5 +1,60 @@
PREFIX?=/
.PHONY: install
install:
install -Dm 755 tiny-ec2-bootstrap $(PREFIX)/etc/init.d/tiny-ec2-bootstrap
SUBPACKAGES = core network openrc aws azure gcp oci
.PHONY: install $(SUBPACKAGES)
# installs all subpackages, then replaces cloud-specific config with example
install: $(SUBPACKAGES)
mv "$(PREFIX)"/etc/conf.d/tiny-cloud.example "$(PREFIX)"/etc/conf.d/tiny-cloud
core:
install -Dm755 -t "$(PREFIX)"/bin \
bin/imds
install -Dm644 -t "$(PREFIX)"/etc/conf.d \
etc/conf.d/tiny-cloud.example
install -Dm644 -t "$(PREFIX)"/lib/tiny-cloud \
lib/tiny-cloud/common \
lib/tiny-cloud/init-* \
lib/tiny-cloud/mdev
network:
install -Dm644 -t "$(PREFIX)"/etc/network/interfaces.d \
etc/network/interfaces.d/*
install -Dm755 -t "$(PREFIX)"/lib/mdev \
lib/mdev/vnic-eth-hotplug
install -Dm755 -t "$(PREFIX)"/sbin \
sbin/*
install -Dm755 -t "$(PREFIX)"/usr/libexec/ifupdown-ng \
usr/libexec/ifupdown-ng/imds
openrc:
install -Dm755 -t "$(PREFIX)"/etc/init.d \
etc/init.d/*
aws:
install -Dm755 -t "$(PREFIX)"/lib/mdev \
lib/mdev/nvme-ebs-links
install -Dm644 -t "$(PREFIX)"/lib/tiny-cloud/aws \
lib/tiny-cloud/aws/*
sed -e 's/^#?CLOUD=.*/CLOUD=aws/' \
-e 's/^#?HOTPLUG_MODULES=.*/HOTPLUG_MODULES="vnic_eth_hotplug nvme_ebs_links"/' \
etc/conf.d/tiny-cloud.example > "$(PREFIX)"/etc/conf.d/tiny-cloud
azure:
install -Dm644 -t $(PREFIX)/lib/tiny-cloud/azure \
lib/tiny-cloud/azure/*
sed -e 's/^#?CLOUD=.*/CLOUD=azure/' \
etc/conf.d/tiny-cloud.example > "$(PREFIX)"/etc/conf.d/tiny-cloud
gcp:
install -Dm644 -t $(PREFIX)/lib/tiny-cloud/gcp \
lib/tiny-cloud/gcp/*
sed -e 's/^#?CLOUD=.*/CLOUD=gcp/' \
etc/conf.d/tiny-cloud.example > "$(PREFIX)"/etc/conf.d/tiny-cloud
oci:
install -Dm644 -t $(PREFIX)/lib/tiny-cloud/oci \
lib/tiny-cloud/oci/*
sed -e 's/^#?CLOUD=.*/CLOUD=oci/' \
etc/conf.d/tiny-cloud.example > "$(PREFIX)"/etc/conf.d/tiny-cloud

198
README.md
View File

@ -1,70 +1,158 @@
# Tiny EC2 Bootstrapper
# Tiny Cloud
This is designed to do the minimal amount of work required to bootstrap an EC2
instance based on the local settings assigned at boot time as well as the
user's configured settings. This is, in-concept, similar to
[cloud-init](https://cloudinit.readthedocs.io/en/latest/) but trades features
and cloud platform support for small size and limited external dependencies.
The Tiny Cloud bootstrapper performs critical initialization tasks for cloud
instances during their first boot. Unlike the more popular and feature-rich
[cloud-init](https://cloudinit.readthedocs.io/en/latest), Tiny Cloud seeks to
do just what is necessary with a small footprint and minimal dependencies.
A direct descendant of [tiny-ec2-bootstrap](
https://gitlab.alpinelinux.org/alpine/cloud/tiny-ec2-bootstrap), Tiny Cloud
works with multiple cloud providers. Currently, the following are supported:
* AWS (Amazon Web Services)
* Azure (Microsoft Azure)
* GCP (Google Cloud Platform)
* OCI (Oracle Cloud Infrastructure)
## Features
The following actions will occur ***only once***, during the initial boot of an
instance:
* expand the root filesystem to use all available root device space, during the
**sysinit** runlevel
* set the instance's hostname from instance metadata
* install SSH keys from instance metadata to the cloud user account's
`authorized_keys` file (the user must already exist)
* save the instance user-data to a file, and if it's a script, execute it at
the end of the **default** runlevel
Optional features, which may not be universally necessary:
* manage symlinks from NVMe block devices to `/dev/xvd` and `/dev/sd` devices
(i.e. AWS Nitro instances)
* manage hotpluggable network interfaces
* sync IMDS-provided secondary IPv4 and IPv6 addresses network interfaces
Also included is a handy `imds` client script for easy access to an instance's
IMDS data.
## Requirements
The most important feature of this bootstrapper is the very limited set of
dependencies. In-fact, this works with just BusyBox (provided ash and wget
are built in) and a couple utilities for expanding the root filesystem.
The full list of required dependencies are:
As Tiny Cloud is meant to be tiny, it has very few dependencies:
* Busybox (`ash`, `wget`, etc.)
* `ifupdown-ng` (optional, for network management)
* `iproute2-minimal` (optional, for syncing IPv4/IPv6 from IMDS)
* `nvme-cli` (optional, for AWS nitro NVMe symlinks)
* `partx`
* `resize2fs`
* `sfdisk`
- bash-like shell (e.g. bash, dash, ash)
- wget
- sfdisk
- partx
- resize2fs
Tiny Cloud has been developed specifically for use with the
[Alpine Cloud Images](https://gitlab.alpinelinux.org/alpine/cloud/alpine-cloud-images)
project, and as such, it is currently tailored for use with [Alpine Linux](
https://alpinelinux.org), the [OpenRC](https://github.com/OpenRC/openrc) init
system, and the `ext4` root filesystem. If you would like to see Tiny Cloud
supported on additional distributions, init systems, and/or filesystems, please
open an issue with your request -- or better yet, submit a merge request!
## Supported Features and Environments
## Installation
cloud-init has support for many different cloud providers. This project only
supports EC2; [EC2 metadata
service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)
is a hard requirement of using this bootstrapper. All of the data for the
supported features below is sourced from the EC2 instance metadata service
which runs on every EC2 instance at IP 169.254.169.254.
cloud-init also has a very rich feature set with support for adding users,
installing packages, and many other things. This bootstrap does not support
those things. Instead it supports:
- setting system hostname
- installing the instance's SSH keys in the EC2 user's authorized_keys file
- running any script-like user data (must start with #!)
- disabling root and the EC2 user's password
- expanding root partition to available disk space
These steps only run once. After the initial bootstrap the bootstrapper script
is a no-op. To force the script to run again at boot time remove the file
`/var/lib/cloud/.bootstrap-complete` and reboot the instance.
The default EC2 user is `alpine`; this can be overriden with a
`/etc/conf.d/tiny-ec2-bootstrap` containing...
Typically, Tiny Cloud is installed and configured when building a cloud image,
and is available on Alpine Linux as the [`tiny-cloud`](
https://pkgs.alpinelinux.org/packages?name=tiny-cloud) APK...
```
EC2_USER="otheruser"
apk install tiny-cloud
```
The EC2 user *must* already exist in the AMI -- `tiny-ec2-bootstrap` will
**NOT** add the user automatically.
This will install the necessary init scripts, libraries, etc. plus any missing
dependencies.
## User Data
Alternately, you can download a release tarball, and use `make` to install it.
User data is provided at instance boot time and can be any arbitrary string of
data. The bootstrapper will consider any user data that begins with the ASCII
characters `#!` to be a script. It will write the entire contents of the user
data to `/var/lib/cloud/user-data.sh`, make the file executable, and execute
the file piping any output to `/var/log/cloud-bootstrap.log`.
Next, enable the three primary init scripts...
```
rc-update add tiny-cloud-early sysinit
rc-update add tiny-cloud default
rc-update add tiny-cloud-final default
```
The user data script can do anything it pleases with the instance. It will be
run as root and networking will be up. No other guarantees about system state
are made at the point the script runs.
## Configuration
## Assumptions
By default, Tiny Cloud expects configuration at `/etc/conf.d/tiny-cloud`,
The stock [`etc/conf.d/tiny-cloud`](etc/conf.d/tiny-cloud) file contains
details of all tuneable settings.
- This was written for Alpine Linux; use on other distributions has not been
tested.
*Because Tiny Cloud does not currently do auto-detection, you **MUST** set a
configuration value for `CLOUD` indicating which cloud provider will be used.
Current valid values are `aws`, `azure`, `gcp`, and `oci`.*
- The script is run by OpenRC.
## Operation
The first time an instance boots -- either freshly instantiated from an image,
or after installation on a pre-existing instance -- Tiny Cloud sets up the
instance in three phases...
### Early Phase
The `tiny-cloud-early` init script does not depend on the cloud provider's
Instance MetaData Service (IMDS), and therefore does not have a dependency on
networking. During this "early" phase, the root filesystem is expanded, and
any necessary `mdev` rules for device hotplug are set up.
### Main Phase
The main `tiny-cloud` init script *does* depend on the cloud provider's IMDS
data, and sets up instance's hostname and the cloud user's SSH keys before
`sshd` starts.
### Final Phase
`tiny-cloud-final` should be the very last init script to run in the
**default** runlevel. By default, it saves the instance's user data to
`/var/lib/cloud/user-data`, which is overrideable via the `TINY_CLOUD_VAR`
andr `CLOUD_USERDATA` config settings.
If the user data is a script starting with `#!/`, it will be executed; its
output (combined STDOUT and STDERR) and exit code are saved to
`/var/log/user-data.log` and `/var/log/user-data.exit`, respectively -- unless
overriden with `TINY_CLOUD_LOGS` and `CLOUD_USERDATA` config settings.
If all went well, the very last thing `tiny-cloud-final` does is touch
a `.bootstrap-complete` file into existence in `/var/lib/cloud` or another
directory specified by the `TINY_CLOUD_VAR` config setting.
### Skipping Init Actions
If you need to skip any individual init script actions (for example, if you
have a different means to set the instance hostname), you can set the
`SKIP_INIT_ACTIONS` config to a whitespace-separated list of actions to skip.
### Further Reboots
After the initial bootstrap of an instance, the init scripts are largely a
no-op.
To force the init scripts to re-run on the next boot...
```
rm -f /var/lib/cloud/.bootstrap-complete
```
If you're instantiating an instance in order to create a new cloud image
(using [Packer](https://packer.io), or some other means), you will need to
remove this file before creating the image to ensure that instances using the
new image will also run Tiny Cloud init scripts during their first boot.
## Cloud Hotplug Modules
### `vnic_eth_hotplug`
This hotplug module adds and removes ethernet interfaces as virtual NICs are
attached/detached from the instance.
An `ifupdown-ng` executor also syncs the interfaces' secondary IPv4 and IPV6
addresses associated with those VNICs, if the cloud's IMDS provides that
configuration data.
### `nvme_ebs_links`
EBS volumes are attached to AWS EC2 Nitro instances using the NVMe driver.
Unfortunately, the `/dev/nvme*` device names do not match the device name
assigned to the attached EBS volume. This hotplug module figures out what the
assigned device name is, and sets up `/dev/xvd*` and `/dev/sd*` symlinks to
the right NVMe devices for EBS volumes and their partitions.

147
bin/imds Executable file
View File

@ -0,0 +1,147 @@
#!/bin/sh
# vim:set ts=4 et ft=sh:
# Tiny Cloud - Instance MetaData Service client
### configuration
source /etc/conf.d/tiny-cloud
### cloud-specific variables/functions
unset \
IMDS_HEADER \
IMDS_URI \
IMDS_QUERY \
IMDS_HOSTNAME \
IMDS_SSH_KEYS \
IMDS_USERDATA \
IMDS_NICS \
IMDS_MAC \
IMDS_IPV4 \
IMDS_IPV6 \
IMDS_IPV4_NET \
IMDS_IPV6_NET \
IMDS_IPV4_PREFIX \
IMDS_IPV6_PREFIX
unset -f \
_imds_token \
_imds_header \
_imds_nic_index \
2>/dev/null || true
### default variables/functions
CLOUD="${CLOUD:-unknown}"
IMDS_ENDPOINT="169.254.169.254"
_imds_ssh_keys() { _imds "$IMDS_SSH_KEYS"; }
_imds_userdata() { _imds "$IMDS_USERDATA"; }
### load cloud-specific variables and functions
if [ ! -d /lib/tiny-cloud/"$CLOUD" ]; then
echo "ERROR: Unknown Cloud '$CLOUD'" >&2
exit 1
fi
source /lib/tiny-cloud/"$CLOUD"/imds
### non-overrideable functions
_imds() {
wget --quiet --timeout 1 --output-document - \
--header "$(_imds_header)" \
"http://$IMDS_ENDPOINT/$IMDS_URI/$1$IMDS_QUERY"
}
imds() {
local cmd args key rv err=1
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
_imds_help
return
fi
while [ -n "$1" ]; do
cmd=_imds
args=
key="$1"; shift
case $key in
# error handling
-e) err=0; continue ;; # ignore
+e) err=1; continue ;; # return
# TODO: retry/deadline
# output control
+n) echo; continue ;; # insert newline
+s) echo -n " "; continue ;; # insert space
+t) echo -en "\t"; continue ;; # insert tab
# key aliasing
@hostname) args="$IMDS_HOSTNAME" ;;
@ssh-keys) cmd=_imds_ssh_keys ;;
@userdata) cmd=_imds_userdata ;;
@nics) args="$IMDS_NICS" ;;
@nic:*)
cmd=imds
args=$(_imds_nic_args $(echo "${key#@nic:}" | tr , ' '))
;;
# use key verbatim
*) args="$key" ;;
esac
# TODO: retry/deadline
"$cmd" $args 2>/dev/null
rv=$?
[ $err -eq 0 ] && continue
[ $rv = "0" ] || return $rv
done
}
_imds_nic_args() {
local key nic
nic=$(_imds_nic_index "$1") || return 1
if [ -z "$2" ]; then
echo "$IMDS_NICS/$nic"
return
fi
while [ -n "$2" ]; do
key="$2"
shift
case "$key" in
@mac) key="$IMDS_MAC" ;;
@ipv4) key="$IMDS_IPV4" ;;
@ipv6) key="$IMDS_IPV6" ;;
@ipv4-net) key="$IMDS_IPV4_NET" ;;
@ipv6-net) key="$IMDS_IPV6_NET" ;;
@ipv4-prefix) key="$IMDS_IPV4_PREFIX" ;;
@ipv6-prefix) key="$IMDS_IPV6_PREFIX" ;;
# error/output control passthrough
-e|+[enst]) printf "$key\n"; continue ;;
esac
printf "$IMDS_NICS/$nic/$key\n"
done
}
_imds_help() {
cat <<EOT
Usage: imds [-h] { -e | +e | +n | +s | +t | @<alias> | <imds-path> } ...
-h : help
-e / +e : ignore / catch errors
+n / +s / +t : insert newline / space / tab
<alias> :-
hostname : instance hostname
ssh-keys : instance SSH keys
userdata : instance user data
nics : instance NICs
nic:<iface>[,<nic-key> ...] : specific NIC interface
<iface> : network interface (i.e. eth1)
<nic-key> :- { -e | +e | +n | +s | +t | @<nic-alias> | <nic-path> }
<nic-alias> :-
mac : mac address
ipv4 : ipv4 address(es)
ipv6 : ipv6 address(es)
ipv4-net : subnet ipv4 network(s)
ipv6-net : subnet ipv6 network(s)
ipv4-prefix : delegated ipv4 CIDR(s)
ipv6-prefix : delegated ipv6 CIDR(s)
EOT
}
imds "$@"

View File

@ -0,0 +1,32 @@
# Tiny Cloud configuration
# REQUIRED: The instance's cloud provider (valid: aws, azure, gcp, oci)
# valid: aws, azure, gcp, oci
#CLOUD=
# User account where instance SSH keys are installed
#CLOUD_USER=alpine
# Filename of userdata file (in TINY_CLOUD_VAR directory)
#CLOUD_USERDATA=user-data
# IMDS token validity, in seconds (AWS only)
#IMDS_TOKEN_TTL=5
# Location of var directory
#TINY_CLOUD_VAR=/var/lib/cloud
# Location of log directory
#TINY_CLOUD_LOGS=/var/log
# Hotplug Method (valid: mdev)
#HOTPLUG_TYPE=mdev
# Cloud-related Hotplug Modules
# valid: vnic_eth_hotplug, nvme_ebs_links (aws)
#HOTPLUG_MODULES=
# Explicitly skip these (whitespace delimited) things during init
# valid: expand_root install_hotplugs set_hostname set_ssh_keys
# save_userdata run_userdata
#SKIP_INIT_ACTIONS=

23
etc/init.d/tiny-cloud Executable file
View File

@ -0,0 +1,23 @@
#!/sbin/openrc-run
# vim:set ts=8 noet ft=sh:
description="Tiny Cloud Bootstrap - main phase"
depend() {
need net
before sshd
}
start() {
source /lib/tiny-cloud/init-main
is_bootstrapped && return 0
ebegin "Setting Instance Hostname"
set_hostname
eend $?
ebegin "Installing SSH Keys for $CLOUD_USER User"
set_ssh_keys "$CLOUD_USER"
eend $?
}

24
etc/init.d/tiny-cloud-early Executable file
View File

@ -0,0 +1,24 @@
#!/sbin/openrc-run
# vim:set ts=8 noet ft=sh:
description="Tiny Cloud Bootstrap - early phase"
depend() {
before mdev
}
start() {
source /lib/tiny-cloud/init-early
is_bootstrapped && return 0
ebegin "Expanding Root Volume/Partition"
expand_root
eend $?
if has_cloud_hotplugs; then
ebegin "Installing Cloud Hotplugs"
install_hotplugs
eend $?
fi
}

29
etc/init.d/tiny-cloud-final Executable file
View File

@ -0,0 +1,29 @@
#!/sbin/openrc-run
# vim:set ts=8 noet ft=sh:
description="Tiny Cloud Bootstrap - final phase"
depend() {
after *
provide cloud-final
}
start() {
source /lib/tiny-cloud/init-final
is_bootstrapped && return 0
ebegin "Saving Instance UserData"
save_userdata
eend $?
if is_userdata_script; then
ebegin "Executing UserData Script"
run_userdata
eend $?
fi
ebegin "Marking Instance Bootstrap Complete"
bootstrap_complete
eend $?
}

View File

@ -0,0 +1,4 @@
auto %%
iface %%
use dhcp
use imds

View File

@ -0,0 +1,4 @@
auto lo
iface lo
use link
use loopback

45
lib/mdev/nvme-ebs-links Executable file
View File

@ -0,0 +1,45 @@
#!/bin/sh
# vim:set ts=2 et:
source /lib/tiny-cloud/common
# nvme tool not installed?
[ -x /usr/sbin/nvme ] || log crit "nvme cli not installed"
raw_ebs_alias() {
/usr/sbin/nvme id-ctrl /dev/"$BASE" -b 2>/dev/null |
dd bs=32 skip=96 count=1 2>/dev/null
}
case $ACTION in
add|"")
BASE=$(echo "$MDEV" | sed -re 's/^(nvme[0-9]+n[0-9]+).*/\1/')
PART=$(echo "$MDEV" | sed -re 's/nvme[0-9]+n[0-9]+p?//g')
# TODO: deadline instead of max tries
MAXTRY=30
TRY=0
until [ -n "$EBS" ]; do
EBS=$(raw_ebs_alias | sed -nre '/^(\/dev\/)?(s|xv)d[a-z]{1,2} /p' | tr -d ' ')
[ -n "$EBS" ] && break
TRY=$((TRY + 1))
if [ $TRY -eq $MAXTRY ]; then
log err "Failed to get EBS volume alias for $MDEV after $MAXTRY attempts ($(raw_ebs_alias))"
exit 1
fi
sleep 0.1
done
# remove any leading '/dev/', 'sd', or 'xvd', and append partition
EBS=${EBS#/dev/}
EBS=${EBS#sd}
EBS=${EBS#xvd}$PART
ln -sf "$MDEV" "sd$EBS" && log notice "Added sd$EBS symlink for $MDEV"
ln -sf "$MDEV" "xvd$EBS" && log notice "Added xvd$EBS symlink for $MDEV"
;;
remove)
for TARGET in sd* xvd*
do
[ $(readlink "$TARGET" 2>/dev/null) = "$MDEV" ] && rm -f "$TARGET" && \
log notice "Removed $TARGET symlink for $MDEV"
done
;;
esac

86
lib/mdev/vnic-eth-hotplug Executable file
View File

@ -0,0 +1,86 @@
#!/bin/sh
# vim:set ts=4 et:
set -e
source /lib/tiny-cloud/common
if [ -z "$MDEV" ] || [ -z "$ACTION" ]; then
log crit "MDEV or ACTION undefined, aborting"
fi
IFACE_CFG=/etc/network/interfaces
ip() {
local v=-4 lev=info
if [ "$1" = '-4' ] || [ "$1" = '-6' ]; then
v="$1"
shift
fi
local op="$2"
[ "$op" = show ] && lev=debug
if /sbin/ip "$v" "$@" || [ -n "$FAIL_OK" ]; then
log "$lev" "OK: ip $v $*"
else
log err "FAIL: ip $v $*"
fi
}
interface_up() {
log info "Bringing up $MDEV"
# umask so udhcpc PID file isn't non-owner writeable
(umask 0022 && ifup "$MDEV")
}
cleanup_interface() {
local v pref rtable="${MDEV#eth}"
let rtable+=10000
log info "Cleaning up $MDEV"
# kill related udhcpc, don't panic if it's not there
kill "$(cat "/run/udhcpc.$MDEV.pid")" || true
# tidy up /run/ifstate, if it exists
[ -f /run/ifstate ] && sed -i -e "/^$MDEV=/d" /run/ifstate
rm -f /run/ifstate."$MDEV".lock
# remove related rules
for v in 4 6; do
for pref in $(ip -"$v" rule show table "$rtable" | cut -d: -f1); do
ip -"$v" rule del pref "$pref"
done
done
}
is_networking_started() { service networking status -q 2>/dev/null; }
log info "STARTING: $ACTION $MDEV"
if exec 200>>"$IFACE_CFG"; then
if flock 200; then
case $ACTION in
add|"")
assemble-interfaces
is_networking_started && interface_up
;;
remove)
assemble-interfaces
is_networking_started && cleanup_interface
;;
*)
log err "Unknown action '$ACTION'"
exit 1
;;
esac
else
log err "Unable to flock $IFACE_CFG"
exit 1
fi
else
log err "Unable to assign fd 200 to flock $IFACE_CFG"
exit 1
fi
log info "FINISHED: $ACTION $MDEV"

40
lib/tiny-cloud/aws/imds Normal file
View File

@ -0,0 +1,40 @@
# AWS Instance MetaData Service variables and functions
# vim:set ts=4 et ft=sh:
IMDS_HEADER="X-aws-ec2-metadata-token"
IMDS_TOKEN_TTL_HEADER="X-aws-ec2-metadata-token-ttl-seconds"
IMDS_TOKEN_TTL=${IMDS_TOKEN_TTL:-5}
IMDS_URI="latest"
IMDS_HOSTNAME="meta-data/hostname"
IMDS_SSH_KEYS="meta-data/public-keys"
IMDS_USERDATA="user-data"
IMDS_NICS="meta-data/network/interfaces/macs"
IMDS_MAC="mac"
IMDS_IPV4="local-ipv4s"
IMDS_IPV6="ipv6s"
IMDS_IPV4_NET="subnet-ipv4-cidr-block"
IMDS_IPV6_NET="subnet-ipv6-cidr-blocks"
IMDS_IPV4_PREFIX="ipv4-prefix"
IMDS_IPV6_PREFIX="ipv6-prefix"
_imds_token() {
echo -ne "PUT /latest/api/token" \
"HTTP/1.0\r\n$IMDS_TOKEN_TTL_HEADER: $IMDS_TOKEN_TTL\r\n\r\n" |
nc -w 1 "$IMDS_ENDPOINT" 80 | tail -n 1
}
_imds_header() {
echo "$IMDS_HEADER: $(_imds_token)"
}
# digs deeper than the default
_imds_ssh_keys() {
local key
for key in $(imds "$IMDS_SSH_KEYS"); do
imds "$IMDS_SSH_KEYS/${key%=*}/openssh-key"
done | sort -u
}
_imds_nic_index() { cat "/sys/class/net/$1/address"; }

11
lib/tiny-cloud/aws/mdev Normal file
View File

@ -0,0 +1,11 @@
# AWS mdev Hotplug Modules
# vim:set ts=4 et ft=sh:
# makes symlinks for NVMe devices that correlate to AWS EBS sd/xvd devices
mod__nvme_ebs_links() {
# nvme-cli not installed?
[ -x /usr/sbin/nvme ] || return 1
install_before '^nvme\.\*' \
'nvme[0-9]+n.* root:disk 0660 */lib/mdev/nvme-ebs-links'
}

30
lib/tiny-cloud/azure/imds Normal file
View File

@ -0,0 +1,30 @@
# Azure Instance MetaData Service variables and functions
# vim:set ts=4 et ft=sh:
IMDS_HEADER="Metadata"
IMDS_QUERY="?format=text&api-version=2021-05-01"
IMDS_URI="metadata/instance"
IMDS_HOSTNAME="compute/name"
IMDS_SSH_KEYS="compute/publicKeys"
IMDS_USERDATA="compute/userData"
IMDS_NICS=""
_imds_header() {
echo "$IMDS_HEADER: true"
}
# dig deeper than default
_imds_ssh_keys() {
local key
for key in $(imds "$IMDS_SSH_KEYS"); do
imds "$IMDS_SSH_KEYS/${key}/keyData"
done | sort -u
}
# decode userdata value
_imds_userdata() {
imds "$IMDS_USERDATA" | base64 -d
}

24
lib/tiny-cloud/common Normal file
View File

@ -0,0 +1,24 @@
# Tiny Cloud - common script functions
# vim: ts=4 et ft=sh:
log() {
local facility=kern
local stderr
local tag=$(basename "$0")
while [ "${1:0:1}" = '-' ]; do
case "$1" in
-f) facility="$2"; shift ;;
-s) stderr=-s ;;
-t) tag="$tag/$2"; shift ;;
esac
shift
done
local level="$1"
[ -z "$DEBUG" ] && [ "$level" = debug ] && return
shift
logger $stderr -p "$facility.$level" -t "$tag" "$@"
case "$level" in
crit|alert|emerg) exit 1 ;;
esac
}

26
lib/tiny-cloud/gcp/imds Normal file
View File

@ -0,0 +1,26 @@
# Google Cloud Instance MetaData Service variables and functions
# vim:set ts=4 et ft=sh:
IMDS_HEADER="Metadata-Flavor"
IMDS_URI="computeMetadata/v1"
IMDS_HOSTNAME="instance/hostname"
IMDS_SSH_KEYS="
project/attributes/ssh-keys
instance/attributes/ssh-keys
"
IMDS_USERDATA="instance/attributes/user-data"
_imds_header() {
echo "$IMDS_HEADER: Google"
}
# merge project and instance keys
_imds_ssh_keys() {
local ssh_keys
for ssh_keys in $IMDS_SSH_KEYS; do
# ignore errors and strip leading '<login>:'
imds -e "$ssh_keys" | cut -d: -f2-
done | sort -u
}

View File

@ -0,0 +1,29 @@
# Tiny Cloud - Common Initialization
# vim:set ts=4 et ft=sh:
source /etc/conf.d/tiny-cloud
# set defaults
CLOUD_USER=${CLOUD_USER:-alpine}
CLOUD_USERDATA=${CLOUD_USERDATA:-user-data}
TINY_CLOUD_LOGS=${TINY_CLOUD_LOGS:-/var/log}
TINY_CLOUD_VAR=${TINY_CLOUD_VAR:-/var/lib/cloud}
SKIP_INIT_ACTIONS=${SKIP_INIT_ACTIONS:-}
# is initial bootstrap already done?
is_bootstrapped() { [ -f "$TINY_CLOUD_VAR"/.bootstrap-complete ]; }
# indicate bootstrap is done
bootstrap_complete() { touch "$TINY_CLOUD_VAR"/.bootstrap-complete ]; }
# should we skip this action?
skip_action() {
local action="$1"
# no action? don't skip.
[ -z "$action" ] && return 1
# action not in the skip list?
echo "$SKIP_INIT_ACTIONS" | grep -Eq "\b$action\b" || return 1
echo -n " SKIPPING"
}

45
lib/tiny-cloud/init-early Normal file
View File

@ -0,0 +1,45 @@
# Tiny Cloud - Early Phase Functions
# vim:set ts=4 et ft=sh:
source /lib/tiny-cloud/init-common
expand_root() {
skip_action expand_root && return
# explicitly use busybox, in case util-linux is also installed
local mountpoint=$(busybox mountpoint -n / | cut -d' ' -f1)
local volume=$(echo "$mountpoint" |
sed -Ee "s/(nvme\d+n\d|(xv|s)d[a-z])p?\d?$/\1/"
)
local partition
if [ "$mountpoint" != "$volume" ]; then
# it's a partition, resize it
partition=$(echo "$mountpoint" | sed -Ee "s/.*(\d+)$/\1/")
echo ", +" | sfdisk -q --no-reread -N "$partition" "$volume"
partx -u "$volume"
fi
# resize filesystem
mount -orw,remount /
resize2fs "$mountpoint"
}
has_cloud_hotplugs() { [ -n "$HOTPLUG_MODULES" ]; }
install_hotplugs() {
skip_action install_hotplugs && return
local result
for module in $HOTPLUG_MODULES; do
result='-'
echo -n " $module"
if type "mod__$module" | grep -q "is a function"; then
"mod__$module" && result='+' || result='!'
fi
echo -n "($result)"
done
}
HOTPLUG_TYPE=${HOTPLUG_TYPE:-mdev}
[ -f /lib/tiny-cloud/"$HOTPLUG_TYPE" ] && source /lib/tiny-cloud/"$HOTPLUG_TYPE"

27
lib/tiny-cloud/init-final Normal file
View File

@ -0,0 +1,27 @@
# Tiny Cloud - Final Phase Functions
# vim:set ts=4 et ft=sh:
source /lib/tiny-cloud/init-common
save_userdata() {
skip_action save_userdata && return
imds -e @userdata > "$TINY_CLOUD_VAR/$CLOUD_USERDATA"
}
is_userdata_script() {
head -n1 "$TINY_CLOUD_VAR/$CLOUD_USERDATA" | grep -q '#!/'
}
run_userdata() {
skip_action run_userdata && return
local log="$TINY_CLOUD_LOGS/$CLOUD_USERDATA.log"
local exit="$TINY_CLOUD_LOGS/$CLOUD_USERDATA.exit"
local userdata="$TINY_CLOUD_VAR/$CLOUD_USERDATA"
chmod +x "$userdata"
{ "$userdata" 2>& 1; echo $? > "$exit"; } | tee "$log"
return $(cat "$exit")
}

39
lib/tiny-cloud/init-main Normal file
View File

@ -0,0 +1,39 @@
# Tiny Cloud - Main Phase Functions
# vim:set ts=4 et ft=sh:
source /lib/tiny-cloud/init-common
# ensure existence of output directories
[ ! -d "$TINY_CLOUD_LOGS" ] && mkdir -p "$TINY_CLOUD_LOGS"
[ ! -d "$TINY_CLOUD_VAR" ] && mkdir -p "$TINY_CLOUD_VAR"
set_hostname() {
skip_action set_hostname && return
local fqdn=$(imds @hostname)
local host="${fqdn%%\.*}"
echo "$host" > /etc/hostname
hostname -F /etc/hostname
echo -e "127.0.1.1\t$fqdn $host" >> /etc/hosts
}
set_ssh_keys() {
skip_action set_ssh_keys && return
local user="$CLOUD_USER"
local pwent=$(getent passwd "$user")
local group=$(echo "$pwent" | cut -d: -f4)
local ssh_dir="$(echo "$pwent" | cut -d: -f6)/.ssh"
local keys_file="$ssh_dir/authorized_keys"
if [ ! -d "$ssh_dir" ]; then
mkdir -p "$ssh_dir"
chmod 700 "$ssh_dir"
fi
touch "$keys_file"
chmod 600 "$keys_file"
chown -R "$user:$group" "$ssh_dir"
imds @ssh-keys > "$keys_file"
}

37
lib/tiny-cloud/mdev Normal file
View File

@ -0,0 +1,37 @@
# Tiny Cloud - mdev hotplug functions
# vim:set ts=4 et ft=sh:
# generic helper function to install mdev rules
install_before() {
local before="$1"
shift
local line="$*"
# already installed
fgrep -q "$line" /etc/mdev.conf && return 0
if grep -q "$before" /etc/mdev.conf; then
# install before existing rule
line="-$line"
else
# no rule exists, put it before the catch-all fallback
before='^# fallback'
line="$line\n"
fi
sed -i -Ee "s|($before.*)|$line\n\1|" /etc/mdev.conf
}
# hotpluggable VNICs (multi-cloud)
mod__vnic_eth_hotplug() {
[ -f /lib/mdev/vnic-eth-hotplug ] || return 1
install_before '^eth' \
'eth[0-9] root:root 0644 */lib/mdev/vnic-eth-hotplug'
# NICs attached at launch don't get added with mdev -s
assemble-interfaces
}
# load cloud-specific functions
[ -f /lib/tiny-cloud/"$CLOUD"/mdev ] && source /lib/tiny-cloud/"$CLOUD"/mdev

22
lib/tiny-cloud/oci/imds Normal file
View File

@ -0,0 +1,22 @@
# OCI Instance MetaData Service variables and functions
# vim:set ts=4 et ft=sh:
IMDS_HEADER="Authorization"
IMDS_URI="opc/v2"
IMDS_HOSTNAME="instance/hostname"
IMDS_SSH_KEYS="instance/metadata/ssh_authorized_keys"
IMDS_USERDATA="instance/metadata/userdata"
_imds_header() {
echo "$IMDS_HEADER: Bearer Oracle"
}
_imds_nic_index() {
local m n=0
local mac=$(cat /sys/class/net/$1/mac)
while m=$(imds $IMDS_NICS/$n/mac | tr A-F a-f); do
[ "$m" = "$mac" ] && echo $n; return 0
done
return 1
}

44
sbin/assemble-interfaces Executable file
View File

@ -0,0 +1,44 @@
#!/bin/sh
# vim:set ts=4 et:
set -e
IFACE_CFG=/etc/network/interfaces
IFACE_DIR="${IFACE_CFG}.d"
cd "$IFACE_DIR"
cat > "$IFACE_CFG.new" <<EOT
# NOTE: $0 rewrites this file. Edit files in
# /etc/network/interfaces.d/ to persist any customizations.
EOT
# existing loopback and eths
for i in /sys/class/net/*; do
IFACE="$(basename "$i")"
case $IFACE in
lo|eth*)
[ ! -f "$IFACE" ] && sed -e "s/%%/$IFACE/g" DEFAULT > "$IFACE"
printf "%s\n\n" "$(cat "$IFACE")" >> "$IFACE_CFG.new"
;;
*) continue ;;
esac
done
# all the rest
for i in "$IFACE_DIR"/*; do
IFACE="$(basename "$i")"
case $IFACE in
DEFAULT|lo|eth*)
continue
;;
*)
printf "%s\n\n" "$(cat "$IFACE")" >> "$IFACE_CFG.new"
;;
esac
done
# install new interfaces config
[ -f "$IFACE_CFG" ] && cp -a "$IFACE_CFG" "$IFACE_CFG.bak"
mv "$IFACE_CFG.new" "$IFACE_CFG"

144
sbin/imds-net-sync Executable file
View File

@ -0,0 +1,144 @@
#!/bin/sh
# vim: ts=4 et ft=sh:
# Sync interface's network configuration with IMDS
[ -z "$VERBOSE" ] || set -x
source /lib/tiny-cloud/common
IFACE=${IFACE:-unknown}
[ "$IFACE" = unknown ] && log -s crit "IFACE not set, aborting"
# kill interface's imds-net-sync daemon
[ "$1" = '-k' ] && PHASE=pre=down && shift
PHASE=${PHASE:-post-up}
# route table number
RTABLE=${IFACE#eth}
let RTABLE+=10000
# ip [+F] [-4|-6] <object> <command> [<parameters>]
ip() {
local fail_ok v=-4 cmd level
[ "$1" = '+F' ] && fail_ok=1 && shift
if [ "$1" = '-4' ] || [ "$1" = '-6' ]; then
v="$1"
shift
fi
cmd="$2"
[ "$cmd" = show ] && level=debug || level=info
if /sbin/ip "$v" "$@" || [ -n "$fail_ok" ]; then
log -s "$level" "OK: ip $v $*"
else
log -s err "FAIL: ip $v $*"
fi
}
# get secondary IPv4s currently on the interface
iface_ip4s() {
ip -4 addr show "$IFACE" secondary |
sed -E -e '/inet /!d' -e 's/.*inet ([0-9.]+).*/\1/'
}
# get IPv6s currently on the interface
iface_ip6s() {
ip -6 addr show "$IFACE" scope global |
sed -E -e '/inet6/!d' -e 's/.*inet6 ([0-9a-f:]+).*/\1/'
}
imds_ip4s() {
local ip4=$(imds "@nic:$IFACE,@ipv4")
local ip4s=$(echo "$ip4" | tail +2) # secondary IPv4s
local ip4p ip4_cidr ip4_gw
# non-eth0 interfaces need custom route tables
#
if [ "$IFACE" != eth0 ] && [ -n "$ip4s" ] &&
[ -z $(ip +F -4 route show table "$RTABLE" 2>/dev/null) ]; then
ip4p=$(echo "$ip4" | head -1) # primary IPv4
ip4_cidr=$(imds "@nic:$IFACE,@ipv4-net") # TODO: get from iface instead?
# TODO: this may not hold true for non-AWS clouds
ip4_gw=$(echo "$ip4_cidr" | cut -d/ -f1 |
awk -F. '{ print $1"."$2"."$3"."$4+1 }')
ip -4 route add default via "$ip4_gw" dev "$IFACE" table "$RTABLE"
ip -4 route add "$ip4_cidr" dev "$IFACE" proto kernel scope link \
src "$ip4p" table "$RTABLE"
fi
echo "$ip4s"
}
imds_ip6s() {
local ip6s gw tries=20
ip6s=$(imds "@nic:$IFACE,@ipv6")
# non-eth0 interfaces need custom route tables
#
# NOTE: busybox iproute2 doesn't do 'route show table' properly for IPv6,
# so iproute2-minimal package is required!
#
if [ "$IFACE" != eth0 ] && [ -n "$ip6s" ] &&
[ -z $(ip +F -6 route show table "$RTABLE" 2>/dev/null) ]; then
while true; do
gw=$(ip -6 route show dev "$IFACE" default | awk '{ print $3 }')
[ -n "$gw" ] && break
let tries--
if [ "$tries" -eq 0 ]; then
log -s warn "Unable to get IPv6 gateway RA after 10s"
break
fi
sleep 0.5
done
ip -6 route add default via "$gw" dev "$IFACE" table "$RTABLE"
fi
echo "$ip6s"
}
in_list() {
echo "$2" | grep -q "^$1$"
}
# ip_addr {4|6} {add|del} <ip>
ip_addr() {
local mask=32 # IPv4 always /32
[ "$1" -eq 6 ] && mask=128 # IPv6 always /128
ip -"$1" addr "$2" "$3/$mask" dev "$IFACE"
# TODO: only non eth0? delegated ipv[46] prefixes?
[ "$IFACE" = eth0 ] && return
# non-eth0 interfaces get rules associating IPs with route tables
ip -"$1" rule "$2" from "$3" lookup "$RTABLE"
}
# sync_ips {4|6} "<imds-ips>" "<iface-ips>"
sync_ips() {
local i
# remove extra IPs
for i in $3; do
in_list "$i" "$2" || ip_addr "$1" del "$i"
done
# add missing IPs
for i in $2; do
in_list "$i" "$3" || ip_addr "$1" add "$i"
done
}
imds_iface_sync() {
log -s info "SYNCING: $IFACE"
sync_ips 4 "$(imds_ip4s)" "$(iface_ip4s)"
sync_ips 6 "$(imds_ip6s)" "$(iface_ip6s)"
log -s info "FINISHED: $IFACE"
}
case "$PHASE" in
post-up)
# TODO: daemonize this
imds_iface_sync
;;
pre-down)
# TODO: kill daemon, maybe some cleanup
;;
*)
esac

View File

@ -1,110 +0,0 @@
#!/sbin/openrc-run
# vim:set ft=sh noet ts=4:
description="Provides EC2 cloud bootstrap"
# override in /etc/conf.d/tiny-ec2-bootstrap
EC2_USER=${EC2_USER:-alpine}
IMDS2_TOKEN_TTL=${IMDS2_TOKEN_TTL:-5}
depend() {
need net
provide cloud-final
}
_get_metadata_token() {
echo -ne "PUT /latest/api/token HTTP/1.0\r\nX-aws-ec2-metadata-token-ttl-seconds: $IMDS2_TOKEN_TTL\r\n\r\n" |
nc 169.254.169.254 80 | tail -n 1
}
_get_metadata() {
local uri="$1"
wget -qO - --header "X-aws-ec2-metadata-token: $(_get_metadata_token)" \
"http://169.254.169.254/latest/$uri" 2>/dev/null
}
_update_hostname() {
local ec2_fqdn="$(_get_metadata meta-data/hostname)"
local short_hostname="${ec2_fqdn%%\.*}"
echo "$short_hostname" > /etc/hostname
hostname -F /etc/hostname
echo -e "127.0.1.1\t$ec2_fqdn $short_hostname" >> /etc/hosts
}
_set_ssh_keys() {
local user="$1"
local group="$(getent passwd "$user" | cut -d: -f4)"
local ssh_dir="$(getent passwd "$user" | cut -d: -f6)/.ssh"
local keys_file="$ssh_dir/authorized_keys"
if [ ! -d "$ssh_dir" ]; then
mkdir -p "$ssh_dir"
chmod 755 "$ssh_dir"
fi
[ -f "$keys_file" ] && rm "$keys_file"
touch "$keys_file"
chmod 600 "$keys_file"
chown -R "$user:$group" "$ssh_dir"
for key in $(_get_metadata meta-data/public-keys/); do
_get_metadata "meta-data/public-keys/${key%=*}/openssh-key/" >> "$keys_file"
done
}
_run_userdata() {
local user_data="$(_get_metadata user-data)"
if printf '%s' "$user_data" | head -n1 | grep -q '^#!/'; then
printf '%s' "$user_data" >/var/lib/cloud/user-data.sh
chmod +x /var/lib/cloud/user-data.sh
local log_file=/var/log/cloud-bootstrap.log
local ec_file=/var/log/cloud-bootstrap.exit
{ /var/lib/cloud/user-data.sh 2>&1 ; echo $? >"$ec_file"; } | tee "$log_file"
ec=$(cat "$ec_file")
echo "User Data Script Exit Status: $ec"
return "$ec"
fi
}
_resize_root_partition() {
local mountpoint="$(busybox mountpoint -n / | cut -d' ' -f1)"
# mountpoint is the second partition...
if echo "$mountpoint" | cut -d' ' -f1 | grep -qE '/(nvme\d+n\d+p|xvd[a-z]+)2$'; then
local volume="$(echo "$mountpoint" | sed -Ee 's/(nvme\d+n\d+|xvd[a-z]+)p?2/\1/')"
einfo "Expanding root partition to volume size..."
echo ", +" | sfdisk -q --no-reread -N 2 "$volume"
einfo "Updating kernel with new partition table..."
partx -u "$volume"
fi
einfo "Resizing..."
resize2fs "$mountpoint"
}
_lock_root_account() {
passwd -l root
}
_disable_password() {
echo "$1:*" | chpasswd -e
}
start() {
# Don't bootstrap if the host has already been bootstrapped
[ -f "/var/lib/cloud/.bootstrap-complete" ] && return 0
[ -d "/var/lib/cloud" ] || mkdir -p /var/lib/cloud
ebegin "Locking root account"; _lock_root_account; eend $?
ebegin "Disabling $EC2_USER password"; _disable_password "$EC2_USER"; eend $?
ebegin "Expanding root partition"; _resize_root_partition; eend $?
ebegin "Setting ec2 hostname"; _update_hostname; eend $?
ebegin "Setting ec2 user ssh keys"; _set_ssh_keys "$EC2_USER"; eend $?
ebegin "Running ec2 user data script"; _run_userdata; eend $?
touch "/var/lib/cloud/.bootstrap-complete"
}

14
usr/libexec/ifupdown-ng/imds Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
# vim: set ts=8 noet:
# Tiny Cloud IMDS ifupdown-ng executor
case "$PHASE" in
post-up)
/sbin/imds-net-sync
;;
pre-down)
/sbin/imds-net-sync -k
;;
*) ;;
esac