You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2020/07/10 20:43:27 UTC

[GitHub] [trafficcontrol] rob05c commented on a change in pull request #4872: DRAFT: traffic_ops_ort.pl transliteration to go

rob05c commented on a change in pull request #4872:
URL: https://github.com/apache/trafficcontrol/pull/4872#discussion_r452941642



##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()

Review comment:
       This needs to check the error. I'd say if there's no override arg, and this returns an err, to just exit.
   But it needs to at least log an error.

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()
+	}
+
+	retries := *retriesPtr
+	revalWaitTime := time.Second * time.Duration(*revalWaitTimePtr)
+	reverseProxyDisable := *reverseProxyDisablePtr
+	runMode := *runModePtr
+	skipOsCheck := *skipOSCheckPtr
+	toTimeoutMS := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	waitForParents := *waitForParentsPtr
+	help := *helpPtr
+
+	if help {
+		Usage()
+		os.Exit(0)
+	}
+
+	runModeStr := strings.ToUpper(runMode)
+	var _runMode Mode

Review comment:
       Go local variables should be `camelCase`, shouldn't start with an underscore.

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()
+	}
+
+	retries := *retriesPtr
+	revalWaitTime := time.Second * time.Duration(*revalWaitTimePtr)
+	reverseProxyDisable := *reverseProxyDisablePtr
+	runMode := *runModePtr
+	skipOsCheck := *skipOSCheckPtr
+	toTimeoutMS := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	waitForParents := *waitForParentsPtr
+	help := *helpPtr
+
+	if help {
+		Usage()
+		os.Exit(0)
+	}
+
+	runModeStr := strings.ToUpper(runMode)
+	var _runMode Mode

Review comment:
       Personally, I prefer to always use the := syntax, runMode := Mode(0) because
   (1) It lets you use a consistent declaration syntax, instead of having 2 different declaration syntaxes. It's always better to have One Right Way to do things.
   (2) It makes the initialization explicit. It isn't always obvious what the zero-value is with var, but with := it's immediately visible.
   
   But not everyone feels that way, so I won't push for := and avoiding var if you don't agree.

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()
+	}
+
+	retries := *retriesPtr
+	revalWaitTime := time.Second * time.Duration(*revalWaitTimePtr)
+	reverseProxyDisable := *reverseProxyDisablePtr
+	runMode := *runModePtr
+	skipOsCheck := *skipOSCheckPtr
+	toTimeoutMS := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	waitForParents := *waitForParentsPtr
+	help := *helpPtr
+
+	if help {
+		Usage()
+		os.Exit(0)
+	}
+
+	runModeStr := strings.ToUpper(runMode)
+	var _runMode Mode
+	switch runModeStr {
+	case "INTERACTIVE":
+		_runMode = INTERACTIVE
+	case "REPORT":
+		_runMode = REPORT
+	case "BADASS":
+		_runMode = BADASS
+	case "SYNCDS":
+		_runMode = SYNCDS
+	case "REVALIDATE":
+		_runMode = REVALIDATE
+	default:
+		fmt.Fprintf(os.Stderr, "%s is an invalid mode.\n", runModeStr)

Review comment:
       Is there a reason this prints and exits, but other errors later return? Should this likewise return and let the caller exit?

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()
+	}
+
+	retries := *retriesPtr
+	revalWaitTime := time.Second * time.Duration(*revalWaitTimePtr)
+	reverseProxyDisable := *reverseProxyDisablePtr
+	runMode := *runModePtr
+	skipOsCheck := *skipOSCheckPtr
+	toTimeoutMS := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	waitForParents := *waitForParentsPtr
+	help := *helpPtr
+
+	if help {
+		Usage()
+		os.Exit(0)
+	}
+
+	runModeStr := strings.ToUpper(runMode)
+	var _runMode Mode
+	switch runModeStr {
+	case "INTERACTIVE":
+		_runMode = INTERACTIVE
+	case "REPORT":
+		_runMode = REPORT
+	case "BADASS":
+		_runMode = BADASS
+	case "SYNCDS":
+		_runMode = SYNCDS
+	case "REVALIDATE":
+		_runMode = REVALIDATE
+	default:
+		fmt.Fprintf(os.Stderr, "%s is an invalid mode.\n", runModeStr)
+		Usage()
+		os.Exit(-1)

Review comment:
       It's easier to debug and automate scripts with unique error codes. Would you object to putting this in a const, and making it something unique instead of `-1`? I'd suggest starting at `132` since lower codes are used by Linux and Bash.

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]

Review comment:
       Nitpick: path/filepath.Join

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {

Review comment:
       Nitpick: `else` could be removed, reduce indentation and make it easier to read

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)

Review comment:
       Nitpick: `sl := []string{}` is terser

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {

Review comment:
       Nitpick: since the above returns, this `else` isn't necessary, can reduce indentation to make it easier to read

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]

Review comment:
       Go local vars shouldn't be camelCase, not start with `_`. I'd also vote more than 1 letter, for variables with a scope this long.

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"

Review comment:
       This needs to be vendored. Traffic Control vendors all libraries, to prevent things from breaking if they change on the internet, and to make building without the internet possible.

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
+		if rc == 1 { // file is not part of any package.
+			return nil, nil
+		} else if rc == 0 { // add the package name the file belongs to.
+			log.Debugf("output from file-query: %s\n", string(output))
+			result = append(result, string(strings.TrimSpace(string(output))))
+			log.Debugf("result length: %d, result: %s\n", len(result), string(output))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-provides": // returns the package name that provides 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
+		log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
+		if rc == 1 { // no package provides 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-query": // returns the package name for 'name'.
+		output, rc, err := ExecCommand("/bin/rpm", "-q", name)
+		if rc == 1 { // the package is not installed.
+			return nil, nil
+		} else if rc == 0 { // add the rpm name
+			result = append(result, string(strings.TrimSpace(string(output))))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-requires": // returns a list of packages that requires package 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
+		if rc == 1 { // no package reuires package 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func RandomDuration(max time.Duration) time.Duration {
+	rand.Seed(time.Now().UnixNano())
+	return time.Duration(rand.Int63n(int64(max)))
+}
+
+func CheckUser(cfg config.Cfg) bool {
+	result := true
+	user, _ := user.Current()
+
+	switch cfg.RunMode {
+	case config.BADASS:
+		fallthrough
+	case config.INTERACTIVE:
+		fallthrough
+	case config.SYNCDS:
+		if user.Username != "root" {
+			log.Errorf("Only the root user may run in BADASS, INTERACTIVE, or SYNCDS mode, current user: %s\n",
+				user.Username)
+			result = false
+		}
+	default:
+		log.Infof("current mode: %s, run user: %s\n", cfg.RunMode, user.Username)
+	}
+	return result
+}
+
+func CleanTmpDir() bool {
+	now := time.Now().Unix()
+
+	files, err := ioutil.ReadDir(config.TmpBase)
+	if err != nil {
+		log.Errorf("opening %s: %v\n", config.TmpBase, err)
+		return false
+	}
+	for _, f := range files {
+		// remove any directory and its contents under the config.TmpBase (/tmp/ort) that
+		// is more than a week old.
+		if f.IsDir() && (now-f.ModTime().Unix()) > 604800 {
+			dir := config.TmpBase + "/" + f.Name()
+			if f.Name() == "." || f.Name() == ".." {
+				continue
+			}
+			err = os.RemoveAll(dir)

Review comment:
       Would you mind adding a safety check that `dir != "/"` here? I'm worried if `config.TmpBase` and `f.Name()` were both `/`, this could end up doing an `rm /`.
   
   Maybe I'm paranoid, but that'd be super-bad, I'd feel better if we at least checked for it

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0

Review comment:
       Go consts should be CamelCase, not ALL_CAPS
   https://github.com/golang/go/wiki/CodeReviewComments#mixed-caps

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)

Review comment:
       This should check if `status.Name == nil`. It'll panic if it gets a bad object from TO.

##########
File path: traffic_ops_ort/traffic_ops_t3c/config/config.go
##########
@@ -0,0 +1,343 @@
+package config
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/pborman/getopt/v2"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+)
+
+const (
+	StatusDir          = "/opt/ort/status"
+	AtsTcConfig        = "/opt/ort/atstccfg"
+	Chkconfig          = "/sbin/chkconfig"
+	Service            = "/sbin/service"
+	SystemCtl          = "/bin/systemctl"
+	TmpBase            = "/tmp/ort"
+	TSHome             = "/opt/trafficserver"
+	TrafficCtl         = TSHome + "/bin/traffic_ctl"
+	TrafficServerOwner = "ats"
+)
+
+type Mode int
+
+const (
+	BADASS      Mode = 0
+	INTERACTIVE      = 1
+	REPORT           = 2
+	REVALIDATE       = 3
+	SYNCDS           = 4
+)
+
+func (m Mode) String() string {
+	switch m {
+	case 0:
+		return "BADASS"
+	case 1:
+		return "INTERACTIVE"
+	case 2:
+		return "REPORT"
+	case 3:
+		return "REVALIDATE"
+	case 4:
+		return "SYNCDS"
+	}
+	return ""
+}
+
+type SvcManagement int
+
+const (
+	UNKNOWN = 0
+	SYSTEMD = 1
+	SYSTEMV = 2 // legacy System V Init.
+)
+
+func (s SvcManagement) String() string {
+	switch s {
+	case UNKNOWN:
+		return "UNKNOWN"
+	case SYSTEMD:
+		return "SYSTEMD"
+	case SYSTEMV:
+		return "SYSTEMV"
+	}
+	return "UNKNOWN"
+}
+
+type Cfg struct {
+	Dispersion          time.Duration
+	LogLocationDebug    string
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	LoginDispersion     time.Duration
+	CacheHostName       string
+	SvcManagement       SvcManagement
+	Retries             int
+	RevalWaitTime       time.Duration
+	ReverseProxyDisable bool
+	RunMode             Mode
+	SkipOSCheck         bool
+	TOTimeoutMS         time.Duration
+	TOUser              string
+	TOPass              string
+	TOURL               string
+	WaitForParents      bool
+	YumOptions          string
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationDebug) }
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // event logging is not used.
+
+func GetCfg() (Cfg, error) {
+	var err error
+
+	dispersionPtr := getopt.IntLong("dispersion", 'D', 300, "[seconds] wait a random number of seconds between 0 and [seconds] before starting, default 300")
+	loginDispersionPtr := getopt.IntLong("login-dispersion", 'l', 0, "[seconds] wait a random number of seconds betwee 0 and [seconds] befor login to traffic ops, default 0")
+	logLocationDebugPtr := getopt.StringLong("log-location-debug", 'd', "", "Where to log debugs. May be a file path, stdout, stderr, or null, default ''")
+	logLocationErrorPtr := getopt.StringLong("log-location-error", 'e', "stderr", "Where to log errors. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationInfoPtr := getopt.StringLong("log-location-info", 'i', "stderr", "Where to log info. May be a file path, stdout, stderr, or null, default stderr")
+	logLocationWarnPtr := getopt.StringLong("log-location-warning", 'w', "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null, default stderr")
+	cacheHostNamePtr := getopt.StringLong("cache-host-name", 'H', "", "Host name of the cache to generate config for. Must be the server host name in Traffic Ops, not a URL, and not the FQDN")
+	retriesPtr := getopt.IntLong("num-retries", 'r', 3, "[number] retry connection to Traffic Ops URL [number] times, default is 3")
+	revalWaitTimePtr := getopt.IntLong("reval-wait-time", 'T', 60, "[seconds] wait a random number of seconds between 0 and [seconds] before revlidation, default is 60")
+	reverseProxyDisablePtr := getopt.BoolLong("reverse-proxy-disable", 'p', "[false | true] bypass the reverse proxy even if one has been configured default is false")
+	runModePtr := getopt.StringLong("run-mode", 'm', "report", "[badass | interactive | report | revalidate | syncds] run mode, default is 'report'")
+	skipOSCheckPtr := getopt.BoolLong("skip-os-check", 's', "[false | true] skip os check, default is false")
+	toTimeoutMSPtr := getopt.IntLong("traffic-ops-timeout-milliseconds", 't', 30000, "Timeout in milli-seconds for Traffic Ops requests, default is 30000")
+	toURLPtr := getopt.StringLong("traffic-ops-url", 'u', "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL")
+	toUserPtr := getopt.StringLong("traffic-ops-user", 'U', "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER")
+	toPassPtr := getopt.StringLong("traffic-ops-password", 'P', "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS")
+	waitForParentsPtr := getopt.BoolLong("wait-for-parents", 'W', "[true | false] do not update if parent_pending = 1 in the update json. default is false, wait for parents")
+	helpPtr := getopt.BoolLong("help", 'h', "Print usage information and exit")
+	getopt.Parse()
+
+	dispersion := time.Second * time.Duration(*dispersionPtr)
+	loginDispersion := time.Second * time.Duration(*loginDispersionPtr)
+	logLocationDebug := *logLocationDebugPtr
+	logLocationError := *logLocationErrorPtr
+	logLocationInfo := *logLocationInfoPtr
+	logLocationWarn := *logLocationWarnPtr
+
+	var cacheHostName string
+	if len(*cacheHostNamePtr) > 0 {
+		cacheHostName = *cacheHostNamePtr
+	} else {
+		cacheHostName, _ = os.Hostname()
+	}
+
+	retries := *retriesPtr
+	revalWaitTime := time.Second * time.Duration(*revalWaitTimePtr)
+	reverseProxyDisable := *reverseProxyDisablePtr
+	runMode := *runModePtr
+	skipOsCheck := *skipOSCheckPtr
+	toTimeoutMS := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	waitForParents := *waitForParentsPtr
+	help := *helpPtr
+
+	if help {
+		Usage()
+		os.Exit(0)
+	}
+
+	runModeStr := strings.ToUpper(runMode)
+	var _runMode Mode
+	switch runModeStr {
+	case "INTERACTIVE":
+		_runMode = INTERACTIVE
+	case "REPORT":
+		_runMode = REPORT
+	case "BADASS":
+		_runMode = BADASS
+	case "SYNCDS":
+		_runMode = SYNCDS
+	case "REVALIDATE":
+		_runMode = REVALIDATE
+	default:
+		fmt.Fprintf(os.Stderr, "%s is an invalid mode.\n", runModeStr)
+		Usage()
+		os.Exit(-1)
+	}
+
+	urlSourceStr := "argument" // for error messages
+	if toURL == "" {
+		urlSourceStr = "environment variable"
+		toURL = os.Getenv("TO_URL")
+	}
+	if toUser == "" {
+		toUser = os.Getenv("TO_USER")
+	}
+	if toPass == "" {
+		toPass = os.Getenv("TO_PASS")
+	}
+
+	usageStr := "basic usage: traffic_ops_t3c  --traffic-ops-url=myurl --traffic-ops-user=myuser --traffic-ops-password=mypass --cache-host-name=my-cache"
+	if strings.TrimSpace(toURL) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-url or TO_URL environment variable. " + usageStr)
+	}
+	if strings.TrimSpace(toUser) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-user or TO_USER environment variable. " + usageStr)
+	}
+	if strings.TrimSpace(toPass) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-password or TO_PASS environment variable. " + usageStr)
+	}
+	if strings.TrimSpace(cacheHostName) == "" {
+		return Cfg{}, errors.New("Missing required argument --cache-host-name. " + usageStr)
+	}
+
+	toURLParsed, err := url.Parse(toURL)
+	if err != nil {
+		return Cfg{}, errors.New("parsing Traffic Ops URL from " + urlSourceStr + " '" + toURL + "': " + err.Error())
+	} else if err = validateURL(toURLParsed); err != nil {
+		return Cfg{}, errors.New("invalid Traffic Ops URL from " + urlSourceStr + " '" + toURL + "': " + err.Error())
+	}
+
+	svcManagement := getOSSvcManagement()
+	yumOptions := os.Getenv("YUM_OPTIONS")
+
+	cfg := Cfg{
+		Dispersion:          dispersion,
+		LogLocationDebug:    logLocationDebug,
+		LogLocationErr:      logLocationError,
+		LogLocationInfo:     logLocationInfo,
+		LogLocationWarn:     logLocationWarn,
+		LoginDispersion:     time.Second * time.Duration(loginDispersion),
+		CacheHostName:       cacheHostName,
+		SvcManagement:       svcManagement,
+		Retries:             retries,
+		RevalWaitTime:       revalWaitTime,
+		ReverseProxyDisable: reverseProxyDisable,
+		RunMode:             _runMode,
+		SkipOSCheck:         skipOsCheck,
+		TOTimeoutMS:         toTimeoutMS,
+		TOUser:              toUser,
+		TOPass:              toPass,
+		TOURL:               toURL,
+		WaitForParents:      waitForParents,
+		YumOptions:          yumOptions,
+	}
+
+	if err = log.InitCfg(cfg); err != nil {
+		return Cfg{}, errors.New("Initializing loggers: " + err.Error() + "\n")
+	}
+
+	printConfig(cfg)
+
+	return cfg, nil
+}
+
+func validateURL(u *url.URL) error {
+	if u == nil {
+		return errors.New("nil url")
+	}
+	if u.Scheme != "http" && u.Scheme != "https" {
+		return errors.New("scheme expected 'http' or 'https', actual '" + u.Scheme + "'")
+	}
+	if strings.TrimSpace(u.Host) == "" {
+		return errors.New("no host")
+	}
+	return nil
+}
+
+func isCommandAvailable(name string) bool {
+	cmd := exec.Command("/bin/sh", "-c", name, "--version")
+	if err := cmd.Run(); err != nil {
+		return false
+	}
+
+	return true
+}
+
+func getOSSvcManagement() SvcManagement {
+	var _svcManager SvcManagement
+
+	if isCommandAvailable(SystemCtl) {
+		_svcManager = SYSTEMD
+	} else if isCommandAvailable(Service) {
+		_svcManager = SYSTEMV
+	}
+	if !isCommandAvailable(Chkconfig) {
+		return UNKNOWN
+	}
+
+	// we have what we need
+	return _svcManager
+}
+
+func printConfig(cfg Cfg) {
+	log.Debugf("Dispersion: %d\n", cfg.Dispersion)
+	log.Debugf("LogLocationDebug: %s\n", cfg.LogLocationDebug)
+	log.Debugf("LogLocationErr: %s\n", cfg.LogLocationErr)
+	log.Debugf("LogLocationInfo: %s\n", cfg.LogLocationInfo)
+	log.Debugf("LogLocationWarn: %s\n", cfg.LogLocationWarn)
+	log.Debugf("LoginDispersion: %d\n", cfg.LoginDispersion)
+	log.Debugf("CacheHostName: %s\n", cfg.CacheHostName)
+	log.Debugf("SvcManagement: %s\n", cfg.SvcManagement)
+	log.Debugf("Retries: %d\n", cfg.Retries)
+	log.Debugf("RevalWaitTime: %d\n", cfg.RevalWaitTime)
+	log.Debugf("ReverseProxyDisable: %t\n", cfg.ReverseProxyDisable)
+	log.Debugf("RunMode: %s\n", cfg.RunMode)
+	log.Debugf("SkipOSCheck: %t\n", cfg.SkipOSCheck)
+	log.Debugf("TOTimeoutMS: %d\n", cfg.TOTimeoutMS)
+	log.Debugf("TOUser: %s\n", cfg.TOUser)
+	log.Debugf("TOPass: %s\n", cfg.TOPass)
+	log.Debugf("TOURL: %s\n", cfg.TOURL)
+	log.Debugf("WaitForParents: %t\n", cfg.WaitForParents)
+	log.Debugf("YumOptions: %s\n", cfg.YumOptions)
+}
+
+func Usage() {

Review comment:
       Could this use https://godoc.org/github.com/pborman/getopt#PrintUsage ?

##########
File path: traffic_ops_ort/traffic_ops_t3c/traffic_ops_t3c.go
##########
@@ -0,0 +1,169 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/torequest"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"os"
+	"time"
+)
+
+func runSysctl(cfg config.Cfg) {
+	var ans bool
+
+	switch cfg.RunMode {
+	case config.INTERACTIVE:
+		ans, _ = util.AskYesNo("sysctl configuration has changed. 'sysctl -p' needs to be run. Should I do that now? (Y/[n])")
+	case config.BADASS:
+		ans = true
+	}
+
+	if ans == true {
+		_, rc, err := util.ExecCommand("/usr/sbin/sysctl", "-p")

Review comment:
       This should be in the path, right? Better to use just `sysctl` or `/usr/bin/env sysctl`, in case some Distro puts it somewhere else?

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.

Review comment:
       Should be `len(pkgs) == 0` for safety.
   `pkgs` could be length 0 but not nil, and `[0]` is dereferenced below.

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]

Review comment:
       I don't see a check for `len(r.pkgs)` anywhere. Is it ever possible for `len(r.pkgs) < pkgs[0]`? If so, this would panic.

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus

Review comment:
       Nitpick: `path/filepath.Join`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {
+		return errors.New("invalid config file data received from Traffic Ops, not in multipart format.")
+	}
+	msg := &mail.Message{
+		Header: map[string][]string{har[0]: {har[1]}},
+		Body:   bytes.NewReader(buf.Bytes()),
+	}
+	mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
+	if err != nil {
+		return err
+	}
+	if strings.HasPrefix(mediaType, "multipart/") {
+		mr := multipart.NewReader(msg.Body, params["boundary"])
+		// if the configFiles map is not empty, create a new map
+		// and the old map will be garbage collected.
+		if len(r.configFiles) > 0 {
+			log.Infoln("intializing a new configFiles map")
+			r.configFiles = make(map[string]*ConfigFile)
+		}
+		for {
+			var cf *ConfigFile
+
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				return nil
+			}
+			if err != nil {
+				return err
+			}
+			dataBytes, err := ioutil.ReadAll(p)
+			if err != nil {
+				return err
+			}
+			path := p.Header.Get("Path")
+			if path != "" {
+				cf = new(ConfigFile)

Review comment:
       Nitpick: could be created in a single call and without `new`: 
   ```
   r.configFiles[cf.Name]  = &ConfigFile{
   	Header: p.Header,
   	Name: filepath.Base(path),
   	Path: path,
   	Dir: filepath.Dir(path),
   	Body: dataBytes,
   	Uid: atsUid,
   	Gid: atsGid,
   	Perm: 0644,
   }
   ```

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)

Review comment:
       I haven't tested yet, but one of the things I had to fix in the Perl, was that calling `rpm` for every file and/or remap line was super-slow, it made ORT take like 20mins instead of 20s. This will probably need to cache the results for the same name.
   
   I'll try to verify when I run the code.

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin

Review comment:
       Nitpick: https://golang.org/pkg/path/filepath/#Join would be better.
   It guarantees the right slashes are used for the OS, and multiple duplicate slashes are removed.

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops

Review comment:
       GoDoc, `GetConfigFileList fetches and parses the multipart config files for a cache from Traffic Ops.`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {

Review comment:
       Should this trim spaces first, in case it's " # foo" ?

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
+		if rc == 1 { // file is not part of any package.
+			return nil, nil
+		} else if rc == 0 { // add the package name the file belongs to.
+			log.Debugf("output from file-query: %s\n", string(output))
+			result = append(result, string(strings.TrimSpace(string(output))))
+			log.Debugf("result length: %d, result: %s\n", len(result), string(output))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-provides": // returns the package name that provides 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
+		log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
+		if rc == 1 { // no package provides 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-query": // returns the package name for 'name'.
+		output, rc, err := ExecCommand("/bin/rpm", "-q", name)
+		if rc == 1 { // the package is not installed.
+			return nil, nil
+		} else if rc == 0 { // add the rpm name
+			result = append(result, string(strings.TrimSpace(string(output))))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-requires": // returns a list of packages that requires package 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
+		if rc == 1 { // no package reuires package 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func RandomDuration(max time.Duration) time.Duration {
+	rand.Seed(time.Now().UnixNano())
+	return time.Duration(rand.Int63n(int64(max)))
+}
+
+func CheckUser(cfg config.Cfg) bool {
+	result := true
+	user, _ := user.Current()
+
+	switch cfg.RunMode {
+	case config.BADASS:
+		fallthrough
+	case config.INTERACTIVE:
+		fallthrough
+	case config.SYNCDS:
+		if user.Username != "root" {
+			log.Errorf("Only the root user may run in BADASS, INTERACTIVE, or SYNCDS mode, current user: %s\n",
+				user.Username)
+			result = false
+		}
+	default:
+		log.Infof("current mode: %s, run user: %s\n", cfg.RunMode, user.Username)
+	}
+	return result
+}
+
+func CleanTmpDir() bool {
+	now := time.Now().Unix()
+
+	files, err := ioutil.ReadDir(config.TmpBase)
+	if err != nil {
+		log.Errorf("opening %s: %v\n", config.TmpBase, err)
+		return false
+	}
+	for _, f := range files {
+		// remove any directory and its contents under the config.TmpBase (/tmp/ort) that
+		// is more than a week old.
+		if f.IsDir() && (now-f.ModTime().Unix()) > 604800 {
+			dir := config.TmpBase + "/" + f.Name()

Review comment:
       Nitpick: `filepath.Join`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")

Review comment:
       Just to let you know, I asked on the Mailing List, and nobody ever uses Interactive Mode, so we agreed to remove it.
   
   Not a huge deal if you want to leave it in for now. But you could remove it too if you wanted.

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {

Review comment:
       Is this necessary? It seems like anything calling it can just call `string.Contains`?

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0

Review comment:
       Nitpick: all of these can be terser with `:=`, and they can avoid the separate map/slice initialization:
   `lineCount := 0`, `overrides := map[string]int{}`, `newLines := []string{}`

##########
File path: traffic_ops_ort/traffic_ops_t3c/traffic_ops_t3c.go
##########
@@ -0,0 +1,169 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/torequest"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"os"
+	"time"
+)
+
+func runSysctl(cfg config.Cfg) {
+	var ans bool
+
+	switch cfg.RunMode {
+	case config.INTERACTIVE:
+		ans, _ = util.AskYesNo("sysctl configuration has changed. 'sysctl -p' needs to be run. Should I do that now? (Y/[n])")
+	case config.BADASS:
+		ans = true
+	}
+
+	if ans == true {
+		_, rc, err := util.ExecCommand("/usr/sbin/sysctl", "-p")
+		if err != nil {
+			log.Errorln("sysctl -p failed")
+		} else if rc == 0 {
+			log.Debugf("sysctl -p ran succesfully.")
+		}
+	}
+}
+
+func main() {
+	var syncdsUpdate torequest.UpdateStatus
+	var lock util.FileLock
+	cfg, err := config.GetCfg()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(-1)
+	}
+
+	trops := torequest.NewTrafficOpsReq(cfg)
+
+	// if doing os checks, insure there is a 'systemctl' or 'service' and 'chkconfig' commands.
+	if !cfg.SkipOSCheck && cfg.SvcManagement == config.UNKNOWN {
+		log.Errorln("OS checks are enabled and unable to find any know service management tools.")
+	}
+
+	// create and clean the config.TmpBase (/tmp/ort)
+	if !util.MkDir(config.TmpBase, cfg) {
+		os.Exit(1)
+	} else if !util.CleanTmpDir() {
+		os.Exit(1)
+	}
+	if cfg.RunMode != config.REPORT {
+		if !lock.GetLock(config.TmpBase + "/to_ort.lock") {
+			os.Exit(1)
+		}
+	}
+
+	fmt.Println(time.Now().Format(time.UnixDate))
+
+	if !util.CheckUser(cfg) {
+		lock.UnlockAndExit(-1)
+	}
+
+	toolName := trops.GetHeaderComment()
+	log.Debugf("toolname: %s\n", toolName)
+
+	// if running in REVALIDATE mode, check to see if it's
+	// necessary to continue
+	if cfg.RunMode == config.REVALIDATE {
+		syncdsUpdate, err = trops.CheckRevalidateState(false)
+		if err != nil || syncdsUpdate == torequest.UPDATE_TROPS_NOTNEEDED {
+			if err != nil {
+				log.Errorln(err)
+			}
+			os.Exit(1)
+		}
+	} else {
+		syncdsUpdate, err = trops.CheckSyncDSState()
+		if err != nil {
+			log.Errorln(err)
+			os.Exit(1)

Review comment:
       Custom/const error codes?

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
+		if rc == 1 { // file is not part of any package.
+			return nil, nil
+		} else if rc == 0 { // add the package name the file belongs to.
+			log.Debugf("output from file-query: %s\n", string(output))
+			result = append(result, string(strings.TrimSpace(string(output))))
+			log.Debugf("result length: %d, result: %s\n", len(result), string(output))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-provides": // returns the package name that provides 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
+		log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
+		if rc == 1 { // no package provides 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-query": // returns the package name for 'name'.
+		output, rc, err := ExecCommand("/bin/rpm", "-q", name)
+		if rc == 1 { // the package is not installed.
+			return nil, nil
+		} else if rc == 0 { // add the rpm name
+			result = append(result, string(strings.TrimSpace(string(output))))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-requires": // returns a list of packages that requires package 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
+		if rc == 1 { // no package reuires package 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func RandomDuration(max time.Duration) time.Duration {
+	rand.Seed(time.Now().UnixNano())
+	return time.Duration(rand.Int63n(int64(max)))
+}
+
+func CheckUser(cfg config.Cfg) bool {
+	result := true
+	user, _ := user.Current()
+
+	switch cfg.RunMode {
+	case config.BADASS:
+		fallthrough
+	case config.INTERACTIVE:
+		fallthrough
+	case config.SYNCDS:
+		if user.Username != "root" {
+			log.Errorf("Only the root user may run in BADASS, INTERACTIVE, or SYNCDS mode, current user: %s\n",
+				user.Username)
+			result = false
+		}
+	default:
+		log.Infof("current mode: %s, run user: %s\n", cfg.RunMode, user.Username)
+	}
+	return result
+}
+
+func CleanTmpDir() bool {
+	now := time.Now().Unix()
+
+	files, err := ioutil.ReadDir(config.TmpBase)
+	if err != nil {
+		log.Errorf("opening %s: %v\n", config.TmpBase, err)
+		return false
+	}
+	for _, f := range files {
+		// remove any directory and its contents under the config.TmpBase (/tmp/ort) that
+		// is more than a week old.
+		if f.IsDir() && (now-f.ModTime().Unix()) > 604800 {

Review comment:
       Mind putting `604800` in a constant, just so it's obvious what it is? It's not immediately obvious that it's a week.

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
+		if rc == 1 { // file is not part of any package.
+			return nil, nil
+		} else if rc == 0 { // add the package name the file belongs to.
+			log.Debugf("output from file-query: %s\n", string(output))
+			result = append(result, string(strings.TrimSpace(string(output))))
+			log.Debugf("result length: %d, result: %s\n", len(result), string(output))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-provides": // returns the package name that provides 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
+		log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
+		if rc == 1 { // no package provides 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-query": // returns the package name for 'name'.
+		output, rc, err := ExecCommand("/bin/rpm", "-q", name)
+		if rc == 1 { // the package is not installed.
+			return nil, nil
+		} else if rc == 0 { // add the rpm name
+			result = append(result, string(strings.TrimSpace(string(output))))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-requires": // returns a list of packages that requires package 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
+		if rc == 1 { // no package reuires package 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func RandomDuration(max time.Duration) time.Duration {
+	rand.Seed(time.Now().UnixNano())
+	return time.Duration(rand.Int63n(int64(max)))
+}
+
+func CheckUser(cfg config.Cfg) bool {
+	result := true
+	user, _ := user.Current()

Review comment:
       The error here needs handled

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.

Review comment:
       GoDoc comments should be full sentences, starting with the name of the symbol and ending in a period. `isPackageInstalled returns true/false if the named rpm package is installed.`
   
   This doesn't technically break GoDoc because lowercase private funcs aren't documented. But it's still a really good idea to conform to GoDoc so it's easier for people to read.
   
   Typo, `fale`.

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"

Review comment:
       Needs to be vendored

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.

Review comment:
       GoDoc, should be `PackageInfo runs the rpm command.`.
   
   Sentences below need capitalized too.

##########
File path: traffic_ops_ort/traffic_ops_t3c/util/util.go
##########
@@ -0,0 +1,367 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/gofrs/flock"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"os/exec"
+	"os/user"
+	"strings"
+	"time"
+)
+
+type FileLock struct {
+	f_lock    *flock.Flock
+	is_locked bool
+}
+
+// Try to get a file lock, non-blocking.
+func (f *FileLock) GetLock(lockFile string) bool {
+	f.f_lock = flock.New(lockFile)
+	is_locked, err := f.f_lock.TryLock()
+	f.is_locked = is_locked
+
+	if err != nil { // some OS error attempting to obtain a file lock
+		log.Errorf("unable to obtain a lock on %s\n", lockFile)
+		return false
+	}
+	if !f.is_locked { // another process is running.
+		log.Errorf("Another traffic_ops_t3c process is already running, try again later\n")
+		return false
+	}
+
+	return f.is_locked
+}
+
+// Releases a file lock and exits with the given status code.
+func (f *FileLock) UnlockAndExit(code int) {
+	if f.is_locked {
+		f.f_lock.Unlock()
+	}
+	os.Exit(code)
+}
+
+func AskYesNo(msg string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	for {
+		fmt.Printf("%s [Y/n]: ", msg)
+		ans, err := reader.ReadString('\n')
+		if err != nil {
+			return false, err
+		}
+
+		if strings.ToUpper(strings.TrimSpace(ans)) == "Y" {
+			return true, nil
+		} else if strings.ToUpper(strings.TrimSpace(ans)) == "N" {
+			return false, nil
+		}
+	}
+	return false, nil
+}
+
+func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
+	var output bytes.Buffer
+	cmd := exec.Command(fullCommand, arg...)
+	cmd.Stdout = &output
+	err := cmd.Run()
+	return output.Bytes(), cmd.ProcessState.ExitCode(), err
+}
+
+func FileExists(fn string) (bool, os.FileInfo) {
+	info, err := os.Stat(fn)
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return !info.IsDir(), info
+}
+
+func ReadFile(fn string) ([]byte, error) {
+	var data []byte
+	info, err := os.Stat(fn)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fn)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func serviceStatus(name string) (int, error) {
+	var pid int = -1
+	var result bool
+
+	output, _, err := ExecCommand("/usr/sbin/service", name, "status")
+	if err != nil {
+		return pid, errors.New("could not get status for service '" + name + "'\n")
+	}
+	lines := strings.Split(string(output), "\n")
+	for ii := range lines {
+		line := strings.TrimSpace(lines[ii])
+		if strings.Contains(line, "Active: active") {
+			result = true
+		}
+		if result && strings.Contains(line, "Main PID: ") {
+			fmt.Sscanf(line, "Main PID: %d", &pid)
+		}
+	}
+
+	return pid, nil
+}
+
+// start or restart the service 'service'. cmd is 'start | restart'
+func ServiceStart(service string, cmd string) (bool, error) {
+	log.Infof("ServiceStart called for '%s'\n", service)
+	pid, err := serviceStatus(service)
+	if err != nil {
+		return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
+	} else if pid > 0 && cmd == "start" {
+		log.Infof("service '%s' is already running, pid: %d\n", service, pid)
+	} else {
+		_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
+		if err != nil {
+			return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
+		} else if rc == 0 {
+			// service was sucessfully started
+			return true, nil
+		}
+	}
+	// not started, service is already running
+	return false, nil
+}
+
+func WriteFile(fn string, data []byte, perm os.FileMode) (int, error) {
+	return WriteFileWithOwner(fn, data, -1, -1, perm)
+}
+
+func WriteFileWithOwner(fn string, data []byte, uid int, gid int, perm os.FileMode) (int, error) {
+	fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fn + "': " + err.Error())
+	}
+	fd.Close()
+
+	if uid > -1 && gid > -1 {
+		err = os.Chown(fn, uid, gid)
+		if err != nil {
+			return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
+		}
+	}
+	return c, nil
+}
+
+func FileMatch(name string, pattern string) bool {
+	matched := strings.Contains(name, pattern)
+	return matched
+}
+
+func PackageAction(cmdstr string, name string) (bool, error) {
+	var rc int = -1
+	var err error = nil
+	var result bool = false
+
+	switch cmdstr {
+	case "info":
+		_, rc, err = ExecCommand("/usr/bin/yum", "info", name)
+	case "install":
+		_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
+	case "remove":
+		_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
+	}
+
+	if rc == 0 {
+		result = true
+		err = nil
+	}
+	return result, err
+}
+
+// runs the rpm command.
+// if the return code from rpm == 0, then a valid package list is returned.
+//
+// if the return code is 1, the the 'name' queried for is not part of a
+//   package or is not installed.
+//
+// otherwise, if the return code is not 0 or 1 and error is set, a general
+// rpm command execution error is assumed and the error is returned.
+func PackageInfo(cmdstr string, name string) ([]string, error) {
+	var result []string
+	switch cmdstr {
+	case "file-query": // returns the rpm name that owns the file 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
+		if rc == 1 { // file is not part of any package.
+			return nil, nil
+		} else if rc == 0 { // add the package name the file belongs to.
+			log.Debugf("output from file-query: %s\n", string(output))
+			result = append(result, string(strings.TrimSpace(string(output))))
+			log.Debugf("result length: %d, result: %s\n", len(result), string(output))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-provides": // returns the package name that provides 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
+		log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
+		if rc == 1 { // no package provides 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-query": // returns the package name for 'name'.
+		output, rc, err := ExecCommand("/bin/rpm", "-q", name)
+		if rc == 1 { // the package is not installed.
+			return nil, nil
+		} else if rc == 0 { // add the rpm name
+			result = append(result, string(strings.TrimSpace(string(output))))
+		} else if err != nil {
+			return nil, err
+		}
+	case "pkg-requires": // returns a list of packages that requires package 'name'
+		output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
+		if rc == 1 { // no package reuires package 'name'
+			return nil, nil
+		} else if rc == 0 {
+			pkgs := strings.Split(string(output), "\n")
+			for ii := range pkgs {
+				result = append(result, strings.TrimSpace(pkgs[ii]))
+			}
+		} else if err != nil {
+			return nil, err
+		}
+	}
+	return result, nil
+}
+
+func RandomDuration(max time.Duration) time.Duration {
+	rand.Seed(time.Now().UnixNano())
+	return time.Duration(rand.Int63n(int64(max)))
+}
+
+func CheckUser(cfg config.Cfg) bool {
+	result := true
+	user, _ := user.Current()

Review comment:
       This variable shadows the package name. Mind renaming it, just to avoid confusion?

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {

Review comment:
       GoDoc, `GetConfigFile fetches a 'Configfile' by file name.`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {
+		return errors.New("invalid config file data received from Traffic Ops, not in multipart format.")
+	}
+	msg := &mail.Message{
+		Header: map[string][]string{har[0]: {har[1]}},
+		Body:   bytes.NewReader(buf.Bytes()),
+	}
+	mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
+	if err != nil {
+		return err
+	}
+	if strings.HasPrefix(mediaType, "multipart/") {
+		mr := multipart.NewReader(msg.Body, params["boundary"])
+		// if the configFiles map is not empty, create a new map
+		// and the old map will be garbage collected.
+		if len(r.configFiles) > 0 {
+			log.Infoln("intializing a new configFiles map")
+			r.configFiles = make(map[string]*ConfigFile)
+		}
+		for {
+			var cf *ConfigFile
+
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				return nil
+			}
+			if err != nil {
+				return err
+			}
+			dataBytes, err := ioutil.ReadAll(p)
+			if err != nil {
+				return err
+			}
+			path := p.Header.Get("Path")
+			if path != "" {
+				cf = new(ConfigFile)
+				cf.Header = p.Header
+				cf.Name = filepath.Base(path)
+				cf.Path = path
+				cf.Dir = filepath.Dir(path)
+				cf.Body = dataBytes
+				cf.Uid = atsUid
+				cf.Gid = atsGid
+				cf.Perm = 0644
+			}
+			r.configFiles[cf.Name] = cf
+		}
+	}
+	return nil
+}
+
+// looks up the tm.toolname parameter from traffic ops.
+func (r *TrafficOpsReq) GetHeaderComment() string {
+	var toolname string = ""

Review comment:
       Go naming convention: `toolName := ""`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {
+		return errors.New("invalid config file data received from Traffic Ops, not in multipart format.")
+	}
+	msg := &mail.Message{
+		Header: map[string][]string{har[0]: {har[1]}},
+		Body:   bytes.NewReader(buf.Bytes()),
+	}
+	mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
+	if err != nil {
+		return err
+	}
+	if strings.HasPrefix(mediaType, "multipart/") {
+		mr := multipart.NewReader(msg.Body, params["boundary"])
+		// if the configFiles map is not empty, create a new map
+		// and the old map will be garbage collected.
+		if len(r.configFiles) > 0 {
+			log.Infoln("intializing a new configFiles map")
+			r.configFiles = make(map[string]*ConfigFile)
+		}
+		for {
+			var cf *ConfigFile
+
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				return nil
+			}
+			if err != nil {
+				return err
+			}
+			dataBytes, err := ioutil.ReadAll(p)
+			if err != nil {
+				return err
+			}
+			path := p.Header.Get("Path")
+			if path != "" {
+				cf = new(ConfigFile)
+				cf.Header = p.Header
+				cf.Name = filepath.Base(path)
+				cf.Path = path
+				cf.Dir = filepath.Dir(path)
+				cf.Body = dataBytes
+				cf.Uid = atsUid
+				cf.Gid = atsGid
+				cf.Perm = 0644
+			}
+			r.configFiles[cf.Name] = cf
+		}
+	}
+	return nil
+}
+
+// looks up the tm.toolname parameter from traffic ops.
+func (r *TrafficOpsReq) GetHeaderComment() string {
+	var toolname string = ""
+	out, err := r.atsTcExec("system-info")
+	if err != nil {
+		log.Errorln(err)
+	} else {
+		var result map[string]interface{}
+		if err = json.Unmarshal(out, &result); err != nil {
+			log.Errorln(err)
+		} else {
+			_tn := result["tm.toolname"]
+			if _tn, ok := _tn.(string); ok {
+				toolname = _tn
+				log.Infof("Found tm.toolname: %v\n", toolname)
+			} else {
+				log.Errorln("Did not find tm.toolname!")
+			}
+		}
+	}
+	return toolname
+}
+
+func (r *TrafficOpsReq) CheckRevalidateState(sleepOverride bool) (UpdateStatus, error) {
+	updateStatus := UPDATE_TROPS_NOTNEEDED
+	log.Infoln("Checking revalidate state.")
+
+	if r.Cfg.RunMode == config.REVALIDATE || sleepOverride {
+		serverStatus, err := r.getUpdateStatus()
+		log.Infof("my status: %s\n", serverStatus.Status)
+		if err != nil {
+			log.Errorln(err)
+			return updateStatus, err
+		} else {
+			if serverStatus.UseRevalPending == false {
+				log.Errorln("Update URL: Instant invalidate is not enabled.  Separated revalidation requires upgrading to Traffic Ops version 2.2 and enabling this feature.")
+				return UPDATE_TROPS_NOTNEEDED, nil
+			}
+			if serverStatus.RevalPending == true {
+				log.Errorln("Traffic Ops is signaling that a revalidation is waiting to be applied.")
+				updateStatus = UPDATE_TROPS_NEEDED
+				if serverStatus.ParentRevalPending == true {
+					log.Errorln("Traffic Ops is signaling that my parents need to revalidate.")
+					// no update needed until my parents are updated.
+					updateStatus = UPDATE_TROPS_NOTNEEDED
+				}
+			} else if serverStatus.RevalPending == false {
+				log.Errorln("In revalidate mode, but no update needs to be applied. I'm outta here.")
+				return UPDATE_TROPS_NOTNEEDED, nil
+			} else {
+				log.Errorln("Traffic Ops is signaling that no revalidations are waiting to be applied.")
+			}
+		}
+
+		err = r.checkStatusFiles(serverStatus.Status)
+		if err != nil {
+			log.Errorln(err)
+		}
+	}
+
+	return updateStatus, nil
+}
+
+func (r *TrafficOpsReq) CheckSyncDSState() (UpdateStatus, error) {
+	updateStatus := UPDATE_TROPS_NOTNEEDED
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion)
+	log.Debugln("Checking syncds state.")
+	if r.Cfg.RunMode == config.SYNCDS || r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.REPORT {
+		serverStatus, err := r.getUpdateStatus()
+		if err != nil {
+			log.Errorln(err)
+			return updateStatus, err
+		}
+
+		if serverStatus.UpdatePending {
+			if r.Cfg.Dispersion > 0 {
+				log.Infof("Sleeping for %ds (dispersion) before proceeding with updates.\n\n", (randDispSec / time.Second))
+				r.sleepTimer(serverStatus)
+			}
+			log.Errorln("Traffic Ops is signaling that an update is waiting to be applied")
+			updateStatus = UPDATE_TROPS_NEEDED
+
+			if serverStatus.ParentPending && r.Cfg.WaitForParents && !serverStatus.UseRevalPending {
+				log.Errorln("Traffic Ops is signaling that my parents need an update.")
+				if r.Cfg.RunMode == config.SYNCDS {
+					log.Infof("In syncds mode, sleeping for %ds to see if the update my parents need is cleared.", randDispSec/time.Second)
+					r.sleepTimer(serverStatus)
+					serverStatus, err = r.getUpdateStatus()
+					if err != nil {
+						return updateStatus, err
+					}
+					if serverStatus.ParentPending || serverStatus.ParentRevalPending {
+						log.Errorln("My parents still need an update, bailing.")
+						return UPDATE_TROPS_NOTNEEDED, nil
+					} else {
+						log.Debugln("The update on my parents cleared; continuing.")
+					}
+				}
+			} else {
+				log.Debugf("Traffic Ops is signaling that my parents do not need an update, or wait_for_parents is false.")
+			}
+		} else if r.Cfg.RunMode == config.SYNCDS {
+			log.Errorln("In syncds mode, but no syncds update needs to be applied.  Running revalidation befor exiting.")
+			r.RevalidateWhileSleeping()
+			return UPDATE_TROPS_NOTNEEDED, nil
+		} else {
+			log.Errorln("Traffic Ops is signaling that no update is waiting to be applied.")
+		}
+
+		// check local status files.
+		err = r.checkStatusFiles(serverStatus.Status)
+		if err != nil {
+			log.Errorln(err)
+		}
+	}
+	return updateStatus, nil
+}
+
+// process all config files retrieved from Traffic Ops.  perl version is process_config_files
+func (r *TrafficOpsReq) ProcessConfigFiles() (UpdateStatus, error) {
+	var updateStatus UpdateStatus = UPDATE_TROPS_NOTNEEDED
+
+	log.Infoln(" ======== Start processing config files ========")
+
+	for _, cfg := range r.configFiles {
+		// add service metadata
+		if util.FileMatch(cfg.Path, "/opt/trafficserver/") || util.FileMatch(cfg.Dir, "udev") {
+			arr, err := util.PackageInfo("pkg-query", "trafficserver")
+			if err != nil {
+				return UPDATE_TROPS_FAILED, errors.New("rpm query error: " + err.Error())
+			} else if len(arr) > 0 && strings.HasPrefix(arr[0], "trafficserver") {
+				cfg.Service = "trafficserver"
+			} else if r.Cfg.RunMode == config.SYNCDS {
+				return UPDATE_TROPS_FAILED, errors.New("In syncds mode, but trafficserver isn't installed. Bailing.")
+			}
+		} else if util.FileMatch(cfg.Path, "/opt/ort") && util.FileMatch(cfg.Name, "12M_facts") {
+			cfg.Service = "puppet"
+		} else if util.FileMatch(cfg.Path, "cron") || util.FileMatch(cfg.Name, "sysctl.conf") || util.FileMatch(cfg.Name, "50-ats.rules") || util.FileMatch(cfg.Name, "cron") {
+			cfg.Service = "system"
+		} else if util.FileMatch(cfg.Path, "ntp.conf") {
+			cfg.Service = "ntpd"
+		} else {
+			cfg.Service = "unknown"
+		}
+
+		log.Debugf("In %s mode, I'm about to process config file: %s, service: %s\n", r.Cfg.RunMode, cfg.Path, cfg.Service)
+
+		err := r.checkConfigFile(cfg)
+		if err != nil {
+			log.Errorln(err)
+		}
+	}
+
+	for _, cfg := range r.configFiles {
+		if cfg.ChangeNeeded &&
+			!cfg.ChangeApplied &&
+			cfg.AuditComplete &&
+			!cfg.PreReqFailed &&
+			!cfg.AuditFailed {
+			if cfg.Name == "plugin.config" && r.configFiles["remap.config"].PreReqFailed == true {
+				updateStatus = UPDATE_TROPS_FAILED
+				log.Errorln("plugin.config changed however, prereqs failed for remap.config so I am skipping updates for plugin.config")
+				continue
+			} else if cfg.Name == "remap.config" && r.configFiles["plugin.config"].PreReqFailed == true {
+				updateStatus = UPDATE_TROPS_FAILED
+				log.Errorln("remap.config changed however, prereqs failed for plugin.config so I am skipping updates for remap.config")
+				continue
+			} else {
+				log.Debugf("All Prereqs passed for replacing %s on disk with that in Traffic Ops.\n", cfg.Name)
+				err := r.replaceCfgFile(cfg)
+				if err != nil {
+					log.Errorf("failed to replace the config file, '%s',  on disk with data in Traffic Ops.\n", cfg.Name)
+				}
+			}
+		}
+	}
+	return updateStatus, nil
+}
+
+func (r *TrafficOpsReq) ProcessPackages() error {
+	var pkgs []Package
+	var install []string   // install package list.
+	var uninstall []string // uninstall package list
+
+	// get the package list for this cache from Traffic Ops.
+	out, err := r.atsTcExec("packages")
+	if err != nil {
+		return err
+	}
+
+	if err = json.Unmarshal(out, &pkgs); err != nil {
+		return err
+	}
+
+	// loop through the package list to build an install and uninstall list.
+	for ii := range pkgs {
+		var instpkg string // installed package
+		var reqpkg string  // required package
+		log.Infof("Processing package %s-%s\n", pkgs[ii].Name, pkgs[ii].Version)
+		// check to see if any package by name is installed.
+		arr, err := util.PackageInfo("pkg-query", pkgs[ii].Name)
+		if err != nil {
+			return err
+		}
+		// go needs the ternary operator :)

Review comment:
       Yes, yes it does ☹️  
   Expressions instead of statements would also be nice, 
   ```
   instpkg = if len(arr) == 1 {
     arr[0]
   } else {
    "" 
   }
   ```
   
   

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {
+		return errors.New("invalid config file data received from Traffic Ops, not in multipart format.")
+	}
+	msg := &mail.Message{
+		Header: map[string][]string{har[0]: {har[1]}},
+		Body:   bytes.NewReader(buf.Bytes()),
+	}
+	mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
+	if err != nil {
+		return err
+	}
+	if strings.HasPrefix(mediaType, "multipart/") {
+		mr := multipart.NewReader(msg.Body, params["boundary"])
+		// if the configFiles map is not empty, create a new map
+		// and the old map will be garbage collected.
+		if len(r.configFiles) > 0 {
+			log.Infoln("intializing a new configFiles map")
+			r.configFiles = make(map[string]*ConfigFile)
+		}
+		for {
+			var cf *ConfigFile
+
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				return nil
+			}
+			if err != nil {
+				return err
+			}
+			dataBytes, err := ioutil.ReadAll(p)
+			if err != nil {
+				return err
+			}
+			path := p.Header.Get("Path")
+			if path != "" {
+				cf = new(ConfigFile)
+				cf.Header = p.Header
+				cf.Name = filepath.Base(path)
+				cf.Path = path
+				cf.Dir = filepath.Dir(path)
+				cf.Body = dataBytes
+				cf.Uid = atsUid
+				cf.Gid = atsGid
+				cf.Perm = 0644
+			}
+			r.configFiles[cf.Name] = cf
+		}
+	}
+	return nil
+}
+
+// looks up the tm.toolname parameter from traffic ops.
+func (r *TrafficOpsReq) GetHeaderComment() string {
+	var toolname string = ""
+	out, err := r.atsTcExec("system-info")
+	if err != nil {
+		log.Errorln(err)
+	} else {
+		var result map[string]interface{}
+		if err = json.Unmarshal(out, &result); err != nil {
+			log.Errorln(err)
+		} else {
+			_tn := result["tm.toolname"]

Review comment:
       Go naming convention, `tn`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {

Review comment:
       This needs to check `len(har) > 1` before dereferencing `har[1]`, or it'll panic on malformed data

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {
+		return errors.New("invalid config file data received from Traffic Ops, not in multipart format.")
+	}
+	msg := &mail.Message{
+		Header: map[string][]string{har[0]: {har[1]}},
+		Body:   bytes.NewReader(buf.Bytes()),
+	}
+	mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))

Review comment:
       Nitpick: `rfc.ContentType`

##########
File path: traffic_ops_ort/traffic_ops_t3c/torequest/torequest.go
##########
@@ -0,0 +1,1466 @@
+package torequest
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/config"
+	"github.com/apache/trafficcontrol/traffic_ops_ort/traffic_ops_t3c/util"
+	"io"
+	"io/ioutil"
+	"mime"
+	"mime/multipart"
+	"net/mail"
+	"net/textproto"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+)
+
+type UpdateStatus int
+
+const (
+	UPDATE_TROPS_NOTNEEDED  UpdateStatus = 0
+	UPDATE_TROPS_NEEDED     UpdateStatus = 1
+	UPDATE_TROPS_SUCCESSFUL UpdateStatus = 2
+	UPDATE_TROPS_FAILED     UpdateStatus = 3
+)
+
+type Package struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+type TrafficOpsReq struct {
+	Cfg                  config.Cfg
+	pkgs                 map[string]bool // map of installed packages
+	plugins              map[string]bool // map of verified plugins
+	configFiles          map[string]*ConfigFile
+	baseBackupDir        string
+	TrafficCtlReload     bool // a traffic_ctl_reload is required
+	SysCtlReload         bool // a reload of the sysctl.conf is required
+	NtpdRestart          bool // ntpd needs restarting
+	TeakdRestart         bool // a restart of teakd is required
+	TrafficServerRestart bool // a trafficserver restart is required
+	RemapConfigReload    bool // remap.config should be reloaded
+}
+
+type ConfigFile struct {
+	Header            textproto.MIMEHeader
+	Name              string // file name
+	Dir               string // install directory
+	Path              string // full path
+	Service           string // service assigned to
+	CfgBackup         string // location to backup the config at 'Path'
+	TropsBackup       string // location to backup the TrafficOps Version
+	AuditComplete     bool   // audit is complete
+	AuditFailed       bool   // audit failed
+	ChangeApplied     bool   // a change has been applied
+	ChangeNeeded      bool   // change required
+	PreReqFailed      bool   // failed plugin prerequiste check
+	RemapPluginConfig bool   // file is a remap plugin config file
+	Body              []byte
+	Perm              os.FileMode // default file permissions
+	Uid               int         // owner uid, default is 0
+	Gid               int         // owner gid, default is 0
+}
+
+func (u UpdateStatus) String() string {
+	var result string
+	switch u {
+	case 0:
+		result = "UPDATE_TROPS_NOTNEEDED"
+	case 1:
+		result = "UPDATE_TROPS_NEEDED"
+	case 2:
+		result = "UPDATE_TROPS_SUCCESSFUL"
+	case 3:
+		result = "UPDATE_TROPS_FAILED"
+	}
+	return result
+}
+
+func commentsFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+
+	for ii := range body {
+		line := body[ii]
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+		newlines = append(newlines, line)
+	}
+
+	return newlines
+}
+
+func newLineFilter(str string) string {
+	str = strings.ReplaceAll(str, "\r\n", "\n")
+	return strings.TrimSpace(str)
+}
+
+func unencodeFilter(body []string) []string {
+	var newlines []string
+
+	newlines = make([]string, 0)
+	sp := regexp.MustCompile(`\s+`)
+	el := regexp.MustCompile(`^\s+|\s+$`)
+	am := regexp.MustCompile(`amp;`)
+	lt := regexp.MustCompile(`&lt;`)
+	gt := regexp.MustCompile(`&gt;`)
+
+	for ii := range body {
+		s := body[ii]
+		s = sp.ReplaceAllString(s, " ")
+		s = el.ReplaceAllString(s, "")
+		s = am.ReplaceAllString(s, "")
+		s = lt.ReplaceAllString(s, "<")
+		s = gt.ReplaceAllString(s, ">")
+		s = strings.TrimSpace(s)
+		newlines = append(newlines, s)
+	}
+
+	return newlines
+}
+
+// for debugging
+func (r *TrafficOpsReq) DumpConfigFiles() {
+	for _, cfg := range r.configFiles {
+		fmt.Printf("Name: %s, Dir: %s, Service: %s\n",
+			cfg.Name, cfg.Dir, cfg.Service)
+	}
+}
+
+// returns a new TrafficOpsReq object.
+func NewTrafficOpsReq(cfg config.Cfg) *TrafficOpsReq {
+	unixTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
+	// backup directories
+	bkupDir := config.TmpBase + "/" + unixTimeStr
+
+	return &TrafficOpsReq{
+		Cfg:           cfg,
+		pkgs:          make(map[string]bool),
+		plugins:       make(map[string]bool),
+		configFiles:   make(map[string]*ConfigFile),
+		baseBackupDir: bkupDir,
+	}
+}
+
+// wrapper to run an atstccfg command.
+func (r *TrafficOpsReq) atsTcExec(cmdstr string) ([]byte, error) {
+	log.Debugf("cmdstr: %s\n", cmdstr)
+	result, err := r.atsTcExecCommand(cmdstr, -1, -1)
+	return result, err
+}
+
+// runs an atstccfg command.
+func (r *TrafficOpsReq) atsTcExecCommand(cmdstr string, queueState int, revalState int) ([]byte, error) {
+	args := []string{
+		"--traffic-ops-timeout-milliseconds=" + strconv.FormatInt(int64(r.Cfg.TOTimeoutMS), 10),
+		"--traffic-ops-disable-proxy=" + strconv.FormatBool(r.Cfg.ReverseProxyDisable),
+		"--traffic-ops-user=" + r.Cfg.TOUser,
+		"--traffic-ops-password=" + r.Cfg.TOPass,
+		"--traffic-ops-url=" + r.Cfg.TOURL,
+		"--cache-host-name=" + r.Cfg.CacheHostName,
+		"--log-location-error=" + r.Cfg.LogLocationErr,
+		"--log-location-info=" + r.Cfg.LogLocationInfo,
+		"--log-location-warning=" + r.Cfg.LogLocationWarn,
+	}
+
+	switch cmdstr {
+	case "chkconfig":
+		args = append(args, "--get-data=chkconfig")
+	case "packages":
+		args = append(args, "--get-data=packages")
+	case "statuses":
+		args = append(args, "--get-data=statuses")
+	case "system-info":
+		args = append(args, "--get-data=system-info")
+	case "update-status":
+		args = append(args, "--get-data=update-status")
+	case "send-update":
+		var queueStatus string = "false"
+		var revalStatus string = "false"
+		if queueState > 0 {
+			queueStatus = "true"
+		}
+		if revalState > 0 {
+			revalStatus = "true"
+		}
+		args = append(args, "--set-queue-status", queueStatus, "--set-reval-status", revalStatus)
+	case "get-config-files":
+		if r.Cfg.RunMode == config.REVALIDATE {
+			args = append(args, "--revalidate-only")
+		}
+	default:
+		return nil, errors.New("invalid command '" + cmdstr + "'")
+	}
+
+	cmd := exec.Command(config.AtsTcConfig, args...)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	err := cmd.Run()
+	return out.Bytes(), err
+}
+
+// backup the config file.
+func (r *TrafficOpsReq) backUpFile(cfg *ConfigFile) error {
+
+	// init backup directories
+	configBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_bkp"
+	cfg.CfgBackup = configBkupDir + "/" + cfg.Name
+	tropsBkupDir := r.baseBackupDir + "/" + cfg.Service + "/config_trops"
+	cfg.TropsBackup = tropsBkupDir + "/" + cfg.Name
+
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		// create config file backup directory
+		err := os.MkdirAll(configBkupDir, 0755)
+		if err != nil {
+			return errors.New("Unable to create config backup directory '" + configBkupDir + "': " + err.Error())
+		}
+
+		// backup the config file
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.Path + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, configBkupDir, data)
+		if err != nil {
+			return errors.New("Failed to write the config file: " + err.Error())
+		}
+
+	} else {
+		log.Debugf("Config file: %s doesn't exist. No need to back up.\n", cfg.Name)
+	}
+
+	// backup the traffic ops file.
+	err := os.MkdirAll(tropsBkupDir, 0755)
+	if err != nil {
+		return errors.New("Unable to create Trops backup directory '" + tropsBkupDir + "': " + err.Error())
+	}
+	_, err = r.writeCfgFile(cfg, tropsBkupDir, cfg.Body)
+	if err != nil {
+		return errors.New("Failed to write the config file from traffic ops: " + err.Error())
+	}
+
+	// backup the current config file.
+	return nil
+}
+
+// checks and audits config files.  perl version is process_cfg_file
+func (r *TrafficOpsReq) checkConfigFile(cfg *ConfigFile) error {
+	if cfg.Name == "" {
+		cfg.AuditFailed = true
+		return errors.New("Config file name is empty is empty, skipping further checks.")
+	}
+
+	if cfg.Dir == "" {
+		return errors.New("No location information for " + cfg.Name)
+	}
+	// return if audit has already been done.
+	if cfg.AuditComplete == true {
+		return nil
+	}
+
+	if !util.MkDir(cfg.Dir, r.Cfg) {
+		return errors.New("Unable to create the directory '" + cfg.Dir + " for " + "'" + cfg.Name + "'")
+	}
+
+	log.Debugf("======== Start processing config file: %s ========\n", cfg.Name)
+
+	if cfg.Name == "remap.config" {
+		err := r.processRemapOverrides(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// perform plugin verification
+	if cfg.Name == "remap.config" || cfg.Name == "plugin.config" {
+		err := r.verifyPlugins(cfg)
+		if err != nil {
+			return err
+		}
+	}
+
+	// apply traffic ops data filters in preparation for comparison
+	// to data on disk.
+	tropsData := strings.Split(string(cfg.Body), "\n")
+	tropsData = unencodeFilter(tropsData)
+	tropsData = commentsFilter(tropsData)
+
+	var diskData []string
+	fileExists, _ := util.FileExists(cfg.Path)
+	if fileExists {
+		data, err := r.readCfgFile(cfg, "")
+		if err != nil {
+			return errors.New("reading from '" + cfg.Path + "' failed: " + err.Error())
+		}
+		diskData = strings.Split(string(data), "\n")
+	} else { // file doesn't exist on, it's new from Traffic Ops.
+		cfg.AuditComplete = true
+		cfg.ChangeNeeded = true
+		log.Infof("No such file on disk, '%s'\n", cfg.Path)
+	}
+
+	// apply disk file data filters in preparation for comparison
+	// to data from traffic ops
+	diskData = unencodeFilter(diskData)
+	diskData = commentsFilter(diskData)
+
+	// apply final new line filters disk and traffic ops data for comparison
+	disk := strings.Join(diskData, "\n")
+	disk = newLineFilter(disk)
+
+	trops := strings.Join(tropsData, "\n")
+	trops = newLineFilter(trops)
+
+	if disk != trops {
+		cfg.ChangeNeeded = true
+		log.Infof("change needed to %s\n", cfg.Name)
+		err := r.backUpFile(cfg)
+		if err != nil {
+			return errors.New("unable to back up '" + cfg.Name + "': " + err.Error())
+		}
+	} else {
+		cfg.ChangeNeeded = false
+		log.Infof("All lines match TrOps for config file: %s\n", cfg.Name)
+	}
+
+	if cfg.Name == "50-ats.rules" {
+		err := r.processUdevRules(cfg)
+		if err != nil {
+			return errors.New("unable to process udev rules in '" + cfg.Name + "': " + err.Error())
+		}
+	}
+
+	log.Infof("======== End processing config file: %s for service: %s ========\n", cfg.Name, cfg.Service)
+	cfg.AuditComplete = true
+
+	return nil
+}
+
+func (r *TrafficOpsReq) checkPlugin(plugin string) error {
+	// already verified
+	if r.plugins[plugin] == true {
+		return nil
+	}
+	pluginFile := config.TSHome + "/libexec/trafficserver/" + plugin
+	pkgs, err := util.PackageInfo("pkg-provides", pluginFile)
+	if err != nil {
+		return errors.New("unable to verify plugin " + pluginFile + ": " + err.Error())
+	}
+	if pkgs == nil { // no package is installed that provides the plugin.
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	_, ok := r.pkgs[pkgs[0]]
+	if !ok {
+		return errors.New(plugin + ": Package for plugin: " + plugin + ", is not installed.")
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) checkStatusFiles(svrStatus string) error {
+	if svrStatus == "" {
+		return errors.New("Returning; did not find status from Traffic Ops!")
+	} else {
+		log.Debugf("Found %s status from Traffic Ops.\n", svrStatus)
+	}
+	statusFile := config.StatusDir + "/" + svrStatus
+	fileExists, _ := util.FileExists(statusFile)
+	if !fileExists {
+		log.Errorf("status file %s does not exist.\n", statusFile)
+	}
+	statuses, err := r.getStatuses()
+	if err != nil {
+		return fmt.Errorf("could not retrieves a statuses list from Traffic Ops: %s\n", err)
+	}
+
+	for f := range statuses {
+		otherStatus := config.StatusDir + "/" + statuses[f]
+		if otherStatus == statusFile {
+			continue
+		}
+		fileExists, _ := util.FileExists(otherStatus)
+		if r.Cfg.RunMode != config.REPORT && fileExists {
+			log.Errorf("Removing other status file %s that exists\n", otherStatus)
+			err = os.Remove(otherStatus)
+			if err != nil {
+				log.Errorf("Error removing %s: %s\n", otherStatus, err)
+			}
+		}
+	}
+
+	if r.Cfg.RunMode != config.REPORT {
+		if !util.MkDir(config.StatusDir, r.Cfg) {
+			return fmt.Errorf("unable to create '%s'\n", config.StatusDir)
+		}
+		fileExists, _ := util.FileExists(statusFile)
+		if !fileExists {
+			err = util.Touch(statusFile)
+			if err != nil {
+				return fmt.Errorf("unable to touch %s - %s\n", statusFile, err)
+			}
+		}
+	}
+	return nil
+}
+
+// fetches a list of cache statuses from traffic ops.
+func (r *TrafficOpsReq) getStatuses() ([]string, error) {
+	var statuses []tc.StatusNullable
+	sl := make([]string, 0)
+	out, err := r.atsTcExec("statuses")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	} else {
+		if err = json.Unmarshal(out, &statuses); err != nil {
+			log.Errorln(err)
+			return nil, err
+		} else {
+			for val := range statuses {
+				sl = append(sl, *statuses[val].Name)
+			}
+		}
+	}
+
+	return sl, nil
+}
+
+func (r *TrafficOpsReq) getUpdateStatus() (*tc.ServerUpdateStatus, error) {
+	var status tc.ServerUpdateStatus
+	out, err := r.atsTcExec("update-status")
+	if err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	if err = json.Unmarshal(out, &status); err != nil {
+		log.Errorln(err)
+		return nil, err
+	}
+	log.Debugf("ServerUpdateStatus: %#v\n", status)
+	return &status, nil
+}
+
+func (r *TrafficOpsReq) processRemapOverrides(cfg *ConfigFile) error {
+	var from string
+	var newlines []string
+	var overrides map[string]int
+	var lineCount int = 0
+	var overrideCount int = 0
+	var overridenCount int = 0
+	data := cfg.Body
+
+	overrides = make(map[string]int)
+	newlines = make([]string, 0)
+
+	if len(data) > 0 {
+		lines := strings.Split(string(data), "\n")
+		for ii := range lines {
+			str := lines[ii]
+			fields := strings.Fields(str)
+			if str == "" || len(fields) < 2 {
+				continue
+			}
+			lineCount++
+			from = fields[1]
+
+			_, ok := overrides[from]
+			if ok == true { // check if this line should be overriden
+				newstr := "##OVERRIDDEN## " + str
+				newlines = append(newlines, newstr)
+				overridenCount++
+			} else if fields[0] == "##OVERRIDE##" { // check for an override
+				from = fields[2]
+				newlines = append(newlines, "##OVERRIDE##")
+				// remove the ##OVERRIDE## comment along with the trailing space
+				newstr := strings.TrimPrefix(str, "##OVERRIDE## ")
+				// save the remap 'from field' to overrides.
+				overrides[from] = 1
+				newlines = append(newlines, newstr)
+				overrideCount++
+			} else { // no override is necessary
+				newlines = append(newlines, str)
+			}
+		}
+	} else {
+		return errors.New("The " + cfg.Name + " file is empty, nothing to process.")
+	}
+	if overrideCount > 0 {
+		log.Infof("Overrode %d old remap rule(s) with %d new remap rule(s).\n",
+			overridenCount, overrideCount)
+		newdata := strings.Join(newlines, "\n")
+		// strings.Join doesn't add a newline character to
+		// the last element in the array and we need one
+		// when the data is written out to a file.
+		if !strings.HasSuffix(newdata, "\n") {
+			newdata = newdata + "\n"
+		}
+		body := []byte(newdata)
+		cfg.Body = body
+	}
+	return nil
+}
+
+// verify disk drive device ownership and mode
+func (r *TrafficOpsReq) processUdevRules(cfg *ConfigFile) error {
+	var udevDevices map[string]string
+
+	data := string(cfg.Body)
+	lines := strings.Split(data, "\n")
+
+	udevDevices = make(map[string]string)
+	for ii := range lines {
+		var owner string
+		var device string
+		line := lines[ii]
+		if strings.HasPrefix(line, "KERNEL==") {
+			vals := strings.Split(line, "\"")
+			if len(vals) >= 3 {
+				device = vals[1]
+				owner = vals[3]
+				if owner == "root" {
+					continue
+				}
+				userInfo, err := user.Lookup(owner)
+				if err != nil {
+					log.Errorf("no such user on this system: '%s'\n", owner)
+					continue
+				} else {
+					devPath := "/dev/" + device
+					fileExists, fileInfo := util.FileExists(devPath)
+					if fileExists {
+						udevDevices[device] = devPath
+						log.Infof("Found device in 50-ats.rules: %s\n", devPath)
+						if statStruct, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+							uid := strconv.Itoa(int(statStruct.Uid))
+							if uid != userInfo.Uid {
+								log.Errorf("Device %s is owned by uid %s, not %s (%s)\n", devPath, uid, owner, userInfo.Uid)
+							} else {
+								log.Infof("Ownership for disk device %s, is okay\n", devPath)
+							}
+						} else {
+							log.Errorf("Unable to read device owner info for %s\n", devPath)
+						}
+					}
+				}
+			}
+		}
+	}
+	fs, err := ioutil.ReadDir("/proc/fs/ext4")
+	if err != nil {
+		log.Errorln("unable to read /proc/fs/ext4, cannot audit disks for filesystem usage.")
+	} else {
+		for _, disk := range fs {
+			for k, _ := range udevDevices {
+				if strings.HasPrefix(k, disk.Name()) {
+					log.Warnf("Device %s has an active partition and filesystem!!!!\n", k)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// read a config file and return its contents.
+func (r *TrafficOpsReq) readCfgFile(cfg *ConfigFile, dir string) ([]byte, error) {
+	var data []byte
+	var fullFileName string
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	info, err := os.Stat(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	size := info.Size()
+
+	fd, err := os.Open(fullFileName)
+	if err != nil {
+		return nil, err
+	}
+	data = make([]byte, size)
+	c, err := fd.Read(data)
+	if err != nil || int64(c) != size {
+		return nil, errors.New("unable to completely read from '" + cfg.Name + "': " + err.Error())
+	}
+	fd.Close()
+
+	return data, nil
+}
+
+func (r *TrafficOpsReq) replaceCfgFile(cfg *ConfigFile) error {
+	var ans bool
+	if r.Cfg.RunMode == config.INTERACTIVE {
+		fmt.Printf("\nThe '%s' file on disk needs updated with one from Traffic Ops.\n", cfg.Name)
+		ans, _ = util.AskYesNo(" [Y] override files on disk with data from Traffic Ops, [N] ignore and continue.")
+	}
+	if ans == true ||
+		r.Cfg.RunMode == config.BADASS ||
+		r.Cfg.RunMode == config.SYNCDS ||
+		r.Cfg.RunMode == config.REVALIDATE {
+
+		log.Infof("Copying '%s' to '%s'\n", cfg.TropsBackup, cfg.Path)
+		data, err := util.ReadFile(cfg.TropsBackup)
+		if err != nil {
+			return errors.New("Unable to read the config file '" + cfg.TropsBackup + "': " + err.Error())
+		}
+		_, err = r.writeCfgFile(cfg, "", data)
+		if err != nil {
+			return errors.New("Failed to write the new config file: " + err.Error())
+		} else {
+			cfg.ChangeApplied = true
+			if cfg.RemapPluginConfig == true { // trafficserver config reload is required
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			}
+			if cfg.Name == "plugin.config" { // trafficserver restart is required
+				r.TrafficServerRestart = true
+			} else if cfg.Name == "remap.config" {
+				r.TrafficCtlReload = true
+				r.RemapConfigReload = true
+			} else if cfg.Name == "sysctl.conf" { // sysctl reload is required
+				r.SysCtlReload = true
+			} else if cfg.Name == "ssl_multicert.config" {
+				r.TrafficCtlReload = true
+			} else if cfg.Name == "ntpd.conf" { // ntpd reload is required
+				r.NtpdRestart = true
+			}
+			log.Debugf("Setting change applied for '%s'\n", cfg.Name)
+		}
+	} else {
+		log.Infof("You elected not to replace %s with the version from Traffic Ops.\n")
+		cfg.ChangeApplied = false
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) sleepTimer(serverStatus *tc.ServerUpdateStatus) {
+	randDispSec := util.RandomDuration(r.Cfg.Dispersion) / time.Second
+	revalClockSec := r.Cfg.RevalWaitTime / time.Second
+
+	if serverStatus.UseRevalPending && r.Cfg.RunMode != config.BADASS {
+		log.Infoln("Performing a revalidation check before sleeping...")
+		_, err := r.RevalidateWhileSleeping()
+		if err != nil {
+			log.Errorf("Revalidation check completed with error: %s\n", err)
+		} else {
+			log.Infoln("Revalidation check complete.")
+		}
+	}
+	if randDispSec < revalClockSec || serverStatus.UseRevalPending == false || r.Cfg.RunMode == config.BADASS {
+		log.Infof("Sleeping for %d seconds: ", randDispSec)
+	} else {
+		log.Infof("%d seconds until next revalidation check.\n", revalClockSec)
+		log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+		log.Infof("Sleeping for %d seconds: ", revalClockSec)
+	}
+
+	for randDispSec > 0 {
+		fmt.Printf(".")
+		time.Sleep(time.Second)
+		revalClockSec--
+		if revalClockSec < 1 && r.Cfg.RunMode != config.BADASS && serverStatus.UseRevalPending {
+			fmt.Printf("\n")
+			log.Infoln("Interrupting dispersion sleep period for revalidation check.")
+			_, err := r.RevalidateWhileSleeping()
+			revalClockSec = r.Cfg.RevalWaitTime / time.Second
+			if err != nil {
+				log.Errorf("Revalidation check completed with error: %s\n", err)
+			} else {
+				log.Infoln("Revalidation check complete.")
+			}
+			if revalClockSec < randDispSec {
+				log.Infof("Revalidation check complete. %d seconds until next revalidation check.", revalClockSec)
+				log.Infof("%d seconds remaining in dispersion sleep period\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", revalClockSec)
+			} else {
+				log.Infof("Revalidation check complete. %d seconds remaining in dispersion sleep period.\n", randDispSec)
+				log.Infof("Sleeping for %d seconds: ", randDispSec)
+			}
+		}
+		randDispSec--
+	}
+	fmt.Printf("\n")
+}
+
+func (r *TrafficOpsReq) writeCfgFile(cfg *ConfigFile, dir string, data []byte) (int, error) {
+	var fullFileName string
+
+	if dir == "" {
+		fullFileName = cfg.Path
+	} else {
+		fullFileName = dir + "/" + cfg.Name
+	}
+
+	fd, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cfg.Perm)
+	if err != nil {
+		return 0, errors.New("unable to open '" + fullFileName + "' for writing: " + err.Error())
+	}
+
+	c, err := fd.Write(data)
+	if err != nil {
+		return 0, errors.New("error writing to '" + fullFileName + "': " + err.Error())
+	}
+	fd.Close()
+
+	if cfg.Service == "trafficserver" {
+		err = os.Chown(fullFileName, cfg.Uid, cfg.Gid)
+	} else {
+		err = os.Chown(fullFileName, 0, 0)
+	}
+	if err != nil {
+		return 0, errors.New("error changing ownership on '" + fullFileName + "': " + err.Error())
+	}
+
+	return c, nil
+}
+
+func (r *TrafficOpsReq) verifyPlugins(cfg *ConfigFile) error {
+
+	log.Debugf("Checking plugins for %s\n", cfg.Name)
+
+	str := string(cfg.Body)
+	lines := strings.Split(str, "\n")
+	if cfg.Name == "plugin.config" {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") {
+				continue
+			}
+			fields := strings.Fields(line)
+			if len(fields) > 0 {
+				plugin := fields[0]
+				// already verified
+				if r.plugins[plugin] == true {
+					continue
+				}
+				err := r.checkPlugin(plugin)
+				if err != nil {
+					cfg.PreReqFailed = true
+					return err
+				} else {
+					r.plugins[plugin] = true
+					log.Infof("Plugin %s has been verified.\n", plugin)
+				}
+			}
+		}
+	} else if cfg.Name == "remap.config" && len(lines) > 0 {
+		for ii := range lines {
+			line := lines[ii]
+			if strings.HasPrefix(line, "#") || line == "" {
+				continue
+			}
+			plugins := strings.Split(line, "@plugin=")
+			if len(plugins) > 0 {
+				for jj := range plugins {
+					var plugin string = ""
+					var plugin_config string = ""
+					if jj > 0 {
+						params := strings.Split(plugins[jj], "@pparam=")
+						param_length := len(params)
+						if param_length > 0 {
+							switch param_length {
+							case 1:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = ""
+							default:
+								plugin = strings.TrimSpace(params[0])
+								plugin_config = strings.TrimSpace(params[1])
+							}
+						}
+						if strings.HasSuffix(plugin, ".so") {
+							// already verified
+							if r.plugins[plugin] == true {
+								continue
+							}
+							err := r.checkPlugin(plugin)
+							if err != nil {
+								cfg.PreReqFailed = true
+								return err
+							} else {
+								r.plugins[plugin] = true
+								log.Infof("Plugin %s has been verified.\n", plugin)
+							}
+						}
+						if plugin_config != "" {
+							if strings.HasPrefix(plugin_config, "proxy.config") || strings.HasPrefix(plugin_config, "-") {
+								continue
+							} else {
+								plugin_config = filepath.Base(plugin_config)
+								cfg, ok := r.configFiles[plugin_config]
+								if ok {
+									cfg.RemapPluginConfig = true
+									log.Debugf("%s is a remap plugin config file\n", plugin_config)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (r *TrafficOpsReq) CheckSystemServices() error {
+	if r.Cfg.RunMode == config.BADASS || r.Cfg.RunMode == config.INTERACTIVE {
+		if r.Cfg.RunMode == config.INTERACTIVE {
+			ans, _ := util.AskYesNo("Do you wish to enable a system service with chkconfig or systemctl?")
+			if ans == false {
+				return nil
+			}
+		}
+		out, err := r.atsTcExec("chkconfig")
+		if err != nil {
+			log.Errorln(err)
+			return err
+		} else {
+			var result []map[string]string
+			if err = json.Unmarshal(out, &result); err != nil {
+				return err
+			} else {
+				for ii := range result {
+					_n := result[ii]["name"]
+					_v := result[ii]["value"]
+					arrv := strings.Fields(_v)
+					var level []string
+					var enabled bool = false
+					for jj := range arrv {
+						nv := strings.Split(arrv[jj], ":")
+						if len(nv) == 2 && strings.Contains(nv[1], "on") {
+							level = append(level, nv[0])
+							enabled = true
+						}
+					}
+					if enabled == true {
+						if r.Cfg.SvcManagement == config.SYSTEMD {
+							out, rc, err := util.ExecCommand("/bin/systemctl", "enable", _n)
+							if err != nil {
+								log.Errorf(string(out))
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else if r.Cfg.SvcManagement == config.SYSTEMV {
+							levelValue := strings.Join(level, "")
+							_, rc, err := util.ExecCommand("/bin/chkconfig", "--level", levelValue, _n, "on")
+							if err != nil {
+								return errors.New("Unable to enable service " + _n + ": " + err.Error())
+							}
+							if rc == 0 {
+								log.Infof("The %s service has been enabled\n", _n)
+							}
+						} else {
+							log.Errorf("Unable to insure %s service is enabled, SvcMananagement type is %s\n", r.Cfg.SvcManagement)
+						}
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// returns true/fale if the named rpm package is installed.
+func (r *TrafficOpsReq) isPackageInstalled(name string) bool {
+	for k, _ := range r.pkgs {
+		if strings.HasPrefix(k, name) {
+			return true
+		}
+	}
+	return false
+}
+
+// fetch a 'Configfile' by file name
+func (r *TrafficOpsReq) GetConfigFile(name string) (*ConfigFile, bool) {
+	cfg, ok := r.configFiles[name]
+	return cfg, ok
+}
+
+// fetches and parses the multipart config files for a cache from traffic ops
+// and loads them into the configFiles map.
+func (r *TrafficOpsReq) GetConfigFileList() error {
+	var atsUid int = 0
+	var atsGid int = 0
+
+	atsUser, err := user.Lookup(config.TrafficServerOwner)
+	if err != nil {
+		log.Errorf("could not lookup the trafficserver, '%s', owner uid, using uid/gid 0")
+	} else {
+		atsUid, err = strconv.Atoi(atsUser.Uid)
+		if err != nil {
+			log.Errorf("could not parse the ats UID.")
+			atsUid = 0
+		}
+		atsGid, err = strconv.Atoi(atsUser.Gid)
+		if err != nil {
+			log.Errorf("could not parse the ats GID.")
+			atsUid = 0
+		}
+	}
+
+	fBytes, err := r.atsTcExec("get-config-files")
+	if err != nil {
+		return err
+	}
+	buf := bytes.NewBuffer(fBytes)
+	var hdr string
+	for {
+		hdr, err = buf.ReadString('\n')
+		if err != nil {
+			return err
+		} else {
+			if strings.HasPrefix(hdr, "Content-Type: multipart/mixed") {
+				break
+			}
+		}
+	}
+	// split on the ": " to trim off the leading space
+	har := strings.Split(hdr, ": ")
+	if har[0] != "Content-Type" || !strings.HasPrefix(har[1], "multipart/mixed") {

Review comment:
       Nitpick: we try to keep RFC constants in `lib/go-rfc`. Can be `rfc.ContentType`
   
   https://github.com/apache/trafficcontrol/blob/master/lib/go-rfc/http.go#L32
   
   It'd also be good if you wanted to add `multipart/mixed` there.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org