summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2023-03-26 17:29:36 -0400
committerJesse Luehrs <doy@tozt.net>2023-03-27 22:52:01 -0400
commite04ae5b511181c5955a8672005c4e96fe04fed63 (patch)
tree8908110f0125264fcdc5981847cb522036feab34
parent2d75d42766a6d77a9e6f81751f05ac61add1574c (diff)
downloadpuppet-tozt-e04ae5b511181c5955a8672005c4e96fe04fed63.tar.gz
puppet-tozt-e04ae5b511181c5955a8672005c4e96fe04fed63.zip
convert tozt launch script to pulumi
-rw-r--r--.gitignore2
-rw-r--r--Pulumi.main.yaml1
-rw-r--r--Pulumi.yaml6
-rw-r--r--__main__.py25
-rwxr-xr-xbin/pulumi16
-rw-r--r--bootstrap/arch21
-rw-r--r--bootstrap/debian17
-rw-r--r--requirements.txt3
-rw-r--r--tozt/__init__.py0
-rw-r--r--tozt/instance.py137
-rw-r--r--tozt/ssh.py69
11 files changed, 297 insertions, 0 deletions
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
--- /dev/null
+++ b/tozt/__init__.py
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)