diff --git a/build.gradle b/build.gradle index 6dd3c14..d76cdd7 100644 --- a/build.gradle +++ b/build.gradle @@ -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" diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/demos/HelloWorld.kt b/src/main/kotlin/io/provs/ubuntu/extensions/demos/HelloWorld.kt new file mode 100644 index 0000000..87910bf --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/demos/HelloWorld.kt @@ -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() +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/demos/SystemInfos.kt b/src/main/kotlin/io/provs/ubuntu/extensions/demos/SystemInfos.kt new file mode 100644 index 0000000..dbb70e9 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/demos/SystemInfos.kt @@ -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 +} + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt new file mode 100644 index 0000000..4efd560 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionMonitoring.kt @@ -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()) +} + + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt new file mode 100644 index 0000000..d200608 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_compounds/monitoring/ProvisionNginxAndMonitoring.kt @@ -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) +} + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt new file mode 100644 index 0000000..b77a033 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/certbot/ProvisionCertbot.kt @@ -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) + } + +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt new file mode 100644 index 0000000..5c15732 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewall.kt @@ -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") +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt new file mode 100644 index 0000000..fa416e3 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/firewall/base/FirewallBackup.kt @@ -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.") +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt new file mode 100644 index 0000000..5fb50c4 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexus.kt @@ -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) + } + } +} + + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt new file mode 100644 index 0000000..28e8512 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nexus/base/NginxConf.kt @@ -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"; + } + } +} + """ +} + + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt new file mode 100644 index 0000000..6407a33 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginx.kt @@ -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() +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt new file mode 100644 index 0000000..9bcd1ba --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/BasicAuth.kt @@ -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()}") +} + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt new file mode 100644 index 0000000..09d15fa --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Configs.kt @@ -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"; + } + } +} + """ + ) +} + diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt new file mode 100644 index 0000000..ced12b7 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/Locations.kt @@ -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) + } +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt new file mode 100644 index 0000000..b4347d0 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/SelfSignedCertificate.kt @@ -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")) +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt new file mode 100644 index 0000000..97ec14e --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/ProvisionPrometheus.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt new file mode 100644 index 0000000..7d4317c --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/NodeExporter.kt @@ -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 +""" +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt new file mode 100644 index 0000000..f84e89f --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/Prometheus.kt @@ -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=` 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'] +""" diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt new file mode 100644 index 0000000..c8c8d7b --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/server_software/prometheus/base/PrometheusNginxConfig.kt @@ -0,0 +1,5 @@ +package io.provs.ubuntu.extensions.server_software.prometheus.base + +val prometheusNginxConfig = """ + proxy_pass http://localhost:9090/prometheus; +""" \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt new file mode 100644 index 0000000..df3addb --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplace.kt @@ -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 + * 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) { + 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) + * + * 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) { + provisionRemote(args = args) +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt new file mode 100644 index 0000000..246299a --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/WorkplaceConfig.kt @@ -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)) } +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Docker.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Docker.kt new file mode 100644 index 0000000..9e6e237 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Docker.kt @@ -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") +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Fakturama.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Fakturama.kt new file mode 100644 index 0000000..9ce4f52 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Fakturama.kt @@ -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") +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Gopass.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Gopass.kt new file mode 100644 index 0000000..fe5334c --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Gopass.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridge.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridge.kt new file mode 100644 index 0000000..113ec83 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridge.kt @@ -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 +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt new file mode 100644 index 0000000..c066f79 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/NoSwappiness.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/PackageBundles.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/PackageBundles.kt new file mode 100644 index 0000000..abdf162 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/PackageBundles.kt @@ -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" \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Redshift.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Redshift.kt new file mode 100644 index 0000000..9bccfa4 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Redshift.kt @@ -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() diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCode.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCode.kt new file mode 100644 index 0000000..acb88a2 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCode.kt @@ -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) = 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/") +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt new file mode 100644 index 0000000..611ff60 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/VirtualBoxGuest.kt @@ -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) +} diff --git a/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Zim.kt b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Zim.kt new file mode 100644 index 0000000..39815e0 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/extensions/workplace/base/Zim.kt @@ -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") + } +} diff --git a/src/main/kotlin/io/provs/ubuntu/filesystem/base/Filesystem.kt b/src/main/kotlin/io/provs/ubuntu/filesystem/base/Filesystem.kt new file mode 100644 index 0000000..ddd8bd6 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/filesystem/base/Filesystem.kt @@ -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() +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/git/ProvisionGit.kt b/src/main/kotlin/io/provs/ubuntu/git/ProvisionGit.kt new file mode 100644 index 0000000..5376b30 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/git/ProvisionGit.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/git/base/Git.kt b/src/main/kotlin/io/provs/ubuntu/git/base/Git.kt new file mode 100644 index 0000000..8d85484 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/git/base/Git.kt @@ -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) = 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")) +} + + diff --git a/src/main/kotlin/io/provs/ubuntu/install/base/Install.kt b/src/main/kotlin/io/provs/ubuntu/install/base/Install.kt new file mode 100644 index 0000000..197b5bc --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/install/base/Install.kt @@ -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") +} diff --git a/src/main/kotlin/io/provs/ubuntu/keys/ProvisionKeys.kt b/src/main/kotlin/io/provs/ubuntu/keys/ProvisionKeys.kt new file mode 100644 index 0000000..249145e --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/keys/ProvisionKeys.kt @@ -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 +} + diff --git a/src/main/kotlin/io/provs/ubuntu/keys/base/Gpg.kt b/src/main/kotlin/io/provs/ubuntu/keys/base/Gpg.kt new file mode 100644 index 0000000..41ae91f --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/keys/base/Gpg.kt @@ -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) } +} diff --git a/src/main/kotlin/io/provs/ubuntu/keys/base/Ssh.kt b/src/main/kotlin/io/provs/ubuntu/keys/base/Ssh.kt new file mode 100644 index 0000000..e33e82c --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/keys/base/Ssh.kt @@ -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 +} + diff --git a/src/main/kotlin/io/provs/ubuntu/secret/SecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/SecretSource.kt new file mode 100644 index 0000000..03628b6 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/SecretSource.kt @@ -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) + } +} + diff --git a/src/main/kotlin/io/provs/ubuntu/secret/secretSources/FileSecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/FileSecretSource.kt new file mode 100644 index 0000000..6c9f3c5 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/FileSecretSource.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/secret/secretSources/GopassSecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/GopassSecretSource.kt new file mode 100644 index 0000000..a2a77ce --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/GopassSecretSource.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PassSecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PassSecretSource.kt new file mode 100644 index 0000000..535cda0 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PassSecretSource.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PlainSecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PlainSecretSource.kt new file mode 100644 index 0000000..8854e72 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PlainSecretSource.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSource.kt b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSource.kt new file mode 100644 index 0000000..e5cfe37 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSource.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/provs/ubuntu/user/UserConfig.kt b/src/main/kotlin/io/provs/ubuntu/user/UserConfig.kt new file mode 100644 index 0000000..e4bed8e --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/user/UserConfig.kt @@ -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)) } +} diff --git a/src/main/kotlin/io/provs/ubuntu/user/base/User.kt b/src/main/kotlin/io/provs/ubuntu/user/base/User.kt new file mode 100644 index 0000000..20ae5c4 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/user/base/User.kt @@ -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) +} + diff --git a/src/main/kotlin/io/provs/ubuntu/utils/Utils.kt b/src/main/kotlin/io/provs/ubuntu/utils/Utils.kt new file mode 100644 index 0000000..8dc558e --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/utils/Utils.kt @@ -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()}\"" +} + diff --git a/src/main/kotlin/io/provs/ubuntu/web/base/Web.kt b/src/main/kotlin/io/provs/ubuntu/web/base/Web.kt new file mode 100644 index 0000000..bd2e270 --- /dev/null +++ b/src/main/kotlin/io/provs/ubuntu/web/base/Web.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt new file mode 100644 index 0000000..3704bc6 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/firewall/ProvisionFirewallKtTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt new file mode 100644 index 0000000..d8b8972 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nexus/ProvisionNexusKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt new file mode 100644 index 0000000..eb55cb7 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/ProvisionNginxKtTest.kt @@ -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) + } +} + diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt new file mode 100644 index 0000000..c37eaf3 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/server_software/nginx/base/LocationsKtTest.kt @@ -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()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/test_keys/TestKeys.kt b/src/test/kotlin/io/provs/ubuntu/extensions/test_keys/TestKeys.kt new file mode 100644 index 0000000..9f1617c --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/test_keys/TestKeys.kt @@ -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() +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt new file mode 100644 index 0000000..a15c67b --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/ProvisionWorkplaceKtTest.kt @@ -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) + } +} + + diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt new file mode 100644 index 0000000..edba2e2 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/FakturamaKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt new file mode 100644 index 0000000..4a4d868 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassBridgeKtTest.kt @@ -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") + } +} diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt new file mode 100644 index 0000000..8d8c8d3 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/GopassKtTest.kt @@ -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) + } +} diff --git a/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt new file mode 100644 index 0000000..35b864e --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/extensions/workplace/base/VSCodeKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/filesystem/base/FilesystemKtTest.kt b/src/test/kotlin/io/provs/ubuntu/filesystem/base/FilesystemKtTest.kt new file mode 100644 index 0000000..52fb46a --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/filesystem/base/FilesystemKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/git/base/GitKtTest.kt b/src/test/kotlin/io/provs/ubuntu/git/base/GitKtTest.kt new file mode 100644 index 0000000..7ec749a --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/git/base/GitKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/install/base/InstallKtTest.kt b/src/test/kotlin/io/provs/ubuntu/install/base/InstallKtTest.kt new file mode 100644 index 0000000..ba695a4 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/install/base/InstallKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/keys/ProvisionKeysTest.kt b/src/test/kotlin/io/provs/ubuntu/keys/ProvisionKeysTest.kt new file mode 100644 index 0000000..1ce816e --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/keys/ProvisionKeysTest.kt @@ -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) + } +} + diff --git a/src/test/kotlin/io/provs/ubuntu/keys/TestKeys.kt b/src/test/kotlin/io/provs/ubuntu/keys/TestKeys.kt new file mode 100644 index 0000000..6042307 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/keys/TestKeys.kt @@ -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() +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/keys/base/GpgKtTest.kt b/src/test/kotlin/io/provs/ubuntu/keys/base/GpgKtTest.kt new file mode 100644 index 0000000..602bd92 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/keys/base/GpgKtTest.kt @@ -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 ", 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) + } +} + + diff --git a/src/test/kotlin/io/provs/ubuntu/keys/base/SshKtTest.kt b/src/test/kotlin/io/provs/ubuntu/keys/base/SshKtTest.kt new file mode 100644 index 0000000..d83cdb0 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/keys/base/SshKtTest.kt @@ -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) + + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt b/src/test/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt new file mode 100644 index 0000000..63800b4 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/secret/secretSources/PromptSecretSourceTest.kt @@ -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()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/user/ProvisionUserKtTest.kt b/src/test/kotlin/io/provs/ubuntu/user/ProvisionUserKtTest.kt new file mode 100644 index 0000000..897d2c9 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/user/ProvisionUserKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/utils/UtilsKtTest.kt b/src/test/kotlin/io/provs/ubuntu/utils/UtilsKtTest.kt new file mode 100644 index 0000000..1bf8846 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/utils/UtilsKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/io/provs/ubuntu/web/base/WebKtTest.kt b/src/test/kotlin/io/provs/ubuntu/web/base/WebKtTest.kt new file mode 100644 index 0000000..3358f86 --- /dev/null +++ b/src/test/kotlin/io/provs/ubuntu/web/base/WebKtTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/resources/WorkplaceConfigExample.json b/src/test/resources/WorkplaceConfigExample.json new file mode 100644 index 0000000..5ea87e4 --- /dev/null +++ b/src/test/resources/WorkplaceConfigExample.json @@ -0,0 +1,7 @@ +{ + "type": "MINIMAL", + "ssh": null, + "gpg": null, + "gitUserName": "mygitusername", + "gitEmail": "my@git.email" +} \ No newline at end of file