#!/usr/bin/perl
#
# deploy_ontap_cert.pl
#
# Dehydrated hook script that installs certificates on a NetApp ONTAP
# system via the REST API.
#
# Called by dehydrated as:
#   deploy_cert <domain> <privkey.pem> <cert.pem> <fullchain.pem> <chain.pem> <timestamp>
#
# The ONTAP host is derived from <domain> (e.g. eprod.glanzmann.de).
# Credentials can be overridden via ONTAP_USER and ONTAP_PASS env vars.
# The SVM can be overridden via ONTAP_SVM env var.
#
# Strategy to avoid downtime:
# 1. Upload new cert under unique name (<cn>-<timestamp>)
# 2. Switch active cert via REST API (PATCH /api/cluster/web, poll job)
# 3. Verify REST API still responds
# 4. Delete old cert

# For the first deploy add the '-k' flag to curl below
# Create the following role on ontap:
# security login rest-role create -vserver nprod -role certmanager -api /api/security/certificates -access all
# security login rest-role create -vserver nprod -role certmanager -api /api/cluster/web -access read_modify
# security login rest-role create -vserver nprod -role certmanager -api /api/cluster/jobs -access readonly
# security login create -user-or-group-name certdeploy -application http -authentication-method password -role certmanager -vserver nprod

use strict;
use warnings FATAL => 'all';
use JSON::PP;

# --- config ---

my $action = shift @ARGV || '';
my $domain = shift @ARGV || '';
exit 0 unless $action eq 'deploy_cert' && $domain;
my ($privkey, $cert, $fullchain, $chain, $timestamp) = @ARGV;

my $ontap_host = $domain;
my $ontap_user = $ENV{ONTAP_USER} || 'certdeploy';
my $ontap_pass = $ENV{ONTAP_PASS};
if (!$ontap_pass) {
    my $pw_file = "$ENV{HOME}/.ontapletsencryptpassword";
    if (open(my $fh, '<', $pw_file)) {
        chomp($ontap_pass = <$fh>);
        close($fh);
    }
}
my $ontap_svm  = $ENV{ONTAP_SVM}  || (split(/\./, $domain))[0];
my $api_base   = "https://$ontap_host/api/security/certificates";

# --- helpers ---

sub slurp {
    my ($path) = @_;
    open(my $fh, '<', $path) or return;
    local $/; my $d = <$fh>; close $fh; return $d;
}

sub json_string {
    my ($s) = @_;
    $s =~ s/\\/\\\\/g;
    $s =~ s/"/\\"/g;
    $s =~ s/\n/\\n/g;
    $s =~ s/\t/\\t/g;
    $s =~ s/\r//g;
    return qq("$s");
}

sub api_call {
    my ($method, $url, $payload) = @_;
    my @args = ('curl', '-k', '-sS', '-X', $method,
        '-u', "$ontap_user:$ontap_pass",
        '-H', 'Content-Type: application/json');
    push @args, ('-d', $payload) if $payload;
    push @args, $url;
    open(my $fh, '-|', @args) or die "curl: $!";
    my $body = do { local $/; <$fh> };
    close $fh;
    return ($? >> 8, $body);
}

sub api_get {
    my ($url) = @_;
    my ($exit, $body) = api_call('GET', $url, undef);
    return ($exit, eval { JSON::PP::decode_json($body) } // $body);
}

sub api_post {
    my ($url, $payload) = @_;
    return api_call('POST', $url, $payload);
}

sub api_delete {
    my ($url) = @_;
    return api_call('DELETE', $url, undef);
}

sub api_patch {
    my ($url, $payload) = @_;
    return api_call('PATCH', $url, $payload);
}

sub ssh_cmd {
    my ($cmd) = @_;
    system('sshpass', '-p', $ontap_pass, 'ssh',
        '-o', 'StrictHostKeyChecking=no',
        '-o', 'UserKnownHostsFile=/dev/null',
        "$ontap_user\@$ontap_host", $cmd);
    return $? >> 8;
}

sub get_serial {
    my ($pem) = @_;
    my $tmp = "/tmp/ontap_ser_$$.pem";
    open(my $fh, '>', $tmp) or return '';
    print $fh $pem;
    close $fh;
    my $serial = `openssl x509 -noout -serial -in $tmp 2>/dev/null`;
    unlink $tmp;
    $serial =~ /serial\s*=\s*(.+)/i;
    return $1 || '';
}

sub get_cn {
    my ($pem) = @_;
    my $tmp = "/tmp/ontap_cn_$$.pem";
    open(my $fh, '>', $tmp) or return 'cert';
    print $fh $pem;
    close $fh;
    my $subj = `openssl x509 -noout -subject -in $tmp 2>/dev/null`;
    unlink $tmp;
    return $1 if $subj && $subj =~ /CN\s*=\s*([^\s,]+)/;
    return 'cert';
}

sub get_issuer {
    my ($pem) = @_;
    my $tmp = "/tmp/ontap_iss_$$.pem";
    open(my $fh, '>', $tmp) or return '';
    print $fh $pem;
    close $fh;
    my $issuer = `openssl x509 -noout -issuer -in $tmp 2>/dev/null`;
    unlink $tmp;
    # Extract CN from issuer DN
    $issuer =~ /CN\s*=\s*([^\s,\/]+)/;
    return $1 || '';
}

# --- main ---

my $cert_pem    = slurp($cert)    or die "Cannot read $cert: $!";
my $privkey_pem = slurp($privkey) or die "Cannot read $privkey: $!";

# Extract CN from cert for naming
my $cn = get_cn($cert_pem);
my $cert_name = "$cn-$timestamp";

print STDERR "deploy_ontap_cert: installing as '$cert_name' on $ontap_host (SVM: $ontap_svm)\n";

# --- Step 1: Find old active cert ---

my ($exit, $existing) = api_get($api_base);
if ($exit != 0 || !ref $existing) {
    print STDERR "deploy_ontap_cert: ERROR listing existing certs: $existing\n";
    exit 1;
}

my $old_uuid;
my $old_serial;
for my $r (@{$existing->{records} || []}) {
    next unless defined $r->{name} && $r->{name} =~ /^\Q$cn\E(-|$)/;
    ($old_uuid, $old_serial) = ($r->{uuid}, $r->{serial_number});
    last;
}

# --- Step 2: Upload new cert with unique name ---

# Build intermediate chain: chain.pem + root CA (downloaded if needed)
my $chain_pem  = $chain ? slurp($chain) : '';
my @intermediates;
if ($chain_pem) {
    while ($chain_pem =~ m/(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/sg) {
        push @intermediates, $1;
    }
}
# Try to fetch ISRG Root X1 for a complete chain
if (!slurp("/tmp/isrg_root_x1.pem")) {
    system("curl -sS -o /tmp/isrg_root_x1.pem https://letsencrypt.org/certs/isrgrootx1.pem 2>/dev/null");
}
my $root_pem = slurp("/tmp/isrg_root_x1.pem");
if ($root_pem) {
    while ($root_pem =~ m/(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/sg) {
        push @intermediates, $1;
    }
}

my $json = qq({
    "svm": { "name": "$ontap_svm" },
    "type": "server",
    "name": ) . json_string($cert_name) . qq(,
    "public_certificate": ) . json_string($cert_pem) . qq(,
    "private_key": ) . json_string($privkey_pem);
if (@intermediates) {
    $json .= qq(,
    "intermediate_certificates": [) . join(",", map { json_string($_) } @intermediates) . qq(]);
}
$json .= qq(
});

my ($http_status, $response) = api_post("$api_base?return_records=true", $json);

if ($http_status != 0 && $http_status != 201) {
    print STDERR "deploy_ontap_cert: ERROR $http_status: $response\n";
    exit 1;
}

my $decoded = eval { JSON::PP::decode_json($response) };
if (ref $decoded eq 'HASH' && $decoded->{error}) {
    my $msg = $decoded->{error}->{message} // '(no message)';
    print STDERR "deploy_ontap_cert: API ERROR uploading new cert: $msg\n";
    exit 1;
}

# Extract serial from cert PEM directly (API does not return it)
my $new_serial = get_serial($cert_pem);
my $ca_name    = get_issuer($cert_pem) || 'R13';

print STDERR "deploy_ontap_cert: uploaded '$cert_name' (serial=$new_serial ca=$ca_name)\n";

# --- Step 3: Switch active cert via REST API ---
# PATCH /api/cluster/web with the new cert UUID, then poll the async job.

# First get the UUID of the newly uploaded cert
my $new_uuid;
{
    my ($l_exit, $list) = api_get($api_base);
    if (ref $list eq 'HASH' && $list->{records}) {
        for my $r (@{$list->{records}}) {
            next unless defined $r->{name} && $r->{name} eq $cert_name;
            $new_uuid = $r->{uuid};
            last;
        }
    }
}
if (!$new_uuid) {
    print STDERR "deploy_ontap_cert: ERROR could not find UUID for uploaded cert '$cert_name'\n";
    exit 1;
}

my $patch_url = "https://$ontap_host/api/cluster/web";
my $patch_json = qq({"certificate":{"uuid":") . $new_uuid . qq("}});

my ($patch_status, $patch_body) = api_patch($patch_url, $patch_json);
if ($patch_status != 0 && $patch_status != 200) {
    print STDERR "deploy_ontap_cert: ERROR activating new cert: $patch_status $patch_body\n";
    exit 1;
}

my $patch_decoded = eval { JSON::PP::decode_json($patch_body) };
if (ref $patch_decoded eq 'HASH' && $patch_decoded->{error}) {
    my $msg = $patch_decoded->{error}->{message} // '(no message)';
    print STDERR "deploy_ontap_cert: API ERROR activating new cert: $msg\n";
    exit 1;
}

# Poll the async job until completion
my $job_uuid = ref $patch_decoded eq 'HASH' && $patch_decoded->{job} ? $patch_decoded->{job}->{uuid} : undef;
if ($job_uuid) {
    my $job_url = "https://$ontap_host/api/cluster/jobs/$job_uuid";
    print STDERR "deploy_ontap_cert: waiting for job $job_uuid...\n";
    for (1..30) {
        sleep(2);
        my ($j_exit, $j_data) = api_get($job_url);
        if (ref $j_data eq 'HASH' && $j_data->{state}) {
            last if $j_data->{state} eq 'success';
            if ($j_data->{state} eq 'failure') {
                print STDERR "deploy_ontap_cert: ERROR job failed: " . ($j_data->{message} // '(no message)') . "\n";
                exit 1;
            }
        }
    }
}

print STDERR "deploy_ontap_cert: activated cert '$cert_name'\n";

# --- Step 5: Verify REST API still responds ---

sleep(2);
my ($exit3, $verify) = api_get($api_base);
if ($exit3 != 0) {
    print STDERR "deploy_ontap_cert: ERROR REST API not responding after cert switch: $verify\n";
    exit 1;
}

print STDERR "deploy_ontap_cert: REST API verified OK\n";

# --- Step 6: Delete old cert (captured before upload) ---

if ($old_uuid) {
    my ($del_status, $del_body) = api_delete("$api_base/$old_uuid");
    if ($del_status == 200 || $del_status == 0 || $del_status == 204) {
        print STDERR "deploy_ontap_cert: deleted old cert ($old_uuid)\n";
    } else {
        my $del_err = eval { JSON::PP::decode_json($del_body)->{error}->{message} } || $del_body || '(no body)';
        print STDERR "deploy_ontap_cert: WARNING could not delete old cert: $del_err\n";
    }
}

print STDERR "deploy_ontap_cert: done\n";
