This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 57bae11a79 Remove Perl postinstall script (#7841)
57bae11a79 is described below

commit 57bae11a79c644af95740c7a29b90cb518dd821e
Author: ocket8888 <[email protected]>
AuthorDate: Mon Feb 12 14:07:05 2024 -0700

    Remove Perl postinstall script (#7841)
    
    * Removed old Perl postinstall script
    
    * Change 'hidden' values to actual booleans
    
    * Add deprecation notices for Python2
    
    * Update docs
    
    * Update changelog
    
    * Add Python3 as a TO dependency
    
    * Remove Python2 from Postinstall tests
    
    * Remove the ability to run Postinstall with Python2
    
    * Remove Python2 compatibility from the postinstall Python script
    
    * Update changelog
    
    * Update docs
    
    * Fix bad encoding name
    
    * Added removed notice for the Perl postinstall script
    
    * allow integer port numbers
    
    * Use range instead of explicit list
    
    * Try manually installing python 3.8
    
    * Switch minimum Python version to 3.6
    
    * Fix integer being treated as an integer
    
    Yes, you read that right. No, it's not a typo. Traffic Ops will refuse to 
load
    a configuration file that gives the Traffic Ops Golang port as an integer
    instead of a string.
    
    * Remove now-unnecessary python-abi dependency
---
 .github/workflows/postinstall.tests.yml            |   6 -
 CHANGELOG.md                                       |   4 +
 docs/source/admin/traffic_ops.rst                  |  13 +-
 infrastructure/cdn-in-a-box/traffic_ops/Dockerfile |   3 +-
 traffic_ops/build/traffic_ops.spec                 |   2 +-
 traffic_ops/install/bin/_postinstall.pl            | 918 ---------------------
 traffic_ops/install/bin/_postinstall.py            | 650 ++++++++-------
 traffic_ops/install/bin/input.json                 |  10 +-
 traffic_ops/install/bin/postinstall                |   4 +-
 traffic_ops/install/bin/postinstall.test.sh        |  31 +-
 10 files changed, 348 insertions(+), 1293 deletions(-)

diff --git a/.github/workflows/postinstall.tests.yml 
b/.github/workflows/postinstall.tests.yml
index ec5735a134..9f759ed135 100644
--- a/.github/workflows/postinstall.tests.yml
+++ b/.github/workflows/postinstall.tests.yml
@@ -32,7 +32,6 @@ on:
 
 env:
   PYTHON3_VERSION: '3.6'
-  PYTHON2_VERSION: '2.7'
 
 jobs:
   postinstall-tests:
@@ -48,11 +47,6 @@ jobs:
         if: ${{ steps.checkout.outcome == 'success' }}
         with:
           python-version: '${{ env.PYTHON3_VERSION }}'
-      - name: Install Python ${{ env.PYTHON2_VERSION }}
-        uses: actions/setup-python@v4
-        if: ${{ steps.checkout.outcome == 'success' }}
-        with:
-          python-version: '${{ env.PYTHON2_VERSION }}'
       - name: Run Postinstall Tests
         if: ${{ steps.checkout.outcome == 'success' }}
         run: traffic_ops/install/bin/postinstall.test.sh
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1722cd0858..2decbb98f3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,10 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#7918](https://github.com/apache/trafficcontrol/pull/7918) *Traffic Portal* 
Fixed topology link under DS-Servers tables page
 - [#7846](https://github.com/apache/trafficcontrol/pull/7846) *Traffic Portal* 
Increase State character limit
 
+### Removed
+- [#7832](https://github.com/apache/trafficcontrol/pull/7832) *t3c* Removed 
Perl dependency
+- [#7841](https://github.com/apache/trafficcontrol/pull/7841) *Postinstall* 
Removed Perl implementation and Python 2.x support
+
 ## [8.0.0] - 2023-09-20
 ### Added
 - [#7672](https://github.com/apache/trafficcontrol/pull/7672) *Traffic Control 
Health Client*: Added peer monitor flag while using `strategies.yaml`.
diff --git a/docs/source/admin/traffic_ops.rst 
b/docs/source/admin/traffic_ops.rst
index db97fb5580..ce6fdc9a25 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -208,17 +208,20 @@ Guide
                | Password for the admin user                        | The 
password for the administrative Traffic Ops user.                               
           |
                
+----------------------------------------------------+------------------------------------------------------------------------------------------------+
 
-.. deprecated:: ATCv6
-       The postinstall script is now written in Python. If you run into issues 
with the postinstall script, you are encouraged to file an issue at 
https://github.com/apache/trafficcontrol/issues/new/choose. The original Perl 
postinstall script is deprecated and will be removed in a future ATC release. 
To use the deprecated version anyway, run 
``/opt/traffic_ops/install/bin/_postinstall.pl`` directly instead of 
``/opt/traffic_ops/install/bin/postinstall``.
-
 The postinstall script can also be run non-interactively using 
:atc-file:`traffic_ops/install/bin/input.json`. To use it, first change the 
values to match your environment, then pass it to the ``postinstall`` script:
        .. code-block:: console
                :caption: Postinstall in Automatic (-a) mode
 
                /opt/traffic_ops/install/bin/postinstall -a --cfile 
/opt/traffic_ops/install/bin/input.json
 
-.. deprecated:: ATCv6
-       Once the Perl script is removed, the values in ``input.json`` for the 
``"hidden"`` properties will be changed from ``"1"`` and ``"0"`` to ``true`` 
and ``false``.
+.. versionchanged:: ATCv8
+       The values in ``input.json`` for the ``"hidden"`` properties have been 
changed from ``"1"`` and ``"0"`` to ``true`` and ``false``.
+
+.. versionchanged:: ATCv8
+       Python 2.x is no longer supported by the ``postinstall`` script.
+
+.. versionremoved:: ATCv8
+       In earlier versions of ATC, it was possible to run ``postinstall`` 
using Perl - no longer.
 
 .. _to-upgrading:
 
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile 
b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
index 0c62a00e2e..1a5b001069 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
@@ -84,7 +84,8 @@ RUN set -o nounset -o errexit -o xtrace && \
                perl-libwww-perl     \
                perl-TermReadKey     \
                perl-Test-CPAN-Meta  \
-               perl-WWW-Curl;       \
+               perl-WWW-Curl        \
+               python3; \
        dnf clean all
 
 FROM trafficops-dependencies AS trafficops
diff --git a/traffic_ops/build/traffic_ops.spec 
b/traffic_ops/build/traffic_ops.spec
index c2d404c558..536e95074e 100644
--- a/traffic_ops/build/traffic_ops.spec
+++ b/traffic_ops/build/traffic_ops.spec
@@ -38,7 +38,7 @@ Requires:         openssl-devel, perl, perl-core, 
perl-DBD-Pg, perl-DBI, perl-Di
 Requires:         libidn-devel, libcurl-devel, libcap
 Requires:         postgresql13 >= 13.2
 Requires:         perl-JSON, perl-libwww-perl, perl-Test-CPAN-Meta, 
perl-WWW-Curl, perl-TermReadKey, perl-Crypt-ScryptKDF
-Requires:         python(abi)
+Requires:         python3
 Requires(pre):    /usr/sbin/useradd, /usr/bin/getent
 Requires(postun): /usr/sbin/userdel
 
diff --git a/traffic_ops/install/bin/_postinstall.pl 
b/traffic_ops/install/bin/_postinstall.pl
deleted file mode 100755
index ef15725260..0000000000
--- a/traffic_ops/install/bin/_postinstall.pl
+++ /dev/null
@@ -1,918 +0,0 @@
-#!/usr/bin/perl
-
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-use lib qw(/opt/traffic_ops/install/lib /opt/traffic_ops/app/lib 
/opt/traffic_ops/app/local/lib/perl5);
-
-$ENV{PERL5LIB} = 
"/opt/traffic_ops/install/lib:/opt/traffic_ops/app/lib:/opt/traffic_ops/app/local/lib/perl5:$ENV{PERL5LIB}";
-$ENV{PATH}     = 
"/usr/bin:/usr/local/go/bin:/opt/traffic_ops/install/bin:$ENV{PATH}";
-
-use strict;
-use warnings;
-
-use DBI;
-use POSIX;
-use File::Basename qw{dirname};
-use File::Path qw{make_path};
-use Crypt::ScryptKDF qw(scrypt_hash);
-use Data::Dumper qw(Dumper);
-use Scalar::Util qw(looks_like_number);
-use Getopt::Long;
-
-use InstallUtils qw{ :all };
-use GenerateCert qw{ :all };
-use Database qw{ connect };
-
-# paths of the output configuration files
-my $databaseConfFile = "/opt/traffic_ops/app/conf/production/database.conf";
-my $dbConfFile       = "/opt/traffic_ops/app/db/dbconf.yml";
-my $cdnConfFile      = "/opt/traffic_ops/app/conf/cdn.conf";
-my $ldapConfFile     = "/opt/traffic_ops/app/conf/ldap.conf";
-my $usersConfFile    = "/opt/traffic_ops/install/data/json/users.json";
-my $profilesConfFile = "/opt/traffic_ops/install/data/profiles/";
-my $opensslConfFile  = 
"/opt/traffic_ops/install/data/json/openssl_configuration.json";
-my $paramConfFile    = "/opt/traffic_ops/install/data/json/profiles.json";
-
-my $custom_profile_dir = $profilesConfFile . "custom";
-
-# stores parameters for traffic ops config
-my $parameters;
-
-# location of traffic ops profiles
-my $profileDir       = "/opt/traffic_ops/install/data/profiles/";
-my $post_install_cfg = "/opt/traffic_ops/install/data/json/post_install.json";
-
-# log file for the installer
-my $logFile = "/var/log/traffic_ops/postinstall.log";
-
-# debug mode
-my $debug = 1;
-
-# log file for cpan output
-my $cpanLogFile = "/var/log/traffic_ops/cpan.log";
-
-# maximum size the uncompressed log file should be before rotating it - 
rotating it copies the current log
-#  file to the same name appended with .bkp replacing the old backup if any is 
there
-my $maxLogSize = 10000000;    #bytes
-
-# whether to create a config file with default values
-my $dumpDefaults;
-
-# configuration file output with answers which can be used as input to 
postinstall
-my $outputConfigFile = "/opt/traffic_ops/install/bin/configuration_file.json";
-
-my $inputFile = "";
-my $automatic = 0;
-my %defaultInputs;
-
-# given a var to the hash of config_var and question, will return the question
-sub getConfigQuestion {
-    my $var = shift;
-    foreach my $key ( keys %{ $var } ) {
-        if ( $key ne "hidden" && $key ne "config_var" ) {
-            return $key;
-        }
-    }
-}
-
-# question: The question given in the config file
-# config_answer: The answer given in the config file - if no config file given 
will be defaultInput
-# hidden: Whether or not the answer should be hidden from the terminal and 
logs, ex. passwords
-#
-# Determines if the script is being run in complete interactive mode and 
prompts user - otherwise
-#  returns answer to question in config or defaults
-
-sub getField {
-    my $question      = shift;
-    my $config_answer = shift;
-    my $hidden        = shift;
-
-    # if there is no config file and not in automatic mode prompt for all 
questions with default answers
-    if ( !$inputFile && !$automatic ) {
-
-        # if hidden then dont show password in terminal
-        if ($hidden) {
-            return InstallUtils::promptPasswordVerify($question);
-        }
-        else {
-            return InstallUtils::promptUser( $question, $config_answer );
-        }
-    }
-
-    return $config_answer;
-}
-
-# userInput: The entire input config file which is either user input or the 
defaults
-# fileName: The name of the output config file given by the input config file
-#
-# Loops through an input config file and determines answers to each question 
using getField
-#  and returns the hash of answers
-
-sub getConfig {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my %config;
-
-    if ( !defined $userInput{$fileName} ) {
-        InstallUtils::logger( "No $fileName found in config", "error" );
-    }
-
-    InstallUtils::logger( "===========$fileName===========", "info" );
-
-    foreach my $var ( @{ $userInput{$fileName} } ) {
-        my $question = getConfigQuestion($var);
-        my $hidden   = $var->{"hidden"} if ( exists $var->{"hidden"} );
-        my $answer   = $config{ $var->{"config_var"} } = getField( $question, 
$var->{$question}, $hidden );
-
-        $config{ $var->{"config_var"} } = $answer;
-        if ( !$hidden ) {
-            InstallUtils::logger( "$question: $answer", "info" );
-        }
-    }
-    return %config;
-}
-
-# userInput: The entire input config file which is either user input or the 
defaults
-# dbFileName: The filename of the output config file for the database
-# toDBFileName: The filename of the output config file for the Traffic Ops 
database
-#
-# Generates a config file for the database based on the questions and answers 
in the input config file
-
-sub generateDbConf {
-    my %userInput = %{$_[0]}; shift;
-    my $dbFileName   = shift;
-    my $toDBFileName = shift;
-
-    my %dbconf = getConfig( \%userInput, $dbFileName );
-    $dbconf{"description"} = "$dbconf{type} database on 
$dbconf{hostname}:$dbconf{port}";
-    make_path( dirname($dbFileName), { mode => 0755 } );
-    InstallUtils::writeJson( $dbFileName, \%dbconf );
-    InstallUtils::logger( "Database configuration has been saved", "info" );
-
-    # broken out into separate file/config area
-    my %todbconf = getConfig( \%userInput, $toDBFileName );
-
-    # Check if the Postgres db is used and set the driver to be "postgres"
-    my $dbDriver = $dbconf{type};
-    if ( $dbconf{type} eq "Pg" ) {
-        $dbDriver = "postgres";
-    }
-
-    # No YAML library installed, but this is a simple file..
-    open( my $fh, '>', $toDBFileName ) or errorOut("Can't write to 
$toDBFileName!");
-    print $fh "production:\n";
-    print $fh "    driver: $dbDriver\n";
-    print $fh "    open: host=$dbconf{hostname} port=$dbconf{port} 
user=$dbconf{user} password=$dbconf{password} dbname=$dbconf{dbname} 
sslmode=disable\n";
-    close $fh;
-
-    return \%todbconf;
-}
-
-# userInput: The entire input config file which is either user input or the 
defaults
-# fileName: The filename of the output config file
-#
-# Generates a config file for the CDN
-
-sub generateCdnConf {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my %cdnConfiguration = getConfig( \%userInput, $fileName );
-
-    # First, read existing one -- already loaded with a bunch of stuff
-    my $cdnConf;
-    if ( -f $fileName ) {
-        $cdnConf = InstallUtils::readJson($fileName) or errorOut("Error 
loading $fileName: $@");
-    }
-    if ( lc $cdnConfiguration{genSecret} =~ /^y(?:es)?/ ) {
-        my @secrets;
-        my $newSecret = InstallUtils::randomWord();
-
-        if (defined($cdnConf->{secrets})) {
-            @secrets   = @{ $cdnConf->{secrets} };
-            $cdnConf->{secrets} = \@secrets;
-            InstallUtils::logger( "Secrets found in cdn.conf file", "debug" );
-        } else {
-            $cdnConf->{secrets} = \@secrets;
-            InstallUtils::logger( "No secrets found in cdn.conf file", "debug" 
);
-        }
-        unshift @secrets, InstallUtils::randomWord();
-        if ( $cdnConfiguration{keepSecrets} > 0 && $#secrets > 
$cdnConfiguration{keepSecrets} - 1 ) {
-
-            # Shorten the array to requested length
-            $#secrets = $cdnConfiguration{keepSecrets} - 1;
-        }
-    }
-    if (exists $cdnConfiguration{base_url}) {
-        $cdnConf->{to}{base_url} = $cdnConfiguration{base_url};
-    }
-    if (exists $cdnConfiguration{port}) {
-        $cdnConf->{"traffic_ops_golang"}{port} = $cdnConfiguration{port};
-    }
-    $cdnConf->{"traffic_ops_golang"}{"log_location_error"} = 
"/var/log/traffic_ops/error.log";
-    $cdnConf->{"traffic_ops_golang"}{"log_location_event"} = 
"/var/log/traffic_ops/access.log";
-
-    #InstallUtils::logger("cdnConf: " . Dumper($cdnConf), "info" );
-    InstallUtils::writeJson( $fileName, $cdnConf );
-    InstallUtils::logger( "CDN configuration has been saved", "info" );
-}
-
-sub hash_pass {
-       my $pass = shift;
-       return scrypt_hash($pass, \64, 16384, 8, 1, 64);
-}
-
-# userInput: The entire input config file which is either user input or the 
defaults
-# fileName: The filename of the output config file
-#
-# Generates an LDAP config file
-
-sub generateLdapConf {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-    my %ldapInput = %{@{$userInput{$fileName}}[0]};
-    my $useLdap = $ldapInput{"Do you want to set up LDAP?"};
-
-    if ( !lc $useLdap =~ /^y(?:es)?/ ) {
-        InstallUtils::logger( "Not setting up ldap", "info" );
-        return;
-    }
-
-    my %ldapConf = getConfig( \%userInput, $fileName );
-    # convert any deprecated keys to the correct key name
-    my %keys_converted = ( password => 'admin_pass', hostname => 'host' );
-    for my $key (keys %ldapConf) {
-        if ( exists $keys_converted{$key} ) {
-            $ldapConf{ $keys_converted{$key} } = delete $ldapConf{$key};
-        }
-    }
-
-    my @requiredKeys = qw{ host admin_dn admin_pass search_base search_query 
insecure ldap_timeout_secs };
-    for my $k (@requiredKeys) {
-        if (! exists $ldapConf{$k} ) {
-            errorOut("$k is a required key in $fileName");
-        }
-    }
-
-    delete $ldapConf{setupLdap};
-
-    # do a very loose check of form -- 'host' must be hostname:port
-    if ( $ldapConf{ host } !~ /^\S+:\d+$/ ) {
-        errorOut("host in $fileName must be of form 'hostname:port'");
-    }
-
-    make_path( dirname($fileName), { mode => 0755 } );
-    InstallUtils::writeJson( $fileName, \%ldapConf );
-}
-
-sub generateUsersConf {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my %user = ();
-    my %config = getConfig( \%userInput, $fileName );
-
-    $user{username} = $config{tmAdminUser};
-    $user{password} = hash_pass( $config{tmAdminPw} );
-
-    InstallUtils::writeJson( $fileName, \%user );
-    $user{password} = $config{tmAdminPw};
-    return \%user;
-}
-
-sub generateProfilesDir {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my $userIn = $userInput{$fileName};
-}
-
-sub generateOpenSSLConf {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my %config = getConfig( \%userInput, $fileName );
-    return \%config;
-}
-
-sub generateParamConf {
-    my %userInput = %{$_[0]}; shift;
-    my $fileName  = shift;
-
-    my %config = getConfig( \%userInput, $fileName );
-    InstallUtils::writeJson( $fileName, \%config );
-    return \%config;
-}
-
-# check default values for missing config_var parameter
-sub sanityCheckDefaults {
-    foreach my $file ( ( keys %defaultInputs ) ) {
-        foreach my $defaultValue ( @{ $defaultInputs{$file} } ) {
-            my $question = getConfigQuestion(\%$defaultValue);
-
-            my %defaultValueHash = %$defaultValue;
-            if ( !defined $defaultValueHash{"config_var"}
-                || $defaultValueHash{"config_var"} eq "" )
-            {
-                errorOut("Question '$question' in file '$file' has no 
config_var");
-            }
-        }
-    }
-}
-
-# userInput: The entire input config file which is either user input or the 
defaults
-#
-# Checks the input config file against the default inputs. If there is a 
question located in the default inputs which
-#  is not located in the input config file it will output a warning message.
-
-sub sanityCheckConfig {
-    my %userInput = %{$_[0]}; shift;
-    my $diffs     = 0;
-
-    foreach my $file ( ( keys %defaultInputs ) ) {
-        if ( !defined $userInput{$file} ) {
-            InstallUtils::logger( "File '$file' found in defaults but not 
config file", "warn" );
-            @{$userInput{$file}} = [];
-        }
-
-        foreach my $defaultValue ( @{ $defaultInputs{$file} } ) {
-
-            my $found = 0;
-            foreach my $configValue ( @{ $userInput{$file} } ) {
-                if ( $defaultValue->{"config_var"} eq 
$configValue->{"config_var"} ) {
-                    $found = 1;
-                }
-            }
-
-            # if the question is not found in the config file add it from 
defaults
-            if ( !$found ) {
-                my $question = getConfigQuestion($defaultValue);
-                InstallUtils::logger( "Question '$question' found in defaults 
but not in '$file'", "warn" );
-
-                my %temp;
-                my $answer;
-                my $hidden = exists $defaultValue->{"hidden"} && 
$defaultValue->{"hidden"} ? 1 : 0;
-
-                # in automatic mode add the missing question with default 
answer
-                if ($automatic) {
-                    $answer = $defaultValue->{$question};
-                    InstallUtils::logger( "Adding question '$question' with 
default answer " . ( $hidden ? "" : "'$answer'" ), "info" );
-                }
-
-                # in interactive mode prompt the user for answer to missing 
question
-                else {
-                    InstallUtils::logger( "Prompting user for answer", "info" 
);
-                    if ($hidden) {
-                        $answer = 
InstallUtils::promptPasswordVerify($question);
-                    }
-                    else {
-                        $answer = InstallUtils::promptUser( $question, 
$defaultValue->{$question} );
-                    }
-                }
-
-                %temp = (
-                    "config_var" => $defaultValue->{"config_var"},
-                    $question    => $answer
-                );
-
-                if ($hidden) {
-                    $temp{"hidden"} .= "true";
-                }
-
-                push @{ $userInput{$file} }, \%temp;
-
-                $diffs++;
-            }
-        }
-    }
-
-    InstallUtils::logger( "File sanity check complete - found $diffs 
difference" . ( $diffs == 1 ? "" : "s" ), "info" );
-}
-
-# A function which returns the default inputs data structure. These questions 
and answers will be used if there is no
-#  user input config file or if there are questions in the input config file 
which do not have answers
-
-sub getDefaults {
-    return (
-        $databaseConfFile => [
-            {
-                "Database type" => "Pg",
-                "config_var"    => "type"
-            },
-            {
-                "Database name" => "traffic_ops",
-                "config_var"    => "dbname"
-            },
-            {
-                "Database server hostname IP or FQDN" => "localhost",
-                "config_var"                          => "hostname"
-            },
-            {
-                "Database port number" => "5432",
-                "config_var"           => "port"
-            },
-            {
-                "Traffic Ops database user" => "traffic_ops",
-                "config_var"                => "user"
-            },
-            {
-                "Password for Traffic Ops database user" => "",
-                "config_var"                             => "password",
-                "hidden"                                 => "true"
-            }
-        ],
-        $dbConfFile => [
-            {
-                "Database server root (admin) user" => "postgres",
-                "config_var"                        => "pgUser"
-            },
-            {
-                "Password for database server admin" => "",
-                "config_var"                         => "pgPassword",
-                "hidden"                             => "true"
-            }
-        ],
-        $cdnConfFile => [
-            {
-                "Generate a new secret?" => "yes",
-                "config_var"             => "genSecret"
-            },
-            {
-                "Number of secrets to keep?" => "1",
-                "config_var"                 => "keepSecrets"
-            },
-            {
-                "Port to serve on?"          => "443",
-                "config_var"                 => "port"
-            },
-            {
-                "Number of workers?" => "12",
-                "config_var"         => "workers"
-            },
-            {
-                "Traffic Ops url?"   => "http://localhost:3000";,
-                "config_var"         => "base_url"
-            },
-            {
-                "ldap.conf location? (default is 
/opt/traffic_ops/app/conf/ldap.conf)" => "",
-                "config_var"         => "ldap_conf_location"
-            }
-        ],
-        $ldapConfFile => [
-            {
-                "Do you want to set up LDAP?" => "no",
-                "config_var"                  => "setupLdap"
-            },
-            {
-                "LDAP server hostname" => "",
-                "config_var"           => "host"
-            },
-            {
-                "LDAP Admin DN" => "",
-                "config_var"    => "admin_dn"
-            },
-            {
-                "LDAP Admin Password" => "",
-                "config_var"          => "admin_pass",
-                "hidden"              => "true"
-            },
-            {
-                "LDAP Search Base" => "",
-                "config_var"       => "search_base"
-            },
-            {
-                "LDAP Search Query" => "",
-                "config_var"       => "search_query"
-            },
-            {
-                "LDAP Skip TLS verify" => "",
-                "config_var"       => "insecure"
-            },
-            {
-                "LDAP Timeout Seconds" => "",
-                "config_var"       => "ldap_timeout_secs"
-            }
-        ],
-        $usersConfFile => [
-            {
-                "Administration username for Traffic Ops" => "admin",
-                "config_var"                              => "tmAdminUser"
-            },
-            {
-                "Password for the admin user" => "",
-                "config_var"                  => "tmAdminPw",
-                "hidden"                      => "true"
-            }
-        ],
-        $profilesConfFile => [
-            {
-                "Add custom profiles?" => "no",
-                "config_var"           => "custom_profiles"
-            }
-        ],
-        $opensslConfFile => [
-            {
-                "Do you want to generate a certificate?" => "yes",
-                "config_var"                             => "genCert"
-            },
-            {
-                "Country Name (2 letter code)" => "",
-                "config_var"                   => "country"
-            },
-            {
-                "State or Province Name (full name)" => "",
-                "config_var"                         => "state"
-            },
-            {
-                "Locality Name (eg, city)" => "",
-                "config_var"               => "locality"
-            },
-            {
-                "Organization Name (eg, company)" => "",
-                "config_var"                      => "company"
-            },
-            {
-                "Organizational Unit Name (eg, section)" => "",
-                "config_var"                             => "org_unit"
-            },
-            {
-                "Common Name (eg, your name or your server's hostname)" => "",
-                "config_var"                                            => 
"common_name"
-            },
-            {
-                "RSA Passphrase" => "CHANGEME!!",
-                "config_var"     => "rsaPassword",
-                "hidden"         => "true"
-            }
-        ],
-        $paramConfFile => [
-            {
-                "Traffic Ops url" => "https://localhost";,
-                "config_var"      => "tm.url"
-            },
-            {
-                "Human-readable CDN Name.  (No whitespace, please)" => 
"kabletown_cdn",
-                "config_var"                                        => 
"cdn_name"
-            },
-            {
-                "DNS sub-domain for which your CDN is authoritative" => 
"cdn1.kabletown.net",
-                "config_var"                                         => 
"dns_subdomain"
-            }
-        ],
-    );
-}
-
-# carried over from old postinstall
-#
-# todbconf: The database configuration to be used
-# opensslconf: The openssl configuration if any
-
-sub setupDatabaseData {
-    my $dbh = shift;
-    my $adminconf = shift;
-    my $paramconf = shift;
-    InstallUtils::logger( "paramconf " . Dumper($paramconf), "info" );
-
-    my $result;
-
-    my $q = <<"QUERY";
-    select exists(select 1 from pg_tables where schemaname = 'public' and 
tablename = 'tm_user')
-QUERY
-
-    my $stmt = $dbh->prepare($q);
-    $stmt->execute();
-
-    InstallUtils::logger( "Setting up the database data", "info" );
-    my $tables_found;
-    while ( my $row = $stmt->fetch() ) {
-       $tables_found = $row->[0];
-    }
-    if ($tables_found) {
-       InstallUtils::logger( "Found existing tables skipping table creation", 
"info" );
-    } else  {
-       invoke_db_admin_pl("load_schema");
-    }
-    invoke_db_admin_pl("migrate");
-    invoke_db_admin_pl("seed");
-    invoke_db_admin_pl("patch");
-
-    # Skip the insert if the admin 'username' is already there.
-    my $hashed_passwd = hash_pass( $adminconf->{"password"} );
-    my $insert_admin = <<"ADMIN";
-    insert into tm_user (username, tenant_id, role, local_passwd, 
confirm_local_passwd)
-                values  ('$adminconf->{"username"}',
-                        (select id from tenant where name = 'root'),
-                        (select id from role where name = 'admin'),
-                         '$hashed_passwd',
-                        '$hashed_passwd' )
-                        ON CONFLICT (username) DO NOTHING;
-ADMIN
-    $dbh->do($insert_admin);
-
-    insert_cdn($dbh, $paramconf);
-    insert_parameters($dbh, $paramconf);
-    insert_profiles($dbh, $paramconf);
-
-
-}
-
-sub invoke_db_admin_pl {
-    my $action    = shift;
-
-    chdir("/opt/traffic_ops/app");
-    my $result = InstallUtils::execCommand( "db/admin", "--env=production", 
$action );
-
-    if ( $result != 0 ) {
-        errorOut("Database $action failed");
-    }
-    else {
-        InstallUtils::logger( "Database $action succeeded", "info" );
-    }
-
-    return $result;
-}
-
-sub setupCertificates {
-    my $opensslconf      = shift;
-
-    my $result;
-
-    if ( lc $opensslconf->{"genCert"} =~ /^y(?:es)?/ ) {
-        if ( -x "/usr/bin/openssl" ) {
-            InstallUtils::logger( "Installing SSL Certificates", "info" );
-            $result = GenerateCert::createCert($opensslconf);
-
-            if ( $result != 0 ) {
-                errorOut("SSL Certificate Installation failed");
-            }
-            else {
-                InstallUtils::logger( "SSL Certificates have been installed", 
"info" );
-            }
-        }
-        else {
-            InstallUtils::logger( "Unable to install SSL certificates as 
openssl is not installed",                                     "error" );
-            InstallUtils::logger( "Install openssl and then run 
/opt/traffic_ops/install/bin/generateCert to install SSL certificates", "error" 
);
-            exit 4;
-        }
-    }
-    else {
-        InstallUtils::logger( "Not generating openssl certification", "info" );
-    }
-}
-
-#------------------------------------
-sub insert_cdn {
-
-    my $dbh = shift;
-    my $paramconf = shift;
-
-    InstallUtils::logger( "=========== Setting up cdn", "info" );
-
-    # Enable multiple inserts into one commit
-    $dbh->{pg_server_prepare} = 0;
-
-       my $cdn_name = $paramconf->{"cdn_name"};
-       my $dns_subdomain = $paramconf->{"dns_subdomain"};
-
-    my $insert_stmt = <<INSERTS;
-
-    -- global parameters
-    insert into cdn (name, domain_name, dnssec_enabled)
-                values ('$cdn_name', '$dns_subdomain', false)
-                ON CONFLICT (name) DO NOTHING;
-
-INSERTS
-    doInsert($dbh, $insert_stmt);
-}
-
-#------------------------------------
-sub insert_parameters {
-    my $dbh = shift;
-    my $paramconf = shift;
-
-    InstallUtils::logger( "=========== Setting up parameters", "info" );
-
-    # Enable multiple inserts into one commit
-    $dbh->{pg_server_prepare} = 0;
-
-       my $tm_url = $paramconf->{"tm.url"};
-
-    my $insert_stmt = <<INSERTS;
-    -- global parameters
-    insert into parameter (name, config_file, value)
-                values ('tm.url', 'global', '$tm_url')
-                ON CONFLICT (name, config_file, value) DO NOTHING;
-
-    insert into parameter (name, config_file, value)
-                values ('tm.infourl', 'global', '$tm_url/doc')
-                ON CONFLICT (name, config_file, value) DO NOTHING;
-
-    -- CRConfig.json parameters
-    insert into parameter (name, config_file, value)
-                values ('geolocation.polling.url', 'CRConfig.json', 
'$tm_url/routing/GeoLite2-City.mmdb.gz')
-                ON CONFLICT (name, config_file, value) DO NOTHING;
-
-    insert into parameter (name, config_file, value)
-                values ('geolocation6.polling.url', 'CRConfig.json', 
'$tm_url/routing/GeoLiteCityv6.dat.gz')
-                ON CONFLICT (name, config_file, value) DO NOTHING;
-
-INSERTS
-    doInsert($dbh, $insert_stmt);
-}
-
-#------------------------------------
-sub insert_profiles {
-    my $dbh = shift;
-    my $paramconf = shift;
-
-    InstallUtils::logger( "\n=========== Setting up profiles", "info" );
-       my $tm_url = $paramconf->{"tm.url"};
-
-    my $insert_stmt = <<INSERTS;
-
-    -- global parameters
-    insert into profile (name, description, type, cdn)
-                values ('GLOBAL', 'Global Traffic Ops profile, DO NOT DELETE', 
'UNK_PROFILE',  (SELECT id FROM cdn WHERE name='ALL'))
-                ON CONFLICT (name) DO NOTHING;
-
-    insert into profile_parameter (profile, parameter)
-                values ( (select id from profile where name = 'GLOBAL'), 
(select id from parameter where name = 'tm.url' and config_file = 'global' and 
value = '$tm_url') )
-                ON CONFLICT (profile, parameter) DO NOTHING;
-
-    insert into profile_parameter (profile, parameter)
-                values ( (select id from profile where name = 'GLOBAL'), 
(select id from parameter where name = 'tm.infourl' and config_file = 'global' 
and value = '$tm_url/doc') )
-                ON CONFLICT (profile, parameter) DO NOTHING;
-
-    insert into profile_parameter (profile, parameter)
-                values ( (select id from profile where name = 'GLOBAL'), 
(select id from parameter where name = 'geolocation.polling.url' and 
config_file = 'CRConfig.json' and value = 
'$tm_url/routing/GeoLite2-City.mmdb.gz') )
-                ON CONFLICT (profile, parameter) DO NOTHING;
-
-    insert into profile_parameter (profile, parameter)
-                values ( (select id from profile where name = 'GLOBAL'), 
(select id from parameter where name = 'geolocation6.polling.url' and 
config_file = 'CRConfig.json' and value = 
'$tm_url/routing/GeoLiteCityv6.dat.gz') )
-                ON CONFLICT (profile, parameter) DO NOTHING;
-
-INSERTS
-    doInsert($dbh, $insert_stmt);
-}
-
-#------------------------------------
-sub doInsert {
-    my $dbh = shift;
-    my $insert_stmt = shift;
-
-    InstallUtils::logger( "\n" . $insert_stmt, "info" );
-    my $stmt = $dbh->prepare($insert_stmt);
-    $stmt->execute();
-}
-
-
-
-# -cfile     - Input File:       The input config file used to ask and answer 
questions
-# -a         - Automatic mode:   If there are questions in the config file 
which do not have answers, the script
-#                                will look to the defaults for the answer. If 
the answer is not in the defaults
-#                                the script will exit
-# -defaults  - Defaults:         Writes out a configuration file with defaults 
which can be used as input
-# -debug     - Debug Mode:       More output to the terminal
-# -h         - Help:             Basic command line help menu
-
-sub main {
-    my $help = 0;
-
-    # help string
-    my $usageString = "Usage: postinstall [-a] [-debug] [-defaults[=<outfile]] 
[-r] -cfile=[config_file]\n";
-
-    GetOptions(
-        "cfile=s"     => \$inputFile,
-        "automatic"   => \$automatic,
-        "defaults:s"  => \$dumpDefaults,
-        "debug"       => \$debug,
-        "help"        => \$help
-    ) or die($usageString);
-
-    # stores the default questions and answers
-    %defaultInputs = getDefaults();
-
-    if ($help) {
-        print $usageString;
-        return;
-    }
-
-    # check if the user running postinstall is root
-    if ( $> != 0 ) {
-        errorOut("You must run this script as the root user");
-    }
-
-    InstallUtils::initLogger( $debug, $logFile );
-
-    print("unzipping log\n");
-    if ( -f "$logFile.gz" ) {
-        InstallUtils::execCommand( "/bin/gunzip", "-f", "$logFile.gz" );
-    }
-
-    InstallUtils::logger( "Starting postinstall", "info" );
-
-    InstallUtils::logger( "Debug is on", "info" );
-
-    if ($automatic) {
-        InstallUtils::logger( "Running in automatic mode", "info" );
-    }
-
-    if (defined $dumpDefaults) {
-        # -defaults flag provided.
-        if ($dumpDefaults ne "") {
-           # -defaults=<filename>  -- if -defaults without a file name, use 
the default.
-           # dumpDefaults with value -- use that as output file name
-           $outputConfigFile = $dumpDefaults;
-        }
-        InstallUtils::logger( "Writing default configuration to 
$outputConfigFile", "info" );
-        InstallUtils::writeJson( $outputConfigFile, %defaultInputs );
-        return;
-    }
-
-    InstallUtils::rotateLog($cpanLogFile);
-
-    if ( -s $logFile > $maxLogSize ) {
-        InstallUtils::logger( "Postinstall log above max size of $maxLogSize 
bytes - rotating", "info" );
-        rotateLog($logFile);
-    }
-
-    # used to store the questions and answers provided by the user
-    my %userInput;
-
-    # if no input file provided use the defaults
-    if ( $inputFile eq "" ) {
-        InstallUtils::logger( "No input file given - using defaults", "info" );
-        %userInput = %defaultInputs;
-    }
-    else {
-        InstallUtils::logger( "Using input file $inputFile", "info" );
-
-        # check if the input file exists
-        errorOut("File '$inputFile' not found") if ( !-f $inputFile );
-
-        # read and store the input file
-        %userInput = %{InstallUtils::readJson($inputFile)};
-    }
-
-    # sanity check the defaults if running them automatically
-    sanityCheckDefaults();
-
-    # check the input config file against the defaults to check for missing 
questions
-    sanityCheckConfig(\%userInput) if ( $inputFile ne "" );
-
-    chdir("/opt/traffic_ops/install/bin");
-
-    # The generator functions handle checking input/default/automatic mode
-    # todbconf will be used later when setting up the database
-    my $todbconf = generateDbConf( \%userInput, $databaseConfFile, $dbConfFile 
);
-    generateLdapConf( \%userInput, $ldapConfFile );
-    my $adminconf = generateUsersConf( \%userInput, $usersConfFile );
-    my $custom_profile = generateProfilesDir( \%userInput, $profilesConfFile );
-    my $opensslconf = generateOpenSSLConf( \%userInput, $opensslConfFile );
-    my $paramconf = generateParamConf( \%userInput, $paramConfFile );
-
-    if ( !-f $post_install_cfg ) {
-        InstallUtils::writeJson( $post_install_cfg, {} );
-    }
-
-    setupCertificates( $opensslconf );
-    generateCdnConf( \%userInput, $cdnConfFile );
-
-    my $dbh = Database::connect($databaseConfFile, $todbconf);
-    if (!$dbh) {
-        InstallUtils::logger("Can't connect to the database.  Use the script 
`/opt/traffic_ops/install/bin/todb_bootstrap.sh` on the db server to create it 
and run `postinstall` again.", "error");
-        exit(-1);
-    }
-
-    setupDatabaseData( $dbh, $adminconf, $paramconf );
-
-    InstallUtils::logger("Starting Traffic Ops", "info" );
-    InstallUtils::execCommand("/sbin/service traffic_ops restart");
-
-    InstallUtils::logger("Waiting for Traffic Ops to restart", "info" );
-
-    InstallUtils::logger("Success! Postinstall complete.");
-
-    #InstallUtils::logger("Zipping up $logFile to $logFile.gz");
-    #InstallUtils::execCommand( "/bin/gzip", "$logFile" );
-
-   # Success!
-    $dbh->disconnect();
-}
-
-main;
-
-# vi:syntax=perl
diff --git a/traffic_ops/install/bin/_postinstall.py 
b/traffic_ops/install/bin/_postinstall.py
index 192eaf516a..62c5309360 100755
--- a/traffic_ops/install/bin/_postinstall.py
+++ b/traffic_ops/install/bin/_postinstall.py
@@ -1,23 +1,4 @@
 #!/usr/bin/env python3
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# There's a bug in asteroid with Python 3.9's NamedTuple being
-# recognized for the dynamically generated class that it is. Should be fixed
-# with the next release, but 'til then...
-#pylint:disable=inherit-non-class
-from __future__ import print_function
-
 """
 This script is meant as a drop-in replacement for the old _postinstall.pl Perl 
script.
 
@@ -45,6 +26,23 @@ testing.
 >>> [c for c in [[a for a in b if not a.config_var] for b in 
 >>> DEFAULTS.values()] if c]
 []
 """
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# There's a bug in asteroid with Python 3.9's NamedTuple being
+# recognized for the dynamically generated class that it is. Should be fixed
+# with the next release, but 'til then...
+
 import argparse
 import base64
 import errno
@@ -63,8 +61,8 @@ import string
 import subprocess
 import sys
 
-from collections import namedtuple
 from struct import unpack, pack
+from typing import Any, Dict, List, Optional, Union, NamedTuple
 
 # Paths for output configuration files
 DATABASE_CONF_FILE = "/opt/traffic_ops/app/conf/production/database.conf"
@@ -95,14 +93,9 @@ POST_INSTALL_CFG = 
"/opt/traffic_ops/install/data/json/post_install.json"
 # Python, instead, outputs to stdout. This is breaking, but more flexible. 
Change it?
 # OUTPUT_CONFIG_FILE = "/opt/traffic_ops/install/bin/configuration_file.json"
 
-if sys.version_info.major >= 3:
-       # Accepting a string for json.dump()'s `indent` keyword argument is a 
Python 3 feature
-       indent = "\t"  # type: str
-else:
-       indent = 4 #  type: int
-       str = unicode  # type: type[unicode]
+INDENT = "\t"
 
-class Question(object):
+class Question:
        """
        Question represents a single question to be asked of the user, to 
determine a configuration
        value.
@@ -111,25 +104,25 @@ class Question(object):
        Question(question='question', default='answer', config_var='var', 
hidden=False)
        """
 
-       def __init__(self, question, default, config_var, hidden = False): # 
type: (str, str, str, bool) -> None
+       def __init__(self, question: str, default: str, config_var: str, 
hidden: bool = False) -> None:
                self.question = question
                self.default = default
                self.config_var = config_var
                self.hidden = hidden
 
-       def __str__(self): # type: () -> str
+       def __str__(self) -> str:
                if self.default:
-                       return "{question} [{default}]: 
".format(question=self.question, default=self.default)
-               return "{question}: ".format(question=self.question)
+                       return f"{self.question} [{self.default}]: "
+               return f"{self.question}: "
 
-       def __repr__(self): # type: () -> str
+       def __repr__(self) -> str:
                qstn = self.question
                ans = self.default
                cfgvr = self.config_var
                hddn = self.hidden
-               return "Question(question='{qstn}', default='{ans}', 
config_var='{cfgvr}', hidden={hddn})".format(qstn=qstn, ans=ans, cfgvr=cfgvr, 
hddn=hddn)
+               return f"Question(question='{qstn}', default='{ans}', 
config_var='{cfgvr}', hidden={hddn})"
 
-       def ask(self): # type: () -> str
+       def ask(self) -> str:
                """
                Asks the user the Question interactively.
 
@@ -140,13 +133,13 @@ class Question(object):
                                passwd = getpass.getpass(str(self))
                                if not passwd:
                                        continue
-                               if passwd == getpass.getpass("Re-Enter 
{question}: ".format(question=self.question)):
+                               if passwd == getpass.getpass(f"Re-Enter 
{self.question}: "):
                                        return passwd
                                print("Error: passwords do not match, try 
again")
                ipt = input(self)
                return ipt if ipt else self.default
 
-       def to_json(self): # type: () -> str
+       def to_json(self) -> str:
                """
                Converts a question to JSON encoding.
 
@@ -159,15 +152,16 @@ class Question(object):
                ans = self.default
                cfgvr = self.config_var
                if self.hidden:
-                       return '{{"{qstn}": "{ans}", "config_var": "{cfgvr}", 
"hidden": true}}'.format(qstn=qstn, ans=ans, cfgvr=cfgvr)
-               return '{{"{qstn}": "{ans}", "config_var": 
"{cfgvr}"}}'.format(qstn=qstn, ans=ans, cfgvr=cfgvr)
+                       return f'{{"{qstn}": "{ans}", "config_var": "{cfgvr}", 
"hidden": true}}'
+               return f'{{"{qstn}": "{ans}", "config_var": "{cfgvr}"}}'
 
-       def serialize(self): # type: () -> object
+       def serialize(self) -> Dict[str, Union[str, bool]]:
                """Returns a serializable dictionary, suitable for converting 
to JSON."""
                return {self.question: self.default, "config_var": 
self.config_var, "hidden": self.hidden}
 
-class User(namedtuple('User', ['username', 'password'])):
-       """Users represents a user that will be inserted into the Traffic Ops 
database.
+class User(NamedTuple):
+       """
+       A User represents a user that will be inserted into the Traffic Ops 
database.
 
        Attributes
        ----------
@@ -176,21 +170,29 @@ class User(namedtuple('User', ['username', 'password'])):
        self.password: str
                The user's password - IN PLAINTEXT.
        """
+       username: str
+       password: str
 
 class SSLConfig:
        """SSLConfig bundles the options for generating new (self-signed) SSL 
certificates"""
 
-       def __init__(self, gen_cert, cfg_map): # type: (bool, dict[str, str]) 
-> None
-
+       def __init__(self, gen_cert: bool, cfg_map: Dict[str, str]) -> None:
                self.gen_cert = gen_cert
                self.rsa_password = cfg_map["rsaPassword"]
                self.params = 
"/C={country}/ST={state}/L={locality}/O={company}/OU={org_unit}/CN={common_name}/"
                self.params = self.params.format(**cfg_map)
 
-class CDNConfig(namedtuple('CDNConfig', ['gen_secret', 'num_secrets', 'port', 
'num_workers', 'url', 'ldap_conf_location'])):
+class CDNConfig(NamedTuple):
        """CDNConfig holds all of the options needed to format a cdn.conf 
file."""
 
-       def generate_secret(self, conf):
+       gen_secret: bool
+       num_secrets: int
+       port: str
+       num_workers: int
+       url: str
+       ldap_conf_location: str
+
+       def generate_secret(self, conf: Dict[Any, Any]):
                """
                Generates new secrets - if configured to do so - and adds them 
to the passed cdn.conf
                configuration.
@@ -198,18 +200,19 @@ class CDNConfig(namedtuple('CDNConfig', ['gen_secret', 
'num_secrets', 'port', 'n
                if not self.gen_secret:
                        return
 
-               if isinstance(conf, dict) and "secrets" in conf and 
isinstance(conf["secrets"], list):
+               if "secrets" in conf and isinstance(conf["secrets"], list):
                        logging.debug("Secrets found in cdn.conf file")
                else:
                        conf["secrets"] = []
                        logging.debug("No secrets found in cdn.conf file")
 
-               conf["secrets"].insert(0, random_word())
+               secrets: List[str] = conf["secrets"]
+               secrets.insert(0, random_word())
 
-               if self.num_secrets and len(conf["secrets"]) > self.num_secrets:
-                       conf["secrets"] = conf["secrets"][:self.num_secrets - 1]
+               if self.num_secrets and len(secrets) > self.num_secrets:
+                       conf["secrets"] = secrets[:self.num_secrets - 1]
 
-       def insert_url(self, conf):
+       def insert_url(self, conf: Dict[Any, Any]):
                """
                Inserts the configured URL - if it is not an empty string - 
into the passed cdn.conf
                configuration, in to.base_url.
@@ -293,8 +296,7 @@ class ConfigEncoder(json.JSONEncoder):
        '{"/test/file": [{"question": "default", "config_var": "cfg_var", 
"hidden": true}]}'
        """
 
-       # The linter is just wrong about this
-       def default(self, o): # type: (object) -> object
+       def default(self, o: Any) -> Any:
                """
                Returns a serializable representation of 'o'.
 
@@ -307,12 +309,12 @@ class ConfigEncoder(json.JSONEncoder):
 
                return json.JSONEncoder.default(self, o)
 
-def get_config(questions, fname, automatic = False): # type: (list[Question], 
str, bool) -> dict[str, str]
+def get_config(questions: List[Question], fname: str, automatic: bool = False) 
-> Dict[str, str]:
        """Asks all provided questions, or uses their defaults in automatic 
mode"""
 
        logging.info("===========%s===========", fname)
 
-       config = {}
+       config: Dict[str, str] = {}
 
        for question in questions:
                answer = question.default if automatic else question.ask()
@@ -321,7 +323,7 @@ def get_config(questions, fname, automatic = False): # 
type: (list[Question], st
 
        return config
 
-def generate_db_conf(qstns, fname, automatic, root): # (list[Question], str, 
bool, str) -> dict
+def generate_db_conf(qstns: List[Question], fname: str, automatic: bool, root: 
str) -> Dict[str, str]:
        """
        Generates the database.conf file and returns a map of its configuration.
 
@@ -332,18 +334,18 @@ def generate_db_conf(qstns, fname, automatic, root): # 
(list[Question], str, boo
        hostname = db_conf.get("hostname", "UNKNOWN")
        port = db_conf.get("port", "UNKNOWN")
 
-       db_conf["description"] = "{typ} database on 
{hostname}:{port}".format(typ=typ, hostname=hostname, port=port)
+       db_conf["description"] = f"{typ} database on {hostname}:{port}"
 
        path = os.path.join(root, fname.lstrip('/'))
-       with open(path, 'w+') as conf_file:
-               json.dump(db_conf, conf_file, indent=indent)
+       with open(path, 'w+', encoding="utf-8") as conf_file:
+               json.dump(db_conf, conf_file, indent=INDENT)
                print(file=conf_file)
 
        logging.info("Database configuration has been saved")
 
        return db_conf
 
-def generate_todb_conf(fname, root, conf): # (str, str, dict)
+def generate_todb_conf(fname: str, root: str, conf: Dict[str, str]):
        """
        Generates the dbconf.yml file.
 
@@ -356,20 +358,20 @@ def generate_todb_conf(fname, root, conf): # (str, str, 
dict)
        else:
                driver = "postgres" if conf["type"] == "Pg" else conf["type"]
 
-       path = os.path.join(root, fname.lstrip('/'))
-       hostname = conf.get('hostname', 'UNKNOWN')
-       port = conf.get('port', 'UNKNOWN')
-       user = conf.get('user', 'UNKNOWN')
-       password = conf.get('password', 'UNKNOWN')
-       dbname = conf.get('dbname', 'UNKNOWN')
-
-       open_line = "host={hostname} port={port} user={user} 
password={password} dbname={dbname}".format(hostname=hostname, port=port, 
user=user, password=password, dbname=dbname)
-       with open(path, 'w+') as conf_file:
+       path = os.path.join(root, fname.lstrip("/"))
+       hostname = conf.get("hostname", "UNKNOWN")
+       port = conf.get("port", "UNKNOWN")
+       user = conf.get("user", "UNKNOWN")
+       password = conf.get("password", "UNKNOWN")
+       dbname = conf.get("dbname", "UNKNOWN")
+
+       open_line = f"host={hostname} port={port} user={user} 
password={password} dbname={dbname}"
+       with open(path, "w+", encoding="utf-8") as conf_file:
                print("production:", file=conf_file)
                print("    driver:", driver, file=conf_file)
-               print("    open: {open_line} 
sslmode=disable".format(open_line=open_line), file=conf_file)
+               print("    open:", open_line, "sslmode=disable", file=conf_file)
 
-def generate_ldap_conf(questions, fname, automatic, root): # type: 
(list[Question], str, bool, str) -> None
+def generate_ldap_conf(questions: List[Question], fname: str, automatic: bool, 
root: str):
        """
        Generates the ldap.conf file by asking the questions or using default 
answers in auto mode.
 
@@ -381,32 +383,32 @@ def generate_ldap_conf(questions, fname, automatic, 
root): # type: (list[Questio
                return
        use_ldap = use_ldap_question[0].default if automatic else 
use_ldap_question[0].ask()
 
-       if use_ldap.lower() not in {'y', 'yes'}:
+       if use_ldap.lower() not in {"y", "yes"}:
                logging.info("Not setting up ldap")
                return
 
        ldap_conf = get_config([q for q in questions if q is not 
use_ldap_question[0]], fname, automatic)
        keys = (
-               'host',
-               'admin_dn',
-               'admin_pass',
-               'search_base',
-               'search_query',
-               'insecure',
-               'ldap_timeout_secs'
+               "host",
+               "admin_dn",
+               "admin_pass",
+               "search_base",
+               "search_query",
+               "insecure",
+               "ldap_timeout_secs"
        )
 
        for key in keys:
                if key not in ldap_conf:
-                       raise ValueError("{key} is a required key in 
{fname}".format(key=key, fname=fname))
+                       raise ValueError(f"{key} is a required key in {fname}")
 
-       keys_converted = {'password': 'admin_pass', 'hostname': 'host'}
+       keys_converted = {"password": "admin_pass", "hostname": "host"}
        for deprecated, key in keys_converted.items():
                if deprecated in ldap_conf and ldap_conf[key] == '':
                        ldap_conf[key] = ldap_conf[deprecated]
 
        if not re.match(r"^\S+:\d+$", ldap_conf["host"]):
-               raise ValueError("host in {fname} must be of form 
'hostname:port'".format(fname=fname))
+               raise ValueError(f"host in {fname} must be of form 
'hostname:port'")
 
        path = os.path.join(root, fname.lstrip('/'))
        try:
@@ -414,11 +416,11 @@ def generate_ldap_conf(questions, fname, automatic, 
root): # type: (list[Questio
        except OSError as e:
                if e.errno == errno.EEXIST:
                        pass
-       with open(path, 'w+') as conf_file:
-               json.dump(ldap_conf, conf_file, indent=indent)
+       with open(path, "w+", encoding="utf-8") as conf_file:
+               json.dump(ldap_conf, conf_file, indent=INDENT)
                print(file=conf_file)
 
-def hash_pass(passwd): # type: (str) -> str
+def hash_pass(passwd: str) -> str:
        """
        Generates a Scrypt-based hash of the given password in a 
Perl-compatible format.
        It's hard-coded - like the Perl - to use 64 random bytes for the salt, 
n=16384,
@@ -436,138 +438,144 @@ def hash_pass(passwd): # type: (str) -> str
        hashed_b64 = base64.standard_b64encode(hashed).decode()
        salt_b64 = base64.standard_b64encode(salt).decode()
 
-       return "SCRYPT:{n}:{r_val}:{p_val}:{salt_b64}:{hashed_b64}".format(n=n, 
r_val=r_val, p_val=p_val, salt_b64=salt_b64, hashed_b64=hashed_b64)
-
+       return f"SCRYPT:{n}:{r_val}:{p_val}:{salt_b64}:{hashed_b64}"
 
 class Scrypt:
-       def __init__(self, password, salt, cost_factor, block_size_factor, 
parallelization_factor, key_length):  # type: (bytes, bytes, int, int, int, 
int) -> None
-               self.password = password  # type: bytes
-               self.salt = salt  # type: bytes
-               self.cost_factor = cost_factor  # type: int
-               self.block_size_factor = block_size_factor  # type: int
-               self.parallelization_factor = parallelization_factor  # type: 
int
+       """
+       Implements SCRYPT encryption based on the configuration given at object
+       construction.
+       """
+       def __init__(self, password: bytes, salt: bytes, cost_factor: int, 
block_size_factor: int, parallelization_factor: int, key_length: int):
+               self.password = password
+               self.salt = salt
+               self.cost_factor = cost_factor
+               self.block_size_factor = block_size_factor
+               self.parallelization_factor = parallelization_factor
                self.key_length = key_length
-               self.block_unit = 32 * self.block_size_factor  # 1 block unit = 
32 * block_size_factor 32-bit ints
+               self.block_unit = 32 * self.block_size_factor
 
-       def derive(self):  # type: () -> bytes
-               salt_length = 2 ** 7 * self.block_size_factor * 
self.parallelization_factor  # type: int
-               pack_format = '<' + 'L' * int(salt_length / 4)  # `<` means 
`little-endian` and `L` means `unsigned long`
-               salt = hashlib.pbkdf2_hmac('sha256', password=self.password, 
salt=self.salt, iterations=1, dklen=salt_length)  # type: bytes
-               block = list(unpack(pack_format, salt))  # type: list[int]
+       def derive(self) -> bytes:
+               """
+               Derives an encrypted bytestring representative of the password 
given at
+               initialization.
+               """
+               salt_length = 2 ** 7 * self.block_size_factor * 
self.parallelization_factor
+               pack_format = f"<{'L' * int(salt_length / 4)}"  # `<` means 
`little-endian` and `L` means `unsigned long`
+               salt = hashlib.pbkdf2_hmac('sha256', password=self.password, 
salt=self.salt, iterations=1, dklen=salt_length)
+               block = list(unpack(pack_format, salt))
                block = self.ROMix(block)
                salt = pack(pack_format, *block)
-               key = hashlib.pbkdf2_hmac('sha256', password=self.password, 
salt=salt, iterations=1, dklen=self.key_length)  # type: bytes
+               key = hashlib.pbkdf2_hmac('sha256', password=self.password, 
salt=salt, iterations=1, dklen=self.key_length)
                return key
 
-       def ROMix(self, block):  # type: (list[int]) -> list[int]
-               xored_block = [0] * len(block)  # type: list[int]
-               variations = [list()] * self.cost_factor  # type: 
list[list[int]]
+       def ROMix(self, block: List[int]) -> List[int]:
+               xored_block = [0] * len(block)
+               variations: List[List[int]] = [[]] * self.cost_factor
                variations[0] = block
                index = 1
                while index < self.cost_factor:
                        variations[index] = self.block_mix(variations[index - 
1])
                        index += 1
                block = self.block_mix(variations[-1])
-               for unused in variations:
-                       variation_index = block[self.block_unit - 16] % 
self.cost_factor  # type: int
+               for _ in variations:
+                       variation_index = block[self.block_unit - 16] % 
self.cost_factor
                        variation = variations[variation_index]
-                       for index, unused in enumerate(xored_block):
+                       for index, _unused in enumerate(xored_block):
                                xored_block[index] = block[index] ^ 
variation[index]
                        block = self.block_mix(xored_block)
                return block
 
-       def block_mix(self, previous_block):  # type: (list[int]) -> list[int]
-               block = previous_block[:]  # type: list[int]
-               X_length = 16  # X is the list of numbers within `block` that 
we mix
-               copy_index = self.block_unit - X_length
-               X = previous_block[copy_index:copy_index + X_length]  # type: 
list[int]
-               octet_index = 0  # type: int
+       def block_mix(self, previous_block: List[int]) -> List[int]:
+               block = previous_block.copy()
+               x_length = 16  # x is the list of numbers within `block` that 
we mix
+               copy_index = self.block_unit - x_length
+               x = previous_block[copy_index:copy_index + x_length]
+               octet_index = 0
                block_xor_index = 0
                while octet_index < 2 * self.block_size_factor:
-                       for index, unused in enumerate(X):
-                               X[index] ^= previous_block[block_xor_index + 
index]
-                       block_xor_index += X_length
-                       self.salsa20(X)
-                       block_offset = (int(octet_index / 2) + octet_index % 2 
* self.block_size_factor) * X_length
-                       block[block_offset:block_offset + X_length] = X
+                       for index, _ in enumerate(x):
+                               x[index] ^= previous_block[block_xor_index + 
index]
+                       block_xor_index += x_length
+                       self.salsa20(x)
+                       block_offset = (int(octet_index / 2) + octet_index % 2 
* self.block_size_factor) * x_length
+                       block[block_offset:block_offset + x_length] = x
                        octet_index += 1
                return block
 
-       def salsa20(self, block):  # type: (list[int]) -> None
-               X = block[:]  # make a copy (list.copy() is Python 3-only)
-               for i in range(0, 4):
+       def salsa20(self, block: List[int]):
+               x = block.copy()
+               for _ in range(4):
                        # These bit shifting operations could be condensed into 
a single line of list comprehensions,
                        # but there is a >3x performance benefit from writing 
it out explicitly.
-                       bits = X[0] + X[12] & 0xffffffff
-                       X[4] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[4] + X[0] & 0xffffffff
-                       X[8] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[8] + X[4] & 0xffffffff
-                       X[12] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[12] + X[8] & 0xffffffff
-                       X[0] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[5] + X[1] & 0xffffffff
-                       X[9] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[9] + X[5] & 0xffffffff
-                       X[13] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[13] + X[9] & 0xffffffff
-                       X[1] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[1] + X[13] & 0xffffffff
-                       X[5] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[10] + X[6] & 0xffffffff
-                       X[14] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[14] + X[10] & 0xffffffff
-                       X[2] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[2] + X[14] & 0xffffffff
-                       X[6] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[6] + X[2] & 0xffffffff
-                       X[10] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[15] + X[11] & 0xffffffff
-                       X[3] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[3] + X[15] & 0xffffffff
-                       X[7] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[7] + X[3] & 0xffffffff
-                       X[11] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[11] + X[7] & 0xffffffff
-                       X[15] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[0] + X[3] & 0xffffffff
-                       X[1] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[1] + X[0] & 0xffffffff
-                       X[2] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[2] + X[1] & 0xffffffff
-                       X[3] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[3] + X[2] & 0xffffffff
-                       X[0] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[5] + X[4] & 0xffffffff
-                       X[6] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[6] + X[5] & 0xffffffff
-                       X[7] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[7] + X[6] & 0xffffffff
-                       X[4] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[4] + X[7] & 0xffffffff
-                       X[5] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[10] + X[9] & 0xffffffff
-                       X[11] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[11] + X[10] & 0xffffffff
-                       X[8] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[8] + X[11] & 0xffffffff
-                       X[9] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[9] + X[8] & 0xffffffff
-                       X[10] ^= bits << 18 | bits >> 32 - 18
-                       bits = X[15] + X[14] & 0xffffffff
-                       X[12] ^= bits << 7 | bits >> 32 - 7
-                       bits = X[12] + X[15] & 0xffffffff
-                       X[13] ^= bits << 9 | bits >> 32 - 9
-                       bits = X[13] + X[12] & 0xffffffff
-                       X[14] ^= bits << 13 | bits >> 32 - 13
-                       bits = X[14] + X[13] & 0xffffffff
-                       X[15] ^= bits << 18 | bits >> 32 - 18
-
-               for index in range(0, 16):
-                       block[index] = block[index] + X[index] & 0xffffffff
-
-
-def generate_users_conf(qstns, fname, auto, root): # type: (list[Question], 
str, bool, str) -> User
+                       bits = x[0] + x[12] & 0xffffffff
+                       x[4] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[4] + x[0] & 0xffffffff
+                       x[8] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[8] + x[4] & 0xffffffff
+                       x[12] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[12] + x[8] & 0xffffffff
+                       x[0] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[5] + x[1] & 0xffffffff
+                       x[9] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[9] + x[5] & 0xffffffff
+                       x[13] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[13] + x[9] & 0xffffffff
+                       x[1] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[1] + x[13] & 0xffffffff
+                       x[5] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[10] + x[6] & 0xffffffff
+                       x[14] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[14] + x[10] & 0xffffffff
+                       x[2] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[2] + x[14] & 0xffffffff
+                       x[6] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[6] + x[2] & 0xffffffff
+                       x[10] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[15] + x[11] & 0xffffffff
+                       x[3] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[3] + x[15] & 0xffffffff
+                       x[7] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[7] + x[3] & 0xffffffff
+                       x[11] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[11] + x[7] & 0xffffffff
+                       x[15] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[0] + x[3] & 0xffffffff
+                       x[1] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[1] + x[0] & 0xffffffff
+                       x[2] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[2] + x[1] & 0xffffffff
+                       x[3] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[3] + x[2] & 0xffffffff
+                       x[0] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[5] + x[4] & 0xffffffff
+                       x[6] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[6] + x[5] & 0xffffffff
+                       x[7] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[7] + x[6] & 0xffffffff
+                       x[4] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[4] + x[7] & 0xffffffff
+                       x[5] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[10] + x[9] & 0xffffffff
+                       x[11] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[11] + x[10] & 0xffffffff
+                       x[8] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[8] + x[11] & 0xffffffff
+                       x[9] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[9] + x[8] & 0xffffffff
+                       x[10] ^= bits << 18 | bits >> 32 - 18
+                       bits = x[15] + x[14] & 0xffffffff
+                       x[12] ^= bits << 7 | bits >> 32 - 7
+                       bits = x[12] + x[15] & 0xffffffff
+                       x[13] ^= bits << 9 | bits >> 32 - 9
+                       bits = x[13] + x[12] & 0xffffffff
+                       x[14] ^= bits << 13 | bits >> 32 - 13
+                       bits = x[14] + x[13] & 0xffffffff
+                       x[15] ^= bits << 18 | bits >> 32 - 18
+
+               for index in range(16):
+                       block[index] = block[index] + x[index] & 0xffffffff
+
+def generate_users_conf(qstns: List[Question], fname: str, auto: bool, root: 
str) -> User:
        """
        Generates a users.json file from the given questions and returns a User 
containing the same
        information.
@@ -575,18 +583,18 @@ def generate_users_conf(qstns, fname, auto, root): # 
type: (list[Question], str,
        config = get_config(qstns, fname, auto)
 
        if "tmAdminUser" not in config or "tmAdminPw" not in config:
-               raise ValueError("{fname} must include 'tmAdminUser' and 
'tmAdminPw'".format(fname=fname))
+               raise ValueError(f"{fname} must include 'tmAdminUser' and 
'tmAdminPw'")
 
        hashed_pass = hash_pass(config["tmAdminPw"])
 
        path = os.path.join(root, fname.lstrip('/'))
-       with open(path, 'w+') as conf_file:
-               json.dump({"username": config["tmAdminUser"], "password": 
hashed_pass}, conf_file, indent=indent)
+       with open(path, "w+", encoding="utf-8") as conf_file:
+               json.dump({"username": config["tmAdminUser"], "password": 
hashed_pass}, conf_file, indent=INDENT)
                print(file=conf_file)
 
        return User(config["tmAdminUser"], config["tmAdminPw"])
 
-def generate_profiles_dir(questions): # type: (list[Question]) -> None
+def generate_profiles_dir(questions: List[Question]):
        """
        I truly have no idea what's going on here. This is what the Perl did, 
so I
        copied it. It does nothing. Literally nothing.
@@ -595,7 +603,7 @@ def generate_profiles_dir(questions): # type: 
(list[Question]) -> None
        user_in = questions
        #pylint:enable=unused-variable
 
-def generate_openssl_conf(questions, fname, auto): # type: (list[Question], 
str, bool) -> SSLConfig
+def generate_openssl_conf(questions: List[Question], fname: str, auto: bool) 
-> SSLConfig:
        """
        Constructs an SSLConfig by asking the passed questions, or using their 
default answers if in
        auto mode.
@@ -608,7 +616,7 @@ def generate_openssl_conf(questions, fname, auto): # type: 
(list[Question], str,
 
        return SSLConfig(gen_cert, cfg_map)
 
-def generate_param_conf(qstns, fname, auto, root): # type: (list[Question], 
str, bool, str) -> dict
+def generate_param_conf(qstns: List[Question], fname: str, auto: bool, root: 
str) -> Dict[str, str]:
        """
        Generates a profiles.json by asking the passed questions, or using 
their default answers in auto
        mode.
@@ -617,14 +625,14 @@ def generate_param_conf(qstns, fname, auto, root): # 
type: (list[Question], str,
        """
        conf = get_config(qstns, fname, auto)
 
-       path = os.path.join(root, fname.lstrip('/'))
-       with open(path, 'w+') as conf_file:
-               json.dump(conf, conf_file, indent=indent)
+       path = os.path.join(root, fname.lstrip("/"))
+       with open(path, "w+", encoding="utf-8") as conf_file:
+               json.dump(conf, conf_file, indent=INDENT)
                print(file=conf_file)
 
        return conf
 
-def sanity_check_config(cfg, automatic): # type: (dict[str, list[Question]], 
bool) -> int
+def sanity_check_config(cfg: Dict[str, List[Question]], automatic: bool) -> 
int:
        """
        Checks a user-input configuration file, and outputs the number of files 
in the
        default question set that did not appear in the input.
@@ -664,7 +672,7 @@ def sanity_check_config(cfg, automatic): # type: (dict[str, 
list[Question]], boo
 
        return diffs
 
-def unmarshal_config(dct): # type: (dict) -> dict[str, list[Question]]
+def unmarshal_config(dct: Dict[str, Any]) -> Dict[str, List[Question]]:
        """
        Reads in a raw parsed configuration file and returns the resulting 
configuration.
 
@@ -673,23 +681,25 @@ def unmarshal_config(dct): # type: (dict) -> dict[str, 
list[Question]]
        >>> unmarshal_config({"test": [{"foo": "", "config_var": "bar", 
"hidden": True}]})
        {'test': [Question(question='foo', default='', config_var='bar', 
hidden=True)]}
        """
-       ret = {}
+       ret: Dict[str, List[Question]] = {}
        for file, questions in dct.items():
                if not isinstance(questions, list):
-                       raise ValueError("file '{file}' has malformed 
questions".format(file=file))
+                       raise ValueError(f"file '{file}' has malformed 
questions")
 
-               qstns = []
+               qstns: List[Question] = []
+               qstn: Any
                for qstn in questions:
                        if not isinstance(qstn, dict):
-                               raise ValueError("file '{file}' has a malformed 
question ({qstn})".format(file=file, qstn=qstn))
+                               raise ValueError(f"file '{file}' has a 
malformed question ({qstn})")
+                       question: Any
                        try:
-                               question = next(key for key in qstn.keys() if 
key not in ("hidden", "config_var"))
+                               question = next(key for key in qstn.keys() if 
key not in {"hidden", "config_var"})
                        except StopIteration:
-                               raise ValueError("question in '{file}' has no 
question/answer properties ({qstn})".format(file=file, qstn=qstn))
+                               raise ValueError(f"question in '{file}' has no 
question/answer properties ({qstn})")
 
-                       answer = qstn[question]
+                       answer: Any = qstn[question]
                        if not isinstance(question, str) or not 
isinstance(answer, str):
-                               errstr = "question in '{file}' has malformed 
question/answer property ({question}: {answer})".format(file=file, 
question=question, answer=answer)
+                               errstr = f"question in '{file}' has malformed 
question/answer property ({question}: {answer})"
                                raise ValueError(errstr)
 
                        del qstn[question]
@@ -699,10 +709,10 @@ def unmarshal_config(dct): # type: (dict) -> dict[str, 
list[Question]]
                                del qstn["hidden"]
 
                        if "config_var" not in qstn:
-                               raise ValueError("question in '{file}' has no 
'config_var' property".format(file=file))
-                       cfg_var = qstn["config_var"]
+                               raise ValueError(f"question in '{file}' has no 
'config_var' property")
+                       cfg_var: Any = qstn["config_var"]
                        if not isinstance(cfg_var, str):
-                               raise ValueError("question in '{file}' has 
malformed 'config_var' property ({cfg_var})".format(file=file, cfg_var=cfg_var))
+                               raise ValueError(f"question in '{file}' has 
malformed 'config_var' property ({cfg_var})")
                        del qstn["config_var"]
 
                        if qstn:
@@ -713,7 +723,7 @@ def unmarshal_config(dct): # type: (dict) -> dict[str, 
list[Question]]
 
        return ret
 
-def write_encryption_key(aes_key_location): # type: (str) -> None
+def write_encryption_key(aes_key_location: str):
        """
        Creates an AES encryption key for the postgres traffic vault backend
 
@@ -728,11 +738,11 @@ def write_encryption_key(aes_key_location): # type: (str) 
-> None
                "-base64",
                "32"
        )
-       if not exec_openssl("Generating an AES encryption key to 
{loc}".format(loc=aes_key_location), *args):
+       if not exec_openssl(f"Generating an AES encryption key to 
{aes_key_location}", *args):
                logging.debug("AES key generation failed")
                raise OSError("failed to generate AES key")
 
-def exec_openssl(description, *cmd_args): # type: (str, ...) -> bool
+def exec_openssl(description: str, *cmd_args: str) -> bool:
        """
        Executes openssl with the supplied command-line arguments.
 
@@ -757,13 +767,13 @@ def exec_openssl(description, *cmd_args): # type: (str, 
...) -> bool
                output = proc.communicate()
                logging.debug("openssl exec failed with code %s; stderr: %s", 
proc.returncode, output[1])
                while True:
-                       ans = input("{description} failed. Try again (y/n) [y]: 
".format(description=description))
-                       if not ans or ans.lower().startswith('n'):
+                       ans = input(f"{description} failed. Try again (y/n) 
[y]: ")
+                       if not ans or ans.lower().startswith("n"):
                                return False
-                       if ans.lower().startswith('y'):
+                       if ans.lower().startswith("y"):
                                break
 
-def setup_certificates(conf, root, ops_user, ops_group): # type: (SSLConfig, 
str, str, str) -> int
+def setup_certificates(conf: SSLConfig, root: str, ops_user: str, ops_group: 
str) -> int:
        """
        Generates self-signed SSL certificates from the given configuration.
        :returns: For whatever reason this subroutine needs to dictate the 
return code of the script, so that's what it returns.
@@ -791,7 +801,7 @@ def setup_certificates(conf, root, ops_user, ops_group): # 
type: (SSLConfig, str
                "-out",
                "server.key",
                "-passout",
-               "pass:{rsa_password}".format(rsa_password=conf.rsa_password),
+               f"pass:{conf.rsa_password}",
                "1024"
        )
        if not exec_openssl("Generating an RSA Private Server Key", *args):
@@ -805,7 +815,7 @@ def setup_certificates(conf, root, ops_user, ops_group): # 
type: (SSLConfig, str
                "-out",
                "server.csr",
                "-passin",
-               "pass:{rsa_password}".format(rsa_password=conf.rsa_password),
+               f"pass:{conf.rsa_password}",
                "-subj",
                conf.params
        )
@@ -822,7 +832,7 @@ def setup_certificates(conf, root, ops_user, ops_group): # 
type: (SSLConfig, str
                "-out",
                "server.key",
                "-passin",
-               "pass:{rsa_password}".format(rsa_password=conf.rsa_password)
+               f"pass:{conf.rsa_password}"
        )
        if not exec_openssl("Removing the pass phrase from the server key", 
*args):
                return 1
@@ -879,12 +889,10 @@ def setup_certificates(conf, root, ops_user, ops_group): 
# type: (SSLConfig, str
        cdn_conf_path = os.path.join(root, "opt/traffic_ops/app/conf/cdn.conf")
 
        try:
-               with open(cdn_conf_path) as conf_file:
+               with open(cdn_conf_path, encoding="utf-8") as conf_file:
                        cdn_conf = json.load(conf_file)
        except (OSError, ValueError) as e:
-               exception = OSError("reading {cdn_conf_path}: 
{e}".format(cdn_conf_path=cdn_conf_path, e=e))
-               exception.__cause__ = e
-               raise exception
+               raise OSError(f"reading {cdn_conf_path}: {e}") from e
 
        if (
                not isinstance(cdn_conf, dict) or
@@ -894,7 +902,7 @@ def setup_certificates(conf, root, ops_user, ops_group): # 
type: (SSLConfig, str
                logging.critical("Malformed %s; improper object and/or missing 
'traffic_ops_golang' key", cdn_conf_path)
                return 1
 
-       to_golang = cdn_conf["traffic_ops_golang"]
+       to_golang: Any = cdn_conf["traffic_ops_golang"]
        if (
                "cert" not in to_golang or
                not isinstance(to_golang["cert"], str)
@@ -915,15 +923,15 @@ def setup_certificates(conf, root, ops_user, ops_group): 
# type: (SSLConfig, str
 
        return 0
 
-def random_word(length = 12): # type: (int) -> str
+def random_word(length: int = 12) -> str:
        """
        Returns a randomly generated string 'length' characters long containing 
only word
        characters ([a-zA-Z0-9_]).
        """
-       word_chars = string.ascii_letters + string.digits + '_'
-       return ''.join(random.choice(word_chars) for _ in range(length))
+       word_chars = string.ascii_letters + string.digits + "_"
+       return "".join(random.choice(word_chars) for _ in range(length))
 
-def generate_cdn_conf(questions, fname, automatic, root): # type: 
(list[Question], str, bool, str) -> bool
+def generate_cdn_conf(questions: List[Question], fname: str, automatic: bool, 
root: str) -> bool:
        """
        Generates some properties of a cdn.conf file based on the passed 
questions.
 
@@ -940,62 +948,45 @@ def generate_cdn_conf(questions, fname, automatic, root): 
# type: (list[Question
        try:
                num_secrets = int(cdn_conf["keepSecrets"])
        except KeyError as e:
-               exception = ValueError("missing 'keepSecrets' config_var")
-               exception.__cause__ = e
-               raise exception
+               raise ValueError("missing 'keepSecrets' config_var") from e
        except ValueError as e:
-               exception = ValueError("invalid 'keepSecrets' config_var value: 
{e}".format(e=e))
-               exception.__cause__ = e
-               raise exception
+               raise ValueError(f"invalid 'keepSecrets' config_var value: 
{e}") from e
 
        try:
-               port = cdn_conf["port"]  # type: str
+               port = cdn_conf["port"]
+               int(port)
        except KeyError as e:
-               exception = ValueError("missing 'port' config_var")
-               exception.__cause__ = e
-               raise exception
+               raise ValueError("missing 'port' config_var") from e
        except ValueError as e:
-               exception = ValueError("invalid 'port' config_var value: 
{e}".format(e=e))
-               exception.__cause__ = e
-               raise exception
+               raise ValueError(f"invalid 'port' config_var value: {e}") from e
 
        try:
                workers = int(cdn_conf["workers"])
        except KeyError as e:
-               exception = ValueError("missing 'workers' config_var")
-               exception.__cause__ = e
-               raise exception
+               raise ValueError("missing 'workers' config_var") from e
        except ValueError as e:
-               exception = ValueError("invalid 'workers' config_var value: 
{e}".format(e=e))
-               exception.__cause__ = e
-               raise exception
+               raise ValueError(f"invalid 'workers' config_var value: {e}") 
from e
 
        try:
                url = cdn_conf["base_url"]
        except KeyError as e:
-               exception = ValueError("missing 'base_url' config_var")
-               exception.__cause__ = e
-               raise exception
+               raise ValueError("missing 'base_url' config_var") from e
 
        try:
                ldap_loc = cdn_conf["ldap_conf_location"]
        except KeyError as e:
-               exception = ValueError("missing 'ldap_conf_location' 
config_var")
-               exception.__cause__ = e
-               raise exception
+               raise ValueError("missing 'ldap_conf_location' config_var") 
from e
 
        conf = CDNConfig(gen_secret, num_secrets, port, workers, url, ldap_loc)
 
        path = os.path.join(root, fname.lstrip('/'))
-       existing_conf = {}
+       existing_conf: Dict[Any, Any] = {}
        if os.path.isfile(path):
-               with open(path) as conf_file:
+               with open(path, encoding="utf-8") as conf_file:
                        try:
                                existing_conf = json.load(conf_file)
                        except ValueError as e:
-                               exception = ValueError("invalid existing 
cdn.config at {path}: {e}".format(path=path, e=e))
-                               exception.__cause__ = e
-                               raise exception
+                               raise ValueError(f"invalid existing cdn.config 
at {path}: {e}") from e
 
        if not isinstance(existing_conf, dict):
                logging.warning("Existing cdn.conf (at '%s') is not an object - 
overwriting", path)
@@ -1018,25 +1009,25 @@ def generate_cdn_conf(questions, fname, automatic, 
root): # type: (list[Question
        traffic_vault_backend = "postgres"
        tv_aes_key_location = os.path.join(root, 
TRAFFIC_VAULT_AES_KEY_FILE.lstrip('/'))
 
-       with open(path, "w+") as conf_file:
-               json.dump(existing_conf, conf_file, indent=indent)
+       with open(path, "w+", encoding="utf-8") as conf_file:
+               json.dump(existing_conf, conf_file, indent=INDENT)
                print(file=conf_file)
        logging.info("CDN configuration has been saved")
        try:
-               traffic_vault_backend = 
existing_conf["traffic_ops_golang"]["traffic_vault_backend"]
+               traffic_vault_backend: Any = 
existing_conf["traffic_ops_golang"]["traffic_vault_backend"]
        except KeyError as e:
                logging.warning("no traffic vault backend configured, using 
default postgres")
 
        if traffic_vault_backend == "postgres":
                try:
-                       tv_aes_key_location = 
existing_conf["traffic_ops_golang"]["traffic_vault_config"]["aes_key_location"]
+                       tv_aes_key_location: Any = 
existing_conf["traffic_ops_golang"]["traffic_vault_config"]["aes_key_location"]
                except KeyError as e:
                        logging.warning("no traffic vault aes encryption key 
location specified, using default %s", TRAFFIC_VAULT_AES_KEY_FILE)
                write_encryption_key(tv_aes_key_location)
 
        return traffic_vault_backend == "postgres"
 
-def db_connection_string(dbconf): # type: (dict) -> str
+def db_connection_string(dbconf: Dict[str, Any]) -> str:
        """
        Constructs a database connection string from the passed configuration 
object.
        """
@@ -1045,9 +1036,9 @@ def db_connection_string(dbconf): # type: (dict) -> str
        db_name = "traffic_ops" if dbconf["type"] == "Pg" else dbconf["type"]
        hostname = dbconf["hostname"]
        port = dbconf["port"]
-       return 
"postgresql://{user}:{password}@{hostname}:{port}/{db_name}".format(user=user, 
password=password, hostname=hostname, port=port, db_name=db_name)
+       return f"postgresql://{user}:{password}@{hostname}:{port}/{db_name}"
 
-def exec_psql(conn_str, query, **args): # type: (str, str, dict) -> str
+def exec_psql(conn_str: str, query: str, **args: str) -> str:
        """
        Executes SQL queries by forking and exec-ing '/usr/bin/psql'.
 
@@ -1068,14 +1059,11 @@ def exec_psql(conn_str, query, **args): # type: (str, 
str, dict) -> str
        if proc.returncode != 0:
                logging.debug("psql exec failed; stderr: %s\n\tstdout: %s", 
output[1], output[0])
                raise OSError("failed to execute database query")
-       if sys.version_info.major >= 3:
-               return output[0].strip()
-       else:
-               return string.strip(output[0])
+       return output[0].strip()
 
-def invoke_db_admin_pl(action, root, tv): # type: (str, str, bool) -> None
+def invoke_db_admin_pl(action: str, root: str, tv: bool):
        """
-       Exectues admin with the given action, and looks for it from the given 
root directory.
+       Executes admin with the given action, and looks for it from the given 
root directory.
        """
        path = os.path.join(root, "opt/traffic_ops/app")
        # This is a workaround for admin using hard-coded relative paths. That
@@ -1090,13 +1078,19 @@ def invoke_db_admin_pl(action, root, tv): # type: (str, 
str, bool) -> None
                stdout=subprocess.PIPE,
                universal_newlines=True,
        )
-       output = proc.communicate()  # type: str
+       output = proc.communicate()
        if proc.returncode != 0:
                logging.debug("admin exec failed; stderr: %s\n\tstdout: %s", 
output[1], output[0])
-               raise OSError("Database {action} failed".format(action=action))
+               raise OSError(f"Database {action} failed")
        logging.info("Database %s succeeded", action)
 
-def setup_database_data(conn_str, user, param_conf, root, postgresTV): # type: 
(str, User, dict, str, bool) -> None
+def setup_database_data(
+       conn_str: str,
+       user: User,
+       param_conf: Dict[str, str],
+       root: str,
+       postgresTV: bool
+):
        """
        Sets up all necessary initial database data using `/usr/bin/sql`
        """
@@ -1126,32 +1120,32 @@ def setup_database_data(conn_str, user, param_conf, 
root, postgresTV): # type: (
                invoke_db_admin_pl("migrate", root, True)
 
        hashed_pass = hash_pass(user.password)
-       insert_admin_query = '''
+       insert_admin_query = f'''
                INSERT INTO tm_user (username, tenant_id, role, local_passwd, 
confirm_local_passwd)
                VALUES (
-                       '{}',
+                       '{user.username}',
                        (SELECT id FROM tenant WHERE name = 'root'),
                        (SELECT id FROM role WHERE name = 'admin'),
                        '{hashed_pass}',
                        '{hashed_pass}'
                )
                ON CONFLICT (username) DO NOTHING;
-       '''.format(user.username, hashed_pass=hashed_pass)
+       '''
        _ = exec_psql(conn_str, insert_admin_query)
 
        logging.info("=========== Setting up cdn")
-       insert_cdn_query = "\n\t-- global parameters" + '''
+       insert_cdn_query = "\n\t-- global parameters" + f'''
                INSERT INTO cdn (name, domain_name, dnssec_enabled)
-               VALUES ('{cdn_name}', '{dns_subdomain}', false)
+               VALUES ('{param_conf["cdn_name"]}', 
'{param_conf["dns_subdomain"]}', false)
                ON CONFLICT DO NOTHING;
-       '''.format(**param_conf)
+       '''
        logging.info("\n%s", insert_cdn_query)
        _ = exec_psql(conn_str, insert_cdn_query)
 
        tm_url = param_conf["tm.url"]
 
        logging.info("=========== Setting up parameters")
-       insert_parameters_query = "\n\t-- global parameters" + '''
+       insert_parameters_query = "\n\t-- global parameters" + f'''
                INSERT INTO parameter (name, config_file, value)
                VALUES ('tm.url', 'global', '{tm_url}'),
                        ('tm.infourl', 'global', '{tm_url}/doc'),
@@ -1159,12 +1153,12 @@ def setup_database_data(conn_str, user, param_conf, 
root, postgresTV): # type: (
                        ('geolocation.polling.url', 'CRConfig.json', 
'{tm_url}/routing/GeoLite2-City.mmdb.gz'),
                        ('geolocation6.polling.url', 'CRConfig.json', 
'{tm_url}/routing/GeoLiteCityv6.dat.jz')
                ON CONFLICT (name, config_file, value) DO NOTHING;
-       '''.format(tm_url=tm_url)
+       '''
        logging.info("\n%s", insert_parameters_query)
        _ = exec_psql(conn_str, insert_parameters_query)
 
        logging.info("\n=========== Setting up profiles")
-       insert_profiles_query = "\n\t-- global parameters" + '''
+       insert_profiles_query = "\n\t-- global parameters" + f'''
                INSERT INTO profile (name, description, type, cdn)
                VALUES ('GLOBAL' 'Global Traffic Ops profile, DO NOT DELETE', 
'UNK_PROFILE', (SELECT id FROM cdn WHERE name='ALL'))
                ON CONFLICT DO NOTHING;
@@ -1212,26 +1206,25 @@ def setup_database_data(conn_str, user, param_conf, 
root, postgresTV): # type: (
                                )
                        )
                ON CONFLICT (profile, parameter) DO NOTHING;
-       '''.format(tm_url=tm_url)
+       '''
        logging.info("\n%s", insert_profiles_query)
        _ = exec_psql(conn_str, insert_cdn_query)
 
 def main(
-automatic, # type: bool
-debug, # type: bool
-defaults, # type: str
-cfile, # type: str
-root_dir, # type: str
-ops_user, # type: str
-ops_group, # type: str
-no_restart_to, # type: bool
-no_database, # type: bool
-):
+       automatic: bool,
+       debug: bool,
+       defaults: Optional[str],
+       cfile: str,
+       root_dir: str,
+       ops_user: str,
+       ops_group: str,
+       no_restart_to: bool,
+       no_database: bool,
+) -> int:
        """
        Runs the main routine given the parsed arguments as input.
-       :rtype: int
        """
-       postgresTV = False
+       postgres_tv = False
        if debug:
                logging.getLogger().setLevel(logging.DEBUG)
        else:
@@ -1251,13 +1244,13 @@ no_database, # type: bool
                try:
                        if defaults:
                                try:
-                                       with open(defaults, "w") as dump_file:
-                                               json.dump(DEFAULTS, dump_file, 
indent=indent)
+                                       with open(defaults, "w", 
encoding="utf-8") as dump_file:
+                                               json.dump(DEFAULTS, dump_file, 
indent=INDENT)
                                except OSError as e:
                                        logging.critical("Writing output: %s", 
e)
                                        return 1
                        else:
-                               json.dump(DEFAULTS, sys.stdout, 
cls=ConfigEncoder, indent=indent)
+                               json.dump(DEFAULTS, sys.stdout, 
cls=ConfigEncoder, indent=INDENT)
                                print()
                except ValueError as e:
                        logging.critical("Converting defaults to JSON: %s", e)
@@ -1270,7 +1263,7 @@ no_database, # type: bool
        else:
                logging.info("Using input file %s", cfile)
                try:
-                       with open(cfile) as conf_file:
+                       with open(cfile, encoding="utf-8") as conf_file:
                                user_input = 
unmarshal_config(json.load(conf_file))
                        diffs = sanity_check_config(user_input, automatic)
                        logging.info(
@@ -1291,17 +1284,17 @@ no_database, # type: bool
                        generate_todb_conf(TV_DB_CONF_FILE, root_dir, tv_dbconf)
                generate_ldap_conf(user_input[LDAP_CONF_FILE], LDAP_CONF_FILE, 
automatic, root_dir)
                admin_conf = generate_users_conf(
-               user_input[USERS_CONF_FILE],
-               USERS_CONF_FILE,
-               automatic,
-               root_dir
+                       user_input[USERS_CONF_FILE],
+                       USERS_CONF_FILE,
+                       automatic,
+                       root_dir
                )
                generate_profiles_dir(user_input[PROFILES_CONF_FILE])
                opensslconf = 
generate_openssl_conf(user_input[OPENSSL_CONF_FILE], OPENSSL_CONF_FILE, 
automatic)
                paramconf = generate_param_conf(user_input[PARAM_CONF_FILE], 
PARAM_CONF_FILE, automatic, root_dir)
                postinstall_cfg = os.path.join(root_dir, 
POST_INSTALL_CFG.lstrip('/'))
                if not os.path.isfile(postinstall_cfg):
-                       with open(postinstall_cfg, 'w+') as conf_file:
+                       with open(postinstall_cfg, "w+", encoding="utf-8") as 
conf_file:
                                print("{}", file=conf_file)
        except OSError as e:
                logging.critical("Writing configuration: %s", e)
@@ -1319,7 +1312,7 @@ no_database, # type: bool
                return 1
 
        try:
-               postgresTV = generate_cdn_conf(user_input[CDN_CONF_FILE], 
CDN_CONF_FILE, automatic, root_dir)
+               postgres_tv = generate_cdn_conf(user_input[CDN_CONF_FILE], 
CDN_CONF_FILE, automatic, root_dir)
        except OSError as e:
                logging.critical("Generating cdn.conf: %s", e)
                return 1
@@ -1347,7 +1340,7 @@ no_database, # type: bool
                        )
 
                try:
-                       setup_database_data(conn_str, admin_conf, paramconf, 
root_dir, postgresTV)
+                       setup_database_data(conn_str, admin_conf, paramconf, 
root_dir, postgres_tv)
                except (subprocess.CalledProcessError, OSError) as e:
                        db_connect_failed()
                        return 1
@@ -1360,19 +1353,20 @@ no_database, # type: bool
                logging.info("Starting Traffic Ops")
                try:
                        cmd = ["/sbin/service", "traffic_ops", "restart"]
-                       proc = subprocess.Popen(
+                       with subprocess.Popen(
                                cmd,
                                stderr=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                universal_newlines=True,
-                       )
-                       if proc.wait():
-                               raise 
subprocess.CalledProcessError(proc.returncode, cmd)
-               except (subprocess.CalledProcessError, OSError) as e:
-                       output = proc.communicate()
-                       logging.critical("Failed to restart Traffic Ops, return 
code %s: %s", e.returncode, e)
-                       logging.debug("stderr: %s\n\tstdout: %s", output[1], 
output[0])
-                       return 1
+                       ) as proc:
+                               try:
+                                       if proc.wait():
+                                               raise 
subprocess.CalledProcessError(proc.returncode, cmd)
+                               except (subprocess.CalledProcessError) as e:
+                                       output = proc.communicate()
+                                       logging.critical("Failed to restart 
Traffic Ops, return code %s: %s", e.returncode, e)
+                                       logging.debug("stderr: %s\n\tstdout: 
%s", output[1], output[0])
+                                       return 1
                except OSError as e:
                        logging.critical("Failed to restart Traffic Ops: 
unknown error occurred: %s", e)
                        return 1
@@ -1508,15 +1502,15 @@ if __name__ == '__main__':
 
        try:
                EXIT_CODE = main(
-               ARGS.automatic,
-               DEBUG,
-               DEFAULTS_ARG,
-               CFILE,
-               os.path.abspath(ARGS.root_directory),
-               ARGS.ops_user,
-               ARGS.ops_group,
-               ARGS.no_restart_to,
-               ARGS.no_database
+                       ARGS.automatic,
+                       DEBUG,
+                       DEFAULTS_ARG,
+                       CFILE,
+                       os.path.abspath(ARGS.root_directory),
+                       ARGS.ops_user,
+                       ARGS.ops_group,
+                       ARGS.no_restart_to,
+                       ARGS.no_database
                )
                sys.exit(EXIT_CODE)
        except KeyboardInterrupt:
diff --git a/traffic_ops/install/bin/input.json 
b/traffic_ops/install/bin/input.json
index ad1b601e79..1dc6e4bf3d 100644
--- a/traffic_ops/install/bin/input.json
+++ b/traffic_ops/install/bin/input.json
@@ -33,7 +33,7 @@
     {
       "LDAP Admin Password": "",
       "config_var": "password",
-      "hidden": "1"
+      "hidden": true
     },
     {
       "LDAP Search Base": "",
@@ -64,7 +64,7 @@
     {
       "Traffic Ops database password": "dbpass",
       "config_var": "password",
-      "hidden": "1"
+      "hidden": true
     }
   ],
   "/opt/traffic_ops/app/conf/production/tv.conf": [
@@ -91,7 +91,7 @@
     {
       "Traffic Ops database password": "dbpass",
       "config_var": "password",
-      "hidden": "1"
+      "hidden": true
     }
   ],
   "/opt/traffic_ops/install/data/json/openssl_configuration.json": [
@@ -126,7 +126,7 @@
     {
       "RSA Passphrase": "password",
       "config_var": "rsaPassword",
-      "hidden": "1"
+      "hidden": true
     }
   ],
   "/opt/traffic_ops/install/data/json/profiles.json": [
@@ -151,7 +151,7 @@
     {
       "Password for the admin user": "twelve",
       "config_var": "tmAdminPw",
-      "hidden": "1"
+      "hidden": true
     }
   ],
   "/opt/traffic_ops/install/data/profiles/": [
diff --git a/traffic_ops/install/bin/postinstall 
b/traffic_ops/install/bin/postinstall
index 74f46d01bd..97bab4fc70 100755
--- a/traffic_ops/install/bin/postinstall
+++ b/traffic_ops/install/bin/postinstall
@@ -26,9 +26,9 @@ for arg in "$@"; do
 done
 
 PATH+=:/usr/libexec/
-python_bin="$(command -v {python,platform-python}{3{{.,}{9,8,7,6},},2{7,.7,},} 
| head -n1)"
+python_bin="$(command -v {python,platform-python}{3{{.,}{12..6},},} | head 
-n1)"
 if [[ -z "$python_bin" ]]; then
-       echo 'No python3 or python2 executable was found. Python is required to 
run the Postinstall script.' >/dev/stderr
+       echo 'No python3 executable was found. Python is required to run the 
Postinstall script.' >/dev/stderr
        exit 1
 fi
 
diff --git a/traffic_ops/install/bin/postinstall.test.sh 
b/traffic_ops/install/bin/postinstall.test.sh
index 5507e3c58d..0f37db4cdb 100755
--- a/traffic_ops/install/bin/postinstall.test.sh
+++ b/traffic_ops/install/bin/postinstall.test.sh
@@ -22,34 +22,25 @@ readonly MY_DIR="$(pwd)";
 
 help_string="$(<<-'HELP_STRING' cat
        Usage: ./postinstall.test.h [
-           -2        Set Python version to 2
-           -3        Set Python version to 3
            -b        Explicitly set the path to the Python binary as this value
            -h, ?     Print this help text and exit
-           -s        Do not test Python 2 after testing Python 3
 HELP_STRING
 )"
 
-while getopts :23hsb: opt; do
+while getopts :hb: opt; do
        case "$opt" in
-               2) python_version=2;;
-               3) python_version=3;;
                b) python_bin="$OPTARG";;
                h) echo "$help_string" && exit;;
-               s) skip_python2=true;;
                ?) echo "$help_string" && exit;;
                *) echo "Invalid flag received: ${OPTARG}" >&2 && echo 
"$help_string" && exit 1;;
        esac;
 done;
 
-python_version="${python_version:-3}";
-python_bin="${python_bin:-/usr/bin/python${python_version}}";
+python_bin="${python_bin:-/usr/bin/python3}";
 
-if [[ ! -x "$python_bin" && "$python_version" -ge 3 ]]; then
+if [[ ! -x "$python_bin" ]]; then
        echo "Python 3.6+ is required to run - or test - _postinstall.py" >&2;
        exit 1;
-elif [[ ! -x "$python_bin" && "$python_version" == 2 ]]; then
-       echo "Python ${python_version} is required to run - or test - 
_postinstall.py against Python 2" >&2;
 fi
 
 readonly TO_PASSWORD=twelve;
@@ -58,7 +49,6 @@ readonly ROOT_DIR="$(mktemp -d)";
 trap 'rm -rf $ROOT_DIR' EXIT;
 
 "$python_bin" <<EOF;
-from __future__ import print_function
 import importlib
 import sys
 from os.path import dirname, join
@@ -110,7 +100,6 @@ cat > "$ROOT_DIR/opt/traffic_ops/app/conf/cdn.conf" <<EOF
 EOF
 
 "$python_bin" <<TESTS 2>/dev/null | tee -a "${ROOT_DIR}/stdout";
-from __future__ import print_function
 import subprocess
 import sys
 import _postinstall
@@ -359,13 +348,9 @@ fi
 readonly 
USERS_JSON_FILE="$ROOT_DIR/opt/traffic_ops/install/data/json/users.json";
 
 "$python_bin" <<EOF
-from __future__ import print_function
 import json
 import sys
 
-if sys.version_info.major < 3:
-       str = unicode
-
 try:
        with open('$USERS_JSON_FILE') as fd:
                users_json = json.load(fd)
@@ -430,14 +415,10 @@ if [[ "$DB_CONF_ACTUAL" != "$DB_CONF_EXPECTED" ]]; then
 fi
 
 "$python_bin" <<EOF
-from __future__ import print_function
 import json
 import string
 import sys
 
-if sys.version_info.major < 3:
-       str = unicode
-
 try:
        with(open('$ROOT_DIR/opt/traffic_ops/app/conf/cdn.conf')) as fd:
                conf = json.load(fd)
@@ -479,7 +460,7 @@ if conf['traffic_ops_golang']['key']!= key:
        print('Incorrect key in cdn.conf, expected:', key, 'got:', 
conf['traffic_ops_golang']['key'], file=sys.stderr)
        exit(1)
 
-if conf['traffic_ops_golang']['port'] != '443':
+if conf['traffic_ops_golang']['port'] != "443":
        print('Incorrect traffic_ops_golang.port, expected: 443, got:', 
conf['traffic_ops_golang']['port'], file=sys.stderr)
        exit(1)
 
@@ -530,7 +511,3 @@ if [[ "$KEY_FILE_TYPE" != "$KEY_FILE: PEM RSA private key" 
]]; then
        echo "Incorrect key file, expected PEM RSA private key, got: 
$KEY_FILE_TYPE" >&2;
        exit 1;
 fi
-
-if [[ "$python_version" != 2 && -z "$skip_python2" ]]; then
-       exec "${MY_DIR}/$(basename "${BASH_SOURCE[0]}")" -2;
-fi;


Reply via email to