From e04ae5b511181c5955a8672005c4e96fe04fed63 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 26 Mar 2023 17:29:36 -0400 Subject: convert tozt launch script to pulumi --- .gitignore | 2 + Pulumi.main.yaml | 1 + Pulumi.yaml | 6 +++ __main__.py | 25 ++++++++++ bin/pulumi | 16 +++++++ bootstrap/arch | 21 +++++++++ bootstrap/debian | 17 +++++++ requirements.txt | 3 ++ tozt/__init__.py | 0 tozt/instance.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ tozt/ssh.py | 69 ++++++++++++++++++++++++++++ 11 files changed, 297 insertions(+) create mode 100644 Pulumi.main.yaml create mode 100644 Pulumi.yaml create mode 100644 __main__.py create mode 100755 bin/pulumi create mode 100644 bootstrap/arch create mode 100644 bootstrap/debian create mode 100644 requirements.txt create mode 100644 tozt/__init__.py create mode 100644 tozt/instance.py create mode 100644 tozt/ssh.py diff --git a/.gitignore b/.gitignore index 0040936..603a7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /env /modules/secret/files/ +*.pyc +venv/ diff --git a/Pulumi.main.yaml b/Pulumi.main.yaml new file mode 100644 index 0000000..5353f5e --- /dev/null +++ b/Pulumi.main.yaml @@ -0,0 +1 @@ +encryptionsalt: v1:pVoAFxhgt/0=:v1:bRLbV3nl2WsnzkhV:ewSqOQe5WR6dw59MEB+sSX160W4G6g== diff --git a/Pulumi.yaml b/Pulumi.yaml new file mode 100644 index 0000000..3e17272 --- /dev/null +++ b/Pulumi.yaml @@ -0,0 +1,6 @@ +name: puppet-tozt +description: puppet-tozt +runtime: + name: python + options: + virtualenv: venv diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..154cb7e --- /dev/null +++ b/__main__.py @@ -0,0 +1,25 @@ +import sys + +import pulumi + +sys.path.append(".") + +from tozt.instance import Instance # noqa: E402 + +tozt = Instance( + "tozt", + region="nyc3", + size="s-1vcpu-2gb", + dns_name="tozt.net", + reserved_ip="138.197.58.11", + volume_name="tozt-persistent", +) + +pulumi.export( + "tozt", + { + "ip": tozt.instance.ipv4_address, + "domain": tozt.dns_name, + "reserved_ip": tozt.reserved_ip, + }, +) diff --git a/bin/pulumi b/bin/pulumi new file mode 100755 index 0000000..d8219ca --- /dev/null +++ b/bin/pulumi @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +script_path="$(realpath "$(dirname "$0")")" +secrets_bin="${script_path}/secrets" + +"$secrets_bin" open +trap '"$secrets_bin" close' EXIT + +DIGITALOCEAN_TOKEN="$(cat /mnt/digitalocean)" +export DIGITALOCEAN_TOKEN +export PULUMI_SKIP_UPDATE_CHECK=1 +PULUMI_CONFIG_PASSPHRASE="$(rbw get --folder=pulumi "$(hostname)" puppet-tozt)" +export PULUMI_CONFIG_PASSPHRASE + +pulumi "$@" diff --git a/bootstrap/arch b/bootstrap/arch new file mode 100644 index 0000000..d05da55 --- /dev/null +++ b/bootstrap/arch @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +conf_location="/usr/local/share/puppet-tozt" +conf_repo="https://github.com/doy/puppet-tozt" + +mkdir -p "$conf_location" +cd "$conf_location" || exit 1 +git clone "$conf_repo" . +git checkout pulumi +git submodule update --init --recursive +cp -r /tmp/secrets/ modules/secret/files + +set +e +puppet apply --modulepath=./modules --hiera_config=./hiera/hiera.yaml --detailed-exitcodes manifests +puppet_exit=$? +if [ $puppet_exit -eq 2 ]; then + exit 0 +else + exit $puppet_exit +fi diff --git a/bootstrap/debian b/bootstrap/debian new file mode 100644 index 0000000..f7cbd9b --- /dev/null +++ b/bootstrap/debian @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +conf_location="/usr/local/share/puppet-tozt" +conf_repo="https://github.com/doy/puppet-tozt" + +apt-get -y update +apt-get -y install git + +mkdir -p "$conf_location" +cd "$conf_location" || exit 1 +git clone "$conf_repo" . +git checkout pulumi +git submodule update --init --recursive + +cd digitalocean-debian-to-arch || exit 1 +bash install.sh --i_understand_that_this_droplet_will_be_completely_wiped --extra_packages "puppet git ruby-shadow" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2698a57 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pulumi>=3.0.0,<4.0.0 +pulumi-digitalocean>=4.0.0,<5.0.0 +pulumi-command>=0.7.0,<0.8.0 diff --git a/tozt/__init__.py b/tozt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tozt/instance.py b/tozt/instance.py new file mode 100644 index 0000000..1d94bda --- /dev/null +++ b/tozt/instance.py @@ -0,0 +1,137 @@ +from pathlib import Path +from typing import Optional + +import pulumi +import pulumi_digitalocean as do +import pulumi_command as command + +from .ssh import SshKey + + +class Instance(pulumi.ComponentResource): + def __init__( + self, + name: str, + region: str, + size: str, + dns_name: str, + reserved_ip: Optional[str] = None, + volume_name: Optional[str] = None, + opts: Optional[pulumi.ResourceOptions] = None, + ): + self.name = name + self.dns_name = dns_name + self.reserved_ip = reserved_ip + + super().__init__( + f"{pulumi.get_project()}:instance:Instance/{name}", name, None, opts + ) + + ssh_key = SshKey( + f"{self.name}-ssh-keypair", + opts=pulumi.ResourceOptions(parent=self), + ) + ssh = do.SshKey( + self.name, + public_key=ssh_key.public_key, + name=f"{pulumi.get_project()}-{pulumi.get_stack()}-{self.name}", + opts=pulumi.ResourceOptions(parent=self), + ) + + volumes = [] + if volume_name is not None: + volume = do.get_volume_output( + name=volume_name, + region=region, + opts=pulumi.InvokeOptions(parent=self), + ) + volumes.append(volume.id) + self.instance = do.Droplet( + self.name, + name=dns_name, + image="debian-10-x64", + region=region, + size=size, + ssh_keys=[ssh.id], + volume_ids=volumes, + opts=pulumi.ResourceOptions(parent=self), + ) + + if reserved_ip is not None: + self.ip_assignment = do.ReservedIpAssignment( + self.name, + ip_address=reserved_ip, + droplet_id=self.instance.id.apply(lambda id: int(id)), + opts=pulumi.ResourceOptions(parent=self), + ) + + connection = command.remote.ConnectionArgs( + host=self.instance.ipv4_address, + private_key=ssh_key.private_key, + user="root", + dial_error_limit=100, + ) + bootstrap_debian_file = command.remote.CopyFile( + f"{self.name}-bootstrap_debian", + connection=connection, + local_path="bootstrap/debian", + remote_path="/tmp/bootstrap", + opts=pulumi.ResourceOptions(parent=self), + ) + bootstrap_debian = command.remote.Command( + f"{self.name}-bootstrap_debian", + connection=connection, + create="sh /tmp/bootstrap", + opts=pulumi.ResourceOptions( + parent=self, depends_on=[bootstrap_debian_file] + ), + ) + sleep = command.local.Command( + f"{self.name}-sleep", + create="sleep 30", + opts=pulumi.ResourceOptions( + parent=self, depends_on=[bootstrap_debian] + ), + ) + make_secrets_dir = command.remote.Command( + f"{self.name}-make_secrets_dir", + connection=connection, + create="mkdir /tmp/secrets", + opts=pulumi.ResourceOptions(parent=self, depends_on=[sleep]), + ) + bootstrap_arch_file = command.remote.CopyFile( + f"{self.name}-bootstrap_arch", + connection=connection, + local_path="bootstrap/arch", + remote_path="/tmp/bootstrap", + opts=pulumi.ResourceOptions(parent=self, depends_on=[sleep]), + ) + secret_files = [] + for file in Path(f"/mnt/puppet/{name}").glob("*"): + secret_files.append( + command.remote.CopyFile( + f"{self.name}-secret-{file.name}", + connection=connection, + local_path=str(file), + remote_path=f"/tmp/secrets/{file.name}", + opts=pulumi.ResourceOptions( + parent=self, depends_on=[make_secrets_dir] + ), + ) + ) + command.remote.Command( + f"{self.name}-bootstrap_arch", + connection=connection, + create="sh /tmp/bootstrap", + opts=pulumi.ResourceOptions( + parent=self, + depends_on=[bootstrap_arch_file, *secret_files], + ), + ) + + self.register_outputs( + { + "dns_name": dns_name, + "ip_address": self.instance.ipv4_address, + } + ) diff --git a/tozt/ssh.py b/tozt/ssh.py new file mode 100644 index 0000000..51111c8 --- /dev/null +++ b/tozt/ssh.py @@ -0,0 +1,69 @@ +import binascii +import os +import subprocess +import tempfile +from typing import Any, Optional, Tuple + +import pulumi + + +class SshKeyProvider(pulumi.dynamic.ResourceProvider): + def create(self, inputs: Any): + (private_key, public_key) = ssh_keygen() + return pulumi.dynamic.CreateResult( + id_=str(binascii.b2a_hex(os.urandom(16))), + outs={"private_key": private_key, "public_key": public_key}, + ) + + +class SshKey(pulumi.dynamic.Resource): + private_key: pulumi.Output[str] + public_key: pulumi.Output[str] + + def __init__( + self, name: str, opts: Optional[pulumi.ResourceOptions] = None + ): + super().__init__( + SshKeyProvider(), + name, + {"private_key": None, "public_key": None}, + opts, + ) + + +def ssh_keygen() -> Tuple[str, str]: + with tempfile.TemporaryDirectory() as dir: + key = f"{dir}/id_rsa" + close = [] + try: + (priv_r, priv_w) = os.pipe() + close.extend([priv_r, priv_w]) + (pub_r, pub_w) = os.pipe() + close.extend([pub_r, pub_w]) + (yes_r, yes_w) = os.pipe() + close.extend([yes_r, yes_w]) + os.symlink(f"/proc/self/fd/{priv_w}", f"{key}") + os.symlink(f"/proc/self/fd/{pub_w}", f"{key}.pub") + os.write(yes_w, b"y\n") + os.close(yes_w) + subprocess.check_call( + ["ssh-keygen", "-q", "-N", "", "-f", key], + pass_fds=(pub_w, priv_w), + stdin=yes_r, + stdout=subprocess.DEVNULL, + ) + os.close(priv_w) + os.close(pub_w) + os.close(yes_r) + with open(priv_r) as f: + privkey = f.read() + with open(pub_r) as f: + pubkey = f.read() + except Exception: + for fd in close: + try: + os.close(fd) + except Exception: + pass + raise + return (privkey, pubkey) -- cgit v1.2.3-54-g00ecf