# Mgmt
# Copyright (C) James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Additional permission under GNU GPL version 3 section 7
#
# If you modify this program, or any covered work, by linking or combining it
# with embedded mcl code and modules (and that the embedded mcl code and
# modules which link with this program, contain a copy of their source code in
# the authoritative form) containing parts covered by the terms of any other
# license, the licensors of this program grant you additional permission to
# convey the resulting work. Furthermore, the licensors of this program grant
# the original author, James Shubin, additional permission to update this
# additional permission if he deems it necessary to achieve the goals of this
# additional permission.

import "fmt"
import "golang"
import "golang/strings" as golang_strings
import "iter"
import "local"
import "net"
import "os"
import "strings"
import "world/collect"

# ssh_keygen creates an ssh key pair in the user's home directory if the private
# key doesn't exist.
# TODO: add more parameters such as key size and type in the future
class ssh_keygen($user) {
	include ssh_keygen_type($user, "ed25519")
}

# $type is rsa or ed25519
class ssh_keygen_type($user, $type) {
	panic($user == "") # panic if $user is empty
	panic($type == "") # panic if $type is empty
	panic($type != "rsa" and $type != "ed25519") # panic if $type is invalid
	$p = os.expand_home("~${user}/") # eg: ~james/
	exec "ssh-keygen-${user}" {
		cmd => "/usr/bin/ssh-keygen",
		args => [
		        "-t", "${type}", # type
		        "-f", "${p}.ssh/id_${type}", # private key file
		        "-N", "", # empty password
		],
		creates => "${p}.ssh/id_${type}",
		user => $user,

		Before => File["${p}.ssh/id_${type}"],
	}
	# This also serves as a "handle" so that other resources can depend on
	# this file getting created before they run.
	file "${p}.ssh/" {
		state => "exists",
		mode => "u=rwx,go=",
		owner => $user,
	}
	file "${p}.ssh/id_${type}" {
		mode => "u=rw,go=",
		owner => $user,

		Depend => File["${p}.ssh/"],
	}

	line "${user}@${hostname}" {
		#file => "", # specified on collect
		#state => "exists", # specified on collect
		content => os.readfilewait("${p}.ssh/id_${type}.pub"),

		Meta:hidden => true,
		Meta:export => ["*",],
	}
}

# ssh_authorized_keys pulls down an export key for a $user, from a user@host.
class ssh_authorized_keys($user, $from) {
	panic($user == "") # panic if $user is empty
	$p = os.expand_home("~${user}/") # eg: ~james/

	$all = collect.res("line") # []struct{name str; host str;}
	$fn = func($st) {
		$st->name == "${from}" and $st->host != "${hostname}"
	}
	$filtered = iter.filter($all, $fn)

	collect line $filtered { # pull down everyone's public keys
		file => "${p}.ssh/authorized_keys",
		state => $const.res.file.state.exists,

		Meta:hidden => false,
	}
}

# network_rename takes the device with the $mac address and renames it to $dev.
class network_rename($mac, $dev) {
	#panic(not net.is_mac("${mac}"))

	file "/etc/systemd/network/70-rename-${dev}.link" {
		state => "exists",
		content => "
# Pick the device name based on the mac address.
[Match]
MACAddress=${mac}

[Link]
Name=${dev}
",
		mode => "u=rw,go=r",
		owner => "root",
		group => "root",

		Notify => Exec["udevadm trigger"],
	}

	# TODO: we only want to run this once, but it's harmless for now
	exec "udevadm trigger" {
		cmd => "/usr/sbin/udevadm trigger --type=all --action=add --prioritized-subsystem=net --settle",
	}
}

# networkd_dhcp sets up a dhcp client with systemd-networkd.
class networkd_dhcp($dev, $st) {
	$dns = $st->dns || ["8.8.8.8",]

	$tmpl =
"
[Match]
Name=${dev}

[Network]
DHCP=yes
{{ range .dns -}}
DNS={{ . }}
{{ end -}}
#UseGateway=false

[DHCP]
UseDNS=false
RouteMetric=100
"

	$args = struct{
		#dev => $dev,
		dns => $dns,
	}

	file "/etc/systemd/network/${dev}-dhcp.network" {
		state => "exists",
		content => golang.template($tmpl, $args),
		mode => "u=rw,go=r",
		owner => "root",
		group => "root",

		Notify => Svc["systemd-networkd"],
	}

	svc "systemd-networkd" {
		state => "running",
		startup => "enabled",
	}
}

# networkd_static sets up a static ip address with systemd-networkd.
class networkd_static($dev, $st) {
	$cidr = $st->cidr # cidr
	$ip = net.cidr_to_ip($cidr)
	$prefix = net.cidr_to_prefix($cidr)
	#$router = $st->router || ""
	$dns = $st->dns || ["8.8.8.8",]
	#$vips []str = $st->vips || [] # []cidr

	$tmpl =
"
[Match]
Name=${dev}

[Network]
Address=${ip}/${prefix}
{{ range .dns -}}
DNS={{ . }}
{{ end -}}
#UseGateway=false

[Address]
RouteMetric=101

[Route]
Metric=1001
#Gateway=
#Destination=0.0.0.0/0
"

	$args = struct{
		#dev => $dev,
		dns => $dns,
	}

	file "/etc/systemd/network/${dev}-static.network" {
		state => "exists",
		content => golang.template($tmpl, $args),
		mode => "u=rw,go=r",
		owner => "root",
		group => "root",

		Notify => Svc["systemd-networkd"],
	}

	svc "systemd-networkd" {
		state => "running",
		startup => "enabled",
	}
}

# network_manager_dhcp sets up a dhcp client with network manager.
# NOTE: To see what it's using run: `nmcli -f name,uuid,filename connection`.
class network_manager_dhcp($st) {
	$uuid = $st->uuid || "" # 01234567-89ab-cdef-0123-456789abcdef
	$mac = $st->mac || ""
	$dev = $st->dev || "eth0"

	$dns = $st->dns || ["8.8.8.8",]
	$dns_str = golang_strings.join($dns, ";") # the line also ends with a semicolon
	$tmpl =
"
[connection]
id=${dev}
{{ if .uuid -}}
uuid={{ .uuid }}
{{ end -}}
type=ethernet
interface-name=${dev}
autoconnect=true

[ipv4]
{{ if .dns -}}
dns=${dns_str};
{{ end -}}
dns-search=
may-fail=false
method=auto

[ethernet]
{{ if .mac -}}
mac-address={{ .mac }}
{{ end -}}
"

	$args = struct{
		uuid => $uuid,
		mac => $mac,
		dns => $dns,
	}

	file "/etc/NetworkManager/system-connections/${dev}.nmconnection" {
		state => "exists",
		content => golang.template($tmpl, $args),
		mode => "u=rw,go=",
		owner => "root",

		Notify => Svc["NetworkManager"],
	}

	svc "NetworkManager" {
	}
}

# network_manager_static sets up a static ip address with network manager.
# NOTE: To see what it's using run: `nmcli -f name,uuid,filename connection`.
class network_manager_static($st) {
	$uuid = $st->uuid || "" # 01234567-89ab-cdef-0123-456789abcdef
	$mac = $st->mac || ""
	$dev = $st->dev || "eth0"
	$cidr = $st->cidr # cidr
	$ip = net.cidr_to_ip($cidr)
	$prefix = net.cidr_to_prefix($cidr)
	$router = $st->router || ""
	$dns = $st->dns || ["8.8.8.8",]
	$vips []str = $st->vips || [] # []cidr

	$dns_str = golang_strings.join($dns, ";") # the line also ends with a semicolon
	$tmpl =
"
[connection]
id=${dev}
{{ if .uuid -}}
uuid={{ .uuid }}
{{ end -}}
type=ethernet
interface-name=${dev}
autoconnect=true

[ipv4]
{{ if .router -}}
address1=${ip}/${prefix},{{ .router }}
{{ else -}}
address1=${ip}/${prefix}
{{ end -}}
{{ if .vips -}}
{{ range $index, $ip := .vips }}
{{ $ix := len (printf \"xx%*s\" $index \"\") -}}
address{{ $ix }}={{ $ip }}
{{ end -}}
{{ end -}}
{{ if .dns -}}
dns=${dns_str};
{{ end -}}
dns-search=
may-fail=false
method=manual

[ethernet]
{{ if .mac -}}
mac-address={{ .mac }}
{{ end -}}
"

	$args = struct{
		uuid => $uuid,
		mac => $mac,
		router => $router,
		dns => $dns,
		vips => $vips,
	}

	file "/etc/NetworkManager/system-connections/${dev}.nmconnection" {
		state => "exists",
		content => golang.template($tmpl, $args),
		mode => "u=rw,go=",
		owner => "root",

		Notify => Svc["NetworkManager"],
	}

	svc "NetworkManager" {
	}
}

# systemd_daemon_reload creates an exec which runs a systemctl daemon-reload if
# one is needed. The exec resource uses the input name, to make it easy to
# attach an edge to it.
# TODO: can we use: systemctl show foo.service --property=NeedDaemonReload
class systemd_daemon_reload($name) {
	$vardir = local.vardir("misc/")
	$mtime_file = "${vardir}daemon-reload"

	exec "${name}" {
		cmd => "/usr/bin/systemctl",
		args => [
			"daemon-reload",
		],

		donecmd => "/usr/bin/date --utc > ${mtime_file}",
		doneshell => "/usr/bin/bash",

		watchfiles => [
			"/lib/systemd/system/",
			"/etc/systemd/system/",
			"${mtime_file}",
		],

		# If we have any file that's newer than our mtime, the run.
		# NOTE: We grep since find doesn't return a useful return code.
		ifcmd => "/usr/bin/test ! -e '${mtime_file}' || /usr/bin/find /lib/systemd/system/ /etc/systemd/system/ -type f -name '*.service' -newer '${mtime_file}' | /usr/bin/grep -q .",
		ifshell => "/usr/bin/bash",
	}
}

# Instead of running `dnf copr enable purpleidea/foo` you can use
# `purpleidea/foo` as the $name here to accomplish the same thing.
class copr_enable($name) {
	# example: sudo dnf copr enable gsauthof/dracut-sshd
	# /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:gsauthof:dracut-sshd.repo
	$sp = strings.split($name, "/")
	$user = $sp[0]
	$repo = $sp[1]
	$p = fmt.printf("/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:%s:%s.repo", $user, $repo)

	# TODO: If we wanted to be more thorough, we could template the file
	# instead or running this copr enable command.
	exec "dnf-copr-enable-${name}" {
		cmd => "/usr/bin/dnf",
		args => [
			"-y",
			"copr",
			"enable",
			"${name}",
		],
		creates => $p,
		user => "root",
	}
}
