add code from provs-ubuntu resp. provs-ubuntu-extensions

This commit is contained in:
az 2021-08-07 15:50:55 +02:00
parent 68de42c5ae
commit f5bf7865d5
70 changed files with 3851 additions and 2 deletions

View file

@ -2,10 +2,11 @@ buildscript {
ext.kotlin_version = '1.4.31'
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
repositories { jcenter() }
repositories { mavenCentral() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
@ -13,9 +14,12 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'java-library'
apply plugin: 'java-test-fixtures'
apply plugin: 'maven-publish'
//apply plugin: 'kotlin'
apply plugin: 'kotlinx-serialization'
group = 'io.provs'
version = '0.8.12-SNAPSHOT'
version = '0.8.13-SNAPSHOT'
repositories {
mavenCentral()
@ -56,6 +60,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0"
implementation group: 'com.hierynomus', name: 'sshj', version: '0.31.0'
api "org.slf4j:slf4j-api:1.7.30"

View file

@ -0,0 +1,14 @@
package io.provs.ubuntu.extensions.demos
import io.provs.Prov
import io.provs.local
fun Prov.helloWorld() = def {
cmd("echo Hello world!")
}
fun main() {
local().helloWorld()
}

View file

@ -0,0 +1,54 @@
package io.provs.ubuntu.extensions.demos
import io.provs.*
/**
* Prints some information and settings of the operating system and environment.
*
* For running locally no arguments are required.
* For running remotely either 2 or 3 arguments must be provided:
* either host and user for connection by ssh key ()
* or host, user and password for password-authenticated connection.
* E.g. 172.0.0.123 username or 172.0.0.123 username password
*/
fun main(vararg args: String) {
if (args.isEmpty()) {
local().printInfos()
} else {
if (args.size !in 2..3) {
println("Wrong number of arguments. Please specify either host and user if connection is done by ssh key or otherwise host, user and password. E.g. 172.0.0.123 username password")
} else {
val password = if (args.size == 2) null else Secret(args[3])
remote(args[0], args[1], password = password).printInfos()
}
}
}
fun Prov.printInfos() = def {
println("\nUbuntu Version:\n${ubuntuVersion()}")
println("\nCurrent directory:\n${currentDir()}")
println("\nTime zone:\n${timeZone()}")
val dir = cmd("pwd").out
println("\nCurrent directory: $dir")
ProvResult(true)
}
fun Prov.ubuntuVersion(): String? {
return cmd("lsb_release -a").out
}
fun Prov.currentDir(): String? {
return cmd("pwd").out
}
fun Prov.timeZone(): String? {
return cmd("cat /etc/timezone").out
}

View file

@ -0,0 +1,18 @@
package io.provs.ubuntu.extensions.server_compounds.monitoring
import io.provs.Prov
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpConf
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
import io.provs.ubuntu.extensions.server_software.prometheus.base.configurePrometheusDocker
import io.provs.ubuntu.extensions.server_software.prometheus.base.runPrometheusDocker
@Suppress("unused") // used externally
fun Prov.provisionMonitoring() = requireAll {
configurePrometheusDocker()
runPrometheusDocker()
provisionNginxStandAlone(NginxConf.nginxHttpConf())
}

View file

@ -0,0 +1,20 @@
package io.provs.ubuntu.extensions.server_compounds.monitoring
import io.provs.Prov
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxAddLocation
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxCreateSelfSignedCertificate
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpsConfWithLocationFiles
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
import io.provs.ubuntu.extensions.server_software.prometheus.base.prometheusNginxConfig
import io.provs.ubuntu.extensions.server_software.prometheus.provisionPrometheusDocker
@Suppress("unused") // used externally
fun Prov.provisionNginxMonitoring(nginxHost: String = "localhost") = def {
provisionPrometheusDocker(nginxHost)
nginxCreateSelfSignedCertificate()
provisionNginxStandAlone(NginxConf.nginxHttpsConfWithLocationFiles())
nginxAddLocation("443", nginxHost, "/prometheus", prometheusNginxConfig)
}

View file

@ -0,0 +1,28 @@
package io.provs.ubuntu.extensions.server_software.certbot
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.fileExists
import io.provs.ubuntu.install.base.aptInstall
/**
* Provisions a certbot for the specified serverName and email to obtain and renew letsencrypt certificates
* Parameter can be used to specify certbot options e.g. "--nginx" to configure nginx, see https://certbot.eff.org/docs/using.html#certbot-command-line-options
*/
fun Prov.provisionCertbot(serverName: String, email: String?, additionalOptions: String? = "") = requireAll {
aptInstall("snapd")
sh("""
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
""".trimIndent())
if (!fileExists("/usr/bin/certbot")) {
cmd("sudo ln -s /snap/bin/certbot /usr/bin/certbot")
val emailOption = email?.let { " -m $it" } ?: "--register-unsafely-without-email"
cmd("sudo certbot $additionalOptions -n --agree-tos $emailOption -d $serverName")
} else {
ProvResult(true)
}
}

View file

@ -0,0 +1,143 @@
package io.provs.ubuntu.extensions.server_software.firewall
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.install.base.aptInstall
fun Prov.saveIpTables() = requireAll {
sh("""
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
netfilter-persistent save""",
sudo = true)
}
fun Prov.makeIpTablesPersistent() = requireAll {
// inspired by https://gist.github.com/alonisser/a2c19f5362c2091ac1e7
// enables iptables-persistent to be installed without manual input
sh("""
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
""".trimIndent())
aptInstall("iptables-persistent netfilter-persistent")
saveIpTables()
}
fun Prov.resetFirewall() = requireAll {
sh("""
#!/bin/bash
sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t mangle -F
sudo iptables -t mangle -X
# the rules allow us to reconnect by opening up all traffic.
sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
# print out all rules to the console after running this file.
sudo iptables -nL
""", sudo = true
)
}
fun Prov.provisionFirewall(addNetworkProtections: Boolean = false) = requireAll {
if (addNetworkProtections) {
networkProtections()
}
// inspired by: https://github.com/ChrisTitusTech/firewallsetup/blob/master/firewall
sh("""
# Firewall
# Accept all traffic first to avoid ssh lockdown via iptables firewall rules #
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
# Flush all chains
iptables --flush
# Allow unlimited traffic on the loopback interface
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Previously initiated and accepted exchanges bypass rule checking
# Allow unlimited outbound traffic
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
#Ratelimit SSH for attack protection
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
# Allow http/https ports to be accessible from the outside
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT # http
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT # https
# UDP packet rule. This is just a random udp packet rule as an example only
# iptables -A INPUT -p udp --dport 5021 -m state --state NEW -j ACCEPT
# Allow pinging of your server
iptables -A INPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
# Drop all other traffic
iptables -A INPUT -j DROP
# print the activated rules to the console when script is completed
iptables -nL
# Set default policies
iptables --policy INPUT DROP
iptables --policy OUTPUT DROP
iptables --policy FORWARD DROP
""", sudo = true)
if (chk("docker -v")) {
ipTablesRecreateDockerRules()
} else {
ProvResult(true, "No need to create iptables docker rules as no docker installed.")
}
}
fun Prov.networkProtections() = def {
sh("""
# Drop ICMP echo-request messages sent to broadcast or multicast addresses
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
# Drop source routed packets
echo 0 > /proc/sys/net/ipv4/conf/all/accept_source_route
# Enable TCP SYN cookie protection from SYN floods
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
# Don't accept ICMP redirect messages
echo 0 > /proc/sys/net/ipv4/conf/all/accept_redirects
# Don't send ICMP redirect messages
echo 0 > /proc/sys/net/ipv4/conf/all/send_redirects
# Enable source address spoofing protection
echo 1 > /proc/sys/net/ipv4/conf/all/rp_filter
# Log packets with impossible source addresses
echo 1 > /proc/sys/net/ipv4/conf/all/log_martians
""".trimIndent())
}
fun Prov.ipTablesRecreateDockerRules() = requireAll {
// see https://stackoverflow.com/questions/25917941/docker-how-to-re-create-dockers-additional-iptables-rules
cmd("sudo service docker restart")
}

View file

@ -0,0 +1,21 @@
package io.provs.ubuntu.extensions.server_software.firewall.base
import io.provs.Prov
import io.provs.ProvResult
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun Prov.saveIpTablesToFile() = def {
val dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("-yyyy-MM-dd--HH:mm:ss"))
val file = "savedrules$dateTime.txt"
sh("""
sudo iptables-save > $file
cat $file""")
}
fun Prov.restoreIpTablesFromFile(file: String? = null) = def {
val fileName = file ?: cmd("ls -r a* | head -1\n").out
fileName?.let { cmd("sudo iptables-restore < $file") }
?: ProvResult(false, err = "File to restore not found.")
}

View file

@ -0,0 +1,92 @@
package io.provs.ubuntu.extensions.server_software.nexus
import io.provs.Prov
import io.provs.ProvResult
import io.provs.docker.containerRuns
import io.provs.remote
import io.provs.ubuntu.filesystem.base.fileExists
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.user.base.createUser
import io.provs.ubuntu.extensions.server_software.certbot.provisionCertbot
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxReverseProxyHttpConfig
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
/**
* Provisions sonatype nexus in a docker container.
* If you would want nexus to be accessible directly from the internet (e.g. for test or demo reasons)
* set parameter portAccessibleFromNetwork to true.
*/
fun Prov.provisionNexusWithDocker(portAccessibleFromNetwork: Boolean = false) = requireAll {
// https://blog.sonatype.com/sonatype-nexus-installation-using-docker
// https://medium.com/@AhGh/how-to-setup-sonatype-nexus-3-repository-manager-using-docker-7ff89bc311ce
aptInstall("docker.io")
if (!containerRuns("nexus")) {
val volume = "nexus-data"
if (!cmdNoEval("docker volume inspect $volume").success) {
cmd("docker volume create --name $volume")
}
cmd("sudo docker run -d --restart unless-stopped -p 8081:8081 --name nexus -v nexus-data:/nexus-data sonatype/nexus3")
for (n in 0..3) {
if (fileExists("/var/lib/docker/volumes/$volume/_data/admin.password", sudo = true)) {
val res = cmd("sudo cat /var/lib/docker/volumes/$volume/_data/admin.password")
println("Admin Password:" + res.out)
break
}
Thread.sleep(20000)
}
}
if (!portAccessibleFromNetwork) {
val netIf = getDefaultNetworkingInterface()
netIf?.also {
val iptablesParameters = "DOCKER-USER -i $it ! -s 127.0.0.1 -j DROP"
if (!cmdNoEval("sudo iptables -C $iptablesParameters").success) {
cmd("sudo iptables -I $iptablesParameters")
}
}
}
ProvResult(true) // dummy
}
private fun Prov.getDefaultNetworkingInterface(): String? {
return cmd("route | grep \"^default\" | grep -o \"[^ ]*\$\"\n").out?.trim()
}
/**
* Provisions sonatype nexus on the specified host.
* Creates user "nexus" on the remote system.
* Installs nexus in a docker container behind an nginx reverse proxy with ssl using letsencrypt certificates.
*
* To run this method it is required to have ssl root access to the host.
*/
@Suppress("unused") // to be used externally
fun provisionNexusServer(serverName: String, certbotEmail: String) {
val userName = "nexus" + 7
remote(serverName, "root").def {
createUser(userName, copyAuthorizedKeysFromCurrentUser = true, sudo = true)
}
remote(serverName, userName).requireAll {
provisionNexusWithDocker()
if (provisionNginxStandAlone(NginxConf.nginxReverseProxyHttpConfig(serverName)).success) {
cmd("sudo cat /etc/nginx/nginx.conf")
provisionCertbot(serverName, certbotEmail, "--nginx")
optional {
cmd("sudo cat /etc/nginx/nginx.conf")
cmd("sudo sed -i 's/X-Forwarded-Proto \"http\"/X-Forwarded-Proto \"https\"/g' /etc/nginx/nginx.conf")
cmd("sudo systemctl reload nginx")
}
} else {
ProvResult(true)
}
}
}

View file

@ -0,0 +1,83 @@
package io.provs.ubuntu.extensions.server_software.nexus.base
fun reverseProxyConfigHttpPort80(serverName: String): String {
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
return """
events {} # event context have to be defined to consider config valid
http {
proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
keepalive_timeout 5 5;
tcp_nodelay on;
server {
listen 80;
server_name $serverName;
# allow large uploads of files
client_max_body_size 1G;
# optimize downloading files larger than 1G
#proxy_max_temp_file_size 2G;
location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host ${'$'}host;
proxy_set_header X-Real-IP ${'$'}remote_addr;
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto "http";
}
}
}
""".trimIndent()
}
fun reverseProxyConfigSsl(serverName: String, ssl_certificate: String? = null, ssl_certificate_key: String? = null): String {
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" } ?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" } ?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
return """
events {} # event context have to be defined to consider config valid
http {
proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
keepalive_timeout 5 5;
tcp_nodelay on;
server {
listen *:443 ssl;
server_name $serverName;
# allow large uploads of files
client_max_body_size 1G;
# optimize downloading files larger than 1G
# proxy_max_temp_file_size 2G;
$sslCertificateEntry
$sslCertificateKeyEntry
location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host ${'$'}host;
proxy_set_header X-Real-IP ${'$'}remote_addr;
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto "https";
}
}
}
"""
}

View file

@ -0,0 +1,46 @@
package io.provs.ubuntu.extensions.server_software.nginx
import io.provs.Prov
import io.provs.ProvResult
import io.provs.remote
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.fileExists
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
import io.provs.ubuntu.extensions.server_software.nginx.base.createNginxLocationFolders
import kotlin.system.exitProcess
internal const val configFile = "/etc/nginx/nginx.conf"
fun Prov.provisionNginxStandAlone(config: NginxConf? = null) = requireAll {
aptInstall("nginx")
createNginxLocationFolders()
if (config != null) {
if (fileExists(configFile)) {
cmd("sudo mv $configFile $configFile-orig")
}
createFile(configFile, config.conf, sudo = true)
val configCheck = cmd("sudo nginx -t")
if (configCheck.success) {
cmd("sudo service nginx restart")
} else {
ProvResult(false, err = "Nginx config is incorrect:\n" + configCheck.err)
}
} else {
ProvResult(true) // dummy
}
}
fun provisionRemote(vararg args: String) {
if (args.size != 2) {
println("Pls specify host and user for remote installation of nginx.")
exitProcess(1)
}
remote(args[0], args[1]).provisionNginxStandAlone()
}

View file

@ -0,0 +1,12 @@
package io.provs.ubuntu.extensions.server_software.nginx.base
import io.provs.Prov
import io.provs.Secret
import io.provs.ubuntu.install.base.aptInstall
fun Prov.nginxAddBasicAuth(user: String, password: Secret) = requireAll {
aptInstall("apache2-utils")
val passwordFile = "/etc/nginx/.htpasswd"
cmdNoLog("sudo htpasswd -b -c $passwordFile $user ${password.plain()}")
}

View file

@ -0,0 +1,162 @@
package io.provs.ubuntu.extensions.server_software.nginx.base
class NginxConf(val conf: String = NGINX_MINIMAL_CONF) {
companion object {}
}
const val NGINX_MINIMAL_CONF = """
events {}
http {
server {
listen 80;
location / {
return 200 'Hi from nginx!';
}
}
}
"""
@Suppress("unused") // use later
fun NginxConf.Companion.nginxHttpConf(
serverName: String = "localhost"
): NginxConf {
return NginxConf(
"""
events {}
http {
server {
listen 80;
server_name $serverName;
include /etc/nginx/locations-enabled/port80*$locationsFileExtension;
}
}
"""
)
}
fun NginxConf.Companion.nginxHttpsConfWithLocationFiles(
sslCertificate: String = "/etc/nginx/ssl/cert/selfsigned.crt",
sslCertificateKey: String = "/etc/nginx/ssl/private/selfsigned.key"
): NginxConf {
return NginxConf(
"""
events {}
http {
server {
listen 443 ssl;
server_name localhost;
ssl_certificate $sslCertificate;
ssl_certificate_key $sslCertificateKey;
include /etc/nginx/locations-enabled/port443*$locationsFileExtension;
}
}
"""
)
}
@Suppress("unused") // use later
fun NginxConf.Companion.nginxReverseProxySslConfig(
serverName: String,
ssl_certificate: String? = null,
ssl_certificate_key: String? = null
): NginxConf {
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" }
?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" }
?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
return NginxConf(
"""
events {} # event context have to be defined to consider config valid
http {
proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
keepalive_timeout 5 5;
tcp_nodelay on;
server {
listen *:443 ssl;
server_name $serverName;
# allow large uploads of files
client_max_body_size 1G;
# optimize downloading files larger than 1G
#proxy_max_temp_file_size 2G;
$sslCertificateEntry
$sslCertificateKeyEntry
location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host ${'$'}host;
proxy_set_header X-Real-IP ${'$'}remote_addr;
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto "https";
}
}
}
"""
)
}
@Suppress("unused") // use later
fun NginxConf.Companion.nginxReverseProxyHttpConfig(
serverName: String
): NginxConf {
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
return NginxConf(
"""
events {} # event context have to be defined to consider config valid
http {
proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
keepalive_timeout 5 5;
tcp_nodelay on;
server {
listen *:80;
server_name $serverName;
# allow large uploads of files
client_max_body_size 1G;
# optimize downloading files larger than 1G
#proxy_max_temp_file_size 2G;
location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://127.0.0.1:8081/;
proxy_set_header Host ${'$'}host;
proxy_set_header X-Real-IP ${'$'}remote_addr;
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto "https";
}
}
}
"""
)
}

View file

@ -0,0 +1,44 @@
package io.provs.ubuntu.extensions.server_software.nginx.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.*
internal const val locationsAvailableDir = "/etc/nginx/locations-available/"
internal const val locationsEnabledDir = "/etc/nginx/locations-enabled/"
internal const val locationsFileExtension = ".locations"
fun Prov.createNginxLocationFolders() = requireAll {
createDirs(locationsEnabledDir, sudo = true)
createDirs(locationsAvailableDir, sudo = true)
}
fun Prov.nginxIncludeLocationFolders() = requireAll {
replaceTextInFile("/etc/nginx/nginx.conf", "listen 80;\n",
"""listen 80;
include ${locationsAvailableDir}port80*$locationsFileExtension;
include ${locationsEnabledDir}port443*$locationsFileExtension;
""")
}
fun Prov.nginxAddLocation(port: String, locationFileName: String, urlPath: String, content: String) = requireAll {
val locationConf = """location $urlPath {""" +
content +
"\n}"
if (!dirExists(locationsAvailableDir, sudo = true)) {
createNginxLocationFolders()
}
createFile("${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension", locationConf, sudo = true)
if (!fileExists("${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension", sudo = true)) {
cmd("sudo ln -s ${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension ${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension ")
} else {
ProvResult(true)
}
}

View file

@ -0,0 +1,36 @@
package io.provs.ubuntu.extensions.server_software.nginx.base
import io.provs.Prov
import io.provs.ubuntu.filesystem.base.createDirs
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
internal val certificateName = "selfsigned"
internal val sslDays = 365
val dirSslCert="/etc/nginx/ssl/cert"
val dirSslKey="/etc/nginx/ssl/private"
fun Prov.nginxCreateSelfSignedCertificate(
country: String = "DE",
state: String = "test",
locality: String = "test",
organization: String = "test",
organizationalUnit: String = "test",
commonName: String = "test",
email : String = "test@test.net"
) = def {
// inspired by https://gist.github.com/adrianorsouza/2bbfe5e197ce1c0b97c8
createDirs(dirSslCert, sudo = true)
createDirs(dirSslKey, sudo = true)
cmd("cd $dirSslKey && sudo openssl req -x509 -nodes -newkey rsa:2048 -keyout $certificateName.key -out $certificateName.crt -days $sslDays -subj \"/C=$country/ST=$state/L=$locality/O=$organization/OU=$organizationalUnit/CN=$commonName/emailAddress=$email\"")
cmd("sudo mv $dirSslKey/$certificateName.crt $dirSslCert/")
}
fun Prov.configureNginxWithSelfSignedCertificate() = def {
// todo: should not call provisionNginxStandAlone, which is defined in the package above
provisionNginxStandAlone(NginxConf.nginxReverseProxySslConfig("localhost",
dirSslCert+"/"+ certificateName + ".crt",
dirSslKey + "/" + certificateName + ".key"))
}

View file

@ -0,0 +1,19 @@
package io.provs.ubuntu.extensions.server_software.prometheus
import io.provs.Prov
import io.provs.ubuntu.extensions.server_software.prometheus.base.*
/**
* Provisions prometheus monitoring.
* If running behind an nginx, pls specify the hostname in parameter nginxHost (e.g. mydomain.com).
* To run it without nodeExporter (which provides system data to prometheus), set withNodeExporter to false.
*/
fun Prov.provisionPrometheusDocker(nginxHost: String? = null, withNodeExporter: Boolean = true) = def {
configurePrometheusDocker()
if (withNodeExporter) {
installNodeExporter()
runNodeExporter()
addNodeExporterToPrometheusConf()
}
runPrometheusDocker(nginxHost)
}

View file

@ -0,0 +1,85 @@
package io.provs.ubuntu.extensions.server_software.prometheus.base
import io.provs.Prov
import io.provs.local
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.fileContainsText
import io.provs.ubuntu.filesystem.base.replaceTextInFile
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.user.base.whoami
internal val defaultInstallationDir = "/usr/local/bin/"
fun Prov.installNodeExporter() = requireAll {
// inspired by https://devopscube.com/monitor-linux-servers-prometheus-node-exporter/ and
// https://www.howtoforge.com/tutorial/how-to-install-prometheus-and-node-exporter-on-centos-8/#step-install-and-configure-nodeexporter
val downloadFileBasename = "node_exporter-1.0.1.linux-amd64"
val downloadFile = "$downloadFileBasename.tar.gz"
val downloadPath = "~/tmp/"
val fqFile = downloadPath + downloadFile
aptInstall("curl")
createDir("tmp")
sh(
"""
cd tmp && curl -LO https://github.com/prometheus/node_exporter/releases/download/v1.0.1/$downloadFile --output $downloadFile
cd tmp && tar -xvf $fqFile -C $downloadPath
sudo mv $downloadPath$downloadFileBasename/node_exporter $defaultInstallationDir"""
)
}
fun Prov.runNodeExporter() = def {
createFile("/etc/systemd/system/node_exporter.service", nodeExporterServiceConf(whoami()?:"nouserfound"), sudo = true)
sh("""
sudo systemctl daemon-reload
# start the node_exporter service and enable it to launch everytime at system startup.
sudo systemctl start node_exporter
sudo systemctl enable node_exporter
# check if running
sudo systemctl status node_exporter --no-pager -l
""")
}
fun Prov.addNodeExporterToPrometheusConf (
prometheusConf: String = "/etc/prometheus/prometheus.yml",
sudo: Boolean = true
) = requireAll {
val prometheusConfNodeExporter = """
scrape_configs:
- job_name: 'node_exporter'
static_configs:
- targets: ['172.17.0.1:9100']
"""
if (!fileContainsText(prometheusConf, "- job_name: 'node_exporter'", sudo)) {
replaceTextInFile(prometheusConf, "\nscrape_configs:\n", prometheusConfNodeExporter)
}
// cmd("sudo systemctl restart prometheus") for standalone
cmd("sudo docker restart prometheus")
}
fun nodeExporterServiceConf(user: String, installationDir: String = defaultInstallationDir): String {
return """
[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User=$user
ExecStart=${installationDir}node_exporter
[Install]
WantedBy=default.target
"""
}

View file

@ -0,0 +1,76 @@
package io.provs.ubuntu.extensions.server_software.prometheus.base
import io.provs.Prov
import io.provs.docker.containerRuns
import io.provs.local
import io.provs.ubuntu.filesystem.base.createDirs
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.install.base.aptInstall
internal val configDir = "/etc/prometheus/"
internal val configFile = "prometheus.yml"
fun Prov.configurePrometheusDocker(config: String = prometheusDefaultConfig) = requireAll {
createDirs(configDir, sudo = true)
createFile(configDir + configFile, config, sudo = true)
}
fun Prov.runPrometheusDocker(nginxHost: String? = null) = requireAll {
aptInstall("docker.io")
val containerName = "prometheus"
if (containerRuns(containerName)) {
cmd("sudo docker restart $containerName")
} else {
if (nginxHost == null) {
cmd(
"sudo docker run -d -p 9090:9090 " +
" --name $containerName " +
" --restart on-failure:1" +
" -v prometheus-data:/prometheus" +
" -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
" prom/prometheus"
)
} else {
cmd(
"sudo docker run -d -p 9090:9090 " +
" --name $containerName " +
" --restart on-failure:1" +
" -v prometheus-data:/prometheus" +
" -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
" prom/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus " +
"--web.console.libraries=/usr/share/prometheus/console_libraries " +
"--web.console.templates=/usr/share/prometheus/consoles " +
"--web.external-url=http://$nginxHost/prometheus"
)
}
}
}
private const val prometheusDefaultConfig =
"""
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.
# Attach these labels to any time series or alerts when communicating with
# external systems (federation, remote storage, Alertmanager).
external_labels:
monitor: 'codelab-monitor'
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
static_configs:
- targets: ['localhost:9090']
"""

View file

@ -0,0 +1,5 @@
package io.provs.ubuntu.extensions.server_software.prometheus.base
val prometheusNginxConfig = """
proxy_pass http://localhost:9090/prometheus;
"""

View file

@ -0,0 +1,155 @@
package io.provs.ubuntu.extensions.workplace
import io.provs.*
import io.provs.processors.RemoteProcessor
import io.provs.ubuntu.extensions.workplace.base.*
import io.provs.ubuntu.git.provisionGit
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.install.base.aptInstallFromPpa
import io.provs.ubuntu.install.base.aptPurge
import io.provs.ubuntu.keys.KeyPair
import io.provs.ubuntu.keys.base.gpgFingerprint
import io.provs.ubuntu.keys.provisionKeysCurrentUser
import io.provs.ubuntu.secret.secretSources.PromptSecretSource
import io.provs.ubuntu.user.base.currentUserCanSudo
import io.provs.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
import io.provs.ubuntu.user.base.whoami
import java.net.InetAddress
import kotlin.system.exitProcess
enum class WorkplaceType {
MINIMAL, OFFICE, IDE
}
/**
* Provisions software and configurations for a personal workplace.
* Offers the possibility to choose between different types.
* Type OFFICE installs office-related software like Thunderbird, LibreOffice, and much more.
* Type IDE provides additional software for a development environment, such as Visual Studio Code, IntelliJ, etc.
*
* Prerequisites: user must be sudoer. If password is required for user to execute sudo, then also parameter userPassword must be provided
*
* @param workplaceType
* @param userPassword only needs to be provided if user cannot sudo without password
*/
fun Prov.provisionWorkplace(
workplaceType: WorkplaceType = WorkplaceType.MINIMAL,
ssh: KeyPair? = null,
gpg: KeyPair? = null,
gitUserName: String? = null,
gitEmail: String? = null,
userPassword: Secret? = null
) = requireAll {
userPassword?.also { makeUserSudoerWithNoSudoPasswordRequired(it) }
if (!currentUserCanSudo()) {
throw Exception("Current user ${whoami()} cannot execute sudo without a password, but he must be able to in order to provisionWorkplace")
}
aptInstall("ssh gnupg curl git")
provisionKeysCurrentUser(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
installVirtualBoxGuestAdditions()
aptPurge("remove-power-management xfce4-power-manager " +
"xfce4-power-manager-plugins xfce4-power-manager-data")
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
configureNoSwappiness()
if (workplaceType == WorkplaceType.OFFICE || workplaceType == WorkplaceType.IDE) {
aptInstall("seahorse")
aptInstall(BASH_UTILS)
aptInstall(OS_ANALYSIS)
aptInstall(ZIP_UTILS)
aptInstall("firefox chromium-browser")
aptInstall("thunderbird libreoffice")
aptInstall("xclip")
installZimWiki()
installGopass()
aptInstallFromPpa("nextcloud-devs", "client", "nextcloud-client")
aptInstall("inkscape")
aptInstall("dia")
aptInstall(SPELLCHECKING_DE)
installRedshift()
configureRedshift()
}
if (workplaceType == WorkplaceType.IDE) {
aptInstall(JAVA_JDK)
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
installDocker()
// IDEs
cmd("sudo snap install intellij-idea-community --classic")
installVSC("python", "clojure")
}
ProvResult(true) // dummy
}
/**
* Provisions a workplace on a remote machine.
* Prerequisite: you have built the uberjar by ./gradlew uberJarLatest
* The remote host and remote user are specified by args parameters.
* The first argument specifies hostName or IP-Address of the remote machine,
* the second argument defines the user on the remote machine for whom the workplace is provisioned;
* You can invoke this method e.g. using the jar-file from the project root by:
* java -jar build/libs/provs-extensions-uber.jar io.provs.ubuntu.extensions.workplace.ProvisionWorkplaceKt provisionRemote <ip> <user>
* You will be prompted for the password of the remote user.
*
* @param args host and userName of the remote machine as the first resp. second argument
*/
fun provisionRemote(args: Array<String>) {
if (args.size != 2) {
println("Please specify host and user.")
exitProcess(1)
}
val host = InetAddress.getByName(args[0])
val userName = args[1]
val pwSecret = PromptSecretSource("Password for user $userName on $host").secret()
val pwFromSecret = Password(pwSecret.plain())
val config = readWorkplaceConfigFromFile() ?: WorkplaceConfig()
Prov.newInstance(RemoteProcessor(host, userName, pwFromSecret), OS.LINUX.name).provisionWorkplace(
config.type,
config.ssh?.keyPair(),
config.gpg?.keyPair(),
config.gitUserName,
config.gitEmail,
pwFromSecret
)
}
/**
* Provisions a workplace on a remote machine by calling method provisionRemote.
*
* @ see #provisionRemote(args: Array<String>)
*
* You can invoke this method e.g. using the jar-file from the project root by:
* java -jar build/libs/provs-ext-latest.jar workplace.WorkplaceKt main
*
* @param args host and userName of the remote machine as first resp. second argument
*/
fun main(args: Array<String>) {
provisionRemote(args = args)
}

View file

@ -0,0 +1,42 @@
package io.provs.ubuntu.extensions.workplace
import io.provs.ubuntu.keys.KeyPairSource
import io.provs.ubuntu.secret.SecretSource
import io.provs.ubuntu.secret.SecretSourceType
import io.provs.ubuntu.secret.SecretSupplier
import io.provs.ubuntu.secret.secretSources.PlainSecretSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.*
@Serializable
class WorkplaceConfig(
val type: WorkplaceType = WorkplaceType.MINIMAL,
val ssh: KeyPairSource? = null,
val gpg: KeyPairSource? = null,
val gitUserName: String? = null,
val gitEmail: String? = null,
)
// -------------------------------------------- file methods ------------------------------------
fun readWorkplaceConfigFromFile(filename: String = "WorkplaceConfig.json"): WorkplaceConfig? {
val file = File(filename)
return if (file.exists())
try {
// read from file
val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
return Json.decodeFromString(WorkplaceConfig.serializer(), inputAsString)
} catch (e: FileNotFoundException) {
null
} else null
}
fun writeWorkplaceConfigToFile(config: WorkplaceConfig) {
val fileName = "WorkplaceConfig.json"
FileWriter(fileName).use { it.write(Json.encodeToString(WorkplaceConfig.serializer(), config)) }
}

View file

@ -0,0 +1,12 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ubuntu.install.base.aptInstall
fun Prov.installDocker() = def {
aptInstall("containerd docker.io")
if (!chk("getent group docker")) {
cmd("sudo groupadd docker")
}
cmd("sudo gpasswd -a \$USER docker")
}

View file

@ -0,0 +1,17 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.web.base.downloadFromURL
fun Prov.installFakturama() = def {
createDir("/tmp", sudo = true)
downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Installer_Fakturama_linux_x64_2.1.1b.deb", "fakturama.deb", "/tmp")
cmd("sudo dpkg -i fakturama.deb", "/tmp")
createDir("/opt/fakturama", sudo = true)
val filename = "Handbuch-Fakturama_2.1.1.pdf"
downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Handbuch-Fakturama_2.1.1.pdf", filename, "/tmp")
cmd("sudo mv /tmp/$filename /opt/fakturama")
}

View file

@ -0,0 +1,90 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createDirs
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.userHome
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.install.base.isPackageInstalled
fun Prov.installGopass(version: String = "1.12.7", enforceVersion: Boolean = false) = def {
if (isPackageInstalled("gopass") && !enforceVersion) {
ProvResult(true)
} else {
// install required dependencies
aptInstall("rng-tools gnupg2 git")
aptInstall("curl")
sh(
"""
curl -L https://github.com/gopasspw/gopass/releases/download/v${version}/gopass_${version}_linux_amd64.deb -o gopass_${version}_linux_amd64.deb
sudo dpkg -i gopass_${version}_linux_amd64.deb
"""
)
gopassEnsureVersion(version)
}
}
fun Prov.configureGopass(gopassRootFolder: String? = null) = def {
val gopassRootFolderNonNull = (gopassRootFolder ?: userHome()) + ".password-store"
// use default
createDir(gopassRootFolderNonNull)
createDirs(".config/gopass", "~/")
createFile("~/.config/gopass/config.yml", gopassConfig(gopassRootFolderNonNull))
}
fun Prov.gopassMountStore(storeName: String, path: String, indexOfRecepientKey: Int = 0) = def {
cmd("printf \"$indexOfRecepientKey\\n\" | gopass mounts add $storeName $path")
}
internal fun gopassConfig(gopassRoot: String): String {
return """
root:
askformore: false
autoclip: true
autoprint: false
autoimport: true
autosync: false
check_recipient_hash: false
cliptimeout: 45
concurrency: 1
editrecipients: false
exportkeys: true
nocolor: false
noconfirm: true
nopager: false
notifications: true
path: gpgcli-gitcli-fs+file://$gopassRoot
recipient_hash:
.gpg-id: 3078303637343130344341383141343930350aa69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26
safecontent: false
usesymbols: false
mounts: {}
""".trim() + "\n"
}
/**
* Returns true if gopass is installed and has the given version.
*
* @param version that is checked; specifies left part of text of installed version, e.g. both "1" and "1.12" will return true if installed version is "1.12.6+8d7a311b9273846bbb618e4bd9ddbae51b1db7b8"
*/
internal fun Prov.gopassEnsureVersion(version: String) = def {
val installedGopassVersion = gopassVersion()
if (installedGopassVersion != null && installedGopassVersion.startsWith("gopass " + version)) {
ProvResult(true, out = "Required gopass version ($version) matches installed version ($installedGopassVersion)")
} else {
ProvResult(false, err = "Wrong gopass version. Expected $version but found $installedGopassVersion")
}
}
internal fun Prov.gopassVersion(): String? {
val result = cmdNoEval("gopass -v")
return if (!result.success) null else result.out
}

View file

@ -0,0 +1,90 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createDirs
import io.provs.ubuntu.filesystem.base.userHome
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.install.base.isPackageInstalled
import io.provs.ubuntu.web.base.downloadFromURL
fun Prov.downloadGopassBridge() = def {
val version = "0.8.0"
val filename = "gopass_bridge-${version}-fx.xpi"
val downloadDir = "${userHome()}Downloads/"
createDirs(downloadDir)
downloadFromURL(
"-L https://addons.mozilla.org/firefox/downloads/file/3630534/" + filename,
downloadDir + filename
)
// needs manual install with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
}
fun Prov.installGopassBridgeJsonApi() = def {
// see https://github.com/gopasspw/gopass-jsonapi
val gopassBridgeVersion = "1.11.1"
val requiredGopassVersion = "1.12"
val filename = "gopass-jsonapi_${gopassBridgeVersion}_linux_amd64.deb"
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassBridgeVersion/$filename"
val downloadDir = "${userHome()}Downloads"
val installedJsonApiVersion = gopassJsonApiVersion()?.trim()
if (installedJsonApiVersion == null) {
if (chk("gopass ls")) {
if (gopassEnsureVersion(requiredGopassVersion).success) {
aptInstall("git gnupg2") // required dependencies
createDir(downloadDir)
downloadFromURL(downloadUrl, filename, downloadDir)
cmd("dpkg -i " + downloadDir + "/" + filename, sudo = true)
} else {
ProvResult(
false,
"Version of currently installed gopass (" + gopassVersion() + ") is incompatible with gopass-jsonapi version to be installed. " +
"Please upgrade gopass to version: " + requiredGopassVersion
)
}
} else {
addResultToEval(
ProvResult(
false,
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
)
)
}
} else {
if (installedJsonApiVersion.startsWith("gopass-jsonapi version " + gopassBridgeVersion)) {
addResultToEval(ProvResult(true, out = "Version $gopassBridgeVersion of gopass-jsonapi is already installed"))
} else {
addResultToEval(
ProvResult(
false,
err = "gopass-jsonapi (version $gopassBridgeVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
" Upgrading gopass-jsonapi is currently not supported by provs."
)
)
}
}
}
fun Prov.configureGopassBridgeJsonApi() = def {
if (isPackageInstalled("gopass-jsonapi")) {
// configure for firefox and choose default for each:
// "Install for all users? [y/N/q]",
// "In which path should gopass_wrapper.sh be installed? [/home/testuser/.config/gopass]"
// "Wrapper Script for gopass_wrapper.sh ..."
cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
} else {
ProvResult(
false,
err = "gopass-jsonapi is missing. Gopass-jsonapi must be installed to be able to configure it."
)
}
}
internal fun Prov.gopassJsonApiVersion(): String? {
val result = cmdNoEval("gopass-jsonapi -v")
return if (!result.success) null else result.out
}

View file

@ -0,0 +1,10 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ubuntu.filesystem.base.addTextToFile
import java.io.File
fun Prov.configureNoSwappiness() = def {
// set swappiness to 0
addTextToFile("vm.swappiness=0", File("/etc/sysctl.conf"), sudo = true)
}

View file

@ -0,0 +1,17 @@
package io.provs.ubuntu.extensions.workplace.base
val OS_ANALYSIS = "lsof strace ncdu iptraf htop iotop iftop"
val ZIP_UTILS = "p7zip-rar p7zip-full rar unrar zip unzip"
val BASH_UTILS = "bash-completion"
val SPELLCHECKING_DE = "hyphen-de hunspell hunspell-de-de"
val OPEN_VPM = "openvpn network-manager-openvpn network-manager-openvpn-gnome"
val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openconnect-gnome"
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
val JAVA_JDK = "openjdk-8-jdk openjdk-11-jdk openjdk-14-jdk"

View file

@ -0,0 +1,35 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.install.base.aptInstall
fun Prov.installRedshift() = def {
aptInstall("redshift redshift-gtk")
}
fun Prov.configureRedshift() = def {
aptInstall("redshift redshift-gtk")
createDir(".config")
createFile("~/.config/redshift.conf", config)
}
val config = """
[redshift]
temp-day=5500
temp-night=2700
brightness-day=1
brightness-night=0.6
fade=1
location-provider=manual
[manual]
lat=48.783333
lon=9.1833334
""".trimIndent()

View file

@ -0,0 +1,75 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.install.base.aptInstall
fun Prov.installVSC(vararg options: String) = requireAll {
val clojureExtensions =
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
val pythonExtensions = arrayListOf("ms-python.python")
prerequisitesVSCinstall()
installVSCPackage()
if (options.contains("clojure")) {
installExtensions(clojureExtensions)
}
if (options.contains("python")) {
installExtensions(pythonExtensions)
}
provisionAdditionalTools()
}
private fun Prov.prerequisitesVSCinstall() = def {
aptInstall("curl gpg unzip apt-transport-https")
}
@Suppress("unused") // only required for installation of vscode via apt
private fun Prov.configurePackageManagerForVsc() = requireAll {
// see https://code.visualstudio.com/docs/setup/linux
// alternatively install with snapd (but this cannot be tested within docker as snapd within docker is not working/supported)
sh(
"""
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sudo sh -c 'echo \"deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/vscode stable main\" > /etc/apt/sources.list.d/vscode.list'
"""
)
aptInstall("apt-transport-https")
aptInstall("code")
}
private fun Prov.installVSCPackage() = def {
cmd("sudo snap install code --classic")
// to install via apt use:
// configurePackageManagerForVsc()
// aptInstall("code")
}
private fun Prov.installExtensions(extensions: List<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("code --install-extension $ext")
}
res
// Settings can be found at $HOME/.config/Code/User/settings.json
}
private fun Prov.provisionAdditionalTools() = requireAll {
// Joker
cmd("curl -Lo joker-0.12.2-linux-amd64.zip https://github.com/candid82/joker/releases/download/v0.12.2/joker-0.12.2-linux-amd64.zip")
cmd("unzip joker-0.12.2-linux-amd64.zip")
cmd("sudo mv joker /usr/local/bin/")
}

View file

@ -0,0 +1,28 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.remote
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.secret.secretSources.GopassSecretSource
import io.provs.ubuntu.secret.secretSources.PlainSecretSource
import io.provs.ubuntu.secret.secretSources.PromptSecretSource
import io.provs.ubuntu.user.base.whoami
fun Prov.installVirtualBoxGuestAdditions() = def {
// if running in a VirtualBox vm
if (!chk("lspci | grep VirtualBox")) {
return@def ProvResult(true, "Not running in a VirtualBox")
}
if (chk("VBoxService --version")) {
return@def ProvResult(true, "VBoxService already installed")
}
// install guest additions
cmd("sudo add-apt-repository multiverse")
aptInstall("virtualbox-guest-x11") // virtualbox-guest-dkms")
// and add user to group vboxsf e.g. to be able to use shared folders
whoami()?.let { cmd("sudo usermod -G vboxsf -a " + it) }
?: ProvResult(true)
}

View file

@ -0,0 +1,17 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.install.base.aptInstallFromPpa
import io.provs.ubuntu.install.base.isPackageInstalled
fun Prov.installZimWiki() = def {
if (isPackageInstalled("zim")) {
ProvResult(true, out = "zim already installed.")
} else {
aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
aptInstall("python3-gtkspellcheck")
}
}

View file

@ -0,0 +1,197 @@
package io.provs.ubuntu.filesystem.base
import io.provs.*
import io.provs.platforms.SHELL
import java.io.File
fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean {
return cmdNoEval((if (sudo) "sudo " else "") + "test -e " + file).success
}
fun Prov.createFile(
fullyQualifiedFilename: String,
text: String?,
posixFilePermission: String? = null,
sudo: Boolean = false
): ProvResult =
def {
val withSudo = if (sudo) "sudo " else ""
posixFilePermission?.let {
ensureValidPosixFilePermission(posixFilePermission)
cmd(withSudo + "install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
}
if (text != null) {
if (sudo) {
cmd(
"printf " + text.escapeProcentForPrintf()
.escapeAndEncloseByDoubleQuoteForShell() + " | sudo tee $fullyQualifiedFilename > /dev/null"
)
} else {
cmd(
"printf " + text.escapeProcentForPrintf()
.escapeAndEncloseByDoubleQuoteForShell() + " > $fullyQualifiedFilename"
)
}
} else {
cmd(withSudo + "touch $fullyQualifiedFilename")
}
}
fun Prov.createSecretFile(
fullyQualifiedFilename: String,
secret: Secret,
posixFilePermission: String? = null
): ProvResult =
def {
posixFilePermission?.let {
ensureValidPosixFilePermission(posixFilePermission)
cmd("install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
}
cmdNoLog("echo '" + secret.plain().escapeSingleQuote() + "' > $fullyQualifiedFilename")
}
fun Prov.deleteFile(file: String, sudo: Boolean = false): ProvResult = def {
cmd((if (sudo) "sudo " else "") + "rm $file")
}
fun Prov.fileContainsText(file: String, content: String, sudo: Boolean = false): Boolean {
return cmdNoEval((if (sudo) "sudo " else "") + "grep '${content.escapeSingleQuote()}' $file").success
}
fun Prov.fileContent(file: String, sudo: Boolean = false): String? {
return cmd((if (sudo) "sudo " else "") + "cat $file").out
}
fun Prov.addTextToFile(
text: String,
file: File,
doNotAddIfExisting: Boolean = true,
sudo: Boolean = false
): ProvResult =
def {
// TODO find solution without trim handling spaces, newlines, etc correctly
val findCmd = "grep '${text.trim().escapeSingleQuote()}' ${file}"
val findResult = cmdNoEval(if (sudo) findCmd.sudoizeCommand() else findCmd)
if (!findResult.success || !doNotAddIfExisting) {
val addCmd = "printf \"" + text.escapeDoubleQuote() + "\" >> " + file
cmd(if (sudo) addCmd.sudoizeCommand() else addCmd)
} else {
ProvResult(true)
}
}
fun Prov.replaceTextInFile(file: String, oldText: String, replacement: String) = def {
replaceTextInFile(file, Regex.fromLiteral(oldText), Regex.escapeReplacement(replacement))
}
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = def {
// todo: only use sudo for root or if owner different from current
val content = fileContent(file, true)
if (content != null) {
cmd("sudo truncate -s 0 $file")
addTextToFile(content.replace(oldText, Regex.escapeReplacement(replacement)), File(file), sudo = true)
} else {
ProvResult(false)
}
}
fun Prov.insertTextInFile(file: String, textBehindWhichToInsert: Regex, textToInsert: String) = def {
// todo: only use sudo for root or if owner different from current
val content = fileContent(file, true)
if (content != null) {
val match = textBehindWhichToInsert.find(content)
if (match != null) {
cmd("sudo truncate -s 0 $file")
addTextToFile(
content.replace(textBehindWhichToInsert, match.value + Regex.escapeReplacement(textToInsert)),
File(file),
sudo = true
)
} else {
ProvResult(false, err = "Text not found")
}
} else {
ProvResult(false)
}
}
fun Prov.dirExists(dir: String, path: String? = null, sudo: Boolean = false): Boolean {
val effectivePath = if (path != null) path else
(if (dir.startsWith(File.separator)) File.separator else "~" + File.separator)
val cmd = "cd $effectivePath && test -d $dir"
return cmdNoEval(if (sudo) cmd.sudoizeCommand() else cmd).success
}
fun Prov.createDir(
dir: String,
path: String = "~/",
failIfExisting: Boolean = false,
sudo: Boolean = false
): ProvResult = def {
if (!failIfExisting && dirExists(dir, path, sudo)) {
ProvResult(true)
} else {
val cmd = "cd $path && mkdir $dir"
cmd(if (sudo) cmd.sudoizeCommand() else cmd)
}
}
fun Prov.createDirs(
dirs: String,
path: String = "~/",
failIfExisting: Boolean = false,
sudo: Boolean = false
): ProvResult = def {
if (!failIfExisting && dirExists(dirs, path, sudo)) {
ProvResult(true)
} else {
val cmd = "cd $path && mkdir -p $dirs"
cmd(if (sudo) cmd.sudoizeCommand() else cmd)
}
}
fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult {
if ("" == path)
throw RuntimeException("In deleteDir: path must not be empty.")
val cmd = "cd $path && rmdir $dir"
return if (!sudo) {
cmd(cmd)
} else {
cmd(cmd.sudoizeCommand())
}
}
fun Prov.userHome(): String {
val user = cmd("whoami").out
if (user == null) {
throw RuntimeException("Could not determine user with whoami")
} else {
// assume default home folder
return "/home/" + user.trim() + "/"
}
}
private fun ensureValidPosixFilePermission(posixFilePermission: String) {
if (!Regex("^[0-7]{3}$").matches(posixFilePermission)) throw RuntimeException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664 ")
}
private fun String.sudoizeCommand(): String {
return "sudo " + SHELL + " -c " + this.escapeAndEncloseByDoubleQuoteForShell()
}

View file

@ -0,0 +1,22 @@
package io.provs.ubuntu.git
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.install.base.aptInstall
fun Prov.provisionGit(
userName: String? = null,
email: String? = null,
signingKey: String? = null,
diffTool: String? = null
): ProvResult = def {
aptInstall("git")
cmd("git config --global push.default simple")
userName?.let { cmd("git config --global user.name $it") }
email?.let { cmd("git config --global user.email $it") }
signingKey?.let { cmd("git config --global user.signingkey $it") }
diffTool?.let { cmd("git config --global --add diff.tool $it") } ?: ProvResult(true)
}

View file

@ -0,0 +1,80 @@
package io.provs.ubuntu.git.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.addTextToFile
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.dirExists
import io.provs.ubuntu.keys.base.isHostKnown
import io.provs.ubuntu.utils.printToShell
import java.io.File
/**
* @param host name or ip
* @param rsaFingerprints
*/
private fun Prov.trustHost(host: String, rsaFingerprints: Set<String>) = def {
if (!isHostKnown(host)) {
// logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
val key = cmd("ssh-keyscan $host").out
if (key == null) {
ProvResult(false, "No key retrieved for $host")
} else {
val c = printToShell(key).trim()
val fpr = cmd(c + " | ssh-keygen -lf -").out
if (rsaFingerprints.contains(fpr)
) {
createDir(".ssh", "~/")
cmd(printToShell(key) + " >> ~/.ssh/known_hosts")
} else {
ProvResult(false, "Fingerprint $fpr not valid for $host")
}
}
} else {
ProvResult(true)
}
}
fun Prov.gitClone(repo: String, path: String, pullIfExisting: Boolean = true): ProvResult = def {
val dir = cmdNoEval("basename $repo .git").out?.trim()
if (dir == null) {
ProvResult(false, err = "$repo is not a valid git repository")
} else {
val pathToDir = if (path.endsWith("/")) path + dir else path + "/" + dir
if (dirExists(pathToDir + "/.git/")) {
if (pullIfExisting) {
cmd("cd $pathToDir && git pull")
} else {
ProvResult(true, out = "Repo $repo is already existing")
}
} else {
cmd("cd $path && git clone $repo")
}
}
}
fun Prov.trustGithub() = def {
// current see https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
val fingerprints = setOf(
"2048 SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com (RSA)\n",
"2048 SHA256:br9IjFspm1vxR3iA35FWE+4VTyz1hYVLIE2t1/CeyWQ github.com (RSA)\n"
)
trustHost("github.com", fingerprints)
}
fun Prov.trustGitlab() = def {
// from https://docs.gitlab.com/ee/user/gitlab_com/
val gitlabFingerprints = """
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
""".trimIndent()
addTextToFile("\n" + gitlabFingerprints+ "\n", File("~/.ssh/known_hosts"))
}

View file

@ -0,0 +1,60 @@
package io.provs.ubuntu.install.base
import io.provs.Prov
import io.provs.ProvResult
private var aptInit = false
/**
* Installs package(s) by using package manager "apt".
*
* @param packages the packages to be installed, packages separated by space if there are more than one
*/
fun Prov.aptInstall(packages: String): ProvResult = def {
if (!aptInit) {
cmd("sudo apt-get update")
cmd("sudo apt-get install -qy apt-utils")
aptInit = true
}
val packageList = packages.split(" ")
for (packg in packageList) {
// see https://superuser.com/questions/164553/automatically-answer-yes-when-using-apt-get-install
cmd("sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy $packg")
}
ProvResult(true) // dummy
}
/**
* Installs a package from a ppa (personal package archive) by using package manager "apt".
*
* @param packageName the package to install
*/
fun Prov.aptInstallFromPpa(launchPadUser: String, ppaName: String, packageName: String): ProvResult = def {
aptInstall("software-properties-common") // for being able to use add-apt-repository
cmd("sudo add-apt-repository -y ppa:$launchPadUser/$ppaName")
aptInstall(packageName)
}
/**
* Checks if a program is installed
*
* @param packageName to check
* @return true if program is installed
*/
@Suppress("unused") // used externally
fun Prov.isPackageInstalled(packageName: String): Boolean {
return chk("timeout 2 dpkg -l $packageName")
}
/**
* Removes a package including its configuration and data files
*/
@Suppress("unused") // used externally
fun Prov.aptPurge(packageName: String): Boolean {
return chk("sudo apt-get purge -qy $packageName")
}

View file

@ -0,0 +1,33 @@
package io.provs.ubuntu.keys
import io.provs.Prov
import io.provs.ProvResult
import io.provs.Secret
import io.provs.ubuntu.keys.base.configureGpgKeys
import io.provs.ubuntu.keys.base.configureSshKeys
import io.provs.ubuntu.secret.SecretSourceType
import kotlinx.serialization.Serializable
open class KeyPair(val publicKey: Secret, val privateKey: Secret)
@Serializable
class KeyPairSource(val sourceType: SecretSourceType, val publicKey: String, val privateKey: String) {
fun keyPair() : KeyPair {
val pub = sourceType.secret(publicKey)
val priv = sourceType.secret(privateKey)
return KeyPair(pub, priv)
}
}
/**
* provisions gpg and/or ssh keys for the current user
*/
fun Prov.provisionKeysCurrentUser(gpgKeys: KeyPair? = null, sshKeys: KeyPair? = null) = requireAll {
gpgKeys?.let { configureGpgKeys(it, true) }
sshKeys?.let { configureSshKeys(it) }
ProvResult(true) // dummy
}

View file

@ -0,0 +1,74 @@
package io.provs.ubuntu.keys.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.createSecretFile
import io.provs.ubuntu.filesystem.base.dirExists
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.keys.KeyPair
import io.provs.ubuntu.utils.printToShell
/**
* Installs a gpg key pair for the current user.
*
* @param gpgKeys
* @param trust whether to trust keys with trust-level 5 (ultimate)
*/
fun Prov.configureGpgKeys(gpgKeys: KeyPair, trust: Boolean = false, skipIfExistin: Boolean = true) = requireAll {
aptInstall("gnupg")
val fingerprint = gpgFingerprint(gpgKeys.publicKey.plain())
if (fingerprint == null) {
ProvResult(false, err = "Fingerprint of key could not be determined")
} else {
if (gpgKeysInstalled(fingerprint) && skipIfExistin) {
ProvResult(true, out = "Keys were already installed")
} else {
val pubkeyFile = "~/pub-key.asc"
val privkeyFile = "~/priv-key.asc"
createSecretFile(pubkeyFile, gpgKeys.publicKey)
createSecretFile(privkeyFile, gpgKeys.privateKey)
cmd("gpg --import $pubkeyFile")
// using option --batch for older keys; see https://superuser.com/questions/1135812/gpg2-asking-for-passphrase-when-importing-secret-keys
cmd("gpg --batch --import $privkeyFile")
if (trust) {
cmd("printf \"5\\ny\\n\" | gpg --no-tty --command-fd 0 --expert --edit-key $fingerprint trust")
}
cmd("shred $pubkeyFile")
cmd("shred $privkeyFile")
configureGPGAgent()
}
}
}
private fun Prov.configureGPGAgent() = def {
if (dirExists(".gnupg")) {
createDir(".gnupg", "~/")
}
val content = """
allow-preset-passphrase
allow-loopback-pinentry
""".trimIndent()
createFile("~/.gnupg/gpg-agent.conf", content)
}
private fun Prov.gpgKeysInstalled(fingerprint: String): Boolean {
return cmdNoLog("gpg --list-keys $fingerprint").success
}
fun Prov.gpgFingerprint(pubKey: String): String? {
val result =
cmdNoLog(" " + printToShell(pubKey) + " | gpg --with-colons --import-options show-only --import --fingerprint")
return result.out?.let { """^fpr:*([A-Z0-9]*):$""".toRegex(RegexOption.MULTILINE).find(it)?.groupValues?.get(1) }
}

View file

@ -0,0 +1,46 @@
package io.provs.ubuntu.keys.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.filesystem.base.createDir
import io.provs.ubuntu.filesystem.base.createSecretFile
import io.provs.ubuntu.keys.KeyPair
/**
* installs ssh keys for active user
*/
fun Prov.configureSshKeys(sshKeys: KeyPair) = def {
createDir(".ssh", "~/")
createSecretFile("~/.ssh/id_rsa.pub", sshKeys.publicKey, "644")
createSecretFile("~/.ssh/id_rsa", sshKeys.privateKey, "600")
configureSSHClient()
}
fun Prov.configureSSHClient() = def {
// TODO("Not yet implemented")
ProvResult(true)
}
/**
* Specifies a host or Ip to be trusted
*
* ATTENTION:
* This method is NOT secure as a man-in-the-middle could compromise the connection.
* Don't use this for critical systems resp. environments
*/
fun Prov.trustServer(hostOrIp: String) = def {
cmd("ssh-keyscan $hostOrIp >> ~/.ssh/known_hosts")
}
/**
* Checks if the specified hostname or Ip is in a known_hosts file
*
* @return whether if was found
*/
fun Prov.isHostKnown(hostOrIp: String) : Boolean {
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
}

View file

@ -0,0 +1,39 @@
package io.provs.ubuntu.secret
import io.provs.Secret
import io.provs.ubuntu.secret.secretSources.*
import kotlinx.serialization.Serializable
@Serializable
abstract class SecretSource(protected val input: String) {
abstract fun secret() : Secret
abstract fun secretNullable() : Secret?
}
@Serializable
enum class SecretSourceType() {
PLAIN, FILE, PROMPT, PASS, GOPASS;
fun secret(input: String) : Secret {
return when (this) {
PLAIN -> PlainSecretSource(input).secret()
FILE -> FileSecretSource(input).secret()
PROMPT -> PromptSecretSource().secret()
PASS -> PassSecretSource(input).secret()
GOPASS -> GopassSecretSource(input).secret()
}
}
}
@Serializable
@Suppress("unused") // for use in other projects
class SecretSupplier(private val source: SecretSourceType, val parameter: String) {
fun secret(): Secret {
return source.secret(parameter)
}
}

View file

@ -0,0 +1,22 @@
package io.provs.ubuntu.secret.secretSources
import io.provs.Prov
import io.provs.Secret
import io.provs.ubuntu.secret.SecretSource
/**
* Retrieve secret from a file
*/
class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "FileSecretSource")
return p.getSecret("cat " + input) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "FileSecretSource")
return p.getSecret("cat " + input)
}
}

View file

@ -0,0 +1,19 @@
package io.provs.ubuntu.secret.secretSources
import io.provs.Prov
import io.provs.Secret
import io.provs.ubuntu.secret.SecretSource
/**
* Retrieve secret from gopass
*/
class GopassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "GopassSecretSource for $input")
return p.getSecret("gopass show -f " + input)
}
}

View file

@ -0,0 +1,20 @@
package io.provs.ubuntu.secret.secretSources
import io.provs.Prov
import io.provs.Secret
import io.provs.ubuntu.secret.SecretSource
/**
* Retrieve secret from passwordstore on Unix
*/
class PassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "PassSecretSource")
return p.getSecret("pass " + input) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "PassSecretSource")
return p.getSecret("pass " + input)
}
}

View file

@ -0,0 +1,14 @@
package io.provs.ubuntu.secret.secretSources
import io.provs.Secret
import io.provs.ubuntu.secret.SecretSource
class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
override fun secret(): Secret {
return Secret(input)
}
override fun secretNullable(): Secret {
return Secret(input)
}
}

View file

@ -0,0 +1,67 @@
package io.provs.ubuntu.secret.secretSources
import io.provs.Secret
import io.provs.ubuntu.secret.SecretSource
import java.awt.FlowLayout
import javax.swing.*
class PasswordPanel : JPanel(FlowLayout()) {
private val passwordField = JPasswordField(20)
private var entered = false
val enteredPassword
get() = if (entered) String(passwordField.password) else null
init {
add(JLabel("Password: "))
add(passwordField)
passwordField.setActionCommand("OK")
passwordField.addActionListener {
if (it.actionCommand == "OK") {
entered = true
SwingUtilities.getWindowAncestor(it.source as JComponent)
.dispose()
}
}
}
private fun request(passwordIdentifier: String) = apply {
JOptionPane.showOptionDialog(null, this@PasswordPanel,
"Enter $passwordIdentifier",
JOptionPane.DEFAULT_OPTION,
JOptionPane.INFORMATION_MESSAGE,
null, emptyArray(), null)
}
companion object {
fun requestPassword(passwordIdentifier: String) = PasswordPanel()
.request(passwordIdentifier)
.enteredPassword
}
}
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
override fun secret(): Secret {
val password = PasswordPanel.requestPassword(input)
if (password == null) {
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
} else {
return Secret(password)
}
}
override fun secretNullable(): Secret? {
val password = PasswordPanel.requestPassword(input)
return if(password == null) {
null
}else {
Secret(password)
}
}
}

View file

@ -0,0 +1,29 @@
package io.provs.ubuntu.user
import io.provs.ubuntu.keys.KeyPairSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.BufferedReader
import java.io.FileReader
import java.io.FileWriter
@Serializable
class UserConfig(val userName: String, val gitEmail: String? = null, val gpg: KeyPairSource? = null, val ssh: KeyPairSource? = null)
// -------------------------------------------- file methods ------------------------------------
@Suppress("unused")
fun readUserConfigFromFile(filename: String = "UserConfig.json") : UserConfig {
// read from file
val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
// serializing objects
return Json.decodeFromString(UserConfig.serializer(), inputAsString)
}
fun writeUserConfigToFile(config: UserConfig) {
val fileName = "UserConfig.json"
FileWriter(fileName).use { it.write(Json.encodeToString(UserConfig.serializer(), config)) }
}

View file

@ -0,0 +1,166 @@
package io.provs.ubuntu.user.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.Secret
import io.provs.processors.RemoteProcessor
import io.provs.ubuntu.filesystem.base.createDirs
import io.provs.ubuntu.filesystem.base.fileExists
import io.provs.ubuntu.git.provisionGit
import io.provs.ubuntu.keys.base.gpgFingerprint
import io.provs.ubuntu.keys.provisionKeysCurrentUser
import io.provs.ubuntu.user.UserConfig
import java.net.InetAddress
fun Prov.userExists(userName: String): Boolean {
return cmdNoEval("grep -c '^$userName:' /etc/passwd").success
}
/**
* Creates a new user.
*/
fun Prov.createUser(
userName: String,
password: Secret? = null,
sudo: Boolean = false,
copyAuthorizedKeysFromCurrentUser: Boolean = false
): ProvResult = requireAll {
if (!userExists(userName)) {
cmd("sudo adduser --gecos \"First Last,RoomNumber,WorkPhone,HomePhone\" --disabled-password --home /home/$userName $userName")
}
password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true)
if (sudo) {
makeUserSudoerWithNoSudoPasswordRequired(userName)
}
val authorizedKeysFile = "~/.ssh/authorized_keys"
if (copyAuthorizedKeysFromCurrentUser && fileExists(authorizedKeysFile)) {
createDirs("/home/$userName/.ssh")
val newAuthorizedKeysFile = "/home/$userName/.ssh/authorized_keys"
cmd("sudo cp $authorizedKeysFile $newAuthorizedKeysFile")
cmd("chown $userName $newAuthorizedKeysFile")
}
ProvResult(true) // dummy
}
/**
* Configures gpg and ssh keys for the current if keys are provided in the config.
* Installs and configures git for the user if gitEmail is provided in the config.
* Does NOT CREATE the user.
*/
fun Prov.configureUser(config: UserConfig) = requireAll {
provisionKeysCurrentUser(
config.gpg?.keyPair(),
config.ssh?.keyPair()
)
config.gitEmail?.run {
provisionGit(
config.userName,
config.gitEmail,
config.gpg?.keyPair()?.let { gpgFingerprint(it.publicKey.plain()) })
} ?: ProvResult(true)
}
@Suppress("unused")
// todo create test
fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResult = requireAll {
val flagToDeleteHomeDir = if (deleteHomeDir) " -r " else ""
if (userExists(userName)) {
cmd("sudo userdel $flagToDeleteHomeDir $userName")
} else {
ProvResult(false, err = "User $userName cannot be deleted as it does not exist.")
}
}
/**
* Makes userName a sudoer who does not need a password to sudo.
* The current (executing) user must already be a sudoer. If he is a sudoer with password required then
* his password must be provided.
*/
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
userName: String,
password: Secret? = null,
overwriteFile: Boolean = false
): ProvResult = def {
val userSudoFile = "/etc/sudoers.d/$userName"
if (!fileExists(userSudoFile) || overwriteFile) {
val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S"
// see https://stackoverflow.com/questions/323957/how-do-i-edit-etc-sudoers-from-a-script
val result = cmdNoLog(sudoPrefix + " sh -c \"echo '$userName ALL=(ALL) NOPASSWD:ALL' | (sudo su -c 'EDITOR=\"tee\" visudo -f " + userSudoFile + "')\"")
// don't log the command (containing the password) resp. don't include it in the ProvResult, just include success and err
ProvResult(result.success, err = result.err)
} else {
ProvResult(true, out = "File already exists")
}
}
/**
* Makes the current (executing) user be able to sudo without password.
* IMPORTANT: Current user must already by sudoer when calling this function.
*/
@Suppress("unused") // used externally
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = def {
val currentUser = whoami()
if (currentUser != null) {
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
} else {
ProvResult(false, "Current user could not be determined.")
}
}
/**
* Checks if user is in group sudo.
*/
@Suppress("unused")
fun Prov.userIsInGroupSudo(userName: String): Boolean {
return cmd("getent group sudo | grep -c '$userName'").success
}
/**
* Checks if current user can execute sudo commands.
*/
@Suppress("unused")
fun Prov.currentUserCanSudo(): Boolean {
return cmd("timeout 1 sudo id").success
}
/**
* Returns username of current user if it can be determined
*/
fun Prov.whoami(): String? {
return cmd("whoami").run { if (success) out?.trim() else null }
}
/**
* Creates a new user on the specified host.
*
* @host hostname or ip-address
* @hostUser user on the remote system, which is used to create the new user,
* hostUser must be sudoer
* @hostPassword pw of hostUser on the remote system;
* ssh-key authentication will be used if hostPassword is null
*/
@Suppress("api") // use externally
fun createRemoteUser(
host: InetAddress,
hostUser: String,
hostPassword: Secret?,
newUserName: String,
newUserPW: Secret,
makeNewUserSudoer: Boolean = false
) {
Prov.newInstance(RemoteProcessor(host, hostUser, hostPassword), name = "createRemoteUser")
.createUser(newUserName, newUserPW, makeNewUserSudoer)
}

View file

@ -0,0 +1,11 @@
package io.provs.ubuntu.utils
import io.provs.escapeBackslash
import io.provs.escapeDoubleQuote
// todo: investigate to use .escapeAndEncloseByDoubleQuoteForShell() or similar instead (?)
internal fun printToShell(text: String): String {
return "echo -n \"${text.escapeBackslash().escapeDoubleQuote()}\""
}

View file

@ -0,0 +1,26 @@
package io.provs.ubuntu.web.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.install.base.isPackageInstalled
/**
* Downloads a file from the given URL using curl
*
* @param path where to download to
* @param url file to download
* @param filename filename after download
*/
@Suppress("unused") // used externally
fun Prov.downloadFromURL(url: String, filename: String? = null, path: String? = null, sudo: Boolean = false) : ProvResult = def {
if (!isPackageInstalled("curl")) aptInstall("curl")
if (filename == null) {
cmd("curl $url", path, sudo)
} else {
cmd("curl $url -o $filename", path, sudo)
}
}

View file

@ -0,0 +1,58 @@
package io.provs.ubuntu.extensions.server_software.firewall
import io.provs.Prov
import io.provs.docker.dockerProvideImage
import io.provs.docker.exitAndRmContainer
import io.provs.docker.images.UbuntuPlusUser
import io.provs.local
import io.provs.processors.ContainerEndMode
import io.provs.processors.ContainerStartMode
import io.provs.processors.ContainerUbuntuHostProcessor
import io.provs.ubuntu.install.base.aptInstall
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class ProvisionFirewallKtTest {
@Test
@Disabled
fun provisionFirewall() {
// todo
}
@Test
@Disabled
fun resetFirewall() {
// todo
}
@Test
fun configureFirewall() {
// given
val dockerImage = UbuntuPlusUser()
local().dockerProvideImage(dockerImage)
val containerName = "firewall_test"
local().exitAndRmContainer(containerName)
local().cmd("sudo docker run --cap-add=NET_ADMIN -dit --name $containerName ${dockerImage.imageName()}")
val a = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
dockerImage.imageName(),
ContainerStartMode.USE_RUNNING_ELSE_CREATE, // already started in previous statement
ContainerEndMode.EXIT_AND_REMOVE
))
// when
val res = a.requireAll {
aptInstall("iptables")
provisionFirewall()
}
local().exitAndRmContainer(containerName)
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,26 @@
package nexus
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.extensions.server_software.nexus.provisionNexusWithDocker
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import io.provs.test.defaultTestContainer
internal class ProvisionNexusKtTest {
@Test
@Disabled("Find out how to run docker in docker")
fun provisionNexusWithDocker() {
// given
val a = defaultTestContainer()
// when
val res = a.requireAll {
provisionNexusWithDocker()
}
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,85 @@
package io.provs.ubuntu.extensions.server_software.nginx
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.filesystem.base.replaceTextInFile
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.extensions.server_software.nginx.base.*
import io.provs.ubuntu.filesystem.base.fileExists
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class ProvisionNginxKtTest {
@Test
fun provisionNginxStandAlone_customConfig() {
// given
val a = defaultTestContainer()
val config = """
events {} # event context have to be defined to consider config valid
http {
server {
listen 80;
server_name localhost;
return 200 "Hello";
}
}
""".trimIndent()
a.aptInstall("curl")
// when
val res = a.requireAll {
provisionNginxStandAlone(NginxConf(config))
cmd("curl localhost")
}
// then
assertTrue(res.success)
}
@Test
fun provisionNginxStandAlone_defaultConfig() {
// given
val a = defaultTestContainer()
// when
val res = a.requireAll {
provisionNginxStandAlone()
}
// then
assertTrue(res.success)
}
@Test
fun provisionNginxStandAlone_sslConfig() {
// given
val a = defaultTestContainer()
a.def {
val file = "/etc/ssl/openssl.cnf"
if (fileExists(file)) {
replaceTextInFile(file, "RANDFILE", "#RANDFILE")
}
aptInstall("openssl")
}
// when
val res = a.def {
nginxCreateSelfSignedCertificate()
provisionNginxStandAlone(
NginxConf.nginxReverseProxySslConfig(
"localhost",
dirSslCert + "/" + certificateName + ".crt",
dirSslKey + "/" + certificateName + ".key"
)
)
}
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,33 @@
package io.provs.ubuntu.extensions.server_software.nginx.base
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.fileContainsText
import io.provs.ubuntu.extensions.server_software.nginx.configFile
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import io.provs.test.defaultTestContainer
internal class LocationsKtTest {
@Test
fun nginxIncludeLocationFolders() {
// given
val a = defaultTestContainer()
a.provisionNginxStandAlone()
a.createFile(configFile, NGINX_MINIMAL_CONF, sudo = true)
// when
val res = a.nginxIncludeLocationFolders()
// then
assertTrue(res.success)
assertTrue(a.fileContainsText(
configFile, """listen 80;
include /etc/nginx/locations-enabled/port80*.conf
include /etc/nginx/locations-enabled/port443*.conf"""))
// just 1 occurrence
assertEquals("1", a.cmd("grep -o 'listen 80;' $configFile | wc -l").out?.trim())
}
}

View file

@ -0,0 +1,166 @@
package io.provs.ubuntu.extensions.test_keys
fun publicGPGSnakeoilKey(): String {
return """-----BEGIN PGP PUBLIC KEY BLOCK-----
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
KpkCQg==
=cS1b
-----END PGP PUBLIC KEY BLOCK----- """
}
fun privateGPGSnakeoilKey(): String {
return """-----BEGIN PGP PRIVATE KEY BLOCK-----
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
i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
=h5SJ
-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
}
fun publicSSHSnakeoilKey(): String {
return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
}
fun privateSSHSnakeoilKey(): String {
return """
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
-----END RSA PRIVATE KEY-----
""".trimIndent()
}

View file

@ -0,0 +1,51 @@
package io.provs.ubuntu.extensions.workplace
import io.provs.Password
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import io.provs.test.defaultTestContainer
internal class ProvisionWorkplaceKtTest {
@Test
fun provisionWorkplace() {
// given
val a = defaultTestContainer()
// when
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
val res = a.provisionWorkplace(
WorkplaceType.MINIMAL,
gitUserName = "testuser",
gitEmail = "testuser@test.org",
userPassword = Password("testuser")
)
// then
assertTrue(res.success)
}
@Test
fun provisionWorkplaceFromConfigFile() {
// given
val a = defaultTestContainer()
// when
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
val config = readWorkplaceConfigFromFile("src/test/resources/WorkplaceConfigExample.json")
?: throw Exception("Could not read WorkplaceConfig")
val res = a.provisionWorkplace(
config.type,
config.ssh?.keyPair(),
config.gpg?.keyPair(),
config.gitUserName,
config.gitEmail,
)
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,19 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.install.base.aptInstall
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class FakturamaKtTest {
@Test
fun installFakturama() {
// given
val a = defaultTestContainer()
// when
val res = a.def { installFakturama() }
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,144 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Prov
import io.provs.ProvResult
import io.provs.Secret
import io.provs.docker.exitAndRmContainer
import io.provs.local
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import io.provs.test.tags.NonCi
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.keys.KeyPair
import io.provs.ubuntu.keys.base.configureGpgKeys
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import io.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
import io.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
internal class GopassBridgeKtTest {
@ContainerTest
@Test
fun test_downloadGopassBridge() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
a.aptInstallCurl()
// when
val res = a.downloadGopassBridge()
// then
assertTrue(res.success)
}
@ContainerTest
@Test
fun test_install_and_configure_GopassBridgeJsonApi() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
val preparationResult = a.def {
aptInstallCurl()
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass()
if (!chk("gopass ls")) {
// configure/init gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
}
assertTrue(preparationResult.success)
// when
val res = a.def {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
}
// then
assertTrue(res.success)
}
@ContainerTest
@Test
@NonCi
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_jsonapi_version_installed() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
val preparationResult = a.def {
aptInstallCurl()
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass("1.11.0", enforceVersion = true)
if (!chk("gopass ls")) {
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
}
assertTrue(preparationResult.success)
// when
val res = a.def {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
}
// then
assertFalse(res.success)
}
@ContainerTest
@Test
@NonCi
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_version_installed() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
val preparationResult = a.def {
aptInstallCurl()
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass("1.9.0", enforceVersion = true)
if (!chk("gopass ls")) {
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
}
assertTrue(preparationResult.success)
// when
val res = a.def {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
}
// then
assertFalse(res.success)
}
private fun Prov.aptInstallCurl() = def {
cmd("apt-get update", sudo = true)
aptInstall("curl")
}
}

View file

@ -0,0 +1,90 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.Secret
import io.provs.remote
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import io.provs.ubuntu.filesystem.base.*
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.keys.KeyPair
import io.provs.ubuntu.keys.base.configureGpgKeys
import io.provs.ubuntu.keys.base.gpgFingerprint
import io.provs.ubuntu.secret.secretSources.GopassSecretSource
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import io.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
import io.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
internal class GopassKtTest {
@ContainerTest
@Test
fun test_installAndConfigureGopassAndMountStore() {
// given
val a = defaultTestContainer()
val gopassRootDir = ".password-store"
a.aptInstall("wget git gnupg")
a.createDir(gopassRootDir, "~/")
a.cmd("git init", "~/$gopassRootDir")
val fpr = a.gpgFingerprint(publicGPGSnakeoilKey())
println("+++++++++++++++++++++++++++++++++++++ $fpr +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
a.createFile("~/" + gopassRootDir + "/.gpg-id", fpr)
a.createDir("exampleStoreFolder", "~/")
a.createFile("~/exampleStoreFolder/.gpg-id", fpr)
a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
// when
val res = a.installGopass()
val res2 = a.configureGopass(a.userHome() + ".password-store")
val res3 = a.gopassMountStore("exampleStore", "~/exampleStoreFolder")
// then
a.fileContent("~/.config/gopass/config.yml") // displays the content in the logs
assertTrue(res.success)
assertTrue(res2.success)
assertTrue(res3.success)
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "/home/testuser/.password-store"))
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "exampleStore"))
}
@Test
@Disabled // Integrationtest; change user, host and keys, then remove this line to run this test
fun test_install_and_configure_Gopass_and_GopassBridgeJsonApi() {
// settings to change
val host = "192.168.56.135"
val user = "xxx"
val pubKey = GopassSecretSource("path-to/pub.key").secret()
val privateKey = GopassSecretSource("path-to/priv.key").secret()
// given
val a = remote(host, user)
// when
val res = a.def {
configureGpgKeys(
KeyPair(
pubKey,
privateKey
),
trust = true,
skipIfExistin = true
)
installGopass()
if (!chk("gopass ls")) {
// configure (=init) gopass
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init " + gpgFingerprint(pubKey.plain())) // gopass init in default location with gpg-key-fingerprint of given key
}
downloadGopassBridge()
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
}
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,25 @@
package io.provs.ubuntu.extensions.workplace.base
import io.provs.ubuntu.install.base.aptInstall
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import io.provs.test.defaultTestContainer
internal class VSCodeKtTest {
@Test
@Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
fun installVSC() {
// given
val a = defaultTestContainer()
a.aptInstall("xvfb libgbm-dev libasound2")
// when
val res = a.installVSC("python", "clojure")
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,194 @@
package io.provs.ubuntu.filesystem.base
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
internal class FilesystemKtTest {
@Test
@ContainerTest
fun checkingCreatingDeletingFile() {
// given
val prov = defaultTestContainer()
// when
val res1 = prov.fileExists("testfile")
val res2 = prov.createFile("testfile", "some content")
val res3 = prov.fileExists("testfile")
val res4a = prov.fileContainsText("testfile", "some content")
val res4b = prov.fileContainsText("testfile", "some non-existing content")
val res5 = prov.deleteFile("testfile")
val res6 = prov.fileExists("testfile")
// then
assertFalse(res1)
assertTrue(res2.success)
assertTrue(res3)
assertTrue(res4a)
assertFalse(res4b)
assertTrue(res5.success)
assertFalse(res6)
}
@Test
@ContainerTest
fun checkingCreatingDeletingFileWithSudo() {
// given
val prov = defaultTestContainer()
// when
val file = "/testfile"
val res1 = prov.fileExists(file)
val res2 = prov.createFile(file, "some content", sudo = true)
val res3 = prov.fileExists(file)
val res4a = prov.fileContainsText(file, "some content")
val res4b = prov.fileContainsText(file, "some non-existing content")
val res5 = prov.deleteFile(file)
val res6 = prov.fileExists(file)
val res7 = prov.deleteFile(file, true)
val res8 = prov.fileExists(file)
// then
assertFalse(res1)
assertTrue(res2.success)
assertTrue(res3)
assertTrue(res4a)
assertFalse(res4b)
assertFalse(res5.success)
assertTrue(res6)
assertTrue(res7.success)
assertFalse(res8)
}
@Test
@ContainerTest
fun checkingCreatingDeletingDir() {
// given
val prov = defaultTestContainer()
// when
val res1 = prov.dirExists("testdir")
val res2 = prov.createDir("testdir", "~/")
val res3 = prov.dirExists("testdir")
val res4 = prov.deleteDir("testdir", "~/")
val res5 = prov.dirExists("testdir")
val res6 = prov.dirExists("testdir", "~/test")
val res7 = prov.createDirs("test/testdir")
val res8 = prov.dirExists("testdir", "~/test")
prov.deleteDir("testdir", "~/test/")
// then
assertFalse(res1)
assertTrue(res2.success)
assertTrue(res3)
assertTrue(res4.success)
assertFalse(res5)
assertFalse(res6)
assertTrue(res7.success)
assertTrue(res8)
}
@Test
@ContainerTest
fun checkingCreatingDeletingDirWithSudo() {
// given
val prov = defaultTestContainer()
// when
val res1 = prov.dirExists("/testdir", sudo = true)
val res2 = prov.createDir("testdir", "/", sudo = true)
val res3 = prov.dirExists("/testdir", sudo = true)
val res4 = prov.deleteDir("testdir", "/", true)
val res5 = prov.dirExists("testdir", sudo = true)
// then
assertFalse(res1)
assertTrue(res2.success)
assertTrue(res3)
assertTrue(res4.success)
assertFalse(res5)
}
@Test
fun userHome() {
// given
val prov = defaultTestContainer()
// when
val res1 = prov.userHome()
// then
assertEquals("/home/testuser/", res1)
}
@Test
@ContainerTest
fun replaceTextInFile() {
// given
val prov = defaultTestContainer()
// when
val file = "replaceTest"
val res1 = prov.createFile(file, "a\nb\nc\nd")
val res2 = prov.replaceTextInFile(file,"b", "hi\nho")
val res3 = prov.fileContent(file).equals("a\nhi\nho\nc\nd")
val res4 = prov.deleteFile(file)
// then
assertTrue(res1.success)
assertTrue(res2.success)
assertTrue(res3)
assertTrue(res4.success)
}
@Test
@ContainerTest
fun replaceTextInFileRegex() {
// given
val prov = defaultTestContainer()
// when
val file = "replaceTest"
val res1 = prov.createFile(file, "a\nbananas\nc\nd")
val res2 = prov.replaceTextInFile(file, Regex("b.*n?nas\n"), "hi\nho\n")
val res3 = prov.fileContent(file)
val res4 = prov.deleteFile(file)
// then
assertTrue(res1.success)
assertTrue(res2.success)
assertEquals("a\nhi\nho\nc\nd",res3)
assertTrue(res4.success)
}
@Test
@ContainerTest
fun insertTextInFile() {
// given
val prov = defaultTestContainer()
// when
val file = "insertTest"
val res1 = prov.createFile(file, "a\nbananas\nc\nd")
val res2 = prov.insertTextInFile(file, Regex("b.*n.nas\n"), "hi\n")
val res3 = prov.fileContent(file)
val res4 = prov.deleteFile(file)
// then
assertTrue(res1.success)
assertTrue(res2.success)
assertEquals("a\nbananas\nhi\nc\nd", res3)
assertTrue(res4.success)
}
}

View file

@ -0,0 +1,45 @@
package io.provs.ubuntu.git.base
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.install.base.aptInstall
import io.provs.ubuntu.keys.base.isHostKnown
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class GitKtTest {
@Test
fun trustGitServers(){
// given
val a = defaultTestContainer()
a.aptInstall("openssh-client")
// when
val res = a.trustGithub()
val known = a.isHostKnown("github.com")
val res2 = a.trustGitlab()
val known2 = a.isHostKnown("gitlab.com")
// then
assertTrue(res.success)
assertTrue(known)
assertTrue(res2.success)
assertTrue(known2)
}
@Test
fun gitClone() {
// given
val prov = defaultTestContainer()
prov.aptInstall("openssh-client ssh git")
// when
prov.trustGithub()
prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,40 @@
package io.provs.ubuntu.install.base
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class InstallKtTest {
@ContainerTest
@Test
fun aptInstall_installsPackage() {
// given
val a = defaultTestContainer()
// when
val res = a.aptInstall("rolldice")
// then
assertTrue(res.success)
}
@ContainerTest
@Test
@Disabled // run manually if needed;
// todo: replace zim by a smaller repo
fun aptInstallFromPpa_installsPackage() {
// given
val a = defaultTestContainer()
a.aptInstall("software-properties-common") // prereq for adding a repo to apt
// when
val res = a.aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,27 @@
package io.provs.ubuntu.keys
import io.provs.Secret
import io.provs.test.defaultTestContainer
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
internal class ProvisionKeysTest {
@Test
@EnabledOnOs(OS.LINUX)
fun provisionKeysCurrentUser() {
// given
val a = defaultTestContainer()
// when
val res = a.provisionKeysCurrentUser(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey()))
)
// then
assert(res.success)
}
}

View file

@ -0,0 +1,166 @@
package io.provs.ubuntu.keys
fun publicGPGSnakeoilKey(): String {
return """-----BEGIN PGP PUBLIC KEY BLOCK-----
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
KpkCQg==
=cS1b
-----END PGP PUBLIC KEY BLOCK----- """
}
fun privateGPGSnakeoilKey(): String {
return """-----BEGIN PGP PRIVATE KEY BLOCK-----
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
i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
=h5SJ
-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
}
fun publicSSHSnakeoilKey(): String {
return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
}
fun privateSSHSnakeoilKey(): String {
return """
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
-----END RSA PRIVATE KEY-----
""".trimIndent()
}

View file

@ -0,0 +1,72 @@
package io.provs.ubuntu.keys.base
import io.provs.Prov
import io.provs.Secret
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import io.provs.ubuntu.keys.KeyPair
import io.provs.ubuntu.keys.privateGPGSnakeoilKey
import io.provs.ubuntu.keys.publicGPGSnakeoilKey
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class GpgKtTest {
@Test
fun gpgFingerprint_returnsCorrectFingerprint() {
// when
val fingerprint = Prov.defaultInstance().gpgFingerprint(publicGPGSnakeoilKey())
// then
assertEquals("85052C6954262D61D4E9977E0674104CA81A4905", fingerprint)
}
@Test
@ContainerTest
fun configureGpgKeys() {
// given
val a = defaultTestContainer()
// when
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
// then
assertTrue(res.success)
}
@Test
@ContainerTest
fun configureGpgKeysTrusted() {
// given
val a = defaultTestContainer()
// when
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
// then
assertTrue(res.success)
val trustedKey = a.cmd("gpg -K | grep ultimate").out
assertEquals("uid [ultimate] snakeoil <snake@oil.com>", trustedKey?.trim())
}
@Test
@ContainerTest
fun configureGpgKeysIsIdempotent() {
// given
val a = defaultTestContainer()
// when
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
val res2 = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
// then
assertTrue(res.success)
assertTrue(res2.success)
}
}

View file

@ -0,0 +1,24 @@
package io.provs.ubuntu.keys.base
import io.provs.Secret
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.keys.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
internal class SshKtTest {
@Test
fun configureSshKeys() {
// given
val a = defaultTestContainer()
// when
val res = a.configureSshKeys(KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey())))
// then
assertTrue(res.success)
}
}

View file

@ -0,0 +1,13 @@
package io.provs.ubuntu.secret.secretSources
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class PromptSecretSourceTest {
@Test
@Disabled // run manually
fun secret() {
println("Secret: " + PromptSecretSource().secret().plain())
}
}

View file

@ -0,0 +1,33 @@
package io.provs.ubuntu.user
import io.provs.test.defaultTestContainer
import io.provs.ubuntu.keys.*
import io.provs.ubuntu.secret.SecretSourceType
import io.provs.ubuntu.user.base.configureUser
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
internal class ProvisionUserKtTest {
@Test
@EnabledOnOs(OS.LINUX)
fun configureUser() {
// given
val a = defaultTestContainer()
// when
val res = a.configureUser(
UserConfig(
"testuser",
"test@mail.com",
KeyPairSource(SecretSourceType.PLAIN, publicGPGSnakeoilKey(), privateGPGSnakeoilKey()),
KeyPairSource(SecretSourceType.PLAIN, publicSSHSnakeoilKey(), privateSSHSnakeoilKey())
)
)
// then
assert(res.success)
}
}

View file

@ -0,0 +1,23 @@
package io.provs.ubuntu.utils
import io.provs.Prov
import io.provs.test.tags.ContainerTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class UtilsKtTest {
@ContainerTest
@Test
fun printToShell_escapes_successfully() {
// given
val a = Prov.defaultInstance()
// when
val testString = "test if newline \n and apostrophe's ' \" and special chars !§$%[]\\ äöüß are handled correctly"
val res = a.cmd(printToShell(testString)).out
// then
assertEquals(testString, res)
}
}

View file

@ -0,0 +1,30 @@
package io.provs.ubuntu.web.base
import io.provs.test.defaultTestContainer
import io.provs.test.tags.ContainerTest
import io.provs.ubuntu.filesystem.base.createFile
import io.provs.ubuntu.filesystem.base.fileContent
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class WebKtTest {
@ContainerTest
@Test
fun downloadFromURL_downloadsFile() {
// given
val a = defaultTestContainer()
val file = "file1"
a.createFile("/tmp/" + file, "hello")
// when
val res = a.downloadFromURL("file:///tmp/" + file, "file2", "/tmp")
// then
val res2 = a.fileContent("/tmp/file2")
assertTrue(res.success)
assertEquals("hello", res2)
}
}

View file

@ -0,0 +1,7 @@
{
"type": "MINIMAL",
"ssh": null,
"gpg": null,
"gitUserName": "mygitusername",
"gitEmail": "my@git.email"
}