You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2020/11/18 17:26:28 UTC

[trafficcontrol] 06/09: Change ORT/atstccfg to use standard TC objects (#5247)

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

ocket8888 pushed a commit to branch 5.0.x
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git

commit 1e3aa0d27709c3f880fd84e4146fe365e2944fbd
Author: Robert O Butts <ro...@users.noreply.github.com>
AuthorDate: Tue Nov 17 14:12:55 2020 -0700

    Change ORT/atstccfg to use standard TC objects (#5247)
    
    * Change ORT/atstccfg to use standard TC objects
    
    * Remove unused symbols
    
    * Change atscfg makeCGMap to unexported
    
    * Change atscfg parent symbol to unexported
    
    * Change atscfg isTopLevelCache to unexported
    
    * Change atscfg getATSVersion to unexported
    
    * Change atscfg genericProfileConfig to unexported
    
    * Remove atscfg unused func
    
    * Change atscfg makeTopologyNameMap to unexported
    
    * Change atscfg filterDSS to unexported
    
    * Change atscfg filterParams, toMap to unexported
    
    * Change atscfg getServerIPAddress to unexported
    
    * Change atscfg cacheConfig symbols to unexported
    
    * Change atscfg cacheurl symbols to unexported
    
    * Change atscfg chkconfig symbols to unexported
    
    * Change atscfg headerRw internals to unexported
    
    * Change atscfg ipallow internals to unexported
    
    * Change atscfg meta internals to unexported
    
    * Change atscfg package internals to unexported
    
    * Change atscfg parent internals to unexported
    
    * Change atscfg regexremap internals to unexported
    
    * Change atscfg regexreval internals to unexported
    
    * Change atscfg remap internals to unexported
    
    * Fix atscfg package test
    
    * Change atscfg unknown internals to unexported
    
    * Change atscfg sslmulticert internals to unexported
    
    * Change atscfg topohdrrw internals to unexported
    
    * Fix atscfg for tc/client changed server
    
    * Change atscfg to DS alias for stability
    
    Changes lib/go-atscfg and traffic_ops_ort/atstccfg to use an alias
    for DeliveryService, so all of atscfg doesn't have to change every
    time the Traffic Ops Client type is changed, but only the alias
    and any broken symbols.
    
    * Fix ORT/atstccfg to use v1-client not vendor
    
    This fixes a breaking issue with the vendored client returning
    tc.ServerNullable, which was changed to be a V2 object not v1.
    
    Because of the member IPIsService which doesn't exist in V1 and
    defaults to false when it doesn't exist, this breaks config gen
    because all interfaces are "not service" and thus not used.
    
    This fixes the bug by using v1-client which returns a ServerV1
    which ORT/atstccfg then upgrades properly, forcibly setting
    all IPIsService to true.
    
    * Add atscfg warnings for nil values
    
    * Fix atscfg godoc
    
    (cherry picked from commit 81c9fd0cc283d7f8c6166827a4f1367cb1efec1a)
---
 lib/go-atscfg/astatsdotconfig.go                   |  34 +-
 lib/go-atscfg/atscfg.go                            | 297 ++++---
 lib/go-atscfg/atscfg_test.go                       | 238 ++---
 lib/go-atscfg/atsdotrules.go                       |  29 +-
 lib/go-atscfg/atsdotrules_test.go                  |  49 +-
 lib/go-atscfg/bgfetchdotconfig.go                  |  27 +-
 lib/go-atscfg/bgfetchdotconfig_test.go             |  25 +-
 lib/go-atscfg/cachedotconfig.go                    | 121 ++-
 lib/go-atscfg/cachedotconfig_test.go               |  51 +-
 lib/go-atscfg/cacheurldotconfig.go                 | 129 ++-
 lib/go-atscfg/cacheurldotconfig_test.go            | 166 ++--
 lib/go-atscfg/chkconfig.go                         |  63 +-
 lib/go-atscfg/chkconfig_test.go                    |  39 +-
 lib/go-atscfg/dropqstringdotconfig.go              |  41 +-
 lib/go-atscfg/dropqstringdotconfig_test.go         |  28 +-
 lib/go-atscfg/facts.go                             |  25 +-
 lib/go-atscfg/facts_test.go                        |  14 +-
 lib/go-atscfg/headerrewritedotconfig.go            | 207 +++--
 lib/go-atscfg/headerrewritedotconfig_test.go       | 141 ++-
 lib/go-atscfg/headerrewritemiddotconfig.go         | 133 ++-
 lib/go-atscfg/headerrewritemiddotconfig_test.go    | 189 +++-
 lib/go-atscfg/hostingdotconfig.go                  | 134 ++-
 lib/go-atscfg/hostingdotconfig_test.go             | 118 ++-
 lib/go-atscfg/ipallowdotconfig.go                  | 169 ++--
 lib/go-atscfg/ipallowdotconfig_test.go             |  81 +-
 lib/go-atscfg/loggingdotconfig.go                  |  33 +-
 lib/go-atscfg/loggingdotconfig_test.go             |  31 +-
 lib/go-atscfg/loggingdotyaml.go                    |  34 +-
 lib/go-atscfg/loggingdotyaml_test.go               |  39 +-
 lib/go-atscfg/logsdotxml.go                        |  29 +-
 lib/go-atscfg/logsdotxml_test.go                   |  30 +-
 lib/go-atscfg/meta.go                              | 537 ++++++------
 lib/go-atscfg/meta_test.go                         | 304 ++-----
 lib/go-atscfg/packages.go                          |  61 +-
 lib/go-atscfg/packages_test.go                     |   9 +-
 lib/go-atscfg/parentdotconfig.go                   | 953 +++++++++++----------
 lib/go-atscfg/parentdotconfig_test.go              | 253 +++---
 lib/go-atscfg/plugindotconfig.go                   |  33 +-
 lib/go-atscfg/plugindotconfig_test.go              |  19 +-
 lib/go-atscfg/recordsdotconfig.go                  |  58 +-
 lib/go-atscfg/recordsdotconfig_test.go             |  19 +-
 lib/go-atscfg/regexremapdotconfig.go               |  98 ++-
 lib/go-atscfg/regexremapdotconfig_test.go          | 140 +--
 lib/go-atscfg/regexrevalidatedotconfig.go          |  97 ++-
 lib/go-atscfg/regexrevalidatedotconfig_test.go     |  24 +-
 lib/go-atscfg/remapdotconfig.go                    | 301 ++++---
 lib/go-atscfg/remapdotconfig_test.go               | 868 +++++++++++--------
 lib/go-atscfg/servercachedotconfig.go              |  58 +-
 lib/go-atscfg/servercachedotconfig_test.go         |  76 +-
 lib/go-atscfg/serverunknown.go                     |  63 +-
 lib/go-atscfg/serverunknown_test.go                |  24 +-
 lib/go-atscfg/setdscpdotconfig.go                  |  33 +-
 lib/go-atscfg/setdscpdotconfig_test.go             |  44 +-
 lib/go-atscfg/sslmulticertdotconfig.go             |  75 +-
 lib/go-atscfg/sslmulticertdotconfig_test.go        |  94 +-
 lib/go-atscfg/storagedotconfig.go                  |  38 +-
 lib/go-atscfg/storagedotconfig_test.go             |  17 +-
 lib/go-atscfg/sysctldotconf.go                     |  33 +-
 lib/go-atscfg/sysctldotconf_test.go                |  16 +-
 lib/go-atscfg/topologyheaderrewritedotconfig.go    | 113 ++-
 lib/go-atscfg/unknownconfig.go                     |  70 --
 lib/go-atscfg/unknownconfig_test.go                |  71 --
 lib/go-atscfg/urisigningconfig.go                  |  43 +-
 lib/go-atscfg/urisigningconfig_test.go             |  31 +-
 lib/go-atscfg/urlsigconfig.go                      |  53 +-
 lib/go-atscfg/urlsigconfig_test.go                 |  50 +-
 lib/go-atscfg/volumedotconfig.go                   |  31 +-
 lib/go-atscfg/volumedotconfig_test.go              |  30 +-
 lib/go-tc/servers.go                               |  55 ++
 traffic_ops_ort/atstccfg/atstccfg.go               |   4 +-
 traffic_ops_ort/atstccfg/cfgfile/all.go            |  74 +-
 .../atstccfg/cfgfile/astatsdotconfig.go            |  56 --
 traffic_ops_ort/atstccfg/cfgfile/atsdotrules.go    |  58 --
 .../atstccfg/cfgfile/bgfetchdotconfig.go           |  36 -
 traffic_ops_ort/atstccfg/cfgfile/cachedotconfig.go |  83 --
 .../atstccfg/cfgfile/cacheurldotconfig.go          |  74 --
 traffic_ops_ort/atstccfg/cfgfile/cfgfile.go        | 114 ++-
 traffic_ops_ort/atstccfg/cfgfile/cfgfile_test.go   |  42 +-
 traffic_ops_ort/atstccfg/cfgfile/chkconfig.go      |  37 -
 .../atstccfg/cfgfile/dropqstringdotconfig.go       |  45 -
 traffic_ops_ort/atstccfg/cfgfile/facts.go          |  34 -
 .../atstccfg/cfgfile/headerrewritedotconfig.go     |  97 ---
 .../atstccfg/cfgfile/headerrewritemiddotconfig.go  | 139 ---
 .../atstccfg/cfgfile/hostingdotconfig.go           | 132 ---
 .../atstccfg/cfgfile/ipallowdotconfig.go           |  46 -
 .../atstccfg/cfgfile/loggingdotconfig.go           |  36 -
 traffic_ops_ort/atstccfg/cfgfile/loggingdotyaml.go |  36 -
 .../atstccfg/cfgfile/logsxmldotconfig.go           |  36 -
 traffic_ops_ort/atstccfg/cfgfile/meta.go           | 153 ----
 traffic_ops_ort/atstccfg/cfgfile/packages.go       |  30 -
 .../atstccfg/cfgfile/parentdotconfig.go            |  43 -
 .../atstccfg/cfgfile/plugindotconfig.go            |  35 -
 .../atstccfg/cfgfile/recordsdotconfig.go           |  36 -
 .../atstccfg/cfgfile/regexremapdotconfig.go        |  64 --
 .../atstccfg/cfgfile/regexrevalidatedotconfig.go   |  62 --
 traffic_ops_ort/atstccfg/cfgfile/remapdotconfig.go |  43 -
 traffic_ops_ort/atstccfg/cfgfile/routing.go        | 203 ++---
 .../atstccfg/cfgfile/servercachedotconfig.go       |  62 --
 .../atstccfg/cfgfile/serverunknownconfig.go        |  40 -
 .../atstccfg/cfgfile/setdscpdotconfig.go           |  42 -
 traffic_ops_ort/atstccfg/cfgfile/sslkeys.go        |  11 +-
 .../atstccfg/cfgfile/sslmulticertdotconfig.go      |  38 -
 .../atstccfg/cfgfile/storagedotconfig.go           |  38 -
 traffic_ops_ort/atstccfg/cfgfile/sysctldotconf.go  |  36 -
 .../cfgfile/topologyheaderrewritedotconfig.go      |  73 --
 traffic_ops_ort/atstccfg/cfgfile/unknownconfig.go  |  53 --
 .../atstccfg/cfgfile/urisigningconfig.go           |  56 --
 .../atstccfg/cfgfile/urisigningconfig_test.go      |  43 -
 traffic_ops_ort/atstccfg/cfgfile/urlsigconfig.go   |  62 --
 .../atstccfg/cfgfile/urlsigconfig_test.go          |  43 -
 .../atstccfg/cfgfile/volumedotconfig.go            |  38 -
 traffic_ops_ort/atstccfg/cfgfile/wrappers.go       | 217 +++++
 traffic_ops_ort/atstccfg/config/config.go          |  26 +-
 traffic_ops_ort/atstccfg/getdata/getdata.go        |  46 +-
 traffic_ops_ort/atstccfg/plugin/hello_world.go     |   4 +-
 traffic_ops_ort/atstccfg/plugin/plugin_test.go     |   8 +-
 traffic_ops_ort/atstccfg/toreq/toreq.go            | 286 ++++---
 .../github.com/apache/trafficcontrol/LICENSE       | 454 ----------
 .../github.com/apache/trafficcontrol/VERSION       |   1 -
 .../github.com/apache/trafficcontrol/changeset.txt |   2 -
 .../trafficcontrol/traffic_ops/client/README.md    |  53 --
 .../trafficcontrol/traffic_ops/client/about.go     |  43 -
 .../trafficcontrol/traffic_ops/client/asn.go       | 132 ---
 .../trafficcontrol/traffic_ops/client/atsconfig.go |  88 --
 .../traffic_ops/client/cachegroup.go               | 306 -------
 .../traffic_ops/client/cachegroup_parameters.go    | 109 ---
 .../trafficcontrol/traffic_ops/client/cdn.go       | 176 ----
 .../traffic_ops/client/cdn_domains.go              |  29 -
 .../traffic_ops/client/cdnfederations.go           |  73 --
 .../traffic_ops/client/coordinate.go               | 155 ----
 .../trafficcontrol/traffic_ops/client/crconfig.go  |  53 --
 .../traffic_ops/client/deliveryservice.go          | 456 ----------
 .../client/deliveryservice_endpoints.go            |  70 --
 .../client/deliveryservice_request_comments.go     | 115 ---
 .../traffic_ops/client/deliveryservice_requests.go | 243 ------
 .../deliveryservices_required_capabilities.go      |  83 --
 .../traffic_ops/client/deliveryserviceserver.go    | 119 ---
 .../trafficcontrol/traffic_ops/client/division.go  | 146 ----
 .../trafficcontrol/traffic_ops/client/dsuser.go    |  60 --
 .../trafficcontrol/traffic_ops/client/endpoints.go |  18 -
 .../traffic_ops/client/federation.go               | 211 -----
 .../traffic_ops/client/federation_resolver.go      | 116 ---
 .../trafficcontrol/traffic_ops/client/hardware.go  |  52 --
 .../trafficcontrol/traffic_ops/client/iso.go       |  54 --
 .../trafficcontrol/traffic_ops/client/job.go       | 196 -----
 .../trafficcontrol/traffic_ops/client/log.go       |  61 --
 .../trafficcontrol/traffic_ops/client/origin.go    | 194 -----
 .../trafficcontrol/traffic_ops/client/parameter.go | 209 -----
 .../traffic_ops/client/phys_location.go            | 143 ----
 .../trafficcontrol/traffic_ops/client/ping.go      |  39 -
 .../trafficcontrol/traffic_ops/client/profile.go   | 264 ------
 .../traffic_ops/client/profile_parameter.go        |  96 ---
 .../trafficcontrol/traffic_ops/client/region.go    | 161 ----
 .../trafficcontrol/traffic_ops/client/role.go      | 160 ----
 .../trafficcontrol/traffic_ops/client/server.go    | 355 --------
 .../client/server_server_capabilities.go           |  83 --
 .../traffic_ops/client/server_update_status.go     |  81 --
 .../traffic_ops/client/servercapability.go         | 104 ---
 .../traffic_ops/client/servercheck.go              |  56 --
 .../traffic_ops/client/serversstatus.go            |  44 -
 .../trafficcontrol/traffic_ops/client/session.go   | 503 -----------
 .../traffic_ops/client/staticdnsentry.go           | 174 ----
 .../traffic_ops/client/stats_summary.go            | 125 ---
 .../trafficcontrol/traffic_ops/client/status.go    | 150 ----
 .../trafficcontrol/traffic_ops/client/steering.go  |  38 -
 .../traffic_ops/client/steeringtarget.go           |  96 ---
 .../trafficcontrol/traffic_ops/client/tenant.go    | 117 ---
 .../traffic_ops/client/tenant_endpoints.go         |  25 -
 .../traffic_ops/client/toextension.go              |  69 --
 .../traffic_ops/client/traffic_monitor.go          |  70 --
 .../traffic_ops/client/traffic_stats.go            |  29 -
 .../trafficcontrol/traffic_ops/client/type.go      | 159 ----
 .../trafficcontrol/traffic_ops/client/update.go    |  72 --
 .../trafficcontrol/traffic_ops/client/user.go      | 185 ----
 .../trafficcontrol/traffic_ops/client/util.go      |  58 --
 traffic_ops_ort/atstccfg/toreqnew/toreqnew.go      |  55 +-
 176 files changed, 5230 insertions(+), 13064 deletions(-)

diff --git a/lib/go-atscfg/astatsdotconfig.go b/lib/go-atscfg/astatsdotconfig.go
index 8ea62d5..129eb5e 100644
--- a/lib/go-atscfg/astatsdotconfig.go
+++ b/lib/go-atscfg/astatsdotconfig.go
@@ -19,6 +19,10 @@ package atscfg
  * under the License.
  */
 
+import (
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
 const AstatsSeparator = "="
 const AstatsFileName = "astats.config"
 
@@ -26,16 +30,30 @@ const ContentTypeAstatsDotConfig = ContentTypeTextASCII
 const LineCommentAstatsDotConfig = LineCommentHash
 
 func MakeAStatsDotConfig(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, AstatsFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
-	txt := GenericProfileConfig(paramData, AstatsSeparator)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "server missing Profile")
+	}
+
+	serverParams = filterParams(serverParams, AstatsFileName, "", "", "location")
+	paramData, paramWarns := paramsToMap(serverParams)
+	warnings = append(warnings, paramWarns...)
+	hdr := makeHdrComment(hdrComment)
+	txt := genericProfileConfig(paramData, AstatsSeparator)
 	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
 	txt = hdr + txt
-	return txt
+
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentTypeAstatsDotConfig,
+		LineComment: LineCommentAstatsDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/atscfg.go b/lib/go-atscfg/atscfg.go
index 2faa830..b572dc6 100644
--- a/lib/go-atscfg/atscfg.go
+++ b/lib/go-atscfg/atscfg.go
@@ -22,56 +22,90 @@ package atscfg
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net"
 	"sort"
 	"strconv"
 	"strings"
-	"time"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const InvalidID = -1
-
 const DefaultATSVersion = "5" // TODO Emulates Perl; change to 6? ATC no longer officially supports ATS 5.
-
 const HeaderCommentDateFormat = "Mon Jan 2 15:04:05 MST 2006"
-
 const ContentTypeTextASCII = `text/plain; charset=us-ascii`
-
 const LineCommentHash = "#"
+const ConfigSuffix = ".config"
+
+type DeliveryServiceID int
+type ProfileID int
+type ServerID int
 
 type TopologyName string
 type CacheGroupType string
 type ServerCapability string
 
-type ServerInfo struct {
-	CacheGroupID                  int
-	CacheGroupName                string
-	CDN                           tc.CDNName
-	CDNID                         int
-	DomainName                    string
-	HostName                      string
-	HTTPSPort                     int
-	ID                            int
-	IP                            string
-	ParentCacheGroupID            int
-	ParentCacheGroupType          string
-	ProfileID                     ProfileID
-	ProfileName                   string
-	Port                          int
-	SecondaryParentCacheGroupID   int
-	SecondaryParentCacheGroupType string
-	Type                          string
-}
-
-func (s *ServerInfo) IsTopLevelCache() bool {
-	return (s.ParentCacheGroupType == tc.CacheGroupOriginTypeName || s.ParentCacheGroupID == InvalidID) &&
-		(s.SecondaryParentCacheGroupType == tc.CacheGroupOriginTypeName || s.SecondaryParentCacheGroupID == InvalidID)
-}
-
-func MakeCGMap(cgs []tc.CacheGroupNullable) (map[tc.CacheGroupName]tc.CacheGroupNullable, error) {
+// Server is a tc.Server for the latest lib/go-tc and traffic_ops/vx-client type.
+// This allows atscfg to not have to change the type everywhere it's used, every time ATC changes the base type,
+// but to only have to change it here, and the places where breaking symbol changes were made.
+type Server tc.ServerV30
+
+// DeliveryService is a tc.DeliveryService for the latest lib/go-tc and traffic_ops/vx-client type.
+// This allows atscfg to not have to change the type everywhere it's used, every time ATC changes the base type,
+// but to only have to change it here, and the places where breaking symbol changes were made.
+type DeliveryService tc.DeliveryServiceNullableV30
+
+// ToDeliveryServices converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
+func ToDeliveryServices(dses []tc.DeliveryServiceNullableV30) []DeliveryService {
+	ad := []DeliveryService{}
+	for _, ds := range dses {
+		ad = append(ad, DeliveryService(ds))
+	}
+	return ad
+}
+
+// OldToDeliveryServices converts a slice of the old traffic_ops/client type to the local alias.
+func OldToDeliveryServices(dses []tc.DeliveryServiceNullable) []DeliveryService {
+	ad := []DeliveryService{}
+	for _, ds := range dses {
+		upgradedDS := tc.DeliveryServiceNullableV30{DeliveryServiceNullableV15: tc.DeliveryServiceNullableV15(ds)}
+		ad = append(ad, DeliveryService(upgradedDS))
+	}
+	return ad
+}
+
+// ToServers converts a slice of the latest lib/go-tc and traffic_ops/vx-client type to the local alias.
+func ToServers(servers []tc.ServerV30) []Server {
+	as := []Server{}
+	for _, sv := range servers {
+		as = append(as, Server(sv))
+	}
+	return as
+}
+
+// CfgFile is all the information necessary to create an ATS config file, including the file name, path, data, and metadata.
+// This is provided as a convenience and unified structure for users. The lib/go-atscfg library doesn't actually use or return this. See ATSConfigFileData.
+type CfgFile struct {
+	Name string
+	Path string
+	Cfg
+}
+
+// Cfg is the data and metadata for an ATS Config File.
+//
+// This includes the text, the content type (which is necessary for HTTP, multipart, and other things), and the line comment syntax if any.
+//
+// This is what is generated by the lib/go-atscfg library. Note it does not include the file name or path, which this library doesn't have enough information to return and is not part of generation. That information should be fetched from Traffic Ops, along with the data used to generate config files, or else generated from the machine. See CfgFile.
+//
+type Cfg struct {
+	Text        string
+	ContentType string
+	LineComment string
+	Warnings    []string
+}
+
+func makeCGMap(cgs []tc.CacheGroupNullable) (map[tc.CacheGroupName]tc.CacheGroupNullable, error) {
 	cgMap := map[tc.CacheGroupName]tc.CacheGroupNullable{}
 	for _, cg := range cgs {
 		if cg.Name == nil {
@@ -82,25 +116,25 @@ func MakeCGMap(cgs []tc.CacheGroupNullable) (map[tc.CacheGroupName]tc.CacheGroup
 	return cgMap, nil
 }
 
-type ServerParentCacheGroupData struct {
+type serverParentCacheGroupData struct {
 	ParentID            int
 	ParentType          CacheGroupType
 	SecondaryParentID   int
 	SecondaryParentType CacheGroupType
 }
 
-// GetParentCacheGroupData returns the parent CacheGroup IDs and types for the given server.
+// getParentCacheGroupData returns the parent CacheGroup IDs and types for the given server.
 // Takes a server and a CG map. To create a CGMap from an API CacheGroup slice, use MakeCGMap.
 // If server's CacheGroup has no parent or secondary parent, returns InvalidID and "" with no error.
-func GetParentCacheGroupData(server *tc.ServerNullable, cgMap map[tc.CacheGroupName]tc.CacheGroupNullable) (ServerParentCacheGroupData, error) {
+func getParentCacheGroupData(server *Server, cgMap map[tc.CacheGroupName]tc.CacheGroupNullable) (serverParentCacheGroupData, error) {
 	if server.Cachegroup == nil || *server.Cachegroup == "" {
-		return ServerParentCacheGroupData{}, errors.New("server missing cachegroup")
+		return serverParentCacheGroupData{}, errors.New("server missing cachegroup")
 	} else if server.HostName == nil || *server.HostName == "" {
-		return ServerParentCacheGroupData{}, errors.New("server missing hostname")
+		return serverParentCacheGroupData{}, errors.New("server missing hostname")
 	}
 	serverCG, ok := cgMap[tc.CacheGroupName(*server.Cachegroup)]
 	if !ok {
-		return ServerParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' not found in CacheGroups")
+		return serverParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' not found in CacheGroups")
 	}
 
 	parentCGID := InvalidID
@@ -108,15 +142,15 @@ func GetParentCacheGroupData(server *tc.ServerNullable, cgMap map[tc.CacheGroupN
 	if serverCG.ParentName != nil && *serverCG.ParentName != "" {
 		parentCG, ok := cgMap[tc.CacheGroupName(*serverCG.ParentName)]
 		if !ok {
-			return ServerParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' parent '" + *serverCG.ParentName + "' not found in CacheGroups")
+			return serverParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' parent '" + *serverCG.ParentName + "' not found in CacheGroups")
 		}
 		if parentCG.ID == nil {
-			return ServerParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
+			return serverParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
 		}
 		parentCGID = *parentCG.ID
 
 		if parentCG.Type == nil {
-			return ServerParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
+			return serverParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
 		}
 		parentCGType = *parentCG.Type
 	}
@@ -126,21 +160,21 @@ func GetParentCacheGroupData(server *tc.ServerNullable, cgMap map[tc.CacheGroupN
 	if serverCG.SecondaryParentName != nil && *serverCG.SecondaryParentName != "" {
 		parentCG, ok := cgMap[tc.CacheGroupName(*serverCG.SecondaryParentName)]
 		if !ok {
-			return ServerParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' secondary parent '" + *serverCG.SecondaryParentName + "' not found in CacheGroups")
+			return serverParentCacheGroupData{}, errors.New("server '" + *server.HostName + "' cachegroup '" + *server.Cachegroup + "' secondary parent '" + *serverCG.SecondaryParentName + "' not found in CacheGroups")
 		}
 
 		if parentCG.ID == nil {
-			return ServerParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
+			return serverParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
 		}
 		secondaryParentCGID = *parentCG.ID
 		if parentCG.Type == nil {
-			return ServerParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
+			return serverParentCacheGroupData{}, errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
 		}
 
 		secondaryParentCGType = *parentCG.Type
 	}
 
-	return ServerParentCacheGroupData{
+	return serverParentCacheGroupData{
 		ParentID:            parentCGID,
 		ParentType:          CacheGroupType(parentCGType),
 		SecondaryParentID:   secondaryParentCGID,
@@ -148,30 +182,22 @@ func GetParentCacheGroupData(server *tc.ServerNullable, cgMap map[tc.CacheGroupN
 	}, nil
 }
 
-// IsTopLevelCache returns whether server is a top-level cache, as defined by traditional CacheGroup parentage.
+// isTopLevelCache returns whether server is a top-level cache, as defined by traditional CacheGroup parentage.
 // This does not consider Topologies, and should not be used if the Delivery Service being considered has a Topology.
 // Takes a ServerParentCacheGroupData, which may be created via GetParentCacheGroupData.
-func IsTopLevelCache(s ServerParentCacheGroupData) bool {
+func isTopLevelCache(s serverParentCacheGroupData) bool {
 	return (s.ParentType == tc.CacheGroupOriginTypeName || s.ParentID == InvalidID) &&
 		(s.SecondaryParentType == tc.CacheGroupOriginTypeName || s.SecondaryParentID == InvalidID)
 }
 
-func HeaderCommentWithTOVersionStr(name string, nameVersionStr string) string {
-	return "# DO NOT EDIT - Generated for " + name + " by " + nameVersionStr + " on " + time.Now().UTC().Format(HeaderCommentDateFormat) + "\n"
-}
-
-func GetNameVersionStringFromToolNameAndURL(toolName string, url string) string {
-	return toolName + " (" + url + ")"
+func makeHdrComment(hdrComment string) string {
+	return "# " + hdrComment + "\n"
 }
 
-func GenericHeaderComment(name string, toolName string, url string) string {
-	return HeaderCommentWithTOVersionStr(name, GetNameVersionStringFromToolNameAndURL(toolName, url))
-}
-
-// GetATSMajorVersionFromATSVersion returns the major version of the given profile's package trafficserver parameter.
+// getATSMajorVersionFromATSVersion returns the major version of the given profile's package trafficserver parameter.
 // The atsVersion is typically a Parameter on the Server's Profile, with the configFile "package" name "trafficserver".
 // Returns an error if atsVersion is empty or does not start with an unsigned integer followed by a period or nothing.
-func GetATSMajorVersionFromATSVersion(atsVersion string) (int, error) {
+func getATSMajorVersionFromATSVersion(atsVersion string) (int, error) {
 	dotPos := strings.Index(atsVersion, ".")
 	if dotPos == -1 {
 		dotPos = len(atsVersion) // if there's no '.' then assume the whole string is just a major version.
@@ -185,14 +211,10 @@ func GetATSMajorVersionFromATSVersion(atsVersion string) (int, error) {
 	return int(majorVer), nil
 }
 
-type DeliveryServiceID int
-type ProfileID int
-type ServerID int
-
-// GenericProfileConfig generates a generic profile config text, from the profile's parameters with the given config file name.
+// genericProfileConfig generates a generic profile config text, from the profile's parameters with the given config file name.
 // This does not include a header comment, because a generic config may not use a number sign as a comment.
 // If you need a header comment, it can be added manually via ats.HeaderComment, or automatically with WithProfileDataHdr.
-func GenericProfileConfig(
+func genericProfileConfig(
 	paramData map[string]string, // GetProfileParamData(tx, profileID, fileName)
 	separator string,
 ) string {
@@ -220,12 +242,6 @@ func trimParamUnderscoreNumSuffix(paramName string) string {
 	return paramName[:underscorePos]
 }
 
-const ConfigSuffix = ".config"
-
-func GetConfigFile(prefix string, xmlId string) string {
-	return prefix + xmlId + ConfigSuffix
-}
-
 // topologyIncludesServer returns whether the given topology includes the given server.
 func topologyIncludesServer(topology tc.Topology, server *tc.Server) bool {
 	for _, node := range topology.Nodes {
@@ -237,7 +253,7 @@ func topologyIncludesServer(topology tc.Topology, server *tc.Server) bool {
 }
 
 // topologyIncludesServerNullable returns whether the given topology includes the given server.
-func topologyIncludesServerNullable(topology tc.Topology, server *tc.ServerNullable) (bool, error) {
+func topologyIncludesServerNullable(topology tc.Topology, server *Server) (bool, error) {
 	if server.Cachegroup == nil {
 		return false, errors.New("server missing Cachegroup")
 	}
@@ -277,11 +293,11 @@ type TopologyPlacement struct {
 	IsLastTier bool
 }
 
-// getTopologyPlacement returns information about the cachegroup's placement in the topology.
+// getTopologyPlacement returns information about the cachegroup's placement in the topology, and any error.
 // - Whether the cachegroup is the last tier in the topology.
 // - Whether the cachegroup is in the topology at all.
 // - Whether it's the first, inner, or last cache tier before the Origin.
-func getTopologyPlacement(cacheGroup tc.CacheGroupName, topology tc.Topology, cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable, ds *tc.DeliveryServiceNullableV30) TopologyPlacement {
+func getTopologyPlacement(cacheGroup tc.CacheGroupName, topology tc.Topology, cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable, ds *DeliveryService) (TopologyPlacement, error) {
 	isMSO := ds.MultiSiteOrigin != nil && *ds.MultiSiteOrigin
 
 	serverNode := tc.TopologyNode{}
@@ -294,7 +310,7 @@ func getTopologyPlacement(cacheGroup tc.CacheGroupName, topology tc.Topology, ca
 		}
 	}
 	if serverNode.Cachegroup == "" {
-		return TopologyPlacement{InTopology: false}
+		return TopologyPlacement{InTopology: false}, nil
 	}
 
 	hasChildren := false
@@ -315,17 +331,14 @@ nodeFor:
 		// TODO extra safety: check other parents, and warn if parents have different types?
 		parentI := serverNode.Parents[0]
 		if parentI >= len(topology.Nodes) {
-			log.Errorln("ATS config generation: topology '" + topology.Name + "' has node with parent larger than nodes size! Config Generation will be malformed!")
-			return TopologyPlacement{}
+			return TopologyPlacement{}, errors.New("topology '" + topology.Name + "' has node with parent larger than nodes size! Config Generation will be malformed!")
 		}
 		parentNode := topology.Nodes[parentI]
 		parentCG, ok := cacheGroups[tc.CacheGroupName(parentNode.Cachegroup)]
 		if !ok {
-			log.Errorln("ATS config generation: topology '" + topology.Name + "' has node with cachegroup '" + parentNode.Cachegroup + "' that wasn't found in cachegroups! Config Generation will be malformed!")
-			return TopologyPlacement{}
+			return TopologyPlacement{}, errors.New("topology '" + topology.Name + "' has node with cachegroup '" + parentNode.Cachegroup + "' that wasn't found in cachegroups! Config Generation will be malformed!")
 		} else if parentCG.Type == nil {
-			log.Errorln("ATS config generation: cachegroup '" + parentNode.Cachegroup + "' with nil type! Config Generation will be malformed!")
-			return TopologyPlacement{}
+			return TopologyPlacement{}, errors.New("ATS config generation: cachegroup '" + parentNode.Cachegroup + "' with nil type! Config Generation will be malformed!")
 		}
 		parentIsOrigin = *parentCG.Type == tc.CacheGroupOriginTypeName
 	}
@@ -336,10 +349,10 @@ nodeFor:
 		IsInnerCacheTier: hasChildren && hasParents && !parentIsOrigin,
 		IsLastCacheTier:  !hasParents || parentIsOrigin,
 		IsLastTier:       !hasParents || (parentIsOrigin && !isMSO), // If the parent CG is an Origin CG, but this DS is not MSO, then ignore the Topology and declare this the last tier
-	}
+	}, nil
 }
 
-func MakeTopologyNameMap(topologies []tc.Topology) map[TopologyName]tc.Topology {
+func makeTopologyNameMap(topologies []tc.Topology) map[TopologyName]tc.Topology {
 	topoNames := map[TopologyName]tc.Topology{}
 	for _, to := range topologies {
 		topoNames[TopologyName(to.Name)] = to
@@ -347,21 +360,21 @@ func MakeTopologyNameMap(topologies []tc.Topology) map[TopologyName]tc.Topology
 	return topoNames
 }
 
-type ParameterWithProfiles struct {
+type parameterWithProfiles struct {
 	tc.Parameter
 	ProfileNames []string
 }
 
-type ParameterWithProfilesMap struct {
+type parameterWithProfilesMap struct {
 	tc.Parameter
 	ProfileNames map[string]struct{}
 }
 
-// TCParamsToParamsWithProfiles unmarshals the Profiles that the tc struct doesn't.
-func TCParamsToParamsWithProfiles(tcParams []tc.Parameter) ([]ParameterWithProfiles, error) {
-	params := make([]ParameterWithProfiles, 0, len(tcParams))
+// tcParamsToParamsWithProfiles unmarshals the Profiles that the tc struct doesn't.
+func tcParamsToParamsWithProfiles(tcParams []tc.Parameter) ([]parameterWithProfiles, error) {
+	params := make([]parameterWithProfiles, 0, len(tcParams))
 	for _, tcParam := range tcParams {
-		param := ParameterWithProfiles{Parameter: tcParam}
+		param := parameterWithProfiles{Parameter: tcParam}
 
 		profiles := []string{}
 		if err := json.Unmarshal(tcParam.Profiles, &profiles); err != nil {
@@ -374,10 +387,10 @@ func TCParamsToParamsWithProfiles(tcParams []tc.Parameter) ([]ParameterWithProfi
 	return params, nil
 }
 
-func ParameterWithProfilesToMap(tcParams []ParameterWithProfiles) []ParameterWithProfilesMap {
-	params := []ParameterWithProfilesMap{}
+func parameterWithProfilesToMap(tcParams []parameterWithProfiles) []parameterWithProfilesMap {
+	params := []parameterWithProfilesMap{}
 	for _, tcParam := range tcParams {
-		param := ParameterWithProfilesMap{Parameter: tcParam.Parameter, ProfileNames: map[string]struct{}{}}
+		param := parameterWithProfilesMap{Parameter: tcParam.Parameter, ProfileNames: map[string]struct{}{}}
 		for _, profile := range tcParam.ProfileNames {
 			param.ProfileNames[profile] = struct{}{}
 		}
@@ -386,7 +399,7 @@ func ParameterWithProfilesToMap(tcParams []ParameterWithProfiles) []ParameterWit
 	return params
 }
 
-func FilterDSS(dsses []tc.DeliveryServiceServer, dsIDs map[int]struct{}, serverIDs map[int]struct{}) []tc.DeliveryServiceServer {
+func filterDSS(dsses []tc.DeliveryServiceServer, dsIDs map[int]struct{}, serverIDs map[int]struct{}) []tc.DeliveryServiceServer {
 	// TODO filter only DSes on this server's CDN? Does anything ever needs DSS cross-CDN? Surely not.
 	//      Then, we can remove a bunch of config files that filter only DSes on the current cdn.
 	filtered := []tc.DeliveryServiceServer{}
@@ -409,10 +422,10 @@ func FilterDSS(dsses []tc.DeliveryServiceServer, dsIDs map[int]struct{}, serverI
 	return filtered
 }
 
-// FilterParams filters params and returns only the parameters which match configFile, name, and value.
+// filterParams filters params and returns only the parameters which match configFile, name, and value.
 // If configFile, name, or value is the empty string, it is not filtered.
 // Returns a slice of parameters.
-func FilterParams(params []tc.Parameter, configFile string, name string, value string, omitName string) []tc.Parameter {
+func filterParams(params []tc.Parameter, configFile string, name string, value string, omitName string) []tc.Parameter {
 	filtered := []tc.Parameter{}
 	for _, param := range params {
 		if configFile != "" && param.ConfigFile != configFile {
@@ -432,27 +445,30 @@ func FilterParams(params []tc.Parameter, configFile string, name string, value s
 	return filtered
 }
 
-// ParamsToMap converts a []tc.Parameter to a map[paramName]paramValue.
+// paramsToMap converts a []tc.Parameter to a map[paramName]paramValue.
 // If multiple params have the same value, the first one in params will be used an an error will be logged.
+// Warnings will be returned if any parameters have the same name but different values.
+// Returns the parameter map, and any warnings.
 // See ParamArrToMultiMap.
-func ParamsToMap(params []tc.Parameter) map[string]string {
+func paramsToMap(params []tc.Parameter) (map[string]string, []string) {
+	warnings := []string{}
 	mp := map[string]string{}
 	for _, param := range params {
 		if val, ok := mp[param.Name]; ok {
 			if val < param.Value {
-				log.Errorln("config generation got multiple parameters for name '" + param.Name + "' - ignoring '" + param.Value + "'")
+				warnings = append(warnings, "got multiple parameters for name '"+param.Name+"' - ignoring '"+param.Value+"'")
 				continue
 			} else {
-				log.Errorln("config generation got multiple parameters for name '" + param.Name + "' - ignoring '" + val + "'")
+				warnings = append(warnings, "config generation got multiple parameters for name '"+param.Name+"' - ignoring '"+val+"'")
 			}
 		}
 		mp[param.Name] = param.Value
 	}
-	return mp
+	return mp, warnings
 }
 
-// ParamArrToMultiMap converts a []tc.Parameter to a map[paramName][]paramValue.
-func ParamsToMultiMap(params []tc.Parameter) map[string][]string {
+// paramArrToMultiMap converts a []tc.Parameter to a map[paramName][]paramValue.
+func paramsToMultiMap(params []tc.Parameter) map[string][]string {
 	mp := map[string][]string{}
 	for _, param := range params {
 		mp[param.Name] = append(mp[param.Name], param.Value)
@@ -460,10 +476,10 @@ func ParamsToMultiMap(params []tc.Parameter) map[string][]string {
 	return mp
 }
 
-// GetServerIPAddress gets the old IPv4 tc.Server.IPAddress from the new tc.Server.Interfaces.
+// getServerIPAddress gets the old IPv4 tc.Server.IPAddress from the new tc.Server.Interfaces.
 // If no IPv4 address set as a ServiceAddress exists, returns nil
 // Malformed addresses are ignored and skipped.
-func GetServerIPAddress(sv *tc.ServerNullable) net.IP {
+func getServerIPAddress(sv *Server) net.IP {
 	for _, iFace := range sv.Interfaces {
 		for _, addr := range iFace.IPAddresses {
 			if !addr.ServiceAddress {
@@ -490,11 +506,11 @@ func GetServerIPAddress(sv *tc.ServerNullable) net.IP {
 	return nil
 }
 
-// GetServerServiceAddresses returns the first "service" addresses for IPv4 and IPv6 that it finds.
+// getServiceAddresses returns the first "service" addresses for IPv4 and IPv6 that it finds.
 // If an IPv4 or IPv6 "service" address is not found, returns nil for that IP.
 // If no IPv4 address set as a ServiceAddress exists, returns nil
 // Malformed addresses are ignored and skipped.
-func getServiceAddresses(sv *tc.ServerNullable) (net.IP, net.IP) {
+func getServiceAddresses(sv *Server) (net.IP, net.IP) {
 	v4 := net.IP(nil)
 	v6 := net.IP(nil)
 	for _, iFace := range sv.Interfaces {
@@ -541,27 +557,60 @@ func getServiceAddresses(sv *tc.ServerNullable) (net.IP, net.IP) {
 	return v4, v6
 }
 
-// GetTOToolNameAndURL takes the Global Parameters and returns the Traffic Ops Tool Name and URL, as set in the tc.GlobalProfileName Profile 'tm.toolname' and 'tm.url' name Parameters.
-func GetTOToolNameAndURL(globalParams []tc.Parameter) (string, string) {
-	// TODO move somewhere generic
-	toToolName := ""
-	toURL := ""
-	for _, param := range globalParams {
-		if param.Name == "tm.toolname" {
-			toToolName = param.Value
-		} else if param.Name == "tm.url" {
-			toURL = param.Value
+// getATSMajorVersion returns the ATS major version from the config_file 'package' name 'trafficserver' Parameter on the given Server Profile Parameters.
+// If no Parameter is found, or the value is malformed, a warning or error is logged and DefaultATSVersion is returned.
+// Returns the ATS major version, and any warnings
+func getATSMajorVersion(serverParams []tc.Parameter) (int, []string) {
+	warnings := []string{}
+	atsVersionParam := ""
+	for _, param := range serverParams {
+		if param.ConfigFile != "package" || param.Name != "trafficserver" {
+			continue
 		}
-		if toToolName != "" && toURL != "" {
-			break
+		atsVersionParam = param.Value
+		break
+	}
+	if atsVersionParam == "" {
+		warnings = append(warnings, "ATS version Parameter (config_file 'package' name 'trafficserver') not found on Server Profile, using default")
+		atsVersionParam = DefaultATSVersion
+	}
+
+	atsMajorVer, err := getATSMajorVersionFromATSVersion(atsVersionParam)
+	if err != nil {
+		warnings = append(warnings, "getting ATS major version from server Profile Parameter, using default: "+err.Error())
+		atsMajorVer, err = getATSMajorVersionFromATSVersion(DefaultATSVersion)
+		if err != nil {
+			// should never happen
+			warnings = append(warnings, "getting ATS major version from default version! Should never happen! Using 0, config will be malformed! : "+err.Error())
 		}
 	}
-	// TODO error here? Perl doesn't.
-	if toToolName == "" {
-		log.Warnln("Global Parameter tm.toolname not found, config may not be constructed properly!")
+	return atsMajorVer, warnings
+}
+
+// hasRequiredCapabilities returns whether the given caps has all the required capabilities in the given reqCaps.
+func hasRequiredCapabilities(caps map[ServerCapability]struct{}, reqCaps map[ServerCapability]struct{}) bool {
+	for reqCap, _ := range reqCaps {
+		if _, ok := caps[reqCap]; !ok {
+			return false
+		}
 	}
-	if toURL == "" {
-		log.Warnln("Global Parameter tm.url not found, config may not be constructed properly!")
+	return true
+}
+
+// makeErr takes a list of warnings and an error string, and combines them to a single error.
+// Configs typically generate a list of warnings as they go. When an error is encountered, we want to combine the warnings encountered and include them in the returned error message, since they're likely hints as to why the error occurred.
+func makeErr(warnings []string, err string) error {
+	if len(warnings) == 0 {
+		return errors.New(err)
 	}
-	return toToolName, toURL
+	return errors.New(`(warnings: ` + strings.Join(warnings, `, `) + `) ` + err)
+}
+
+// makeErrf is a convenience for formatting errors for makeErr.
+func makeErrf(warnings []string, format string, v ...interface{}) error {
+	return makeErr(warnings, fmt.Sprintf(format, v...))
+}
+
+func GetConfigFile(prefix string, xmlId string) string {
+	return prefix + xmlId + ConfigSuffix
 }
diff --git a/lib/go-atscfg/atscfg_test.go b/lib/go-atscfg/atscfg_test.go
index acdf6f7..806bd33 100644
--- a/lib/go-atscfg/atscfg_test.go
+++ b/lib/go-atscfg/atscfg_test.go
@@ -24,32 +24,23 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestGenericHeaderComment(t *testing.T) {
-	objName := "foo"
-	toolName := "bar"
-	toURL := "url"
-
-	txt := GenericHeaderComment(objName, toolName, toURL)
-
-	testComment(t, txt, objName, toolName, toURL)
+	commentTxt := "foo"
+	txt := makeHdrComment(commentTxt)
+	testComment(t, txt, commentTxt)
 }
 
-func testComment(t *testing.T, txt string, objName string, toolName string, toURL string) {
+func testComment(t *testing.T, txt string, commentTxt string) {
 	commentLine := strings.SplitN(txt, "\n", 2)[0] // SplitN always returns at least 1 element, no need to check len before indexing
 
 	if !strings.HasPrefix(strings.TrimSpace(commentLine), "#") {
 		t.Errorf("expected comment on first line, actual: '" + commentLine + "'")
 	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toolName '" + toolName + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toURL '" + toURL + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, objName) {
-		t.Errorf("expected profile '" + objName + "' in comment, actual: '" + commentLine + "'")
+	if !strings.Contains(commentLine, commentTxt) {
+		t.Errorf("expected comment text '" + commentTxt + "' in comment, actual: '" + commentLine + "'")
 	}
 }
 
@@ -106,154 +97,28 @@ func TestGetATSMajorVersionFromATSVersion(t *testing.T) {
 	}
 
 	for input, expected := range inputExpected {
-		if actual, err := GetATSMajorVersionFromATSVersion(input); err != nil {
+		if actual, err := getATSMajorVersionFromATSVersion(input); err != nil {
 			t.Errorf("expected %v actual: error '%v'", expected, err)
 		} else if actual != expected {
 			t.Errorf("expected %v actual: %v", expected, actual)
 		}
 	}
 	for _, input := range errExpected {
-		if actual, err := GetATSMajorVersionFromATSVersion(input); err == nil {
+		if actual, err := getATSMajorVersionFromATSVersion(input); err == nil {
 			t.Errorf("input %v expected: error, actual: nil error '%v'", input, actual)
 		}
 	}
 }
 
-func TestServerInfoIsTopLevelCache(t *testing.T) {
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          "cgTypeUnknown",
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: "cgTypeUnknown",
-		}
-		if s.IsTopLevelCache() {
-			t.Errorf("expected server with non-origin parent types, and non-InvalidID parent IDs to not be top level, actual top level")
-		}
-	}
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            -1,
-			ParentCacheGroupType:          "cgTypeUnknown",
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: "cgTypeUnknown",
-		}
-		if s.IsTopLevelCache() {
-			t.Errorf("expected server with secondary parent non-origin type and non-InvalidID to not be top level, actual top level")
-		}
-	}
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          "cgTypeUnknown",
-			SecondaryParentCacheGroupID:   -1,
-			SecondaryParentCacheGroupType: "cgTypeUnknown",
-		}
-		if s.IsTopLevelCache() {
-			t.Errorf("expected server with parent non-origin type and non-InvalidID to not be top level, actual top level")
-		}
-	}
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            -1,
-			ParentCacheGroupType:          "cgTypeUnknown",
-			SecondaryParentCacheGroupID:   -1,
-			SecondaryParentCacheGroupType: "cgTypeUnknown",
-		}
-		if !s.IsTopLevelCache() {
-			t.Errorf("expected server with parent and secondary parents with InvalidID IDs to be top level, actual not top level")
-		}
-	}
-
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          tc.CacheGroupOriginTypeName,
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: tc.CacheGroupOriginTypeName,
-		}
-		if !s.IsTopLevelCache() {
-			t.Errorf("expected server with parent and secondary parents with origin-type to be top level, actual not top level")
-		}
-	}
-
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          "not origin",
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: tc.CacheGroupOriginTypeName,
-		}
-		if s.IsTopLevelCache() {
-			t.Errorf("expected server with parent valid ID and origin-type to be top level, actual top level")
-		}
-	}
-
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          tc.CacheGroupOriginTypeName,
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: "not origin",
-		}
-		if s.IsTopLevelCache() {
-			t.Errorf("expected server with secondary parent valid ID and not origin-type to not be top level, actual top level")
-		}
-	}
-
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            1,
-			ParentCacheGroupType:          tc.CacheGroupOriginTypeName,
-			SecondaryParentCacheGroupID:   -1,
-			SecondaryParentCacheGroupType: "not origin",
-		}
-		if !s.IsTopLevelCache() {
-			t.Errorf("expected server with secondary parent invalid valid ID and parent origin type to be top level, actual not top level")
-		}
-	}
-
-	{
-		s := &ServerInfo{
-			ParentCacheGroupID:            -1,
-			ParentCacheGroupType:          "not origin",
-			SecondaryParentCacheGroupID:   1,
-			SecondaryParentCacheGroupType: tc.CacheGroupOriginTypeName,
-		}
-		if !s.IsTopLevelCache() {
-			t.Errorf("expected server with parent invalid valid ID and secondary parent origin type to be top level, actual not top level")
-		}
-	}
-}
-
-func TestGetConfigFile(t *testing.T) {
-	expected := "hdr_rw_my-xml-id.config"
-	cfgFile := GetConfigFile(HeaderRewritePrefix, "my-xml-id")
-	if cfgFile != expected {
-		t.Errorf("Expected %s.   Got %s", expected, cfgFile)
-	}
-}
-
-func TestHeaderCommentUTC(t *testing.T) {
-	objName := "foo"
-	toolName := "bar"
-	toURL := "url"
-
-	txt := GenericHeaderComment(objName, toolName, toURL)
-	if !strings.Contains(txt, " UTC ") {
-		t.Error("Expected header comment to print time in UTC, actual '" + txt + "'")
-	}
-}
-
-func setIP(sv *tc.ServerNullable, ipAddress string) {
+func setIP(sv *Server, ipAddress string) {
 	setIPInfo(sv, "", ipAddress, "")
 }
 
-func setIP6(sv *tc.ServerNullable, ip6Address string) {
+func setIP6(sv *Server, ip6Address string) {
 	setIPInfo(sv, "", "", ip6Address)
 }
 
-func setIPInfo(sv *tc.ServerNullable, interfaceName string, ipAddress string, ip6Address string) {
+func setIPInfo(sv *Server, interfaceName string, ipAddress string, ip6Address string) {
 	sv.Interfaces = []tc.ServerInterfaceInfo{
 		tc.ServerInterfaceInfo{
 			Name: interfaceName,
@@ -274,3 +139,82 @@ func setIPInfo(sv *tc.ServerNullable, interfaceName string, ipAddress string, ip
 		})
 	}
 }
+
+func makeGenericServer() *Server {
+	server := &Server{}
+	server.ProfileID = util.IntPtr(42)
+	server.CDNName = util.StrPtr("myCDN")
+	server.Cachegroup = util.StrPtr("cg0")
+	server.CachegroupID = util.IntPtr(422)
+	server.DomainName = util.StrPtr("mydomain.example.net")
+	server.CDNID = util.IntPtr(43)
+	server.HostName = util.StrPtr("myserver")
+	server.HTTPSPort = util.IntPtr(12443)
+	server.ID = util.IntPtr(44)
+	setIP(server, "192.168.2.1")
+	server.ProfileID = util.IntPtr(46)
+	server.Profile = util.StrPtr("serverprofile")
+	server.TCPPort = util.IntPtr(80)
+	server.Type = "EDGE"
+	server.TypeID = util.IntPtr(91)
+	status := string(tc.CacheStatusReported)
+	server.Status = &status
+	server.StatusID = util.IntPtr(99)
+	return server
+}
+
+func makeGenericDS() *DeliveryService {
+	ds := &DeliveryService{}
+	ds.ID = util.IntPtr(42)
+	ds.XMLID = util.StrPtr("ds1")
+	ds.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
+	ds.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
+	dsType := tc.DSTypeDNS
+	ds.Type = &dsType
+	ds.MultiSiteOrigin = util.BoolPtr(false)
+	ds.Active = util.BoolPtr(true)
+	return ds
+}
+
+// makeDSS creates DSS as an outer product of every server and ds given.
+// The given servers and dses must all have non-nil, unique IDs.
+func makeDSS(servers []Server, dses []DeliveryService) []tc.DeliveryServiceServer {
+	dss := []tc.DeliveryServiceServer{}
+	for _, sv := range servers {
+		for _, ds := range dses {
+			dss = append(dss, tc.DeliveryServiceServer{
+				Server:          util.IntPtr(*sv.ID),
+				DeliveryService: util.IntPtr(*ds.ID),
+			})
+		}
+	}
+	return dss
+}
+
+func makeParamsFromMapArr(profile string, configFile string, paramM map[string][]string) []tc.Parameter {
+	params := []tc.Parameter{}
+	for name, vals := range paramM {
+		for _, val := range vals {
+			params = append(params, tc.Parameter{
+				Name:       name,
+				ConfigFile: configFile,
+				Value:      val,
+				Profiles:   []byte(`["` + profile + `"]`),
+			})
+		}
+	}
+	return params
+}
+
+func makeParamsFromMap(profile string, configFile string, paramM map[string]string) []tc.Parameter {
+	params := []tc.Parameter{}
+	for name, val := range paramM {
+		params = append(params, tc.Parameter{
+			Name:       name,
+			ConfigFile: configFile,
+			Value:      val,
+			Profiles:   []byte(`["` + profile + `"]`),
+		})
+	}
+	return params
+}
diff --git a/lib/go-atscfg/atsdotrules.go b/lib/go-atscfg/atsdotrules.go
index 286d44e..77fd206 100644
--- a/lib/go-atscfg/atsdotrules.go
+++ b/lib/go-atscfg/atsdotrules.go
@@ -21,18 +21,28 @@ package atscfg
 
 import (
 	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
+const ATSDotRulesFileName = StorageFileName
 const ContentTypeATSDotRules = ContentTypeTextASCII
 const LineCommentATSDotRules = LineCommentHash
 
 func MakeATSDotRules(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, StorageFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	text := GenericHeaderComment(profileName, toToolName, toURL)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "server missing Profile")
+	}
+
+	serverParams = filterParams(serverParams, ATSDotRulesFileName, "", "", "location")
+	paramData, paramWarns := paramsToMap(serverParams)
+	warnings = append(warnings, paramWarns...)
+	text := makeHdrComment(hdrComment)
 
 	drivePrefix := strings.TrimPrefix(paramData["Drive_Prefix"], `/dev/`)
 	drivePostfix := strings.Split(paramData["Drive_Letters"], ",")
@@ -52,5 +62,10 @@ func MakeATSDotRules(
 		}
 	}
 
-	return text
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeATSDotRules,
+		LineComment: LineCommentATSDotRules,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/atsdotrules_test.go b/lib/go-atscfg/atsdotrules_test.go
index 8c6c87b..272ada8 100644
--- a/lib/go-atscfg/atsdotrules_test.go
+++ b/lib/go-atscfg/atsdotrules_test.go
@@ -22,22 +22,51 @@ package atscfg
 import (
 	"strings"
 	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func TestMakeATSDotRules(t *testing.T) {
-	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
-		"Drive_Prefix":      "/dev/sd",
-		"Drive_Letters":     "a,b,c,d,e",
-		"RAM_Drive_Prefix":  "/dev/ra",
-		"RAM_Drive_Letters": "f,g,h",
+	server := makeGenericServer()
+	serverProfile := "myProfile"
+	server.Profile = &serverProfile
+
+	hdr := "myHeaderComment"
+
+	serverParams := []tc.Parameter{
+		{
+			Name:       "Drive_Prefix",
+			ConfigFile: ATSDotRulesFileName,
+			Value:      "/dev/sd",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
+		{
+			Name:       "Drive_Letters",
+			ConfigFile: ATSDotRulesFileName,
+			Value:      "a,b,c,d,e",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
+		{
+			Name:       "RAM_Drive_Prefix",
+			ConfigFile: ATSDotRulesFileName,
+			Value:      "/dev/ra",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
+		{
+			Name:       "RAM_Drive_Letters",
+			ConfigFile: ATSDotRulesFileName,
+			Value:      "f,g,h",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
 	}
 
-	txt := MakeATSDotRules(profileName, paramData, toolName, toURL)
+	cfg, err := MakeATSDotRules(server, serverParams, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if count := strings.Count(txt, "\n"); count != 9 { // one line for each drive letter, plus 1 comment
 		t.Errorf("expected one line for each drive letter plus a comment, actual: '%v' count %v", txt, count)
diff --git a/lib/go-atscfg/bgfetchdotconfig.go b/lib/go-atscfg/bgfetchdotconfig.go
index 7629e40..55531bd 100644
--- a/lib/go-atscfg/bgfetchdotconfig.go
+++ b/lib/go-atscfg/bgfetchdotconfig.go
@@ -19,19 +19,26 @@ package atscfg
  * under the License.
  */
 
-import (
-	"github.com/apache/trafficcontrol/lib/go-tc"
-)
-
 const ContentTypeBGFetchDotConfig = ContentTypeTextASCII
 const LineCommentBGFetchDotConfig = LineCommentHash
 
 func MakeBGFetchDotConfig(
-	cdnName tc.CDNName,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	text := GenericHeaderComment(string(cdnName), toToolName, toURL)
+	server *Server,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "server missing CDNName")
+	}
+
+	text := makeHdrComment(hdrComment)
 	text += "include User-Agent *\n"
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeBGFetchDotConfig,
+		LineComment: LineCommentBGFetchDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/bgfetchdotconfig_test.go b/lib/go-atscfg/bgfetchdotconfig_test.go
index 0208feb..45ae836 100644
--- a/lib/go-atscfg/bgfetchdotconfig_test.go
+++ b/lib/go-atscfg/bgfetchdotconfig_test.go
@@ -22,24 +22,23 @@ package atscfg
 import (
 	"strings"
 	"testing"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func TestMakeBGFetchDotConfig(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	cdnName := "mycdn"
 
-	txt := MakeBGFetchDotConfig(cdnName, toToolName, toURL)
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
-	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+	hdr := "myHeaderComment"
+
+	cfg, err := MakeBGFetchDotConfig(server, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment '" + hdr + "', actual: missing")
 	}
 
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
diff --git a/lib/go-atscfg/cachedotconfig.go b/lib/go-atscfg/cachedotconfig.go
index 7b23107..53f55aa 100644
--- a/lib/go-atscfg/cachedotconfig.go
+++ b/lib/go-atscfg/cachedotconfig.go
@@ -23,33 +23,105 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const ContentTypeCacheDotConfig = ContentTypeTextASCII
 const LineCommentCacheDotConfig = LineCommentHash
 
-type ProfileDS struct {
-	Type       tc.DSType
-	OriginFQDN *string
-}
-
 // MakeCacheDotConfig makes the ATS cache.config config file.
-// profileDSes must be the list of delivery services, which are assigned to severs, for which this profile is assigned. It MUST NOT contain any other delivery services. Note DSesToProfileDSes may be helpful if you have a []tc.DeliveryServiceNullable, for example from traffic_ops/client.
 func MakeCacheDotConfig(
-	profileName string,
-	profileDSes []ProfileDS,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
+	server *Server,
+	servers []Server,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	hdrComment string,
+) (Cfg, error) {
+	if tc.CacheTypeFromString(server.Type) == tc.CacheTypeMid {
+		return makeCacheDotConfigMid(server, deliveryServices, hdrComment)
+	} else {
+		return makeCacheDotConfigEdge(server, servers, deliveryServices, deliveryServiceServers, hdrComment)
+	}
+}
+
+func makeCacheDotConfigEdge(
+	server *Server,
+	servers []Server,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "server missing profile")
+	}
+
+	profileServerIDsMap := map[int]struct{}{}
+	for _, sv := range servers {
+		if sv.Profile == nil {
+			warnings = append(warnings, "servers had server with nil profile, skipping!")
+			continue
+		}
+		if sv.ID == nil {
+			warnings = append(warnings, "servers had server with nil id, skipping!")
+			continue
+		}
+		if *sv.Profile != *server.Profile {
+			continue
+		}
+		profileServerIDsMap[*sv.ID] = struct{}{}
+	}
+
+	dsServers := filterDSS(deliveryServiceServers, nil, profileServerIDsMap)
+
+	dsIDs := map[int]struct{}{}
+	for _, dss := range dsServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			warnings = append(warnings, "deliveryservice-servers had entry with nil values, skipping!")
+			continue
+		}
+		if _, ok := profileServerIDsMap[*dss.Server]; !ok {
+			continue
+		}
+		dsIDs[*dss.DeliveryService] = struct{}{}
+	}
+
+	profileDSes := []profileDS{}
+	for _, ds := range deliveryServices {
+		if ds.ID == nil {
+			warnings = append(warnings, "deliveryservices had ds with nil id, skipping!")
+			continue
+		}
+		if ds.Type == nil {
+			warnings = append(warnings, "deliveryservices had ds with nil type, skipping!")
+			continue
+		}
+		if ds.OrgServerFQDN == nil {
+			continue // this is normal for steering and anymap dses
+		}
+		if *ds.Type == tc.DSTypeInvalid {
+			warnings = append(warnings, "deliveryservices had ds with invalid type, skipping!")
+			continue
+		}
+		if *ds.OrgServerFQDN == "" {
+			warnings = append(warnings, "deliveryservices had ds with empty origin, skipping!")
+			continue
+		}
+		if _, ok := dsIDs[*ds.ID]; !ok && ds.Topology == nil {
+			continue
+		}
+		origin := *ds.OrgServerFQDN
+		profileDSes = append(profileDSes, profileDS{Type: *ds.Type, OriginFQDN: &origin})
+	}
+
 	lines := map[string]struct{}{} // use a "set" for lines, to avoid duplicates, since we're looking up by profile
 	for _, ds := range profileDSes {
 		if ds.Type != tc.DSTypeHTTPNoCache {
 			continue
 		}
 		if ds.OriginFQDN == nil || *ds.OriginFQDN == "" {
-			log.Warnf("profileCacheDotConfig ds has no origin fqdn, skipping!") // TODO add ds name to data loaded, to put it in the error here?
+			warnings = append(warnings, "profileCacheDotConfig ds has no origin fqdn, skipping!") // TODO add ds name to data loaded, to put it in the error here?
 			continue
 		}
 		originFQDN, originPort := getHostPortFromURI(*ds.OriginFQDN)
@@ -71,17 +143,28 @@ func MakeCacheDotConfig(
 	if text == "" {
 		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
+	hdr := makeHdrComment(hdrComment)
 	text = hdr + text
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeCacheDotConfig,
+		LineComment: LineCommentCacheDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type profileDS struct {
+	Type       tc.DSType
+	OriginFQDN *string
 }
 
-// DSesToProfileDSes is a helper function to convert a []tc.DeliveryServiceNullable to []ProfileDS.
+// dsesToProfileDSes is a helper function to convert a []tc.DeliveryServiceNullable to []ProfileDS.
 // Note this does not check for nil values. If any DeliveryService's Type or OrgServerFQDN may be nil, the returned ProfileDS should be checked for DSTypeInvalid and nil, respectively.
-func DSesToProfileDSes(dses []tc.DeliveryServiceNullable) []ProfileDS {
-	pdses := []ProfileDS{}
+func dsesToProfileDSes(dses []tc.DeliveryServiceNullable) []profileDS {
+	pdses := []profileDS{}
 	for _, ds := range dses {
-		pds := ProfileDS{}
+		pds := profileDS{}
 		if ds.Type != nil {
 			pds.Type = *ds.Type
 		}
diff --git a/lib/go-atscfg/cachedotconfig_test.go b/lib/go-atscfg/cachedotconfig_test.go
index b2d6a4d..3b8bca1 100644
--- a/lib/go-atscfg/cachedotconfig_test.go
+++ b/lib/go-atscfg/cachedotconfig_test.go
@@ -24,24 +24,49 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeCacheDotConfig(t *testing.T) {
-	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	originFQDN0 := "my.fqdn.example.net"
-	originFQDN1 := "my.other.fqdn.example.net"
-	originFQDNNoCache := "nocache-fqn.example.net"
-	profileDSes := []ProfileDS{
-		ProfileDS{Type: tc.DSTypeHTTP, OriginFQDN: &originFQDN0},
-		ProfileDS{Type: tc.DSTypeDNS, OriginFQDN: &originFQDN1},
-		ProfileDS{Type: tc.DSTypeHTTPNoCache, OriginFQDN: &originFQDNNoCache},
-	}
+	server := makeGenericServer()
+	serverProfile := "myProfile"
+	server.Profile = &serverProfile
+	servers := []Server{*server}
+
+	ds0 := makeGenericDS()
+	ds0.ID = util.IntPtr(420)
+	ds0.XMLID = util.StrPtr("ds0")
+	ds0.OrgServerFQDN = util.StrPtr("http://my.fqdn.example.net")
+	ds0Type := tc.DSTypeHTTP
+	ds0.Type = &ds0Type
+
+	ds1 := makeGenericDS()
+	ds1.ID = util.IntPtr(421)
+	ds1.XMLID = util.StrPtr("ds1")
+	ds1.OrgServerFQDN = util.StrPtr("http://my.other.fqdn.example.net")
+	ds1Type := tc.DSTypeDNS
+	ds1.Type = &ds1Type
+
+	ds2 := makeGenericDS()
+	ds2.ID = util.IntPtr(422)
+	ds2.XMLID = util.StrPtr("ds2")
+	ds2.OrgServerFQDN = util.StrPtr("http://nocache-fqn.example.net")
+	ds2Type := tc.DSTypeHTTPNoCache
+	ds2.Type = &ds2Type
 
-	txt := MakeCacheDotConfig(profileName, profileDSes, toolName, toURL)
+	dses := []DeliveryService{*ds0, *ds1, *ds2}
+
+	dss := makeDSS(servers, dses)
+
+	hdr := "myHeaderComment"
+
+	cfg, err := MakeCacheDotConfig(server, servers, dses, dss, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if strings.Contains(txt, "my.fqdn.example.net") {
 		t.Errorf("expected cached DS type 'my.fqdn.example.net' omitted, actual: '%v'", txt)
diff --git a/lib/go-atscfg/cacheurldotconfig.go b/lib/go-atscfg/cacheurldotconfig.go
index 263984a..557a5ce 100644
--- a/lib/go-atscfg/cacheurldotconfig.go
+++ b/lib/go-atscfg/cacheurldotconfig.go
@@ -20,53 +20,77 @@ package atscfg
  */
 
 import (
+	"fmt"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const ContentTypeCacheURLDotConfig = ContentTypeTextASCII
 const LineCommentCacheURLDotConfig = LineCommentHash
 
-type CacheURLDS struct {
-	OrgServerFQDN string
-	QStringIgnore int
-	CacheURL      string
-}
+func MakeCacheURLDotConfig(
+	fileName string,
+	server *Server,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "server missing CDNName")
+	}
 
-func DeliveryServicesToCacheURLDSes(dses []tc.DeliveryServiceNullableV30) map[tc.DeliveryServiceName]CacheURLDS {
-	sDSes := map[tc.DeliveryServiceName]CacheURLDS{}
-	for _, ds := range dses {
-		if ds.OrgServerFQDN == nil || ds.QStringIgnore == nil || ds.XMLID == nil || ds.Active == nil {
-			log.Errorf("atscfg.DeliveryServicesToCacheURLDSes got DS %+v with nil values! Skipping!", ds)
+	dsIDs := map[int]struct{}{}
+	for _, ds := range deliveryServices {
+		if ds.ID != nil {
+			dsIDs[*ds.ID] = struct{}{} // TODO warn?
+		}
+	}
+
+	dss := filterDSS(deliveryServiceServers, dsIDs, nil)
+
+	dssMap := map[int][]int{} // map[dsID]serverID
+	for _, dss := range dss {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			warnings = append(warnings, "Delivery Service Servers had nil entries, skipping!")
 			continue
 		}
-		if !*ds.Active {
+		dssMap[*dss.DeliveryService] = append(dssMap[*dss.DeliveryService], *dss.Server)
+	}
+
+	dsesWithServers := []DeliveryService{}
+	for _, ds := range deliveryServices {
+		if ds.ID == nil {
+			warnings = append(warnings, "Delivery Service had nil id, skipping!")
 			continue
 		}
-		sds := CacheURLDS{OrgServerFQDN: *ds.OrgServerFQDN, QStringIgnore: *ds.QStringIgnore}
-		if ds.CacheURL != nil {
-			sds.CacheURL = *ds.CacheURL
+		// ANY_MAP and STEERING DSes don't have origins, and thus can't be put into the cacheurl config.
+		if ds.Type != nil && (*ds.Type == tc.DSTypeAnyMap || *ds.Type == tc.DSTypeSteering) {
+			continue
 		}
-		sDSes[tc.DeliveryServiceName(*ds.XMLID)] = sds
+		if len(dssMap[*ds.ID]) == 0 && ds.Topology == nil {
+			continue
+		}
+		dsesWithServers = append(dsesWithServers, ds)
 	}
-	return sDSes
-}
 
-func MakeCacheURLDotConfig(
-	cdnName tc.CDNName,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	fileName string,
-	dses map[tc.DeliveryServiceName]CacheURLDS,
-) string {
-	text := GenericHeaderComment(string(cdnName), toToolName, toURL)
+	dses, dsWarns := deliveryServicesToCacheURLDSes(dsesWithServers)
+	warnings = append(warnings, dsWarns...)
+
+	text := makeHdrComment(hdrComment)
 
 	if fileName == "cacheurl_qstring.config" { // This is the per remap drop qstring w cacheurl use case, the file is the same for all remaps
 		text += `http://([^?]+)(?:\?|$)  http://$1` + "\n"
 		text += `https://([^?]+)(?:\?|$)  https://$1` + "\n"
-		return text
+
+		return Cfg{
+			Text:        text,
+			ContentType: ContentTypeCacheURLDotConfig,
+			LineComment: LineCommentCacheURLDotConfig,
+			Warnings:    warnings,
+		}, nil
 	}
 
 	if fileName == "cacheurl.config" { // this is the global drop qstring w cacheurl use case
@@ -86,7 +110,8 @@ func MakeCacheURLDotConfig(
 			}
 
 			if !strings.HasPrefix(org, scheme) {
-				log.Errorln("MakeCacheURLDotConfig got ds '" + string(dsName) + "' origin '" + org + "' with no scheme! cacheurl.config will likely be malformed!")
+				// TODO determine if we should return an empty config here. A bad DS should not break config gen, and MUST NOT for self-service
+				warnings = append(warnings, "ds '"+string(dsName)+"' origin '"+org+"' with no scheme! cacheurl.config will likely be malformed!")
 			}
 
 			fqdnPath := strings.TrimPrefix(org, scheme)
@@ -96,7 +121,12 @@ func MakeCacheURLDotConfig(
 			seenOrigins[ds.OrgServerFQDN] = struct{}{}
 		}
 		text = strings.Replace(text, `__RETURN__`, "\n", -1)
-		return text
+		return Cfg{
+			Text:        text,
+			ContentType: ContentTypeCacheURLDotConfig,
+			LineComment: LineCommentCacheURLDotConfig,
+			Warnings:    warnings,
+		}, nil
 	}
 
 	// TODO verify prefix and suffix exist, and warn if they don't? Perl doesn't
@@ -104,9 +134,42 @@ func MakeCacheURLDotConfig(
 
 	ds, ok := dses[dsName]
 	if !ok {
-		return text // TODO warn? Perl doesn't
+		warnings = append(warnings, "ds '"+string(dsName)+"' not found, not creating in cacheurl config!")
+	} else {
+		text += ds.CacheURL + "\n"
+		text = strings.Replace(text, `__RETURN__`, "\n", -1)
+	}
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeCacheURLDotConfig,
+		LineComment: LineCommentCacheURLDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type cacheURLDS struct {
+	OrgServerFQDN string
+	QStringIgnore int
+	CacheURL      string
+}
+
+// DeliveryServicesToCacheURLDSes returns the "CacheURLDS" map, and any warnings.
+func deliveryServicesToCacheURLDSes(dses []DeliveryService) (map[tc.DeliveryServiceName]cacheURLDS, []string) {
+	warnings := []string{}
+	sDSes := map[tc.DeliveryServiceName]cacheURLDS{}
+	for _, ds := range dses {
+		if ds.OrgServerFQDN == nil || ds.QStringIgnore == nil || ds.XMLID == nil || ds.Active == nil {
+			warnings = append(warnings, fmt.Sprintf("atscfg.DeliveryServicesToCacheURLDSes got DS %+v with nil values! Skipping!", ds))
+			continue
+		}
+		if !*ds.Active {
+			continue
+		}
+		sds := cacheURLDS{OrgServerFQDN: *ds.OrgServerFQDN, QStringIgnore: *ds.QStringIgnore}
+		if ds.CacheURL != nil {
+			sds.CacheURL = *ds.CacheURL
+		}
+		sDSes[tc.DeliveryServiceName(*ds.XMLID)] = sds
 	}
-	text += ds.CacheURL + "\n"
-	text = strings.Replace(text, `__RETURN__`, "\n", -1)
-	return text
+	return sDSes, warnings
 }
diff --git a/lib/go-atscfg/cacheurldotconfig_test.go b/lib/go-atscfg/cacheurldotconfig_test.go
index e1c100d..20aab11 100644
--- a/lib/go-atscfg/cacheurldotconfig_test.go
+++ b/lib/go-atscfg/cacheurldotconfig_test.go
@@ -23,78 +23,84 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeCacheURLDotConfigWithDS(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	server := makeGenericServer()
+	cdnName := "mycdn"
+	server.CDNName = &cdnName
+
+	hdr := "myHeaderComment"
 
 	fileName := "cacheurl_myds.config"
 
-	dses := map[tc.DeliveryServiceName]CacheURLDS{
-		"myds": CacheURLDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-		},
-	}
+	ds0 := makeGenericDS()
+	ds0.ID = util.IntPtr(420)
+	ds0.XMLID = util.StrPtr("myds")
+	ds0.OrgServerFQDN = util.StrPtr("http://myorigin.example.net")
+	ds0.QStringIgnore = util.IntPtr(0)
+	ds0.CacheURL = util.StrPtr("http://mycacheurl.net")
 
-	txt := MakeCacheURLDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	servers := []Server{*server}
+	dses := []DeliveryService{*ds0}
+	dss := makeDSS(servers, dses)
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
-	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
+	cfg, err := MakeCacheURLDotConfig(fileName, server, dses, dss, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment text '"+hdr+"', actual: %v", txt)
 	}
 
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
-		t.Errorf("expected: header comment, actual: missing")
+		t.Errorf("expected: header comment, actual: %v", txt)
 	}
 
 	if !strings.Contains(txt, "mycacheurl") {
-		t.Errorf("expected: contains cacheurl, actual: missing")
+		t.Errorf("expected: contains cacheurl, actual: %v", txt)
 	}
 }
 
 func TestMakeCacheURLDotConfigGlobalFile(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	server := makeGenericServer()
+	cdnName := "mycdn"
+	server.CDNName = &cdnName
+
+	hdr := "myHeaderComment"
 
 	fileName := "cacheurl.config"
 
-	dses := map[tc.DeliveryServiceName]CacheURLDS{
-		"myds": CacheURLDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 1,
-			CacheURL:      "https://mycacheurl.net",
-		},
-	}
+	ds0 := makeGenericDS()
+	ds0.ID = util.IntPtr(420)
+	ds0.XMLID = util.StrPtr("ds0")
+	ds0.OrgServerFQDN = util.StrPtr("http://myorigin.example.net")
+	ds0.QStringIgnore = util.IntPtr(1)
+	ds0.CacheURL = util.StrPtr("http://mycacheurl.net")
 
-	txt := MakeCacheURLDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	servers := []Server{*server}
+	dses := []DeliveryService{*ds0}
+	dss := makeDSS(servers, dses)
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
+	cfg, err := MakeCacheURLDotConfig(fileName, server, dses, dss, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
-	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment text '"+hdr+"', actual: %v", txt)
 	}
 
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
-		t.Errorf("expected: header comment, actual: missing")
+		t.Errorf("expected: header comment, actual: %v", txt)
 	}
 
 	if !strings.Contains(txt, "myorigin") {
-		t.Errorf("expected: contains origin, actual: missing")
+		t.Errorf("expected: contains origin, actual: %v", txt)
 	}
 
 	if strings.Contains(txt, "mycacheurl") {
@@ -103,30 +109,33 @@ func TestMakeCacheURLDotConfigGlobalFile(t *testing.T) {
 }
 
 func TestMakeCacheURLDotConfigGlobalFileNoQStringIgnore(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	server := makeGenericServer()
+	cdnName := "mycdn"
+	server.CDNName = &cdnName
+
+	hdr := "myHeaderComment"
 
 	fileName := "cacheurl.config"
 
-	dses := map[tc.DeliveryServiceName]CacheURLDS{
-		"myds": CacheURLDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-		},
-	}
+	ds0 := makeGenericDS()
+	ds0.ID = util.IntPtr(420)
+	ds0.XMLID = util.StrPtr("ds0")
+	ds0.OrgServerFQDN = util.StrPtr("http://myorigin.example.net")
+	ds0.QStringIgnore = util.IntPtr(0)
+	ds0.CacheURL = util.StrPtr("http://mycacheurl.net")
 
-	txt := MakeCacheURLDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	servers := []Server{*server}
+	dses := []DeliveryService{*ds0}
+	dss := makeDSS(servers, dses)
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
+	cfg, err := MakeCacheURLDotConfig(fileName, server, dses, dss, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
-	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment text '" + hdr + "', actual: missing")
 	}
 
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
@@ -143,30 +152,33 @@ func TestMakeCacheURLDotConfigGlobalFileNoQStringIgnore(t *testing.T) {
 }
 
 func TestMakeCacheURLDotConfigQStringFile(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	server := makeGenericServer()
+	cdnName := "mycdn"
+	server.CDNName = &cdnName
+
+	hdr := "myHeaderComment"
 
 	fileName := "cacheurl_qstring.config"
 
-	dses := map[tc.DeliveryServiceName]CacheURLDS{
-		"myds": CacheURLDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-		},
-	}
+	ds0 := makeGenericDS()
+	ds0.ID = util.IntPtr(420)
+	ds0.XMLID = util.StrPtr("ds0")
+	ds0.OrgServerFQDN = util.StrPtr("http://myorigin.example.net")
+	ds0.QStringIgnore = util.IntPtr(0)
+	ds0.CacheURL = util.StrPtr("http://mycacheurl.net")
 
-	txt := MakeCacheURLDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	servers := []Server{*server}
+	dses := []DeliveryService{*ds0}
+	dss := makeDSS(servers, dses)
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
+	cfg, err := MakeCacheURLDotConfig(fileName, server, dses, dss, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
-	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment '" + hdr + "', actual: missing")
 	}
 
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
diff --git a/lib/go-atscfg/chkconfig.go b/lib/go-atscfg/chkconfig.go
index 9d870c9..dbdc5e1 100644
--- a/lib/go-atscfg/chkconfig.go
+++ b/lib/go-atscfg/chkconfig.go
@@ -23,7 +23,7 @@ import (
 	"encoding/json"
 	"sort"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const ChkconfigFileName = `chkconfig`
@@ -31,42 +31,47 @@ const ChkconfigParamConfigFile = `chkconfig`
 const ContentTypeChkconfig = ContentTypeTextASCII
 const LineCommentChkconfig = LineCommentHash
 
-type ChkConfigEntry struct {
-	Name string `json:"name"`
-	Val  string `json:"value"`
-}
-
-type ChkConfigEntries []ChkConfigEntry
-
-func (e ChkConfigEntries) Len() int { return len(e) }
-func (e ChkConfigEntries) Less(i, j int) bool {
-	if e[i].Name != e[j].Name {
-		return e[i].Name < e[j].Name
-	}
-	return e[i].Val < e[j].Val
-}
-func (e ChkConfigEntries) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
-
 // MakeChkconfig returns the 'chkconfig' ATS config file endpoint.
 // This is a JSON object, and should be served with an 'application/json' Content-Type.
 func MakeChkconfig(
-	params map[string][]string, // map[name]value - config file should always be 'chkconfig'
-) string {
+	serverParams []tc.Parameter,
+) (Cfg, error) {
+	warnings := []string{}
+
+	serverParams = filterParams(serverParams, ChkconfigParamConfigFile, "", "", "")
 
-	chkconfig := []ChkConfigEntry{}
-	for name, vals := range params {
-		for _, val := range vals {
-			chkconfig = append(chkconfig, ChkConfigEntry{Name: name, Val: val})
-		}
+	chkconfig := []chkConfigEntry{}
+	for _, param := range serverParams {
+		chkconfig = append(chkconfig, chkConfigEntry{Name: param.Name, Val: param.Value})
 	}
 
-	sort.Sort(ChkConfigEntries(chkconfig))
+	sort.Sort(chkConfigEntries(chkconfig))
 
 	bts, err := json.Marshal(&chkconfig)
 	if err != nil {
-		// should never happen
-		log.Errorln("marshalling chkconfig NameVals: " + err.Error())
-		bts = []byte("error encoding params to json, see Traffic Ops log for details")
+		return Cfg{}, makeErr(warnings, "marshalling chkconfig NameVals: "+err.Error())
 	}
-	return string(bts)
+
+	return Cfg{
+		Text:        string(bts),
+		ContentType: ContentTypeChkconfig,
+		LineComment: LineCommentChkconfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type chkConfigEntry struct {
+	Name string
+	Val  string
+}
+
+type chkConfigEntries []chkConfigEntry
+
+func (e chkConfigEntries) Len() int { return len(e) }
+func (e chkConfigEntries) Less(i, j int) bool {
+	if e[i].Name != e[j].Name {
+		return e[i].Name < e[j].Name
+	}
+	return e[i].Val < e[j].Val
 }
+func (e chkConfigEntries) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
diff --git a/lib/go-atscfg/chkconfig_test.go b/lib/go-atscfg/chkconfig_test.go
index eaca8cf..45a8efd 100644
--- a/lib/go-atscfg/chkconfig_test.go
+++ b/lib/go-atscfg/chkconfig_test.go
@@ -22,23 +22,48 @@ package atscfg
 import (
 	"encoding/json"
 	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func TestMakeChkconfig(t *testing.T) {
-	params := map[string][]string{
-		"p0": []string{"p0v0", "p0v1"},
-		"1":  []string{"p1v0"},
+	serverProfile := "sp0"
+	params := []tc.Parameter{
+		{
+			Name:       "p0",
+			ConfigFile: ChkconfigParamConfigFile,
+			Value:      "p0v0",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
+		{
+			Name:       "p0",
+			ConfigFile: ChkconfigParamConfigFile,
+			Value:      "p0v1",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
+		{
+			Name:       "1",
+			ConfigFile: ChkconfigParamConfigFile,
+			Value:      "p1v0",
+			Profiles:   []byte(`["` + serverProfile + `"]`),
+		},
 	}
 
-	txt := MakeChkconfig(params)
+	cfg, err := MakeChkconfig(params)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	chkconfig := []ChkConfigEntry{}
+	chkconfig := []chkConfigEntry{}
 	if err := json.Unmarshal([]byte(txt), &chkconfig); err != nil {
 		t.Fatalf("MakePackages expected a JSON array of objects, actual: " + err.Error())
 	}
 
+	paramsMap := paramsToMultiMap(params)
+
 	for _, chkConfigEntry := range chkconfig {
-		vals, ok := params[chkConfigEntry.Name]
+		vals, ok := paramsMap[chkConfigEntry.Name]
 		if !ok {
 			t.Errorf("expected %+v actual %v\n", params, chkConfigEntry.Name)
 		}
@@ -47,6 +72,6 @@ func TestMakeChkconfig(t *testing.T) {
 			t.Errorf("expected %+v actual %v\n", vals, chkConfigEntry.Val)
 		}
 
-		params[chkConfigEntry.Name] = strArrRemove(vals, chkConfigEntry.Val)
+		paramsMap[chkConfigEntry.Name] = strArrRemove(vals, chkConfigEntry.Val)
 	}
 }
diff --git a/lib/go-atscfg/dropqstringdotconfig.go b/lib/go-atscfg/dropqstringdotconfig.go
index 68d9189..42d13a7 100644
--- a/lib/go-atscfg/dropqstringdotconfig.go
+++ b/lib/go-atscfg/dropqstringdotconfig.go
@@ -19,22 +19,49 @@ package atscfg
  * under the License.
  */
 
+import (
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
 const DropQStringDotConfigFileName = "drop_qstring.config"
 const DropQStringDotConfigParamName = "content"
 const ContentTypeDropQStringDotConfig = ContentTypeTextASCII
 const LineCommentDropQStringDotConfig = LineCommentHash
 
 func MakeDropQStringDotConfig(
-	profileName string,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	dropQStringVal *string, // value of the parameter name "content" configFile "drop_qstring.config"; nil if it doesn't exist
-) string {
-	text := GenericHeaderComment(profileName, toToolName, toURL)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Profile")
+	}
+
+	dropQStringVal := (*string)(nil)
+	for _, param := range serverParams {
+		if param.ConfigFile != DropQStringDotConfigFileName {
+			continue
+		}
+		if param.Name != DropQStringDotConfigParamName {
+			continue
+		}
+		dropQStringVal = &param.Value
+		break
+	}
+
+	text := makeHdrComment(hdrComment)
 	if dropQStringVal != nil {
 		text += *dropQStringVal + "\n"
 	} else {
 		text += `/([^?]+) $s://$t/$1` + "\n"
 	}
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeDropQStringDotConfig,
+		LineComment: LineCommentDropQStringDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/dropqstringdotconfig_test.go b/lib/go-atscfg/dropqstringdotconfig_test.go
index d49cc68..29bb6b2 100644
--- a/lib/go-atscfg/dropqstringdotconfig_test.go
+++ b/lib/go-atscfg/dropqstringdotconfig_test.go
@@ -22,17 +22,35 @@ package atscfg
 import (
 	"strings"
 	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func TestMakeDropQStringDotConfig(t *testing.T) {
-	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
 	dropQStringVal := "myDropQStringVal"
+	profileName := "myProfile"
+
+	server := makeGenericServer()
+	server.Profile = &profileName
 
-	txt := MakeDropQStringDotConfig(profileName, toolName, toURL, &dropQStringVal)
+	params := []tc.Parameter{
+		{
+			Name:       DropQStringDotConfigParamName,
+			ConfigFile: DropQStringDotConfigFileName,
+			Value:      dropQStringVal,
+			Profiles:   []byte(`["` + profileName + `"]`),
+		},
+	}
+
+	hdr := "myHeaderComment"
+
+	cfg, err := MakeDropQStringDotConfig(server, params, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, dropQStringVal) {
 		t.Errorf("expected dropQStringVal '"+dropQStringVal+"' actual comment, actual: '%v'", txt)
diff --git a/lib/go-atscfg/facts.go b/lib/go-atscfg/facts.go
index c7e6eb6..a9652df 100644
--- a/lib/go-atscfg/facts.go
+++ b/lib/go-atscfg/facts.go
@@ -23,12 +23,23 @@ const ContentType12MFacts = ContentTypeTextASCII
 const LineComment12MFacts = LineCommentHash
 
 func Make12MFacts(
-	profileName string,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
+	server *Server,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Profile")
+	}
+
+	hdr := makeHdrComment(hdrComment)
 	txt := hdr
-	txt += "profile:" + profileName + "\n"
-	return txt
+	txt += "profile:" + *server.Profile + "\n"
+
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentType12MFacts,
+		LineComment: LineComment12MFacts,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/facts_test.go b/lib/go-atscfg/facts_test.go
index 4c575ab..eeb284d 100644
--- a/lib/go-atscfg/facts_test.go
+++ b/lib/go-atscfg/facts_test.go
@@ -25,13 +25,19 @@ import (
 )
 
 func TestMake12MFacts(t *testing.T) {
+	server := makeGenericServer()
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	server.Profile = &profileName
 
-	txt := Make12MFacts(profileName, toolName, toURL)
+	hdr := "myHeaderComment"
 
-	testComment(t, txt, profileName, toolName, toURL)
+	cfg, err := Make12MFacts(server, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
+
+	testComment(t, txt, hdr)
 
 	lines := strings.SplitN(txt, "\n", 2) // SplitN always returns at least 1 element, no need to check len before indexing
 	if len(lines) < 2 {
diff --git a/lib/go-atscfg/headerrewritedotconfig.go b/lib/go-atscfg/headerrewritedotconfig.go
index ab8575f..a72d2b5 100644
--- a/lib/go-atscfg/headerrewritedotconfig.go
+++ b/lib/go-atscfg/headerrewritedotconfig.go
@@ -39,7 +39,128 @@ const ServiceCategoryHeader = "CDN-SVC"
 
 const MaxOriginConnectionsNoMax = 0 // 0 indicates no limit on origin connections
 
-type HeaderRewriteDS struct {
+func MakeHeaderRewriteDotConfig(
+	fileName string,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	server *Server,
+	servers []Server,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	dsName := strings.TrimSuffix(strings.TrimPrefix(fileName, HeaderRewritePrefix), ConfigSuffix) // TODO verify prefix and suffix? Perl doesn't
+
+	tcDS := DeliveryService{}
+	for _, ds := range deliveryServices {
+		if ds.XMLID == nil {
+			warnings = append(warnings, "deliveryServices had DS with nil xmlId (name)")
+			continue
+		}
+		if *ds.XMLID != dsName {
+			continue
+		}
+		tcDS = ds
+		break
+	}
+	if tcDS.ID == nil {
+		return Cfg{}, makeErr(warnings, "ds '"+dsName+"' not found")
+	}
+
+	if tcDS.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "ds '"+dsName+"' missing cdn")
+	}
+
+	ds, err := headerRewriteDSFromDS(&tcDS)
+	if err != nil {
+		return Cfg{}, makeErr(warnings, "converting ds to config ds: "+err.Error())
+	}
+
+	dsServers := filterDSS(deliveryServiceServers, map[int]struct{}{ds.ID: {}}, nil)
+
+	dsServerIDs := map[int]struct{}{}
+	for _, dss := range dsServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			warnings = append(warnings, "deliveryservice-servers had entry with nil values, skipping!")
+			continue
+		}
+		if *dss.DeliveryService != *tcDS.ID {
+			continue
+		}
+		dsServerIDs[*dss.Server] = struct{}{}
+	}
+
+	assignedEdges := []headerRewriteServer{}
+	for _, server := range servers {
+		if server.CDNName == nil {
+			warnings = append(warnings, "servers had server with missing cdnName, skipping!")
+			continue
+		}
+		if server.ID == nil {
+			warnings = append(warnings, "servers had server with missing id, skipping!")
+			continue
+		}
+		if *server.CDNName != *tcDS.CDNName {
+			continue
+		}
+		if _, ok := dsServerIDs[*server.ID]; !ok && tcDS.Topology == nil {
+			continue
+		}
+		cfgServer, err := headerRewriteServerFromServer(server)
+		if err != nil {
+			warnings = append(warnings, "error getting header rewrite server, skipping: "+err.Error())
+			continue
+		}
+		assignedEdges = append(assignedEdges, cfgServer)
+	}
+
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "this server missing CDNName")
+	}
+
+	text := makeHdrComment(hdrComment)
+
+	// write a header rewrite rule if maxOriginConnections > 0 and the ds does NOT use mids
+	if ds.MaxOriginConnections > 0 && !ds.Type.UsesMidCache() {
+		dsOnlineEdgeCount := 0
+		for _, sv := range assignedEdges {
+			if sv.Status == tc.CacheStatusReported || sv.Status == tc.CacheStatusOnline {
+				dsOnlineEdgeCount++
+			}
+		}
+
+		if dsOnlineEdgeCount > 0 {
+			maxOriginConnectionsPerEdge := int(math.Round(float64(ds.MaxOriginConnections) / float64(dsOnlineEdgeCount)))
+			text += "cond %{REMAP_PSEUDO_HOOK}\nset-config proxy.config.http.origin_max_connections " + strconv.Itoa(maxOriginConnectionsPerEdge)
+			if ds.EdgeHeaderRewrite == "" {
+				text += " [L]"
+			} else {
+				text += "\n"
+			}
+		}
+	}
+
+	// write the contents of ds.EdgeHeaderRewrite to hdr_rw_xml-id.config replacing any instances of __RETURN__ (surrounded by spaces or not) with \n
+	if ds.EdgeHeaderRewrite != "" {
+		re := regexp.MustCompile(`\s*__RETURN__\s*`)
+		text += re.ReplaceAllString(ds.EdgeHeaderRewrite, "\n")
+	}
+
+	if !strings.Contains(text, ServiceCategoryHeader) && ds.ServiceCategory != "" {
+		text += fmt.Sprintf("\nset-header %s \"%s|%s\"", ServiceCategoryHeader, dsName, ds.ServiceCategory)
+	}
+
+	text += "\n"
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeHeaderRewriteDotConfig,
+		LineComment: LineCommentHeaderRewriteDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type headerRewriteDS struct {
 	EdgeHeaderRewrite    string
 	ID                   int
 	MaxOriginConnections int
@@ -48,17 +169,17 @@ type HeaderRewriteDS struct {
 	ServiceCategory      string
 }
 
-type HeaderRewriteServer struct {
+type headerRewriteServer struct {
 	HostName   string
 	DomainName string
 	Port       int
 	Status     tc.CacheStatus
 }
 
-func HeaderRewriteServersFromServers(servers []tc.ServerNullable) ([]HeaderRewriteServer, error) {
-	hServers := []HeaderRewriteServer{}
+func headerRewriteServersFromServers(servers []Server) ([]headerRewriteServer, error) {
+	hServers := []headerRewriteServer{}
 	for _, sv := range servers {
-		hsv, err := HeaderRewriteServerFromServer(sv)
+		hsv, err := headerRewriteServerFromServer(sv)
 		if err != nil {
 			return nil, err
 		}
@@ -67,44 +188,44 @@ func HeaderRewriteServersFromServers(servers []tc.ServerNullable) ([]HeaderRewri
 	return hServers, nil
 }
 
-func HeaderRewriteServerFromServer(sv tc.ServerNullable) (HeaderRewriteServer, error) {
+func headerRewriteServerFromServer(sv Server) (headerRewriteServer, error) {
 	if sv.HostName == nil {
-		return HeaderRewriteServer{}, errors.New("server host name must not be nil")
+		return headerRewriteServer{}, errors.New("server host name must not be nil")
 	}
 	if sv.DomainName == nil {
-		return HeaderRewriteServer{}, errors.New("server domain name must not be nil")
+		return headerRewriteServer{}, errors.New("server domain name must not be nil")
 	}
 	if sv.TCPPort == nil {
-		return HeaderRewriteServer{}, errors.New("server port must not be nil")
+		return headerRewriteServer{}, errors.New("server port must not be nil")
 	}
 	if sv.Status == nil {
-		return HeaderRewriteServer{}, errors.New("server status must not be nil")
+		return headerRewriteServer{}, errors.New("server status must not be nil")
 	}
 	status := tc.CacheStatusFromString(*sv.Status)
 	if status == tc.CacheStatusInvalid {
-		return HeaderRewriteServer{}, errors.New("server status '" + *sv.Status + "' invalid")
+		return headerRewriteServer{}, errors.New("server status '" + *sv.Status + "' invalid")
 	}
-	return HeaderRewriteServer{Status: status, HostName: *sv.HostName, DomainName: *sv.DomainName, Port: *sv.TCPPort}, nil
+	return headerRewriteServer{Status: status, HostName: *sv.HostName, DomainName: *sv.DomainName, Port: *sv.TCPPort}, nil
 }
 
-func HeaderRewriteServerFromServerNotNullable(sv tc.Server) (HeaderRewriteServer, error) {
+func headerRewriteServerFromServerNotNullable(sv tc.Server) (headerRewriteServer, error) {
 	if sv.HostName == "" {
-		return HeaderRewriteServer{}, errors.New("server host name must not be nil")
+		return headerRewriteServer{}, errors.New("server host name must not be nil")
 	}
 	if sv.DomainName == "" {
-		return HeaderRewriteServer{}, errors.New("server domain name must not be nil")
+		return headerRewriteServer{}, errors.New("server domain name must not be nil")
 	}
 	if sv.TCPPort == 0 {
-		return HeaderRewriteServer{}, errors.New("server port must not be nil")
+		return headerRewriteServer{}, errors.New("server port must not be nil")
 	}
 	status := tc.CacheStatusFromString(sv.Status)
 	if status == tc.CacheStatusInvalid {
-		return HeaderRewriteServer{}, errors.New("server status '" + sv.Status + "' invalid")
+		return headerRewriteServer{}, errors.New("server status '" + sv.Status + "' invalid")
 	}
-	return HeaderRewriteServer{Status: status, HostName: sv.HostName, DomainName: sv.DomainName, Port: sv.TCPPort}, nil
+	return headerRewriteServer{Status: status, HostName: sv.HostName, DomainName: sv.DomainName, Port: sv.TCPPort}, nil
 }
 
-func HeaderRewriteDSFromDS(ds *tc.DeliveryServiceNullableV30) (HeaderRewriteDS, error) {
+func headerRewriteDSFromDS(ds *DeliveryService) (headerRewriteDS, error) {
 	errs := []error{}
 	if ds.ID == nil {
 		errs = append(errs, errors.New("ID cannot be nil"))
@@ -113,7 +234,7 @@ func HeaderRewriteDSFromDS(ds *tc.DeliveryServiceNullableV30) (HeaderRewriteDS,
 		errs = append(errs, errors.New("Type cannot be nil"))
 	}
 	if len(errs) > 0 {
-		return HeaderRewriteDS{}, util.JoinErrs(errs)
+		return headerRewriteDS{}, util.JoinErrs(errs)
 	}
 
 	if ds.MaxOriginConnections == nil {
@@ -129,7 +250,7 @@ func HeaderRewriteDSFromDS(ds *tc.DeliveryServiceNullableV30) (HeaderRewriteDS,
 		ds.ServiceCategory = new(string)
 	}
 
-	return HeaderRewriteDS{
+	return headerRewriteDS{
 		EdgeHeaderRewrite:    *ds.EdgeHeaderRewrite,
 		ID:                   *ds.ID,
 		MaxOriginConnections: *ds.MaxOriginConnections,
@@ -138,47 +259,3 @@ func HeaderRewriteDSFromDS(ds *tc.DeliveryServiceNullableV30) (HeaderRewriteDS,
 		ServiceCategory:      *ds.ServiceCategory,
 	}, nil
 }
-
-func MakeHeaderRewriteDotConfig(
-	cdnName tc.CDNName,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	ds HeaderRewriteDS,
-	assignedEdges []HeaderRewriteServer, // the edges assigned to ds
-	dsXmlId string,
-) string {
-	text := GenericHeaderComment(string(cdnName), toToolName, toURL)
-
-	// write a header rewrite rule if maxOriginConnections > 0 and the ds does NOT use mids
-	if ds.MaxOriginConnections > 0 && !ds.Type.UsesMidCache() {
-		dsOnlineEdgeCount := 0
-		for _, sv := range assignedEdges {
-			if sv.Status == tc.CacheStatusReported || sv.Status == tc.CacheStatusOnline {
-				dsOnlineEdgeCount++
-			}
-		}
-
-		if dsOnlineEdgeCount > 0 {
-			maxOriginConnectionsPerEdge := int(math.Round(float64(ds.MaxOriginConnections) / float64(dsOnlineEdgeCount)))
-			text += "cond %{REMAP_PSEUDO_HOOK}\nset-config proxy.config.http.origin_max_connections " + strconv.Itoa(maxOriginConnectionsPerEdge)
-			if ds.EdgeHeaderRewrite == "" {
-				text += " [L]"
-			} else {
-				text += "\n"
-			}
-		}
-	}
-
-	// write the contents of ds.EdgeHeaderRewrite to hdr_rw_xml-id.config replacing any instances of __RETURN__ (surrounded by spaces or not) with \n
-	if ds.EdgeHeaderRewrite != "" {
-		re := regexp.MustCompile(`\s*__RETURN__\s*`)
-		text += re.ReplaceAllString(ds.EdgeHeaderRewrite, "\n")
-	}
-
-	if !strings.Contains(text, ServiceCategoryHeader) && ds.ServiceCategory != "" {
-		text += fmt.Sprintf("\nset-header %s \"%s|%s\"", ServiceCategoryHeader, dsXmlId, ds.ServiceCategory)
-	}
-
-	text += "\n"
-	return text
-}
diff --git a/lib/go-atscfg/headerrewritedotconfig_test.go b/lib/go-atscfg/headerrewritedotconfig_test.go
index 99366a1..f446332 100644
--- a/lib/go-atscfg/headerrewritedotconfig_test.go
+++ b/lib/go-atscfg/headerrewritedotconfig_test.go
@@ -24,35 +24,61 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeHeaderRewriteDotConfig(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
 	xmlID := "xml-id"
+	fileName := "hdr_rw_" + xmlID + ".config"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
 
-	ds := HeaderRewriteDS{
-		EdgeHeaderRewrite:    "edgerewrite",
-		ID:                   24,
-		MaxOriginConnections: 42,
-		MidHeaderRewrite:     "midrewrite",
-		Type:                 tc.DSTypeHTTPLive,
-		ServiceCategory:      "servicecategory",
-	}
-	assignedEdges := []HeaderRewriteServer{
-		HeaderRewriteServer{
-			Status: tc.CacheStatusReported,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOnline,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOffline,
-		},
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+
+	server.HostName = util.StrPtr("my-edge")
+	server.ID = util.IntPtr(990)
+	serverStatus := string(tc.CacheStatusReported)
+	server.Status = &serverStatus
+	server.CDNName = &cdnName
+
+	ds := makeGenericDS()
+	ds.EdgeHeaderRewrite = util.StrPtr("edgerewrite")
+	ds.ID = util.IntPtr(240)
+	ds.XMLID = &xmlID
+	ds.MaxOriginConnections = util.IntPtr(42)
+	ds.MidHeaderRewrite = util.StrPtr("midrewrite")
+	ds.CDNName = &cdnName
+	dsType := tc.DSTypeHTTPLive
+	ds.Type = &dsType
+	ds.ServiceCategory = util.StrPtr("servicecategory")
+
+	sv1 := makeGenericServer()
+	sv1.HostName = util.StrPtr("my-edge-1")
+	sv1.CDNName = &cdnName
+	sv1.ID = util.IntPtr(991)
+	sv1Status := string(tc.CacheStatusOnline)
+	sv1.Status = &sv1Status
+
+	sv2 := makeGenericServer()
+	sv2.HostName = util.StrPtr("my-edge-2")
+	sv2.CDNName = &cdnName
+	sv2.ID = util.IntPtr(992)
+	sv2Status := string(tc.CacheStatusOffline)
+	sv2.Status = &sv2Status
+
+	servers := []Server{*server, *sv1, *sv2}
+	dses := []DeliveryService{*ds}
+
+	dss := makeDSS(servers, dses)
+
+	cfg, err := MakeHeaderRewriteDotConfig(fileName, dses, dss, server, servers, hdr)
+
+	if err != nil {
+		t.Errorf("error expected nil, actual '%v'\n", err)
 	}
 
-	txt := MakeHeaderRewriteDotConfig(cdnName, toToolName, toURL, ds, assignedEdges, xmlID)
+	txt := cfg.Text
 
 	if !strings.Contains(txt, "edgerewrite") {
 		t.Errorf("expected 'edgerewrite' actual '%v'\n", txt)
@@ -63,7 +89,7 @@ func TestMakeHeaderRewriteDotConfig(t *testing.T) {
 	}
 
 	if !strings.Contains(txt, "origin_max_connections") {
-		t.Errorf("expected origin_max_connections on edge header rewrite that uses the mids, actual '%v'\n", txt)
+		t.Errorf("expected origin_max_connections on edge header rewrite that doesn't use the mids, actual '%v'\n", txt)
 	}
 
 	if !strings.Contains(txt, "21") { // 21, because max is 42, and there are 2 not-offline mids, so 42/2=21
@@ -76,32 +102,57 @@ func TestMakeHeaderRewriteDotConfig(t *testing.T) {
 }
 
 func TestMakeHeaderRewriteDotConfigNoMaxOriginConnections(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
 	xmlID := "xml-id"
+	fileName := "hdr_rw_" + xmlID + ".config"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
 
-	ds := HeaderRewriteDS{
-		EdgeHeaderRewrite:    "edgerewrite",
-		ID:                   24,
-		MaxOriginConnections: 42,
-		MidHeaderRewrite:     "midrewrite",
-		Type:                 tc.DSTypeHTTP,
-		ServiceCategory:      "servicecategory",
-	}
-	assignedEdges := []HeaderRewriteServer{
-		HeaderRewriteServer{
-			Status: tc.CacheStatusReported,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOnline,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOffline,
-		},
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+
+	server.HostName = util.StrPtr("my-edge")
+	server.ID = util.IntPtr(990)
+	serverStatus := string(tc.CacheStatusReported)
+	server.Status = &serverStatus
+	server.CDNName = &cdnName
+
+	ds := makeGenericDS()
+	ds.EdgeHeaderRewrite = util.StrPtr("edgerewrite")
+	ds.ID = util.IntPtr(240)
+	ds.XMLID = &xmlID
+	ds.MaxOriginConnections = util.IntPtr(42)
+	ds.MidHeaderRewrite = util.StrPtr("midrewrite")
+	ds.CDNName = &cdnName
+	dsType := tc.DSTypeHTTP
+	ds.Type = &dsType
+	ds.ServiceCategory = util.StrPtr("servicecategory")
+
+	sv1 := makeGenericServer()
+	sv1.HostName = util.StrPtr("my-edge-1")
+	sv1.CDNName = &cdnName
+	sv1.ID = util.IntPtr(991)
+	sv1Status := string(tc.CacheStatusOnline)
+	sv1.Status = &sv1Status
+
+	sv2 := makeGenericServer()
+	sv2.HostName = util.StrPtr("my-edge-2")
+	sv2.CDNName = &cdnName
+	sv2.ID = util.IntPtr(992)
+	sv2Status := string(tc.CacheStatusOffline)
+	sv2.Status = &sv2Status
+
+	servers := []Server{*server, *sv1, *sv2}
+	dses := []DeliveryService{*ds}
+
+	dss := makeDSS(servers, dses)
+
+	cfg, err := MakeHeaderRewriteDotConfig(fileName, dses, dss, server, servers, hdr)
+
+	if err != nil {
+		t.Errorf("error expected nil, actual '%v'\n", err)
 	}
 
-	txt := MakeHeaderRewriteDotConfig(cdnName, toToolName, toURL, ds, assignedEdges, xmlID)
+	txt := cfg.Text
 
 	if strings.Contains(txt, "origin_max_connections") {
 		t.Errorf("expected no origin_max_connections on DS that uses the mid, actual '%v'\n", txt)
diff --git a/lib/go-atscfg/headerrewritemiddotconfig.go b/lib/go-atscfg/headerrewritemiddotconfig.go
index f6a8ccb..920fb35 100644
--- a/lib/go-atscfg/headerrewritemiddotconfig.go
+++ b/lib/go-atscfg/headerrewritemiddotconfig.go
@@ -20,9 +20,11 @@ package atscfg
  */
 
 import (
+	"fmt"
 	"math"
 	"regexp"
 	"strconv"
+	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
@@ -30,14 +32,123 @@ import (
 const HeaderRewriteMidPrefix = "hdr_rw_mid_"
 
 func MakeHeaderRewriteMidDotConfig(
-	cdnName tc.CDNName,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	ds HeaderRewriteDS,
-	assignedMids []HeaderRewriteServer, // the mids assigned to ds (mids whose cachegroup is the parent of the cachegroup of any edge assigned to this ds)
-) string {
-	text := GenericHeaderComment(string(cdnName), toToolName, toURL)
+	fileName string,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	server *Server,
+	servers []Server,
+	cacheGroups []tc.CacheGroupNullable,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "this server missing CDNName")
+	}
+
+	dsName := strings.TrimSuffix(strings.TrimPrefix(fileName, HeaderRewriteMidPrefix), ConfigSuffix) // TODO verify prefix and suffix? Perl doesn't
+
+	tcDS := DeliveryService{}
+	for _, ds := range deliveryServices {
+		if ds.XMLID == nil || *ds.XMLID != dsName {
+			continue
+		}
+		tcDS = ds
+		break
+	}
+	if tcDS.ID == nil {
+		return Cfg{}, makeErr(warnings, "ds '"+dsName+"' not found")
+	}
+
+	if tcDS.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "ds '"+dsName+"' missing cdn")
+	}
+
+	ds, err := headerRewriteDSFromDS(&tcDS)
+	if err != nil {
+		return Cfg{}, makeErr(warnings, "converting ds to config ds: "+err.Error())
+	}
+
+	assignedServers := map[int]struct{}{}
+	for _, dss := range deliveryServiceServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			continue
+		}
+		if *dss.DeliveryService != *tcDS.ID {
+			continue
+		}
+		assignedServers[*dss.Server] = struct{}{}
+	}
 
+	serverCGs := map[tc.CacheGroupName]struct{}{}
+	for _, sv := range servers {
+		if sv.CDNName == nil {
+			warnings = append(warnings, "TO returned Servers server with missing CDNName, skipping!")
+			continue
+		} else if sv.ID == nil {
+			warnings = append(warnings, "TO returned Servers server with missing ID, skipping!")
+			continue
+		} else if sv.Status == nil {
+			warnings = append(warnings, "TO returned Servers server with missing Status, skipping!")
+			continue
+		} else if sv.Cachegroup == nil {
+			warnings = append(warnings, "TO returned Servers server with missing Cachegroup, skipping!")
+			continue
+		}
+
+		if sv.CDNName != server.CDNName {
+			continue
+		}
+		if _, ok := assignedServers[*sv.ID]; !ok && (tcDS.Topology == nil || *tcDS.Topology == "") {
+			continue
+		}
+		if tc.CacheStatus(*sv.Status) != tc.CacheStatusReported && tc.CacheStatus(*sv.Status) != tc.CacheStatusOnline {
+			continue
+		}
+		serverCGs[tc.CacheGroupName(*sv.Cachegroup)] = struct{}{}
+	}
+
+	parentCGs := map[string]struct{}{} // names of cachegroups which are parent cachegroups of the cachegroup of any edge assigned to the given DS
+	for _, cg := range cacheGroups {
+		if cg.Name == nil {
+			warnings = append(warnings, "cachegroups had cachegroup with nil name, skipping!")
+			continue
+		}
+		if cg.ParentName == nil {
+			continue // this is normal for top-level cachegroups
+		}
+		if _, ok := serverCGs[tc.CacheGroupName(*cg.Name)]; !ok {
+			continue
+		}
+		parentCGs[*cg.ParentName] = struct{}{}
+	}
+
+	assignedMids := []headerRewriteServer{}
+	for _, server := range servers {
+		if server.CDNName == nil {
+			warnings = append(warnings, "TO returned Servers server with missing CDNName, skipping!")
+			continue
+		}
+		if server.Cachegroup == nil {
+			warnings = append(warnings, "TO returned Servers server with missing Cachegroup, skipping!")
+			continue
+		}
+		if *server.CDNName != *tcDS.CDNName {
+			continue
+		}
+		if _, ok := parentCGs[*server.Cachegroup]; !ok {
+			continue
+		}
+		cfgServer, err := headerRewriteServerFromServer(server)
+		if err != nil {
+			warnings = append(warnings, "failed to make header rewrite server,skipping! : "+err.Error())
+			continue
+		}
+		assignedMids = append(assignedMids, cfgServer)
+	}
+
+	text := makeHdrComment(hdrComment)
+
+	fmt.Printf("DEBUG moc %v usesmid %v assignedmids %v\n", ds.MaxOriginConnections, ds.Type.UsesMidCache(), len(assignedMids))
 	// write a header rewrite rule if maxOriginConnections > 0 and the ds DOES use mids
 	if ds.MaxOriginConnections > 0 && ds.Type.UsesMidCache() {
 		dsOnlineMidCount := 0
@@ -64,5 +175,11 @@ func MakeHeaderRewriteMidDotConfig(
 	}
 
 	text += "\n"
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeHeaderRewriteDotConfig,
+		LineComment: LineCommentHeaderRewriteDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/headerrewritemiddotconfig_test.go b/lib/go-atscfg/headerrewritemiddotconfig_test.go
index 0d75e41..b6203a5 100644
--- a/lib/go-atscfg/headerrewritemiddotconfig_test.go
+++ b/lib/go-atscfg/headerrewritemiddotconfig_test.go
@@ -24,34 +24,79 @@ import (
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeHeaderRewriteMidDotConfig(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
-
-	ds := HeaderRewriteDS{
-		EdgeHeaderRewrite:    "edgerewrite",
-		ID:                   24,
-		MaxOriginConnections: 42,
-		MidHeaderRewrite:     "midrewrite",
-		Type:                 tc.DSTypeHTTP,
-		ServiceCategory:      "servicecategory",
-	}
-	assignedMids := []HeaderRewriteServer{
-		HeaderRewriteServer{
-			Status: tc.CacheStatusReported,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOnline,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOffline,
-		},
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
+
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+	server.Cachegroup = util.StrPtr("edgeCG")
+	server.HostName = util.StrPtr("myserver")
+	serverStatus := string(tc.CacheStatusReported)
+	server.Status = &serverStatus
+
+	ds := makeGenericDS()
+	ds.EdgeHeaderRewrite = util.StrPtr("edgerewrite")
+	ds.ID = util.IntPtr(24)
+	ds.XMLID = util.StrPtr("ds0")
+	ds.MaxOriginConnections = util.IntPtr(42)
+	ds.MidHeaderRewrite = util.StrPtr("midrewrite")
+	ds.CDNName = &cdnName
+	dsType := tc.DSTypeHTTP
+	ds.Type = &dsType
+	ds.ServiceCategory = util.StrPtr("servicecategory")
+
+	mid0 := makeGenericServer()
+	mid0.CDNName = &cdnName
+	mid0.Cachegroup = util.StrPtr("midCG")
+	mid0.HostName = util.StrPtr("mymid0")
+	mid0Status := string(tc.CacheStatusReported)
+	mid0.Status = &mid0Status
+
+	mid1 := makeGenericServer()
+	mid1.CDNName = &cdnName
+	mid1.Cachegroup = util.StrPtr("midCG")
+	mid1.HostName = util.StrPtr("mymid1")
+	mid1Status := string(tc.CacheStatusOnline)
+	mid1.Status = &mid1Status
+
+	mid2 := makeGenericServer()
+	mid2.CDNName = &cdnName
+	mid2.Cachegroup = util.StrPtr("midCG")
+	mid2.HostName = util.StrPtr("mymid2")
+	mid2Status := string(tc.CacheStatusOffline)
+	mid2.Status = &mid2Status
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG}
+	servers := []Server{*server, *mid0, *mid1, *mid2}
+	dses := []DeliveryService{*ds}
+	dss := makeDSS(servers, dses)
+
+	fileName := "hdr_rw_mid_" + *ds.XMLID + ".config"
+
+	cfg, err := MakeHeaderRewriteMidDotConfig(fileName, dses, dss, server, servers, cgs, hdr)
+	if err != nil {
+		t.Error(err)
 	}
 
-	txt := MakeHeaderRewriteMidDotConfig(cdnName, toToolName, toURL, ds, assignedMids)
+	txt := cfg.Text
 
 	if !strings.Contains(txt, "midrewrite") {
 		t.Errorf("expected no 'midrewrite' actual '%v'\n", txt)
@@ -71,31 +116,83 @@ func TestMakeHeaderRewriteMidDotConfig(t *testing.T) {
 }
 
 func TestMakeHeaderRewriteMidDotConfigNoMaxConns(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
-
-	ds := HeaderRewriteDS{
-		EdgeHeaderRewrite:    "edgerewrite",
-		ID:                   24,
-		MaxOriginConnections: 42,
-		MidHeaderRewrite:     "midrewrite",
-		Type:                 tc.DSTypeHTTPLive,
-		ServiceCategory:      "servicecategory",
-	}
-	assignedMids := []HeaderRewriteServer{
-		HeaderRewriteServer{
-			Status: tc.CacheStatusReported,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOnline,
-		},
-		HeaderRewriteServer{
-			Status: tc.CacheStatusOffline,
-		},
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
+
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+	server.Cachegroup = util.StrPtr("edgeCG")
+	server.HostName = util.StrPtr("myserver")
+	serverStatus := string(tc.CacheStatusReported)
+	server.Status = &serverStatus
+
+	ds := makeGenericDS()
+	ds.EdgeHeaderRewrite = util.StrPtr("edgerewrite")
+	ds.ID = util.IntPtr(24)
+	ds.XMLID = util.StrPtr("ds0")
+	ds.MidHeaderRewrite = util.StrPtr("midrewrite")
+	ds.CDNName = &cdnName
+	dsType := tc.DSTypeHTTP
+	ds.Type = &dsType
+	ds.ServiceCategory = util.StrPtr("servicecategory")
+
+	mid0 := makeGenericServer()
+	mid0.Cachegroup = util.StrPtr("midCG")
+	mid0.HostName = util.StrPtr("mymid0")
+	mid0Status := string(tc.CacheStatusReported)
+	mid0.Status = &mid0Status
+
+	mid1 := makeGenericServer()
+	mid1.Cachegroup = util.StrPtr("midCG")
+	mid1.HostName = util.StrPtr("mymid1")
+	mid1Status := string(tc.CacheStatusOnline)
+	mid1.Status = &mid1Status
+
+	mid2 := makeGenericServer()
+	mid2.Cachegroup = util.StrPtr("midCG")
+	mid2.HostName = util.StrPtr("mymid2")
+	mid2Status := string(tc.CacheStatusOffline)
+	mid2.Status = &mid2Status
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG}
+	servers := []Server{*server, *mid0, *mid1, *mid2}
+	dses := []DeliveryService{*ds}
+	dss := makeDSS(servers, dses)
+
+	// assignedMids := []HeaderRewriteServer{
+	// 	HeaderRewriteServer{
+	// 		Status: tc.CacheStatusReported,
+	// 	},
+	// 	HeaderRewriteServer{
+	// 		Status: tc.CacheStatusOnline,
+	// 	},
+	// 	HeaderRewriteServer{
+	// 		Status: tc.CacheStatusOffline,
+	// 	},
+	// }
+
+	fileName := "hdr_rw_mid_" + *ds.XMLID + ".config"
+
+	cfg, err := MakeHeaderRewriteMidDotConfig(fileName, dses, dss, mid0, servers, cgs, hdr)
+	if err != nil {
+		t.Error(err)
 	}
 
-	txt := MakeHeaderRewriteMidDotConfig(cdnName, toToolName, toURL, ds, assignedMids)
+	txt := cfg.Text
 
 	if strings.Contains(txt, "origin_max_connections") {
 		t.Errorf("expected no origin_max_connections on edge-only DS, actual '%v'\n", txt)
diff --git a/lib/go-atscfg/hostingdotconfig.go b/lib/go-atscfg/hostingdotconfig.go
index f70618f..ded0ef5 100644
--- a/lib/go-atscfg/hostingdotconfig.go
+++ b/lib/go-atscfg/hostingdotconfig.go
@@ -24,7 +24,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
@@ -40,20 +39,120 @@ const ServerHostingDotConfigMidIncludeInactive = false
 const ServerHostingDotConfigEdgeIncludeInactive = true
 
 func MakeHostingDotConfig(
-	server *tc.ServerNullable,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	params map[string]string, // map[name]value - config file should always be storage.config
-	dses []tc.DeliveryServiceNullableV30,
+	server *Server,
+	servers []Server,
+	serverParams []tc.Parameter,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
 	topologies []tc.Topology,
-) string {
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.CDNID == nil {
+		return Cfg{}, makeErr(warnings, "this server missing CDNID")
+	}
 	if server.HostName == nil || *server.HostName == "" {
-		return "Error: server had no host name!"
+		return Cfg{}, makeErr(warnings, "server had no host name!")
+	}
+	if server.ID == nil {
+		return Cfg{}, makeErr(warnings, "this server missing ID")
+	}
+
+	params, paramWarns := paramsToMap(filterParams(serverParams, HostingConfigParamConfigFile, "", "", ""))
+	warnings = append(warnings, paramWarns...)
+
+	cdnServers := map[tc.CacheName]Server{}
+	for _, sv := range servers {
+		if sv.HostName == nil {
+			warnings = append(warnings, "TO Servers had server missing HostName, skipping!")
+			continue
+		} else if sv.CDNID == nil {
+			warnings = append(warnings, "TO Servers had server missing CDNID, skipping!")
+			continue
+		}
+		if *sv.CDNID != *server.CDNID {
+			continue
+		}
+		cdnServers[tc.CacheName(*sv.HostName)] = sv
+	}
+
+	serverIDs := map[int]struct{}{}
+	for _, sv := range cdnServers {
+		if sv.CDNID == nil {
+			warnings = append(warnings, "TO Servers had server missing CDNID, skipping!")
+			continue
+		}
+		serverIDs[*sv.ID] = struct{}{}
 	}
 
-	text := GenericHeaderComment(*server.HostName, toToolName, toURL)
+	dsIDs := map[int]struct{}{}
+	for _, ds := range deliveryServices {
+		if ds.ID != nil {
+			dsIDs[*ds.ID] = struct{}{}
+		}
+	}
+
+	dsServers := filterDSS(deliveryServiceServers, dsIDs, serverIDs)
+
+	dsServerMap := map[int]map[int]struct{}{} // set[dsID][serverID]
+	for _, dss := range dsServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			return Cfg{}, makeErr(warnings, "deliveryserviceservers returned dss with nil values")
+		}
+		if _, ok := dsServerMap[*dss.DeliveryService]; !ok {
+			dsServerMap[*dss.DeliveryService] = map[int]struct{}{}
+		}
+		dsServerMap[*dss.DeliveryService][*dss.Server] = struct{}{}
+	}
 
-	nameTopologies := MakeTopologyNameMap(topologies)
+	isMid := strings.HasPrefix(server.Type, tc.MidTypePrefix)
+
+	filteredDSes := []DeliveryService{}
+	for _, ds := range deliveryServices {
+		if ds.Active == nil || ds.Type == nil || ds.XMLID == nil || ds.CDNID == nil || ds.ID == nil || ds.OrgServerFQDN == nil {
+			// some DSes have nil origins. I think MSO? TODO: verify
+			continue
+		}
+		if *ds.CDNID != *server.CDNID {
+			continue
+		}
+		if ds.Topology == nil {
+			if !*ds.Active && ((!isMid && !ServerHostingDotConfigEdgeIncludeInactive) || (isMid && !ServerHostingDotConfigMidIncludeInactive)) {
+				continue
+			}
+
+			if isMid {
+				if !strings.HasSuffix(string(*ds.Type), tc.DSTypeLiveNationalSuffix) {
+					continue
+				}
+
+				// mids: include all DSes with at least one server assigned
+				if len(dsServerMap[*ds.ID]) == 0 {
+					continue
+				}
+			} else {
+				if !strings.HasSuffix(string(*ds.Type), tc.DSTypeLiveNationalSuffix) && !strings.HasSuffix(string(*ds.Type), tc.DSTypeLiveSuffix) {
+					continue
+				}
+
+				// edges: only include DSes assigned to this edge
+				if dsServerMap[*ds.ID] == nil {
+					continue
+				}
+
+				if _, ok := dsServerMap[*ds.ID][*server.ID]; !ok {
+					continue
+				}
+			}
+		}
+
+		filteredDSes = append(filteredDSes, ds)
+	}
+
+	text := makeHdrComment(hdrComment)
+
+	nameTopologies := makeTopologyNameMap(topologies)
 
 	lines := []string{}
 	if _, ok := params[ParamRAMDrivePrefix]; ok {
@@ -67,9 +166,10 @@ func MakeHostingDotConfig(
 		text += `# TRAFFIC OPS NOTE: volume ` + strconv.Itoa(ramVolume) + ` is the RAM volume` + "\n"
 
 		seenOrigins := map[string]struct{}{}
-		for _, ds := range dses {
+		for _, ds := range filteredDSes {
 			if ds.OrgServerFQDN == nil || ds.XMLID == nil || ds.Active == nil {
-				continue // TODO warn?
+				warnings = append(warnings, "got DS with nil values, skipping!")
+				continue
 			}
 
 			origin := *ds.OrgServerFQDN
@@ -82,7 +182,7 @@ func MakeHostingDotConfig(
 				if hasTopology {
 					topoHasServer, err := topologyIncludesServerNullable(topology, server)
 					if err != nil {
-						log.Errorln("Error checking if topology has server, skipping! : " + err.Error())
+						warnings = append(warnings, "checking if topology has server, skipping! : "+err.Error())
 						topoHasServer = false
 					}
 					if !topoHasServer {
@@ -103,5 +203,11 @@ func MakeHostingDotConfig(
 
 	sort.Strings(lines)
 	text += strings.Join(lines, "")
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeHostingDotConfig,
+		LineComment: LineCommentHostingDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/hostingdotconfig_test.go b/lib/go-atscfg/hostingdotconfig_test.go
index be03b85..f79de62 100644
--- a/lib/go-atscfg/hostingdotconfig_test.go
+++ b/lib/go-atscfg/hostingdotconfig_test.go
@@ -28,15 +28,36 @@ import (
 )
 
 func TestMakeHostingDotConfig(t *testing.T) {
-	server := &tc.ServerNullable{}
+	cdnName := "cdn0"
+
+	server := makeGenericServer()
 	server.HostName = util.StrPtr("server0")
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
-	params := map[string]string{
-		ParamRAMDrivePrefix: "ParamRAMDrivePrefix-shouldnotappearinconfig",
-		ParamDrivePrefix:    "ParamDrivePrefix-shouldnotappearinconfig",
-		"somethingelse":     "somethingelse-shouldnotappearinconfig",
+	server.CDNName = &cdnName
+	server.ProfileID = util.IntPtr(46)
+	server.Profile = util.StrPtr("serverprofile")
+	hdr := "myHeaderComment"
+
+	serverParams := []tc.Parameter{
+		tc.Parameter{
+			Name:       ParamRAMDrivePrefix,
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "ParamRAMDrivePrefix-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
+		tc.Parameter{
+			Name:       ParamDrivePrefix,
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "ParamDrivePrefix-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
+		tc.Parameter{
+			Name:       "somethingelse",
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "somethingelse-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
 	}
+
 	origins := []string{
 		"https://origin0.example.net",
 		"http://origin1.example.net",
@@ -45,14 +66,22 @@ func TestMakeHostingDotConfig(t *testing.T) {
 		"https://origin4.example.net/",
 		"http://origin5.example.net/",
 	}
-	dses := []tc.DeliveryServiceNullableV30{}
+	dses := []DeliveryService{}
 	for _, origin := range origins {
-		ds := tc.DeliveryServiceNullableV30{}
+		ds := makeGenericDS()
+		ds.CDNName = &cdnName
 		ds.OrgServerFQDN = util.StrPtr(origin)
-		dses = append(dses, ds)
+		dses = append(dses, *ds)
 	}
 
-	txt := MakeHostingDotConfig(server, toToolName, toURL, params, dses, nil)
+	servers := []Server{*server}
+	dss := makeDSS(servers, dses)
+
+	cfg, err := MakeHostingDotConfig(server, servers, serverParams, dses, dss, nil, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	lines := strings.Split(txt, "\n")
 
@@ -65,11 +94,8 @@ func TestMakeHostingDotConfig(t *testing.T) {
 	if !strings.HasPrefix(commentLine, "#") {
 		t.Errorf("expected: comment line starting with '#', actual: '%v'\n", commentLine)
 	}
-	if !strings.Contains(commentLine, toToolName) {
-		t.Errorf("expected: comment line containing toolName '%v', actual: '%v'\n", toToolName, commentLine)
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected: comment line containing toURL '%v', actual: '%v'\n", toURL, commentLine)
+	if !strings.Contains(commentLine, hdr) {
+		t.Errorf("expected: comment line containing header comment '%v', actual: '%v'\n", hdr, commentLine)
 	}
 
 	lines = lines[1:] // remove comment line
@@ -93,31 +119,60 @@ func TestMakeHostingDotConfig(t *testing.T) {
 }
 
 func TestMakeHostingDotConfigTopologiesIgnoreDSS(t *testing.T) {
-	server := &tc.ServerNullable{}
+	cdnName := "cdn0"
+
+	server := makeGenericServer()
 	server.HostName = util.StrPtr("server0")
 	server.Cachegroup = util.StrPtr("edgeCG")
-
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
-	params := map[string]string{
-		ParamRAMDrivePrefix: "ParamRAMDrivePrefix-shouldnotappearinconfig",
-		ParamDrivePrefix:    "ParamDrivePrefix-shouldnotappearinconfig",
-		"somethingelse":     "somethingelse-shouldnotappearinconfig",
+	server.CDNName = &cdnName
+	server.CDNID = util.IntPtr(400)
+	server.ProfileID = util.IntPtr(46)
+	server.ID = util.IntPtr(899)
+	server.Profile = util.StrPtr("serverprofile")
+	hdr := "myHeaderComment"
+
+	serverParams := []tc.Parameter{
+		tc.Parameter{
+			Name:       ParamRAMDrivePrefix,
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "ParamRAMDrivePrefix-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
+		tc.Parameter{
+			Name:       ParamDrivePrefix,
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "ParamDrivePrefix-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
+		tc.Parameter{
+			Name:       "somethingelse",
+			ConfigFile: HostingConfigParamConfigFile,
+			Value:      "somethingelse-shouldnotappearinconfig",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		},
 	}
 
-	dsTopology := tc.DeliveryServiceNullableV30{}
+	dsTopology := makeGenericDS()
 	dsTopology.OrgServerFQDN = util.StrPtr("https://origin0.example.net")
 	dsTopology.XMLID = util.StrPtr("ds-topology")
+	dsTopology.CDNID = util.IntPtr(400)
+	dsTopology.ID = util.IntPtr(900)
 	dsTopology.Topology = util.StrPtr("t0")
 	dsTopology.Active = util.BoolPtr(true)
+	dsType := tc.DSTypeHTTP
+	dsTopology.Type = &dsType
 
-	dsTopologyWithoutServer := tc.DeliveryServiceNullableV30{}
+	dsTopologyWithoutServer := makeGenericDS()
+	dsTopologyWithoutServer.ID = util.IntPtr(901)
 	dsTopologyWithoutServer.OrgServerFQDN = util.StrPtr("https://origin1.example.net")
 	dsTopologyWithoutServer.XMLID = util.StrPtr("ds-topology-without-server")
+	dsTopologyWithoutServer.CDNID = util.IntPtr(400)
 	dsTopologyWithoutServer.Topology = util.StrPtr("t1")
 	dsTopologyWithoutServer.Active = util.BoolPtr(true)
+	dsType2 := tc.DSTypeHTTP
+	dsTopologyWithoutServer.Type = &dsType2
 
-	dses := []tc.DeliveryServiceNullableV30{dsTopology, dsTopologyWithoutServer}
+	dses := []DeliveryService{*dsTopology, *dsTopologyWithoutServer}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -146,7 +201,14 @@ func TestMakeHostingDotConfigTopologiesIgnoreDSS(t *testing.T) {
 		},
 	}
 
-	txt := MakeHostingDotConfig(server, toToolName, toURL, params, dses, topologies)
+	servers := []Server{*server}
+	dss := makeDSS(servers, dses)
+
+	cfg, err := MakeHostingDotConfig(server, servers, serverParams, dses, dss, topologies, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	if !strings.Contains(txt, "origin0") {
 		t.Errorf("expected origin0 in topology, actual %v\n", txt)
diff --git a/lib/go-atscfg/ipallowdotconfig.go b/lib/go-atscfg/ipallowdotconfig.go
index 3c8a5a8..246fb6c 100644
--- a/lib/go-atscfg/ipallowdotconfig.go
+++ b/lib/go-atscfg/ipallowdotconfig.go
@@ -25,7 +25,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -34,82 +33,50 @@ const IPAllowConfigFileName = `ip_allow.config`
 const ContentTypeIPAllowDotConfig = ContentTypeTextASCII
 const LineCommentIPAllowDotConfig = LineCommentHash
 
-type IPAllowData struct {
-	Src    string
-	Action string
-	Method string
-}
-
-type IPAllowDatas []IPAllowData
-
-func (is IPAllowDatas) Len() int      { return len(is) }
-func (is IPAllowDatas) Swap(i, j int) { is[i], is[j] = is[j], is[i] }
-func (is IPAllowDatas) Less(i, j int) bool {
-	if is[i].Src != is[j].Src {
-		return is[i].Src < is[j].Src
-	}
-	if is[i].Action != is[j].Action {
-		return is[i].Action < is[j].Action
-	}
-	return is[i].Method < is[j].Method
-}
-
 const ParamPurgeAllowIP = "purge_allow_ip"
 const ParamCoalesceMaskLenV4 = "coalesce_masklen_v4"
 const ParamCoalesceNumberV4 = "coalesce_number_v4"
 const ParamCoalesceMaskLenV6 = "coalesce_masklen_v6"
 const ParamCoalesceNumberV6 = "coalesce_number_v6"
 
-type IPAllowServer struct {
-	IPAddress  string
-	IP6Address string
-}
-
 const DefaultCoalesceMaskLenV4 = 24
 const DefaultCoalesceNumberV4 = 5
 const DefaultCoalesceMaskLenV6 = 48
 const DefaultCoalesceNumberV6 = 5
 
-type ServersSortByName []tc.ServerNullable
-
-func (ss ServersSortByName) Len() int      { return len(ss) }
-func (ss ServersSortByName) Swap(i, j int) { ss[i], ss[j] = ss[j], ss[i] }
-func (ss ServersSortByName) Less(i, j int) bool {
-	if ss[j].HostName == nil {
-		return false
-	} else if ss[i].HostName == nil {
-		return true
-	}
-	return *ss[i].HostName < *ss[j].HostName
-}
-
 // MakeIPAllowDotConfig creates the ip_allow.config ATS config file.
 // The childServers is a list of servers which are children for this Mid-tier server. This should be empty for Edge servers.
 // More specifically, it should be the list of edges whose cachegroup's parent_cachegroup or secondary_parent_cachegroup is the cachegroup of this Mid server.
 func MakeIPAllowDotConfig(
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	params map[string][]string, // map[name]value - config file should always be ip_allow.config
-	server *tc.ServerNullable,
-	servers []tc.ServerNullable,
+	serverParams []tc.Parameter,
+	server *Server,
+	servers []Server,
 	cacheGroups []tc.CacheGroupNullable,
-) string {
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Cachegroup == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Cachegroup")
+	}
 	if server.HostName == nil {
-		return "ERROR: server missing hostname"
+		return Cfg{}, makeErr(warnings, "this server missing HostName")
 	}
 
-	ipAllowData := []IPAllowData{}
+	params := paramsToMultiMap(filterParams(serverParams, IPAllowConfigFileName, "", "", ""))
+
+	ipAllowDat := []ipAllowData{}
 	const ActionAllow = "ip_allow"
 	const ActionDeny = "ip_deny"
 	const MethodAll = "ALL"
 
 	// localhost is trusted.
-	ipAllowData = append(ipAllowData, IPAllowData{
+	ipAllowDat = append(ipAllowDat, ipAllowData{
 		Src:    `127.0.0.1`,
 		Action: ActionAllow,
 		Method: MethodAll,
 	})
-	ipAllowData = append(ipAllowData, IPAllowData{
+	ipAllowDat = append(ipAllowDat, ipAllowData{
 		Src:    `::1`,
 		Action: ActionAllow,
 		Method: MethodAll,
@@ -125,40 +92,40 @@ func MakeIPAllowDotConfig(
 		for _, val := range vals {
 			switch name {
 			case "purge_allow_ip":
-				ipAllowData = append(ipAllowData, IPAllowData{
+				ipAllowDat = append(ipAllowDat, ipAllowData{
 					Src:    val,
 					Action: ActionAllow,
 					Method: MethodAll,
 				})
 			case ParamCoalesceMaskLenV4:
 				if vi, err := strconv.Atoi(val); err != nil {
-					log.Warnln("MakeIPAllowDotConfig got param '" + name + "' val '" + val + "' not a number, ignoring!")
+					warnings = append(warnings, "got param '"+name+"' val '"+val+"' not a number, ignoring!")
 				} else if coalesceMaskLenV4 != DefaultCoalesceMaskLenV4 {
-					log.Warnln("MakeIPAllowDotConfig got multiple param '" + name + "' - ignoring  val '" + val + "'!")
+					warnings = append(warnings, "got multiple param '"+name+"' - ignoring  val '"+val+"'!")
 				} else {
 					coalesceMaskLenV4 = vi
 				}
 			case ParamCoalesceNumberV4:
 				if vi, err := strconv.Atoi(val); err != nil {
-					log.Warnln("MakeIPAllowDotConfig got param '" + name + "' val '" + val + "' not a number, ignoring!")
+					warnings = append(warnings, "got param '"+name+"' val '"+val+"' not a number, ignoring!")
 				} else if coalesceNumberV4 != DefaultCoalesceNumberV4 {
-					log.Warnln("MakeIPAllowDotConfig got multiple param '" + name + "' - ignoring  val '" + val + "'!")
+					warnings = append(warnings, "got multiple param '"+name+"' - ignoring  val '"+val+"'!")
 				} else {
 					coalesceNumberV4 = vi
 				}
 			case ParamCoalesceMaskLenV6:
 				if vi, err := strconv.Atoi(val); err != nil {
-					log.Warnln("MakeIPAllowDotConfig got param '" + name + "' val '" + val + "' not a number, ignoring!")
+					warnings = append(warnings, "got param '"+name+"' val '"+val+"' not a number, ignoring!")
 				} else if coalesceMaskLenV6 != DefaultCoalesceMaskLenV6 {
-					log.Warnln("MakeIPAllowDotConfig got multiple param '" + name + "' - ignoring  val '" + val + "'!")
+					warnings = append(warnings, "got multiple param '"+name+"' - ignoring  val '"+val+"'!")
 				} else {
 					coalesceMaskLenV6 = vi
 				}
 			case ParamCoalesceNumberV6:
 				if vi, err := strconv.Atoi(val); err != nil {
-					log.Warnln("MakeIPAllowDotConfig got param '" + name + "' val '" + val + "' not a number, ignoring!")
+					warnings = append(warnings, "got param '"+name+"' val '"+val+"' not a number, ignoring!")
 				} else if coalesceNumberV6 != DefaultCoalesceNumberV6 {
-					log.Warnln("MakeIPAllowDotConfig got multiple param '" + name + "' - ignoring  val '" + val + "'!")
+					warnings = append(warnings, "got multiple param '"+name+"' - ignoring  val '"+val+"'!")
 				} else {
 					coalesceNumberV6 = vi
 				}
@@ -169,12 +136,12 @@ func MakeIPAllowDotConfig(
 	// for edges deny "PUSH|PURGE|DELETE", allow everything else to everyone.
 	isMid := strings.HasPrefix(server.Type, tc.MidTypePrefix)
 	if !isMid {
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `0.0.0.0-255.255.255.255`,
 			Action: ActionDeny,
 			Method: `PUSH|PURGE|DELETE`,
 		})
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff`,
 			Action: ActionDeny,
 			Method: `PUSH|PURGE|DELETE`,
@@ -187,18 +154,18 @@ func MakeIPAllowDotConfig(
 		cgMap := map[string]tc.CacheGroupNullable{}
 		for _, cg := range cacheGroups {
 			if cg.Name == nil {
-				return "ERROR: got cachegroup with nil name!'"
+				return Cfg{}, makeErr(warnings, "got cachegroup with nil name!")
 			}
 			cgMap[*cg.Name] = cg
 		}
 
 		if server.Cachegroup == nil {
-			return "ERROR: server had nil Cachegroup!"
+			return Cfg{}, makeErr(warnings, "server had nil Cachegroup!")
 		}
 
 		serverCG, ok := cgMap[*server.Cachegroup]
 		if !ok {
-			return "ERROR: Server cachegroup not in cachegroups!"
+			return Cfg{}, makeErr(warnings, "server cachegroup not in cachegroups!")
 		}
 
 		childCGs := map[string]tc.CacheGroupNullable{}
@@ -209,13 +176,13 @@ func MakeIPAllowDotConfig(
 		}
 
 		// sort servers, to guarantee things like IP coalescing are deterministic
-		sort.Sort(ServersSortByName(servers))
+		sort.Sort(serversSortByName(servers))
 		for _, childServer := range servers {
 			if childServer.Cachegroup == nil {
-				log.Errorln("Servers had server with nil Cachegroup, skipping!")
+				warnings = append(warnings, "Servers had server with nil Cachegroup, skipping!")
 				continue
 			} else if childServer.HostName == nil {
-				log.Errorln("Servers had server with nil HostName, skipping!")
+				warnings = append(warnings, "Servers had server with nil HostName, skipping!")
 				continue
 			}
 
@@ -246,10 +213,10 @@ func MakeIPAllowDotConfig(
 						// not an IP, try a CIDR
 						if ip, cidr, err := net.ParseCIDR(svAddr.Address); err != nil {
 							// not a CIDR or IP - error out
-							log.Errorln("MakeIPAllowDotConfig server '" + *server.HostName + "' IP '" + svAddr.Address + " is not an IP address or CIDR - skipping!")
+							warnings = append(warnings, "server '"+*server.HostName+"' IP '"+svAddr.Address+" is not an IP address or CIDR - skipping!")
 						} else if ip == nil {
 							// not a CIDR or IP - error out
-							log.Errorln("MakeIPAllowDotConfig server '" + *server.HostName + "' IP '" + svAddr.Address + " failed to parse as IP or CIDR - skipping!")
+							warnings = append(warnings, "server '"+*server.HostName+"' IP '"+svAddr.Address+" failed to parse as IP or CIDR - skipping!")
 						} else {
 							// got a valid CIDR - add it to the list
 							if ip4 := ip.To4(); ip4 != nil {
@@ -267,14 +234,14 @@ func MakeIPAllowDotConfig(
 		cidr6s := util.CoalesceCIDRs(ip6s, coalesceNumberV6, coalesceMaskLenV6)
 
 		for _, cidr := range cidrs {
-			ipAllowData = append(ipAllowData, IPAllowData{
+			ipAllowDat = append(ipAllowDat, ipAllowData{
 				Src:    util.RangeStr(cidr),
 				Action: ActionAllow,
 				Method: MethodAll,
 			})
 		}
 		for _, cidr := range cidr6s {
-			ipAllowData = append(ipAllowData, IPAllowData{
+			ipAllowDat = append(ipAllowDat, ipAllowData{
 				Src:    util.RangeStr(cidr),
 				Action: ActionAllow,
 				Method: MethodAll,
@@ -282,41 +249,85 @@ func MakeIPAllowDotConfig(
 		}
 
 		// allow RFC 1918 server space - TODO JvD: parameterize
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `10.0.0.0-10.255.255.255`,
 			Action: ActionAllow,
 			Method: MethodAll,
 		})
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `172.16.0.0-172.31.255.255`,
 			Action: ActionAllow,
 			Method: MethodAll,
 		})
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `192.168.0.0-192.168.255.255`,
 			Action: ActionAllow,
 			Method: MethodAll,
 		})
 
 		// order matters, so sort before adding the denys
-		sort.Sort(IPAllowDatas(ipAllowData))
+		sort.Sort(ipAllowDatas(ipAllowDat))
 
 		// end with a deny
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `0.0.0.0-255.255.255.255`,
 			Action: ActionDeny,
 			Method: MethodAll,
 		})
-		ipAllowData = append(ipAllowData, IPAllowData{
+		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff`,
 			Action: ActionDeny,
 			Method: MethodAll,
 		})
 	}
 
-	text := GenericHeaderComment(*server.HostName, toToolName, toURL)
-	for _, al := range ipAllowData {
+	text := makeHdrComment(hdrComment)
+	for _, al := range ipAllowDat {
 		text += `src_ip=` + al.Src + ` action=` + al.Action + ` method=` + al.Method + "\n"
 	}
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeHostingDotConfig,
+		LineComment: LineCommentHostingDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type ipAllowData struct {
+	Src    string
+	Action string
+	Method string
+}
+
+type ipAllowDatas []ipAllowData
+
+func (is ipAllowDatas) Len() int      { return len(is) }
+func (is ipAllowDatas) Swap(i, j int) { is[i], is[j] = is[j], is[i] }
+func (is ipAllowDatas) Less(i, j int) bool {
+	if is[i].Src != is[j].Src {
+		return is[i].Src < is[j].Src
+	}
+	if is[i].Action != is[j].Action {
+		return is[i].Action < is[j].Action
+	}
+	return is[i].Method < is[j].Method
+}
+
+type ipAllowServer struct {
+	IPAddress  string
+	IP6Address string
+}
+
+type serversSortByName []Server
+
+func (ss serversSortByName) Len() int      { return len(ss) }
+func (ss serversSortByName) Swap(i, j int) { ss[i], ss[j] = ss[j], ss[i] }
+func (ss serversSortByName) Less(i, j int) bool {
+	if ss[j].HostName == nil {
+		return false
+	} else if ss[i].HostName == nil {
+		return true
+	}
+	return *ss[i].HostName < *ss[j].HostName
 }
diff --git a/lib/go-atscfg/ipallowdotconfig_test.go b/lib/go-atscfg/ipallowdotconfig_test.go
index beb9f23..9a1c8fa 100644
--- a/lib/go-atscfg/ipallowdotconfig_test.go
+++ b/lib/go-atscfg/ipallowdotconfig_test.go
@@ -28,17 +28,17 @@ import (
 )
 
 func TestMakeIPAllowDotConfig(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
-	params := map[string][]string{
+	hdr := "myHeaderComment"
+
+	params := makeParamsFromMapArr("serverProfile", IPAllowConfigFileName, map[string][]string{
 		"purge_allow_ip":       []string{"192.168.2.99"},
 		ParamCoalesceMaskLenV4: []string{"24"},
 		ParamCoalesceNumberV4:  []string{"3"},
 		ParamCoalesceMaskLenV6: []string{"48"},
 		ParamCoalesceNumberV6:  []string{"4"},
-	}
+	})
 
-	svs := []tc.ServerNullable{
+	svs := []Server{
 		*makeIPAllowChild("child0", "192.168.2.1", "2001:DB8:1::1/64"),
 		*makeIPAllowChild("child1", "192.168.2.100/30", "2001:DB8:2::1/64"),
 		*makeIPAllowChild("child2", "192.168.2.150", ""),
@@ -70,12 +70,17 @@ func TestMakeIPAllowDotConfig(t *testing.T) {
 		},
 	}
 
-	sv := &tc.ServerNullable{}
+	sv := &Server{}
 	sv.HostName = util.StrPtr("server0")
 	sv.Type = string(tc.CacheTypeMid)
 	sv.Cachegroup = cgs[0].Name
 	svs = append(svs, *sv)
-	txt := MakeIPAllowDotConfig(toToolName, toURL, params, sv, svs, cgs)
+
+	cfg, err := MakeIPAllowDotConfig(params, sv, svs, cgs, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	lines := strings.Split(txt, "\n")
 
@@ -88,11 +93,8 @@ func TestMakeIPAllowDotConfig(t *testing.T) {
 	if !strings.HasPrefix(commentLine, "#") {
 		t.Errorf("expected: comment line starting with '#', actual: '%v'\n", commentLine)
 	}
-	if !strings.Contains(commentLine, toToolName) {
-		t.Errorf("expected: comment line containing toolName '%v', actual: '%v'\n", toToolName, commentLine)
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected: comment line containing toURL '%v', actual: '%v'\n", toURL, commentLine)
+	if !strings.Contains(commentLine, hdr) {
+		t.Errorf("expected: comment line containing header comment '%v', actual: '%v'\n", hdr, commentLine)
 	}
 
 	lines = lines[1:] // remove comment line
@@ -105,16 +107,16 @@ func TestMakeIPAllowDotConfig(t *testing.T) {
 }
 
 func TestMakeIPAllowDotConfigEdge(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
-	params := map[string][]string{
+	hdr := "myHeaderComment"
+
+	params := makeParamsFromMapArr("serverProfile", IPAllowConfigFileName, map[string][]string{
 		ParamCoalesceMaskLenV4: []string{"24"},
 		ParamCoalesceNumberV4:  []string{"3"},
 		ParamCoalesceMaskLenV6: []string{"48"},
 		ParamCoalesceNumberV6:  []string{"4"},
-	}
+	})
 
-	svs := []tc.ServerNullable{
+	svs := []Server{
 		*makeIPAllowChild("child0", "192.168.2.1", "2001:DB8:1::1/64"),
 		*makeIPAllowChild("child1", "192.168.2.100/30", "2001:DB8:2::1/64"),
 		*makeIPAllowChild("child2", "192.168.2.150", ""),
@@ -144,12 +146,17 @@ func TestMakeIPAllowDotConfigEdge(t *testing.T) {
 		},
 	}
 
-	sv := &tc.ServerNullable{}
+	sv := &Server{}
 	sv.HostName = util.StrPtr("server0")
 	sv.Type = string(tc.CacheTypeEdge)
 	sv.Cachegroup = cgs[0].Name
 	svs = append(svs, *sv)
-	txt := MakeIPAllowDotConfig(toToolName, toURL, params, sv, svs, cgs)
+
+	cfg, err := MakeIPAllowDotConfig(params, sv, svs, cgs, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	lines := strings.Split(txt, "\n")
 
@@ -162,11 +169,8 @@ func TestMakeIPAllowDotConfigEdge(t *testing.T) {
 	if !strings.HasPrefix(commentLine, "#") {
 		t.Errorf("expected: comment line starting with '#', actual: '%v'\n", commentLine)
 	}
-	if !strings.Contains(commentLine, toToolName) {
-		t.Errorf("expected: comment line containing toolName '%v', actual: '%v'\n", toToolName, commentLine)
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected: comment line containing toURL '%v', actual: '%v'\n", toURL, commentLine)
+	if !strings.Contains(commentLine, hdr) {
+		t.Errorf("expected: comment line containing header comment '%v', actual: '%v'\n", hdr, commentLine)
 	}
 
 	lines = lines[1:] // remove comment line
@@ -185,17 +189,16 @@ func TestMakeIPAllowDotConfigEdge(t *testing.T) {
 }
 
 func TestMakeIPAllowDotConfigNonDefaultV6Number(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
-	params := map[string][]string{
+	hdr := "myHeaderComment"
+	params := makeParamsFromMapArr("serverProfile", IPAllowConfigFileName, map[string][]string{
 		"purge_allow_ip":       []string{"192.168.2.99"},
 		ParamCoalesceMaskLenV4: []string{"24"},
 		ParamCoalesceNumberV4:  []string{"3"},
 		ParamCoalesceMaskLenV6: []string{"48"},
 		ParamCoalesceNumberV6:  []string{"100"},
-	}
+	})
 
-	svs := []tc.ServerNullable{
+	svs := []Server{
 		*makeIPAllowChild("child0", "192.168.2.1", "2001:DB8:1::1/64"),
 		*makeIPAllowChild("child1", "192.168.2.100/30", "2001:DB8:2::1/64"),
 		*makeIPAllowChild("child2", "192.168.2.150", ""),
@@ -227,12 +230,17 @@ func TestMakeIPAllowDotConfigNonDefaultV6Number(t *testing.T) {
 		},
 	}
 
-	sv := &tc.ServerNullable{}
+	sv := &Server{}
 	sv.HostName = util.StrPtr("server0")
 	sv.Type = string(tc.CacheTypeMid)
 	sv.Cachegroup = cgs[0].Name
 	svs = append(svs, *sv)
-	txt := MakeIPAllowDotConfig(toToolName, toURL, params, sv, svs, cgs)
+
+	cfg, err := MakeIPAllowDotConfig(params, sv, svs, cgs, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	lines := strings.Split(txt, "\n")
 
@@ -245,11 +253,8 @@ func TestMakeIPAllowDotConfigNonDefaultV6Number(t *testing.T) {
 	if !strings.HasPrefix(commentLine, "#") {
 		t.Errorf("expected: comment line starting with '#', actual: '%v'\n", commentLine)
 	}
-	if !strings.Contains(commentLine, toToolName) {
-		t.Errorf("expected: comment line containing toolName '%v', actual: '%v'\n", toToolName, commentLine)
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected: comment line containing toURL '%v', actual: '%v'\n", toURL, commentLine)
+	if !strings.Contains(commentLine, hdr) {
+		t.Errorf("expected: comment line containing header comment '%v', actual: '%v'\n", hdr, commentLine)
 	}
 
 	lines = lines[1:] // remove comment line
@@ -261,8 +266,8 @@ func TestMakeIPAllowDotConfigNonDefaultV6Number(t *testing.T) {
 	}
 }
 
-func makeIPAllowChild(name string, ip string, ip6 string) *tc.ServerNullable {
-	sv := &tc.ServerNullable{}
+func makeIPAllowChild(name string, ip string, ip6 string) *Server {
+	sv := &Server{}
 	sv.Cachegroup = util.StrPtr("childcg")
 	sv.HostName = util.StrPtr("child0")
 	sv.Type = tc.MonitorTypeName
diff --git a/lib/go-atscfg/loggingdotconfig.go b/lib/go-atscfg/loggingdotconfig.go
index 0d9f375..bbd559c 100644
--- a/lib/go-atscfg/loggingdotconfig.go
+++ b/lib/go-atscfg/loggingdotconfig.go
@@ -20,10 +20,11 @@ package atscfg
  */
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const MaxLogObjects = 10
@@ -35,13 +36,20 @@ const LineCommentLoggingDotConfig = LineCommentHash
 // MakeStorageDotConfig creates storage.config for a given ATS Profile.
 // The paramData is the map of parameter names to values, for all parameters assigned to the given profile, with the config_file "storage.config".
 func MakeLoggingDotConfig(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, StorageFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrCommentTxt string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Profile")
+	}
+
+	paramData, paramWarns := paramsToMap(filterParams(serverParams, LoggingFileName, "", "", "location"))
+	warnings = append(warnings, paramWarns...)
 
-	hdrComment := GenericHeaderComment(profileName, toToolName, toURL)
+	hdrComment := makeHdrComment(hdrCommentTxt)
 	// This is an LUA file, so we need to massage the header a bit for LUA commenting.
 	hdrComment = strings.Replace(hdrComment, `# `, ``, -1)
 	hdrComment = strings.Replace(hdrComment, "\n", ``, -1)
@@ -56,7 +64,7 @@ func MakeLoggingDotConfig(
 			format := paramData[logFormatField+".Format"]
 			if format == "" {
 				// TODO determine if the line should be excluded. Perl includes it anyway, without checking.
-				log.Errorf("Profile '%v' has logging.config format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", profileName, logFormatField)
+				warnings = append(warnings, fmt.Sprintf("profile '%v' has logging.config format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", *server.Profile, logFormatField))
 			}
 			format = strings.Replace(format, `"`, `\"`, -1)
 			text += logFormatName + " = format {\n"
@@ -75,7 +83,7 @@ func MakeLoggingDotConfig(
 			filter := paramData[logFilterField+".Filter"]
 			if filter == "" {
 				// TODO determine if the line should be excluded. Perl includes it anyway, without checking.
-				log.Errorf("Profile '%v' has logging.config format '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", profileName, logFilterField)
+				warnings = append(warnings, fmt.Sprintf("profile '%v' has logging.config format '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", *server.Profile, logFilterField))
 			}
 
 			filter = strings.Replace(filter, `\`, `\\`, -1)
@@ -124,5 +132,10 @@ func MakeLoggingDotConfig(
 		}
 	}
 
-	return text
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeLoggingDotConfig,
+		LineComment: LineCommentLoggingDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/loggingdotconfig_test.go b/lib/go-atscfg/loggingdotconfig_test.go
index 3e3144a..634aa5d 100644
--- a/lib/go-atscfg/loggingdotconfig_test.go
+++ b/lib/go-atscfg/loggingdotconfig_test.go
@@ -26,20 +26,27 @@ import (
 
 func TestMakeLoggingDotConfig(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdrComment := "myHeaderComment"
+
+	server := makeGenericServer()
+	server.Profile = &profileName
+
+	params := makeParamsFromMap("serverProfile", LoggingFileName, map[string]string{
 		"LogFormat.Name":           "myFormatName",
 		"LogFormat.Format":         "myFormat",
 		"LogObject.Filename":       "myFilename",
 		"LogObject.RollingEnabled": "myRollingEnabled",
 		"LogFormat.Invalid":        "ShouldNotBeHere",
 		"LogObject.Invalid":        "ShouldNotBeHere",
-	}
+	})
 
-	txt := MakeLoggingDotConfig(profileName, paramData, toolName, toURL)
+	cfg, err := MakeLoggingDotConfig(server, params, hdrComment)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testLuaComment(t, txt, profileName, toolName, toURL)
+	testLuaComment(t, txt, profileName, hdrComment)
 
 	if !strings.Contains(txt, "myFormatName") {
 		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
@@ -58,7 +65,7 @@ func TestMakeLoggingDotConfig(t *testing.T) {
 	}
 }
 
-func testLuaComment(t *testing.T, txt string, objName string, toolName string, toURL string) {
+func testLuaComment(t *testing.T, txt string, objName string, hdrComment string) {
 	commentLine := strings.SplitN(txt, "\n", 2)[0] // SplitN always returns at least 1 element, no need to check len before indexing
 
 	if !strings.HasPrefix(strings.TrimSpace(commentLine), "--") {
@@ -67,13 +74,7 @@ func testLuaComment(t *testing.T, txt string, objName string, toolName string, t
 	if !strings.HasSuffix(strings.TrimSpace(commentLine), "--") {
 		t.Errorf("expected ending comment on first line, actual: '" + commentLine + "'")
 	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toolName '" + toolName + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toURL '" + toURL + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, objName) {
-		t.Errorf("expected profile '" + objName + "' in comment, actual: '" + commentLine + "'")
+	if !strings.Contains(commentLine, hdrComment) {
+		t.Errorf("expected comment text '" + hdrComment + "' in comment, actual: '" + commentLine + "'")
 	}
 }
diff --git a/lib/go-atscfg/loggingdotyaml.go b/lib/go-atscfg/loggingdotyaml.go
index 746721d..f7a964e 100644
--- a/lib/go-atscfg/loggingdotyaml.go
+++ b/lib/go-atscfg/loggingdotyaml.go
@@ -20,10 +20,11 @@ package atscfg
  */
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const LoggingYAMLFileName = "logging.yaml"
@@ -31,12 +32,20 @@ const ContentTypeLoggingDotYAML = "application/yaml; charset=us-ascii" // Note Y
 const LineCommentLoggingDotYAML = LineCommentHash
 
 func MakeLoggingDotYAML(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, LoggingYAMLFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Profile")
+	}
+
+	paramData, paramWarns := paramsToMap(filterParams(serverParams, LoggingYAMLFileName, "", "", "location"))
+	warnings = append(warnings, paramWarns...)
+
+	hdr := makeHdrComment(hdrComment)
 
 	// note we use the same const as logs.xml - this isn't necessarily a requirement, and we may want to make separate variables in the future.
 	maxLogObjects := MaxLogObjects
@@ -53,7 +62,7 @@ func MakeLoggingDotYAML(
 			format := paramData[logFormatField+".Format"]
 			if format == "" {
 				// TODO determine if the line should be excluded. Perl includes it anyway, without checking.
-				log.Errorf("Profile '%v' has logging.yaml format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", profileName, logFormatField)
+				warnings = append(warnings, fmt.Sprintf("profile '%v' has logging.yaml format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", *server.Profile, logFormatField))
 			}
 			text += " - name: " + logFormatName + " \n"
 			text += "   format: '" + format + "'\n"
@@ -70,7 +79,7 @@ func MakeLoggingDotYAML(
 			filter := paramData[logFilterField+".Filter"]
 			if filter == "" {
 				// TODO determine if the line should be excluded. Perl includes it anyway, without checking.
-				log.Errorf("Profile '%v' has logging.yaml filter '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", profileName, logFilterField)
+				warnings = append(warnings, fmt.Sprintf("profile '%v' has logging.yaml filter '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", *server.Profile, logFilterField))
 			}
 			logFilterType := paramData[logFilterField+".Type"]
 			if logFilterType == "" {
@@ -130,5 +139,10 @@ func MakeLoggingDotYAML(
 		}
 	}
 
-	return text
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeLoggingDotYAML,
+		LineComment: LineCommentLoggingDotYAML,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/loggingdotyaml_test.go b/lib/go-atscfg/loggingdotyaml_test.go
index 3e6899d..693e51d 100644
--- a/lib/go-atscfg/loggingdotyaml_test.go
+++ b/lib/go-atscfg/loggingdotyaml_test.go
@@ -29,20 +29,27 @@ import (
 
 func TestMakeLoggingDotYAML(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdr := "myHeaderComment"
+
+	server := makeGenericServer()
+	server.Profile = &profileName
+
+	params := makeParamsFromMap("serverProfile", LoggingYAMLFileName, map[string]string{
 		"LogFormat.Name":           "myFormatName",
 		"LogFormat.Format":         "myFormat",
 		"LogObject.Filename":       "myFilename",
 		"LogObject.RollingEnabled": "myRollingEnabled",
 		"LogFormat.Invalid":        "ShouldNotBeHere",
 		"LogObject.Invalid":        "ShouldNotBeHere",
-	}
+	})
 
-	txt := MakeLoggingDotYAML(profileName, paramData, toolName, toURL)
+	cfg, err := MakeLoggingDotYAML(server, params, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "myFormatName") {
 		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
@@ -63,9 +70,8 @@ func TestMakeLoggingDotYAML(t *testing.T) {
 
 func TestMakeLoggingDotYAMLMultiFormat(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdr := "myHeaderComment"
+	paramData := makeParamsFromMap("serverProfile", LoggingYAMLFileName, map[string]string{
 		"LogFormat.Name":           "myFormatName0",
 		"LogFormat.Format":         "myFormat0",
 		"LogFormat1.Name":          "myFormatName1",
@@ -92,11 +98,18 @@ func TestMakeLoggingDotYAMLMultiFormat(t *testing.T) {
 		"LogObject1.Format":        "myFormatName1",
 		"LogFilter.Name":           "myFilterName",
 		"LogFilter.Filter":         "myFilter",
-	}
+	})
+
+	server := makeGenericServer()
+	server.Profile = &profileName
 
-	txt := MakeLoggingDotYAML(profileName, paramData, toolName, toURL)
+	cfg, err := MakeLoggingDotYAML(server, paramData, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "myFormatName") {
 		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
@@ -129,7 +142,7 @@ func TestMakeLoggingDotYAMLMultiFormat(t *testing.T) {
 			Rolling_size_mb      int
 		}
 	}
-	err := yaml.Unmarshal([]byte(txt), &v)
+	err = yaml.Unmarshal([]byte(txt), &v)
 	if err != nil {
 		t.Errorf("expected config to parse as yaml document '%v', actual: '%v'", err, txt)
 	}
diff --git a/lib/go-atscfg/logsdotxml.go b/lib/go-atscfg/logsdotxml.go
index 72e6525..b02ae29 100644
--- a/lib/go-atscfg/logsdotxml.go
+++ b/lib/go-atscfg/logsdotxml.go
@@ -22,6 +22,8 @@ package atscfg
 import (
 	"strconv"
 	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const LogsXMLFileName = "logs_xml.config"
@@ -30,16 +32,23 @@ const ContentTypeLogsDotXML = `text/xml`
 const LineCommentLogsDotXML = `<!--`
 
 func MakeLogsXMLDotConfig(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, LoggingYAMLFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrCommentTxt string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "this server missing Profile")
+	}
+
+	paramData, paramWarns := paramsToMap(filterParams(serverParams, LogsXMLFileName, "", "", "location"))
+	warnings = append(warnings, paramWarns...)
 
 	// Note LineCommentLogsDotXML must be a single-line comment!
 	// But this file doesn't have a single-line format, so we use <!-- for the header and promise it's on a single line
 	// Note! if this file is ever changed to have multi-line comments, LineCommentLogsDotXML will have to be changed to the empty string.
-	hdrComment := GenericHeaderComment(profileName, toToolName, toURL)
+	hdrComment := makeHdrComment(hdrCommentTxt)
 	hdrComment = strings.Replace(hdrComment, `# `, ``, -1)
 	hdrComment = strings.Replace(hdrComment, "\n", ``, -1)
 	text := "<!-- " + hdrComment + " -->\n"
@@ -93,5 +102,11 @@ func MakeLogsXMLDotConfig(
 `
 		}
 	}
-	return text
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeLogsDotXML,
+		LineComment: LineCommentLogsDotXML,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/logsdotxml_test.go b/lib/go-atscfg/logsdotxml_test.go
index 58e130f..8654d28 100644
--- a/lib/go-atscfg/logsdotxml_test.go
+++ b/lib/go-atscfg/logsdotxml_test.go
@@ -26,20 +26,26 @@ import (
 
 func TestMakeLogsXMLDotConfig(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdr := "myHeaderComment"
+	paramData := makeParamsFromMap("serverProfile", LogsXMLFileName, map[string]string{
 		"LogFormat.Name":           "myFormatName",
 		"LogFormat.Format":         "myFormat",
 		"LogObject.Filename":       "myFilename",
 		"LogObject.RollingEnabled": "myRollingEnabled",
 		"LogFormat.Invalid":        "ShouldNotBeHere",
 		"LogObject.Invalid":        "ShouldNotBeHere",
-	}
+	})
+
+	server := makeGenericServer()
+	server.Profile = &profileName
 
-	txt := MakeLogsXMLDotConfig(profileName, paramData, toolName, toURL)
+	cfg, err := MakeLogsXMLDotConfig(server, paramData, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testXMLComment(t, txt, profileName, toolName, toURL)
+	testXMLComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "myFormatName") {
 		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
@@ -58,7 +64,7 @@ func TestMakeLogsXMLDotConfig(t *testing.T) {
 	}
 }
 
-func testXMLComment(t *testing.T, txt string, objName string, toolName string, toURL string) {
+func testXMLComment(t *testing.T, txt string, hdr string) {
 	commentLine := strings.SplitN(txt, "\n", 2)[0] // SplitN always returns at least 1 element, no need to check len before indexing
 
 	if !strings.HasPrefix(strings.TrimSpace(commentLine), "<!--") {
@@ -67,13 +73,7 @@ func testXMLComment(t *testing.T, txt string, objName string, toolName string, t
 	if !strings.HasSuffix(strings.TrimSpace(commentLine), "-->") {
 		t.Errorf("expected ending comment on first line, actual: '" + commentLine + "'")
 	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toolName '" + toolName + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, toURL) {
-		t.Errorf("expected toURL '" + toURL + "' in comment, actual: '" + commentLine + "'")
-	}
-	if !strings.Contains(commentLine, objName) {
-		t.Errorf("expected profile '" + objName + "' in comment, actual: '" + commentLine + "'")
+	if !strings.Contains(commentLine, hdr) {
+		t.Errorf("expected header comment '" + hdr + "' in comment, actual: '" + commentLine + "'")
 	}
 }
diff --git a/lib/go-atscfg/meta.go b/lib/go-atscfg/meta.go
index 7c91e2f..88a20d8 100644
--- a/lib/go-atscfg/meta.go
+++ b/lib/go-atscfg/meta.go
@@ -20,102 +20,148 @@ package atscfg
  */
 
 import (
-	"encoding/json"
 	"errors"
 	"path/filepath"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
-type ConfigProfileParams struct {
-	FileNameOnDisk string
-	Location       string
-	URL            string
+type CfgMeta struct {
+	Name string
+	Path string
 }
 
-// APIVersion is the Traffic Ops API version for config fiels.
-// This is used to generate the meta config, which has API paths.
-// Note the version in the meta config is not used by the atstccfg generator, which isn't actually an API.
-// TODO change the config system to not use old API paths, and remove this.
-const APIVersion = "2.0"
+// MakeMetaObj returns the list of config files, any warnings, and any errors.
+func MakeConfigFilesList(
+	configDir string,
+	server *Server,
+	serverParams []tc.Parameter,
+	deliveryServices []DeliveryService,
+	deliveryServiceServers []tc.DeliveryServiceServer,
+	globalParams []tc.Parameter,
+	cacheGroupArr []tc.CacheGroupNullable,
+	topologies []tc.Topology,
+) ([]CfgMeta, []string, error) {
+	warnings := []string{}
 
-// requiredFiles is a constant (because Go doesn't allow const slices).
-// Note these are not exhaustive. This is only used to error if these are missing.
-// The presence of these is no guarantee the location Parameters are complete and correct.
-func requiredFiles() []string {
-	return []string{
-		"cache.config",
-		"hosting.config",
-		"ip_allow.config",
-		"parent.config",
-		"plugin.config",
-		"records.config",
-		"remap.config",
-		"storage.config",
-		"volume.config",
+	if server.Cachegroup == nil {
+		return nil, warnings, errors.New("this server missing Cachegroup")
+	} else if server.CachegroupID == nil {
+		return nil, warnings, errors.New("this server missing CachegroupID")
+	} else if server.ProfileID == nil {
+		return nil, warnings, errors.New("server missing ProfileID")
+	} else if server.TCPPort == nil {
+		return nil, warnings, errors.New("server missing TCPPort")
+	} else if server.HostName == nil {
+		return nil, warnings, errors.New("server missing HostName")
+	} else if server.CDNID == nil {
+		return nil, warnings, errors.New("server missing CDNID")
+	} else if server.CDNName == nil {
+		return nil, warnings, errors.New("server missing CDNName")
+	} else if server.ID == nil {
+		return nil, warnings, errors.New("server missing ID")
+	} else if server.Profile == nil {
+		return nil, warnings, errors.New("server missing Profile")
 	}
-}
 
-func MakeMetaConfig(
-	server *tc.ServerNullable,
-	tmURL string, // global tm.url Parameter
-	tmReverseProxyURL string, // global tm.rev_proxy.url Parameter
-	locationParams map[string]ConfigProfileParams, // map[configFile]params; 'location' and 'URL' Parameters on serverHostName's Profile
-	uriSignedDSes []tc.DeliveryServiceName,
-	scopeParams map[string]string, // map[configFileName]scopeParam
-	dses map[tc.DeliveryServiceName]tc.DeliveryServiceNullableV30,
-	cacheGroupArr []tc.CacheGroupNullable,
-	topologies []tc.Topology,
-) string {
-	configDir := "" // this should only be used for Traffic Ops, which doesn't have a local ATS install config directory (and thus will fail if any location Parameters are missing or relative).
-	return MetaObjToMetaConfig(MakeMetaObj(server, tmURL, tmReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dses, cacheGroupArr, topologies, configDir))
-}
+	tmURL, tmReverseProxyURL := getTOURLAndReverseProxy(globalParams)
+	if tmURL == "" {
+		warnings = append(warnings, "global tm.url parameter missing or empty! Setting empty in meta config!")
+	}
 
-func MetaObjToMetaConfig(atsData tc.ATSConfigMetaData, err error) string {
-	if err != nil {
-		return "error creating meta config: " + err.Error()
+	dses, dsWarns := filterConfigFileDSes(server, deliveryServices, deliveryServiceServers)
+	warnings = append(warnings, dsWarns...)
+
+	locationParams := getLocationParams(serverParams)
+
+	uriSignedDSes, signDSWarns := getURISignedDSes(dses)
+	warnings = append(warnings, signDSWarns...)
+
+	configFiles := []CfgMeta{}
+
+	if locationParams["remap.config"].Path != "" {
+		configLocation := locationParams["remap.config"].Path
+		for _, ds := range uriSignedDSes {
+			cfgName := "uri_signing_" + string(ds) + ".config"
+			// If there's already a parameter for it, don't clobber it. The user may wish to override the location.
+			if _, ok := locationParams[cfgName]; !ok {
+				p := locationParams[cfgName]
+				p.Name = cfgName
+				p.Path = configLocation
+				locationParams[cfgName] = p
+			}
+		}
 	}
-	bts, err := json.Marshal(atsData)
-	if err != nil {
-		// should never happen
-		log.Errorln("marshalling meta config: " + err.Error())
-		bts = []byte("error encoding to json, see log for details")
+
+locationParamsFor:
+	for cfgFile, cfgParams := range locationParams {
+		if strings.HasSuffix(cfgFile, ".config") {
+			dsConfigFilePrefixes := []string{
+				"hdr_rw_mid_", // must come before hdr_rw_, to avoid thinking we have a "hdr_rw_" with a ds of "mid_x"
+				"hdr_rw_",
+				"regex_remap_",
+				"url_sig_",
+				"uri_signing_",
+			}
+		prefixFor:
+			for _, prefix := range dsConfigFilePrefixes {
+				if strings.HasPrefix(cfgFile, prefix) {
+					dsName := strings.TrimSuffix(strings.TrimPrefix(cfgFile, prefix), ".config")
+					if _, ok := dses[tc.DeliveryServiceName(dsName)]; !ok {
+						warnings = append(warnings, "server profile had 'location' Parameter '"+cfgFile+"', but delivery Service '"+dsName+"' is not assigned to this Server! Not including in meta config!")
+						continue locationParamsFor
+					}
+					break prefixFor // if it has a prefix, don't check the next prefix. This is important for hdr_rw_mid_, which will match hdr_rw_ and result in a "ds name" of "mid_x" if we don't continue here.
+				}
+			}
+		}
+
+		atsCfg := CfgMeta{
+			Name: cfgParams.Name,
+			Path: cfgParams.Path,
+		}
+
+		configFiles = append(configFiles, atsCfg)
 	}
-	return string(bts)
+
+	configFiles, configDirWarns, err := addMetaObjConfigDir(configFiles, configDir, server, tmURL, tmReverseProxyURL, locationParams, uriSignedDSes, dses, cacheGroupArr, topologies)
+	warnings = append(warnings, configDirWarns...)
+	return configFiles, warnings, err
 }
 
-// AddMetaObjConfigDir takes the Meta Object generated from TO data, and the ATS config directory
+// addMetaObjConfigDir takes the Meta Object generated from TO data, and the ATS config directory
 // and prepends the config directory to all relative paths,
 // and creates MetaObj entries for all required config files which have no location parameter.
 // If configDir is empty and any location Parameters have relative paths, returns an error.
-func AddMetaObjConfigDir(
-	metaObj tc.ATSConfigMetaData,
+// Returns the amended config files list, any warnings, and any error.
+func addMetaObjConfigDir(
+	configFiles []CfgMeta,
 	configDir string,
-	server *tc.ServerNullable,
+	server *Server,
 	tmURL string, // global tm.url Parameter
 	tmReverseProxyURL string, // global tm.rev_proxy.url Parameter
-	locationParams map[string]ConfigProfileParams, // map[configFile]params; 'location' and 'URL' Parameters on serverHostName's Profile
+	locationParams map[string]configProfileParams, // map[configFile]params; 'location' and 'URL' Parameters on serverHostName's Profile
 	uriSignedDSes []tc.DeliveryServiceName,
-	scopeParams map[string]string, // map[configFileName]scopeParam
-	dses map[tc.DeliveryServiceName]tc.DeliveryServiceNullableV30,
+	dses map[tc.DeliveryServiceName]DeliveryService,
 	cacheGroupArr []tc.CacheGroupNullable,
 	topologies []tc.Topology,
-) (tc.ATSConfigMetaData, error) {
+) ([]CfgMeta, []string, error) {
+	warnings := []string{}
+
 	if server.Cachegroup == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing Cachegroup")
+		return nil, warnings, errors.New("server missing Cachegroup")
 	}
 
-	cacheGroups, err := MakeCGMap(cacheGroupArr)
+	cacheGroups, err := makeCGMap(cacheGroupArr)
 	if err != nil {
-		return tc.ATSConfigMetaData{}, errors.New("making CG map: " + err.Error())
+		return nil, warnings, errors.New("making CG map: " + err.Error())
 	}
 
 	// Note there may be multiple files with the same name in different directories.
-	configFilesM := map[string][]tc.ATSConfigMetaDataConfigFile{} // map[fileShortName]tc.ATSConfigMetaDataConfigFile
-	for _, fi := range metaObj.ConfigFiles {
-		configFilesM[fi.FileNameOnDisk] = append(configFilesM[fi.FileNameOnDisk], fi)
+	configFilesM := map[string][]CfgMeta{} // map[fileShortName]CfgMeta
+	for _, fi := range configFiles {
+		configFilesM[fi.Name] = append(configFilesM[fi.Path], fi)
 	}
 
 	// add all strictly required files, all of which should be in the base config directory.
@@ -127,35 +173,34 @@ func AddMetaObjConfigDir(
 			continue
 		}
 		if configDir == "" {
-			return metaObj, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
+			return nil, warnings, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
 		}
-		configFilesM[fileName] = []tc.ATSConfigMetaDataConfigFile{{
-			FileNameOnDisk: fileName,
-			Location:       configDir,
-			Scope:          string(getServerScope(fileName, server.Type, nil)),
+		configFilesM[fileName] = []CfgMeta{{
+			Name: fileName,
+			Path: configDir,
 		}}
 	}
 
 	for fileName, fis := range configFilesM {
-		newFis := []tc.ATSConfigMetaDataConfigFile{}
+		newFis := []CfgMeta{}
 		for _, fi := range fis {
-			if !filepath.IsAbs(fi.Location) {
+			if !filepath.IsAbs(fi.Path) {
 				if configDir == "" {
-					return metaObj, errors.New("file '" + fileName + "' has location Parameter with relative path '" + fi.Location + "', but ATS config directory was not found.")
+					return nil, warnings, errors.New("file '" + fileName + "' has location Parameter with relative path '" + fi.Path + "', but ATS config directory was not found.")
 				}
-				absPath := filepath.Join(configDir, fi.Location)
-				fi.Location = absPath
+				absPath := filepath.Join(configDir, fi.Path)
+				fi.Path = absPath
 			}
 			newFis = append(newFis, fi)
 		}
 		configFilesM[fileName] = newFis
 	}
 
-	nameTopologies := MakeTopologyNameMap(topologies)
+	nameTopologies := makeTopologyNameMap(topologies)
 
 	for _, ds := range dses {
 		if ds.XMLID == nil {
-			log.Errorln("meta config generation got Delivery Service with nil XMLID - not considering!")
+			warnings = append(warnings, "got Delivery Service with nil XMLID - not considering!")
 			continue
 		}
 
@@ -165,31 +210,31 @@ func AddMetaObjConfigDir(
 		if ds.Topology != nil && *ds.Topology != "" {
 			topology := nameTopologies[TopologyName(*ds.Topology)]
 
-			placement := getTopologyPlacement(tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups, &ds)
+			placement, err := getTopologyPlacement(tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups, &ds)
+			if err != nil {
+				return nil, warnings, errors.New("getting topology placement: " + err.Error())
+			}
 			if placement.IsFirstCacheTier {
 				if ds.FirstHeaderRewrite != nil && *ds.FirstHeaderRewrite != "" || ds.MaxOriginConnections != nil {
 					fileName := FirstHeaderRewriteConfigFileName(*ds.XMLID)
-					scope := tc.ATSConfigMetaDataConfigFileScopeServers
-					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir, scope); err != nil {
-						log.Errorln("meta config generation: " + err.Error())
+					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
+						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
 					}
 				}
 			}
 			if placement.IsInnerCacheTier {
 				if ds.InnerHeaderRewrite != nil && *ds.InnerHeaderRewrite != "" || ds.MaxOriginConnections != nil {
 					fileName := InnerHeaderRewriteConfigFileName(*ds.XMLID)
-					scope := tc.ATSConfigMetaDataConfigFileScopeServers
-					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir, scope); err != nil {
-						log.Errorln("meta config generation: " + err.Error())
+					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
+						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
 					}
 				}
 			}
 			if placement.IsLastCacheTier {
 				if ds.LastHeaderRewrite != nil && *ds.LastHeaderRewrite != "" || ds.MaxOriginConnections != nil {
 					fileName := LastHeaderRewriteConfigFileName(*ds.XMLID)
-					scope := tc.ATSConfigMetaDataConfigFileScopeServers
-					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir, scope); err != nil {
-						log.Errorln("meta config generation: " + err.Error())
+					if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
+						warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
 					}
 				}
 			}
@@ -197,9 +242,8 @@ func AddMetaObjConfigDir(
 			if (ds.EdgeHeaderRewrite != nil || ds.MaxOriginConnections != nil) &&
 				strings.HasPrefix(server.Type, tc.EdgeTypePrefix) {
 				fileName := "hdr_rw_" + *ds.XMLID + ".config"
-				scope := tc.ATSConfigMetaDataConfigFileScopeCDNs
-				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir, scope); err != nil {
-					log.Errorln("meta config generation: " + err.Error())
+				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
+					warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
 				}
 			}
 		} else if strings.HasPrefix(server.Type, tc.MidTypePrefix) {
@@ -207,226 +251,205 @@ func AddMetaObjConfigDir(
 				ds.Type != nil && ds.Type.UsesMidCache() &&
 				strings.HasPrefix(server.Type, tc.MidTypePrefix) {
 				fileName := "hdr_rw_mid_" + *ds.XMLID + ".config"
-				scope := tc.ATSConfigMetaDataConfigFileScopeCDNs
-				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir, scope); err != nil {
-					log.Errorln("meta config generation: " + err.Error())
+				if configFilesM, err = ensureConfigFile(configFilesM, fileName, configDir); err != nil {
+					warnings = append(warnings, "ensuring config file '"+fileName+"': "+err.Error())
 				}
 			}
 		}
 		if ds.RegexRemap != nil {
 			configFile := "regex_remap_" + *ds.XMLID + ".config"
-			scope := tc.ATSConfigMetaDataConfigFileScopeCDNs
-			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir, scope); err != nil {
-				log.Errorln("meta config generation: " + err.Error())
+			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
+				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
 			}
 		}
 		if ds.CacheURL != nil {
 			configFile := "cacheurl_" + *ds.XMLID + ".config"
-			scope := tc.ATSConfigMetaDataConfigFileScopeCDNs
-			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir, scope); err != nil {
-				log.Errorln("meta config generation: " + err.Error())
+			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
+				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
 			}
 		}
 		if ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == tc.SigningAlgorithmURLSig {
 			configFile := "url_sig_" + *ds.XMLID + ".config"
-			scope := tc.ATSConfigMetaDataConfigFileScopeProfiles
-			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir, scope); err != nil {
-				log.Errorln("meta config generation: " + err.Error())
+			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
+				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
 			}
 		}
 		if ds.SigningAlgorithm != nil && *ds.SigningAlgorithm == tc.SigningAlgorithmURISigning {
 			configFile := "uri_signing_" + *ds.XMLID + ".config"
-			scope := tc.ATSConfigMetaDataConfigFileScopeProfiles
-			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir, scope); err != nil {
-				log.Errorln("meta config generation: " + err.Error())
+			if configFilesM, err = ensureConfigFile(configFilesM, configFile, configDir); err != nil {
+				warnings = append(warnings, "ensuring config file '"+configFile+"': "+err.Error())
 			}
 		}
 	}
 
-	newFiles := []tc.ATSConfigMetaDataConfigFile{}
+	newFiles := []CfgMeta{}
 	for _, fis := range configFilesM {
 		for _, fi := range fis {
 			newFiles = append(newFiles, fi)
 		}
 	}
-	metaObj.ConfigFiles = newFiles
-	return metaObj, nil
+	return newFiles, warnings, nil
 }
 
-// ensureConfigFile ensures files contains the given fileName. If so, returns files unmodified.
-// If not, if configDir is empty, returns an error.
-// If not, and configDir is nonempty, creates the given file, configDir location, and scope, and returns files.
-func ensureConfigFile(files map[string][]tc.ATSConfigMetaDataConfigFile, fileName string, configDir string, scope tc.ATSConfigMetaDataConfigFileScope) (map[string][]tc.ATSConfigMetaDataConfigFile, error) {
-	if _, ok := files[fileName]; ok {
-		return files, nil
-	}
-	if configDir == "" {
-		return files, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
-	}
-	files[fileName] = []tc.ATSConfigMetaDataConfigFile{{
-		FileNameOnDisk: fileName,
-		Location:       configDir,
-		Scope:          string(scope),
-	}}
-	return files, nil
-}
+// getURISignedDSes returns the URI-signed Delivery Services, and any warnings.
+func getURISignedDSes(dses map[tc.DeliveryServiceName]DeliveryService) ([]tc.DeliveryServiceName, []string) {
+	warnings := []string{}
 
-func MakeMetaObj(
-	server *tc.ServerNullable,
-	tmURL string, // global tm.url Parameter
-	tmReverseProxyURL string, // global tm.rev_proxy.url Parameter
-	locationParams map[string]ConfigProfileParams, // map[configFile]params; 'location' and 'URL' Parameters on serverHostName's Profile
-	uriSignedDSes []tc.DeliveryServiceName,
-	scopeParams map[string]string, // map[configFileName]scopeParam
-	dses map[tc.DeliveryServiceName]tc.DeliveryServiceNullableV30,
-	cacheGroupArr []tc.CacheGroupNullable,
-	topologies []tc.Topology,
-	configDir string,
-) (tc.ATSConfigMetaData, error) {
-	if server.ProfileID == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing ProfileID")
-	} else if server.TCPPort == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing TCPPort")
-	} else if server.HostName == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing HostName")
-	} else if server.CDNID == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing CDNID")
-	} else if server.CDNName == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing CDNName")
-	} else if server.ID == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing ID")
-	} else if server.Profile == nil {
-		return tc.ATSConfigMetaData{}, errors.New("server missing Profile")
+	uriSignedDSes := []tc.DeliveryServiceName{}
+	for _, ds := range dses {
+		if ds.ID == nil {
+			warnings = append(warnings, "got delivery service with no id, skipping!")
+			continue
+		}
+		if ds.XMLID == nil {
+			warnings = append(warnings, "got delivery service with no xmlId (name), skipping!")
+			continue
+		}
+		if _, ok := dses[tc.DeliveryServiceName(*ds.XMLID)]; !ok {
+			continue // skip: this ds isn't assigned to this server, this is normal
+		}
+		if ds.SigningAlgorithm == nil || *ds.SigningAlgorithm != tc.SigningAlgorithmURISigning {
+			continue // not signed, so not in our list of signed dses to make config files for.
+		}
+		uriSignedDSes = append(uriSignedDSes, tc.DeliveryServiceName(*ds.XMLID))
 	}
 
-	if tmURL == "" {
-		log.Errorln("ats.GetConfigMetadata: global tm.url parameter missing or empty! Setting empty in meta config!")
-	}
+	return uriSignedDSes, warnings
+}
 
-	atsData := tc.ATSConfigMetaData{
-		Info: tc.ATSConfigMetaDataInfo{
-			ProfileID:         int(*server.ProfileID),
-			TOReverseProxyURL: tmReverseProxyURL,
-			TOURL:             tmURL,
-			ServerPort:        *server.TCPPort,
-			ServerName:        *server.HostName,
-			CDNID:             *server.CDNID,
-			CDNName:           *server.CDNName,
-			ServerID:          *server.ID,
-			ProfileName:       *server.Profile,
-		},
-		ConfigFiles: []tc.ATSConfigMetaDataConfigFile{},
-	}
+// filterConfigFileDSes returns the DSes that should have config files for the given server.
+// Returns the delivery services and any warnings.
+func filterConfigFileDSes(server *Server, deliveryServices []DeliveryService, deliveryServiceServers []tc.DeliveryServiceServer) (map[tc.DeliveryServiceName]DeliveryService, []string) {
+	warnings := []string{}
 
-	if locationParams["remap.config"].Location != "" {
-		configLocation := locationParams["remap.config"].Location
-		for _, ds := range uriSignedDSes {
-			cfgName := "uri_signing_" + string(ds) + ".config"
-			// If there's already a parameter for it, don't clobber it. The user may wish to override the location.
-			if _, ok := locationParams[cfgName]; !ok {
-				p := locationParams[cfgName]
-				p.FileNameOnDisk = cfgName
-				p.Location = configLocation
-				locationParams[cfgName] = p
+	dses := map[tc.DeliveryServiceName]DeliveryService{}
+
+	if tc.CacheTypeFromString(server.Type) != tc.CacheTypeMid {
+		dsIDs := map[int]struct{}{}
+		for _, ds := range deliveryServices {
+			if ds.ID == nil {
+				warnings = append(warnings, "got delivery service with no ID, skipping!")
+				continue
 			}
+			dsIDs[*ds.ID] = struct{}{}
 		}
-	}
 
-locationParamsFor:
-	for cfgFile, cfgParams := range locationParams {
-		if strings.HasSuffix(cfgFile, ".config") {
-			dsConfigFilePrefixes := []string{
-				"hdr_rw_mid_", // must come before hdr_rw_, to avoid thinking we have a "hdr_rw_" with a ds of "mid_x"
-				"hdr_rw_",
-				"regex_remap_",
-				"url_sig_",
-				"uri_signing_",
+		// TODO verify?
+		//		serverIDs := []int{server.ID}
+
+		dssMap := map[int]struct{}{}
+		for _, dss := range deliveryServiceServers {
+			if dss.Server == nil || dss.DeliveryService == nil {
+				warnings = append(warnings, "got deliveryservice-server with nil values, skipping!")
+				continue
 			}
-		prefixFor:
-			for _, prefix := range dsConfigFilePrefixes {
-				if strings.HasPrefix(cfgFile, prefix) {
-					dsName := strings.TrimSuffix(strings.TrimPrefix(cfgFile, prefix), ".config")
-					if _, ok := dses[tc.DeliveryServiceName(dsName)]; !ok {
-						log.Warnln("Server Profile had 'location' Parameter '" + cfgFile + "', but delivery Service '" + dsName + "' is not assigned to this Server! Not including in meta config!")
-						continue locationParamsFor
-					}
-					break prefixFor // if it has a prefix, don't check the next prefix. This is important for hdr_rw_mid_, which will match hdr_rw_ and result in a "ds name" of "mid_x" if we don't continue here.
-				}
+			if *dss.Server != *server.ID {
+				continue
+			}
+			if _, ok := dsIDs[*dss.DeliveryService]; !ok {
+				continue
 			}
+			dssMap[*dss.DeliveryService] = struct{}{}
 		}
 
-		atsCfg := tc.ATSConfigMetaDataConfigFile{
-			FileNameOnDisk: cfgParams.FileNameOnDisk,
-			Location:       cfgParams.Location,
+		for _, ds := range deliveryServices {
+			if ds.ID == nil {
+				warnings = append(warnings, "got deliveryservice with nil id, skipping!")
+				continue
+			}
+			if ds.XMLID == nil {
+				warnings = append(warnings, "got deliveryservice with nil xmlId (name), skipping!")
+				continue
+			}
+			if _, ok := dssMap[*ds.ID]; !ok && ds.Topology == nil {
+				continue
+			}
+			dses[tc.DeliveryServiceName(*ds.XMLID)] = ds
 		}
-
-		scope := getServerScope(cfgFile, server.Type, scopeParams)
-
-		if cfgParams.URL != "" {
-			// TODO this is legacy, from when a custom URL could be set via Parameters.
-			//      verify nobody is relying on it in a production system, and remove.
-			scope = tc.ATSConfigMetaDataConfigFileScopeCDNs
+	} else {
+		for _, ds := range deliveryServices {
+			if ds.ID == nil {
+				warnings = append(warnings, "got deliveryservice with nil id, skipping!")
+				continue
+			}
+			if ds.XMLID == nil {
+				warnings = append(warnings, "got deliveryservice with nil xmlId (name), skipping!")
+				continue
+			}
+			if ds.CDNID == nil || *ds.CDNID != *server.CDNID {
+				continue
+			}
+			dses[tc.DeliveryServiceName(*ds.XMLID)] = ds
 		}
+	}
+	return dses, warnings
+}
 
-		atsCfg.Scope = string(scope)
+// getTOURLAndReverseProxy returns the toURL and toReverseProxyURL if they exist, or empty strings if they don't.
+func getTOURLAndReverseProxy(globalParams []tc.Parameter) (string, string) {
+	toReverseProxyURL := ""
+	toURL := ""
+	for _, param := range globalParams {
+		if param.Name == "tm.rev_proxy.url" {
+			toReverseProxyURL = param.Value
+		} else if param.Name == "tm.url" {
+			toURL = param.Value
+		}
+		if toReverseProxyURL != "" && toURL != "" {
+			break
+		}
+	}
+	return toURL, toReverseProxyURL
+}
 
-		atsData.ConfigFiles = append(atsData.ConfigFiles, atsCfg)
+func getLocationParams(serverParams []tc.Parameter) map[string]configProfileParams {
+	locationParams := map[string]configProfileParams{}
+	for _, param := range serverParams {
+		if param.Name == "location" {
+			p := locationParams[param.ConfigFile]
+			p.Name = param.ConfigFile
+			p.Path = param.Value
+			locationParams[param.ConfigFile] = p
+		}
 	}
+	return locationParams
+}
 
-	return AddMetaObjConfigDir(atsData, configDir, server, tmURL, tmReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dses, cacheGroupArr, topologies)
+type configProfileParams struct {
+	Name string
+	Path string
 }
 
-func getServerScope(cfgFile string, serverType string, scopeParams map[string]string) tc.ATSConfigMetaDataConfigFileScope {
-	switch {
-	case cfgFile == "cache.config" && tc.CacheTypeFromString(serverType) == tc.CacheTypeMid:
-		return tc.ATSConfigMetaDataConfigFileScopeServers
-	default:
-		return getScope(cfgFile, scopeParams)
+// requiredFiles is a constant (because Go doesn't allow const slices).
+// Note these are not exhaustive. This is only used to error if these are missing.
+// The presence of these is no guarantee the location Parameters are complete and correct.
+func requiredFiles() []string {
+	return []string{
+		"cache.config",
+		"hosting.config",
+		"ip_allow.config",
+		"parent.config",
+		"plugin.config",
+		"records.config",
+		"remap.config",
+		"storage.config",
+		"volume.config",
 	}
 }
 
-const DefaultScope = tc.ATSConfigMetaDataConfigFileScopeServers
-
-// getScope returns the ATSConfigMetaDataConfigFileScope for the given config file, and potentially the given server. If the config is not a Server scope, i.e. was part of an endpoint which does not include a server name or id, the server may be nil.
-func getScope(cfgFile string, scopeParams map[string]string) tc.ATSConfigMetaDataConfigFileScope {
-	switch {
-	case cfgFile == "ip_allow.config",
-		cfgFile == "parent.config",
-		cfgFile == "hosting.config",
-		cfgFile == "packages",
-		cfgFile == "chkconfig",
-		cfgFile == "remap.config",
-		strings.HasPrefix(cfgFile, "to_ext_") && strings.HasSuffix(cfgFile, ".config"):
-		return tc.ATSConfigMetaDataConfigFileScopeServers
-	case cfgFile == "12M_facts",
-		cfgFile == "50-ats.rules",
-		cfgFile == "astats.config",
-		cfgFile == "cache.config",
-		cfgFile == "drop_qstring.config",
-		cfgFile == "logs_xml.config",
-		cfgFile == "logging.config",
-		cfgFile == "logging.yaml",
-		cfgFile == "plugin.config",
-		cfgFile == "records.config",
-		cfgFile == "storage.config",
-		cfgFile == "volume.config",
-		cfgFile == "sysctl.conf",
-		strings.HasPrefix(cfgFile, "url_sig_") && strings.HasSuffix(cfgFile, ".config"),
-		strings.HasPrefix(cfgFile, "uri_signing_") && strings.HasSuffix(cfgFile, ".config"):
-		return tc.ATSConfigMetaDataConfigFileScopeProfiles
-	case cfgFile == "bg_fetch.config",
-		cfgFile == "regex_revalidate.config",
-		cfgFile == SSLMultiCertConfigFileName,
-		strings.HasPrefix(cfgFile, "cacheurl") && strings.HasSuffix(cfgFile, ".config"),
-		strings.HasPrefix(cfgFile, "hdr_rw_") && strings.HasSuffix(cfgFile, ".config"),
-		strings.HasPrefix(cfgFile, "regex_remap_") && strings.HasSuffix(cfgFile, ".config"),
-		strings.HasPrefix(cfgFile, "set_dscp_") && strings.HasSuffix(cfgFile, ".config"):
-		return tc.ATSConfigMetaDataConfigFileScopeCDNs
+// ensureConfigFile ensures files contains the given fileName. If so, returns files unmodified.
+// If not, if configDir is empty, returns an error.
+// If not, and configDir is nonempty, creates the given file, configDir location, and returns files.
+func ensureConfigFile(files map[string][]CfgMeta, fileName string, configDir string) (map[string][]CfgMeta, error) {
+	if _, ok := files[fileName]; ok {
+		return files, nil
 	}
-
-	scope, ok := scopeParams[cfgFile]
-	if !ok {
-		scope = string(DefaultScope)
+	if configDir == "" {
+		return files, errors.New("required file '" + fileName + "' has no location Parameter, and ATS config directory not found.")
 	}
-	return tc.ATSConfigMetaDataConfigFileScope(scope)
+	files[fileName] = []CfgMeta{{
+		Name: fileName,
+		Path: configDir,
+	}}
+	return files, nil
 }
diff --git a/lib/go-atscfg/meta_test.go b/lib/go-atscfg/meta_test.go
index b61a695..70d791f 100644
--- a/lib/go-atscfg/meta_test.go
+++ b/lib/go-atscfg/meta_test.go
@@ -28,7 +28,7 @@ import (
 )
 
 func TestMakeMetaConfig(t *testing.T) {
-	server := &tc.ServerNullable{}
+	server := &Server{}
 	server.CachegroupID = util.IntPtr(42)
 	server.Cachegroup = util.StrPtr("cg0")
 	server.CDNName = util.StrPtr("mycdn")
@@ -48,274 +48,150 @@ func TestMakeMetaConfig(t *testing.T) {
 	// server.SecondaryParentCacheGroupType= "MID_LOC"
 	server.Type = "EDGE"
 
-	tmURL := "https://myto.invalid"
-	tmReverseProxyURL := "https://myrp.myto.invalid"
-	locationParams := map[string]ConfigProfileParams{
-		"regex_revalidate.config": ConfigProfileParams{
-			FileNameOnDisk: "regex_revalidate.config",
-			Location:       "/my/location/",
-			URL:            "http://myurl/remap.config", // cdn-scoped endpoint
-		},
-		"cache.config": ConfigProfileParams{
-			FileNameOnDisk: "cache.config", // cache.config on mids is server-scoped
-			Location:       "/my/location/",
-		},
-		"ip_allow.config": ConfigProfileParams{
-			FileNameOnDisk: "ip_allow.config",
-			Location:       "/my/location/",
-		},
-		"volume.config": ConfigProfileParams{
-			FileNameOnDisk: "volume.config",
-			Location:       "/my/location/",
-		},
-		"ssl_multicert.config": ConfigProfileParams{
-			FileNameOnDisk: "ssl_multicert.config",
-			Location:       "/my/location/",
-		},
-		"uri_signing_mydsname.config": ConfigProfileParams{
-			FileNameOnDisk: "uri_signing_mydsname.config",
-			Location:       "/my/location/",
-		},
-		"uri_signing_nonexistentds.config": ConfigProfileParams{
-			FileNameOnDisk: "uri_signing_nonexistentds.config",
-			Location:       "/my/location/",
-		},
-		"regex_remap_nonexistentds.config": ConfigProfileParams{
-			FileNameOnDisk: "regex_remap_nonexistentds.config",
-			Location:       "/my/location/",
-		},
-		"url_sig_nonexistentds.config": ConfigProfileParams{
-			FileNameOnDisk: "url_sig_nonexistentds.config",
-			Location:       "/my/location/",
-		},
-		"hdr_rw_nonexistentds.config": ConfigProfileParams{
-			FileNameOnDisk: "hdr_rw_nonexistentds.config",
-			Location:       "/my/location/",
-		},
-		"hdr_rw_mid_nonexistentds.config": ConfigProfileParams{
-			FileNameOnDisk: "hdr_rw_mid_nonexistentds.config",
-			Location:       "/my/location/",
-		},
-		"unknown.config": ConfigProfileParams{
-			FileNameOnDisk: "unknown.config",
-			Location:       "/my/location/",
-		},
-		"custom.config": ConfigProfileParams{
-			FileNameOnDisk: "custom.config",
-			Location:       "/my/location/",
-		},
-		"external.config": ConfigProfileParams{
-			FileNameOnDisk: "external.config",
-			Location:       "/my/location/",
-			URL:            "http://myurl/remap.config",
-		},
-	}
-	uriSignedDSes := []tc.DeliveryServiceName{"mydsname"}
-	dses := map[tc.DeliveryServiceName]tc.DeliveryServiceNullableV30{"mydsname": {}}
-
-	scopeParams := map[string]string{"custom.config": string(tc.ATSConfigMetaDataConfigFileScopeProfiles)}
+	// uriSignedDSes := []tc.DeliveryServiceName{"mydsname"}
+	// dses := map[tc.DeliveryServiceName]DeliveryService{"mydsname": {}}
 
 	cgs := []tc.CacheGroupNullable{}
 	topologies := []tc.Topology{}
 
 	cfgPath := "/etc/foo/trafficserver"
 
-	cfg, err := MakeMetaObj(server, tmURL, tmReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dses, cgs, topologies, cfgPath)
-	if err != nil {
-		t.Fatalf("MakeMetaObj: " + err.Error())
-	}
-
-	if cfg.Info.ProfileID != int(*server.ProfileID) {
-		t.Errorf("expected Info.ProfileID %v actual %v", server.ProfileID, cfg.Info.ProfileID)
-	}
-
-	if cfg.Info.TOReverseProxyURL != tmReverseProxyURL {
-		t.Errorf("expected Info.TOReverseProxyURL %v actual %v", tmReverseProxyURL, cfg.Info.TOReverseProxyURL)
-	}
-
-	if cfg.Info.TOURL != tmURL {
-		t.Errorf("expected Info.TOURL %v actual %v", tmURL, cfg.Info.TOURL)
-	}
-
-	if *server.TCPPort != cfg.Info.ServerPort {
-		t.Errorf("expected Info.ServerPort %v actual %v", server.TCPPort, cfg.Info.ServerPort)
-	}
+	deliveryServices := []DeliveryService{}
+	dss := []tc.DeliveryServiceServer{}
+	globalParams := []tc.Parameter{}
 
-	if *server.HostName != cfg.Info.ServerName {
-		t.Errorf("expected Info.ServerName %v actual %v", *server.HostName, cfg.Info.ServerName)
-	}
-
-	if cfg.Info.CDNID != *server.CDNID {
-		t.Errorf("expected Info.CDNID %v actual %v", *server.CDNID, cfg.Info.CDNID)
+	makeLocationParam := func(name string) tc.Parameter {
+		return tc.Parameter{
+			Name:       "location",
+			ConfigFile: name,
+			Value:      "/my/location/",
+			Profiles:   []byte(`["` + *server.Profile + `"]`),
+		}
 	}
 
-	if cfg.Info.CDNName != *server.CDNName {
-		t.Errorf("expected Info.CDNName %v actual %v", server.CDNName, cfg.Info.CDNName)
-	}
-	if cfg.Info.ServerID != *server.ID {
-		t.Errorf("expected Info.ServerID %v actual %v", *server.ID, cfg.Info.ServerID)
-	}
-	if cfg.Info.ProfileName != *server.Profile {
-		t.Errorf("expected Info.ProfileName %v actual %v", *server.Profile, cfg.Info.ProfileName)
+	serverParams := []tc.Parameter{
+		makeLocationParam("ssl_multicert.config"),
+		makeLocationParam("volume.config"),
+		makeLocationParam("ip_allow.config"),
+		makeLocationParam("cache.config"),
+		makeLocationParam("regex_revalidate.config"),
+		makeLocationParam("uri_signing_mydsname.config"),
+		makeLocationParam("uri_signing_nonexistentds.config"),
+		makeLocationParam("regex_remap_nonexistentds.config"),
+		makeLocationParam("url_sig_nonexistentds.config"),
+		makeLocationParam("hdr_rw_nonexistentds.config"),
+		makeLocationParam("hdr_rw_mid_nonexistentds.config"),
+		makeLocationParam("unknown.config"),
+		makeLocationParam("custom.config"),
+		makeLocationParam("external.config"),
+	}
+
+	cfg, _, err := MakeConfigFilesList(cfgPath, server, serverParams, deliveryServices, dss, globalParams, cgs, topologies)
+	if err != nil {
+		t.Fatalf("MakeConfigFilesList: " + err.Error())
 	}
 
-	expectedConfigs := map[string]func(cf tc.ATSConfigMetaDataConfigFile){
-		"cache.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+	expectedConfigs := map[string]func(cf CfgMeta){
+		"cache.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"ip_allow.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"ip_allow.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"volume.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"volume.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"ssl_multicert.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeCDNs); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"ssl_multicert.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"uri_signing_mydsname.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"uri_signing_mydsname.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"unknown.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"unknown.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"custom.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"custom.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"remap.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"remap.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"regex_revalidate.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeCDNs); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"regex_revalidate.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"external.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := "/my/location/"; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeCDNs); cf.Scope != expected {
-				t.Errorf("expected scope '%v', actual '%v'", expected, cf.Scope)
+		"external.config": func(cf CfgMeta) {
+			if expected := "/my/location/"; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"hosting.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cf.Scope != expected {
-				t.Errorf("expected scope for %v is '%v', actual '%v'", cf.FileNameOnDisk, expected, cf.Scope)
+		"hosting.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"parent.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cf.Scope != expected {
-				t.Errorf("expected scope for %v is '%v', actual '%v'", cf.FileNameOnDisk, expected, cf.Scope)
+		"parent.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"plugin.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope for %v is '%v', actual '%v'", cf.FileNameOnDisk, expected, cf.Scope)
+		"plugin.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"records.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope for %v is '%v', actual '%v'", cf.FileNameOnDisk, expected, cf.Scope)
+		"records.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
-		"storage.config": func(cf tc.ATSConfigMetaDataConfigFile) {
-			if expected := cfgPath; cf.Location != expected {
-				t.Errorf("expected location '%v', actual '%v'", expected, cf.Location)
-			}
-			if expected := string(tc.ATSConfigMetaDataConfigFileScopeProfiles); cf.Scope != expected {
-				t.Errorf("expected scope for %v is '%v', actual '%v'", cf.FileNameOnDisk, expected, cf.Scope)
+		"storage.config": func(cf CfgMeta) {
+			if expected := cfgPath; cf.Path != expected {
+				t.Errorf("expected location '%v', actual '%v'", expected, cf.Path)
 			}
 		},
 	}
 
-	for _, cfgFile := range cfg.ConfigFiles {
-		if testF, ok := expectedConfigs[cfgFile.FileNameOnDisk]; !ok {
-			t.Errorf("unexpected config '" + cfgFile.FileNameOnDisk + "'")
+	for _, cfgFile := range cfg {
+		if testF, ok := expectedConfigs[cfgFile.Name]; !ok {
+			t.Errorf("unexpected config '" + cfgFile.Name + "'")
 		} else {
 			testF(cfgFile)
-			delete(expectedConfigs, cfgFile.FileNameOnDisk)
+			delete(expectedConfigs, cfgFile.Name)
 		}
 	}
 
 	server.Type = "MID"
-	cfg, err = MakeMetaObj(server, tmURL, tmReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dses, cgs, topologies, cfgPath)
+	cfg, _, err = MakeConfigFilesList(cfgPath, server, serverParams, deliveryServices, dss, globalParams, cgs, topologies)
 	if err != nil {
-		t.Fatalf("MakeMetaObj: " + err.Error())
+		t.Fatalf("MakeConfigFilesList: " + err.Error())
 	}
-	for _, cfgFile := range cfg.ConfigFiles {
-		if cfgFile.FileNameOnDisk != "cache.config" {
+	for _, cfgFile := range cfg {
+		if cfgFile.Name != "cache.config" {
 			continue
 		}
-		if expected := string(tc.ATSConfigMetaDataConfigFileScopeServers); cfgFile.Scope != expected {
-			t.Errorf("expected cache.config on a Mid to be scope '%v', actual '%v'", expected, cfgFile.Scope)
-		}
 		break
 	}
-	for _, fi := range cfg.ConfigFiles {
-		if strings.Contains(fi.FileNameOnDisk, "nonexistentds") {
-			t.Errorf("expected location parameters for nonexistent delivery services to not be added to config, actual '%v'", fi.FileNameOnDisk)
-		}
-		if fi.FileNameOnDisk == "external.config" {
-			if fi.APIURI != "" {
-				t.Errorf("expected: apiUri field to be omitted for external.config, actual: present")
-			}
-			if fi.URL != "" {
-				t.Errorf("expected: url field to be present for external.config, actual: omitted")
-			}
+	for _, fi := range cfg {
+		if strings.Contains(fi.Name, "nonexistentds") {
+			t.Errorf("expected location parameters for nonexistent delivery services to not be added to config, actual '%v'", fi.Name)
 		}
 	}
 }
diff --git a/lib/go-atscfg/packages.go b/lib/go-atscfg/packages.go
index f148d8a..b7bf448 100644
--- a/lib/go-atscfg/packages.go
+++ b/lib/go-atscfg/packages.go
@@ -23,7 +23,7 @@ import (
 	"encoding/json"
 	"sort"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const PackagesFileName = `packages`
@@ -32,39 +32,48 @@ const PackagesParamConfigFile = `package`
 const ContentTypePackages = ContentTypeTextASCII
 const LineCommentPackages = ""
 
-type Package struct {
-	Name    string `json:"name"`
-	Version string `json:"version"`
-}
-
-type Packages []Package
-
-func (ps Packages) Len() int { return len(ps) }
-func (ps Packages) Less(i, j int) bool {
-	if ps[i].Name != ps[j].Name {
-		return ps[i].Name < ps[j].Name
-	}
-	return ps[i].Version < ps[j].Version
-}
-func (ps Packages) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] }
-
 // MakePackages returns the 'packages' ATS config file endpoint.
 // This is a JSON object, and should be served with an 'application/json' Content-Type.
 func MakePackages(
-	params map[string][]string, // map[name]value - config file should always be 'package'
-) string {
-	packages := []Package{}
+	serverParams []tc.Parameter,
+) (Cfg, error) {
+	warnings := []string{}
+
+	params := paramsToMultiMap(filterParams(serverParams, PackagesParamConfigFile, "", "", ""))
+
+	pkgs := []pkg{}
 	for name, versions := range params {
 		for _, version := range versions {
-			packages = append(packages, Package{Name: name, Version: version})
+			pkgs = append(pkgs, pkg{Name: name, Version: version})
 		}
 	}
-	sort.Sort(Packages(packages))
-	bts, err := json.Marshal(&packages)
+	sort.Sort(packages(pkgs))
+	bts, err := json.Marshal(&pkgs)
 	if err != nil {
 		// should never happen
-		log.Errorln("marshalling chkconfig NameVersions: " + err.Error())
-		bts = []byte("error encoding params to json, see Traffic Ops log for details")
+		return Cfg{}, makeErr(warnings, "marshalling chkconfig NameVersions: "+err.Error())
 	}
-	return string(bts)
+
+	return Cfg{
+		Text:        string(bts),
+		ContentType: ContentTypePackages,
+		LineComment: LineCommentPackages,
+		Warnings:    warnings,
+	}, nil
+}
+
+type pkg struct {
+	Name    string
+	Version string
+}
+
+type packages []pkg
+
+func (ps packages) Len() int { return len(ps) }
+func (ps packages) Less(i, j int) bool {
+	if ps[i].Name != ps[j].Name {
+		return ps[i].Name < ps[j].Name
+	}
+	return ps[i].Version < ps[j].Version
 }
+func (ps packages) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] }
diff --git a/lib/go-atscfg/packages_test.go b/lib/go-atscfg/packages_test.go
index 323f22f..0627bb7 100644
--- a/lib/go-atscfg/packages_test.go
+++ b/lib/go-atscfg/packages_test.go
@@ -29,10 +29,15 @@ func TestMakePackages(t *testing.T) {
 		"p0": []string{"p0v0", "p0v1"},
 		"1":  []string{"p1v0"},
 	}
+	paramData := makeParamsFromMapArr("serverProfile", LogsXMLFileName, params)
 
-	txt := MakePackages(params)
+	cfg, err := MakePackages(paramData)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	packages := []Package{}
+	packages := []pkg{}
 	if err := json.Unmarshal([]byte(txt), &packages); err != nil {
 		t.Fatalf("MakePackages expected a JSON array of objects, actual: " + err.Error())
 	}
diff --git a/lib/go-atscfg/parentdotconfig.go b/lib/go-atscfg/parentdotconfig.go
index 917e31b..99669d2 100644
--- a/lib/go-atscfg/parentdotconfig.go
+++ b/lib/go-atscfg/parentdotconfig.go
@@ -22,13 +22,13 @@ package atscfg
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"net/url"
 	"regexp"
 	"sort"
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -63,196 +63,13 @@ const ParentConfigCacheParamUseIP = "use_ip_address"
 const ParentConfigCacheParamRank = "rank"
 const ParentConfigCacheParamNotAParent = "not_a_parent"
 
-// TODO change, this is terrible practice, using a hard-coded key. What if there were a delivery service named "all_parents" (transliterated Perl)
-const DeliveryServicesAllParentsKey = "all_parents"
-
-type ParentConfigDS struct {
-	Name                 tc.DeliveryServiceName
-	QStringIgnore        tc.QStringIgnore
-	OriginFQDN           string
-	MultiSiteOrigin      bool
-	OriginShield         string
-	Type                 tc.DSType
-	QStringHandling      string
-	RequiredCapabilities map[ServerCapability]struct{}
-	Topology             string
-}
-
-type ParentConfigDSTopLevel struct {
-	ParentConfigDS
-	MSOAlgorithm                       string
-	MSOParentRetry                     string
-	MSOUnavailableServerRetryResponses string
-	MSOMaxSimpleRetries                string
-	MSOMaxUnavailableServerRetries     string
-}
-
-type ParentInfo struct {
-	Host            string
-	Port            int
-	Domain          string
-	Weight          string
-	UseIP           bool
-	Rank            int
-	IP              string
-	PrimaryParent   bool
-	SecondaryParent bool
-	Capabilities    map[ServerCapability]struct{}
-}
-
-func (p ParentInfo) Format() string {
-	host := ""
-	if p.UseIP {
-		host = p.IP
-	} else {
-		host = p.Host + "." + p.Domain
-	}
-	return host + ":" + strconv.Itoa(p.Port) + "|" + p.Weight + ";"
-}
-
 type OriginHost string
 type OriginFQDN string
 
-type ParentInfos map[OriginHost]ParentInfo
-
-type ParentInfoSortByRank []ParentInfo
-
-func (s ParentInfoSortByRank) Len() int      { return len(([]ParentInfo)(s)) }
-func (s ParentInfoSortByRank) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s ParentInfoSortByRank) Less(i, j int) bool {
-	if s[i].Rank != s[j].Rank {
-		return s[i].Rank < s[j].Rank
-	} else if s[i].Host != s[j].Host {
-		return s[i].Host < s[j].Host
-	} else if s[i].Domain != s[j].Domain {
-		return s[i].Domain < s[j].Domain
-	} else if s[i].Port != s[j].Port {
-		return s[i].Port < s[j].Port
-	}
-	return s[i].IP < s[j].IP
-}
-
-type ServerWithParams struct {
-	tc.ServerNullable
-	Params ProfileCache
-}
-
-type ServersWithParamsSortByRank []ServerWithParams
-
-func (ss ServersWithParamsSortByRank) Len() int      { return len(ss) }
-func (ss ServersWithParamsSortByRank) Swap(i, j int) { ss[i], ss[j] = ss[j], ss[i] }
-func (ss ServersWithParamsSortByRank) Less(i, j int) bool {
-	if ss[i].Params.Rank != ss[j].Params.Rank {
-		return ss[i].Params.Rank < ss[j].Params.Rank
-	}
-
-	if ss[i].HostName == nil {
-		if ss[j].HostName != nil {
-			return true
-		}
-	} else if ss[j].HostName == nil {
-		return false
-	} else if ss[i].HostName != ss[j].HostName {
-		return *ss[i].HostName < *ss[j].HostName
-	}
-
-	if ss[i].DomainName == nil {
-		if ss[j].DomainName != nil {
-			return true
-		}
-	} else if ss[j].DomainName == nil {
-		return false
-	} else if ss[i].DomainName != ss[j].DomainName {
-		return *ss[i].DomainName < *ss[j].DomainName
-	}
-
-	if ss[i].Params.Port != ss[j].Params.Port {
-		return ss[i].Params.Port < ss[j].Params.Port
-	}
-
-	iIP := GetServerIPAddress(&ss[i].ServerNullable)
-	jIP := GetServerIPAddress(&ss[j].ServerNullable)
-
-	if iIP == nil {
-		if jIP != nil {
-			return true
-		}
-	} else if jIP == nil {
-		return false
-	}
-	return bytes.Compare(iIP, jIP) <= 0
-}
-
-type ParentConfigDSTopLevelSortByName []ParentConfigDSTopLevel
-
-func (s ParentConfigDSTopLevelSortByName) Len() int      { return len(([]ParentConfigDSTopLevel)(s)) }
-func (s ParentConfigDSTopLevelSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s ParentConfigDSTopLevelSortByName) Less(i, j int) bool {
-	return strings.Compare(string(s[i].Name), string(s[j].Name)) < 0
-}
-
-type DSesSortByName []tc.DeliveryServiceNullableV30
-
-func (s DSesSortByName) Len() int      { return len(s) }
-func (s DSesSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s DSesSortByName) Less(i, j int) bool {
-	if s[i].XMLID == nil {
-		return true
-	}
-	if s[j].XMLID == nil {
-		return false
-	}
-	return *s[i].XMLID < *s[j].XMLID
-}
-
-type ProfileCache struct {
-	Weight     string
-	Port       int
-	UseIP      bool
-	Rank       int
-	NotAParent bool
-}
-
-func DefaultProfileCache() ProfileCache {
-	return ProfileCache{
-		Weight:     "0.999",
-		Port:       0,
-		UseIP:      false,
-		Rank:       1,
-		NotAParent: false,
-	}
-}
-
-// CGServer is the server table data needed when selecting the servers assigned to a cachegroup.
-type CGServer struct {
-	ServerID       ServerID
-	ServerHost     string
-	ServerIP       string
-	ServerPort     int
-	CacheGroupID   int
-	CacheGroupName string
-	Status         int
-	Type           int
-	ProfileID      ProfileID
-	ProfileName    string
-	CDN            int
-	TypeName       string
-	Domain         string
-	Capabilities   map[ServerCapability]struct{}
-}
-
-type OriginURI struct {
-	Scheme string
-	Host   string
-	Port   string
-}
-
 func MakeParentDotConfig(
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	dses []tc.DeliveryServiceNullableV30,
-	server *tc.ServerNullable,
-	servers []tc.ServerNullable,
+	dses []DeliveryService,
+	server *Server,
+	servers []Server,
 	topologies []tc.Topology,
 	tcServerParams []tc.Parameter,
 	tcParentConfigParams []tc.Parameter,
@@ -261,46 +78,49 @@ func MakeParentDotConfig(
 	cacheGroupArr []tc.CacheGroupNullable,
 	dss []tc.DeliveryServiceServer,
 	cdn *tc.CDN,
-) string {
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
 	if server.HostName == nil || *server.HostName == "" {
-		return "ERROR: server HostName missing"
+		return Cfg{}, makeErr(warnings, "server HostName missing")
 	} else if server.CDNName == nil || *server.CDNName == "" {
-		return "ERROR: server CDNName missing"
+		return Cfg{}, makeErr(warnings, "server CDNName missing")
 	} else if server.Cachegroup == nil || *server.Cachegroup == "" {
-		return "ERROR: server Cachegroup missing"
+		return Cfg{}, makeErr(warnings, "server Cachegroup missing")
 	} else if server.Profile == nil || *server.Profile == "" {
-		return "ERROR: server Profile missing"
+		return Cfg{}, makeErr(warnings, "server Profile missing")
 	} else if server.TCPPort == nil {
-		return "ERROR: server TCPPort missing"
+		return Cfg{}, makeErr(warnings, "server TCPPort missing")
 	}
 
-	atsMajorVer := getATSMajorVersion(tcServerParams)
+	atsMajorVer, verWarns := getATSMajorVersion(tcServerParams)
+	warnings = append(warnings, verWarns...)
 
-	cacheGroups, err := MakeCGMap(cacheGroupArr)
+	cacheGroups, err := makeCGMap(cacheGroupArr)
 	if err != nil {
-		return "ERROR: making CacheGroup map, config will be malformed! : " + err.Error()
+		return Cfg{}, makeErr(warnings, "making CacheGroup map: "+err.Error())
 	}
-	serverParentCGData, err := GetParentCacheGroupData(server, cacheGroups)
+	serverParentCGData, err := getParentCacheGroupData(server, cacheGroups)
 	if err != nil {
-		log.Errorln("making parent.config, getting server parent cachegroup data, config will be malformed! : " + err.Error())
+		return Cfg{}, makeErr(warnings, "getting server parent cachegroup data: "+err.Error())
 	}
-	isTopLevelCache := IsTopLevelCache(serverParentCGData)
+	cacheIsTopLevel := isTopLevelCache(serverParentCGData)
 	serverCDNDomain := cdn.DomainName
 
-	sort.Sort(DSesSortByName(dses))
+	sort.Sort(dsesSortByName(dses))
 
-	nameVersionStr := GetNameVersionStringFromToolNameAndURL(toToolName, toURL)
-	hdr := HeaderCommentWithTOVersionStr(*server.HostName, nameVersionStr)
+	hdr := makeHdrComment(hdrComment)
 
 	textArr := []string{}
 	processedOriginsToDSNames := map[string]tc.DeliveryServiceName{}
 
-	parentConfigParamsWithProfiles, err := TCParamsToParamsWithProfiles(tcParentConfigParams)
+	parentConfigParamsWithProfiles, err := tcParamsToParamsWithProfiles(tcParentConfigParams)
 	if err != nil {
-		log.Errorln("parent.config generation: error getting profiles from Traffic Ops Parameters, Parameters will not be considered for generation! : " + err.Error())
-		parentConfigParamsWithProfiles = []ParameterWithProfiles{}
+		warnings = append(warnings, "error getting profiles from Traffic Ops Parameters, Parameters will not be considered for generation! : "+err.Error())
+		parentConfigParamsWithProfiles = []parameterWithProfiles{}
 	}
-	parentConfigParams := ParameterWithProfilesToMap(parentConfigParamsWithProfiles)
+	parentConfigParams := parameterWithProfilesToMap(parentConfigParamsWithProfiles)
 
 	// this is an optimization, to avoid looping over all params, for every DS. Instead, we loop over all params only once, and put them in a profile map.
 	profileParentConfigParams := map[string]map[string]string{} // map[profileName][paramName]paramVal
@@ -326,14 +146,13 @@ func MakeParentDotConfig(
 	}
 
 	parentCacheGroups := map[string]struct{}{}
-	if isTopLevelCache {
-		log.Infoln("This cache Is Top Level!")
+	if cacheIsTopLevel {
 		for _, cg := range cacheGroups {
 			if cg.Type == nil {
-				return "ERROR: cachegroup type is nil!"
+				return Cfg{}, makeErr(warnings, "cachegroup type is nil!")
 			}
 			if cg.Name == nil {
-				return "ERROR: cachegroup name is nil!"
+				return Cfg{}, makeErr(warnings, "cachegroup name is nil!")
 			}
 
 			if *cg.Type != tc.CacheGroupOriginTypeName {
@@ -344,10 +163,10 @@ func MakeParentDotConfig(
 	} else {
 		for _, cg := range cacheGroups {
 			if cg.Type == nil {
-				return "ERROR: cachegroup type is nil!"
+				return Cfg{}, makeErr(warnings, "cachegroup type is nil!")
 			}
 			if cg.Name == nil {
-				return "ERROR: cachegroup type is nil!"
+				return Cfg{}, makeErr(warnings, "cachegroup name is nil!")
 			}
 
 			if *cg.Name == *server.Cachegroup {
@@ -362,24 +181,24 @@ func MakeParentDotConfig(
 		}
 	}
 
-	nameTopologies := MakeTopologyNameMap(topologies)
+	nameTopologies := makeTopologyNameMap(topologies)
 
-	cgServers := map[int]tc.ServerNullable{} // map[serverID]server
+	cgServers := map[int]Server{} // map[serverID]server
 	for _, sv := range servers {
 		if sv.ID == nil {
-			log.Errorln("parent.config generation: TO servers had server with missing ID, skipping!")
+			warnings = append(warnings, "TO servers had server with missing ID, skipping!")
 			continue
 		} else if sv.CDNName == nil {
-			log.Errorln("parent.config generation: TO servers had server with missing CDNName, skipping!")
+			warnings = append(warnings, "TO servers had server with missing CDNName, skipping!")
 			continue
 		} else if sv.Cachegroup == nil || *sv.Cachegroup == "" {
-			log.Errorln("parent.config generation: TO servers had server with missing Cachegroup, skipping!")
+			warnings = append(warnings, "TO servers had server with missing Cachegroup, skipping!")
 			continue
 		} else if sv.Status == nil || *sv.Status == "" {
-			log.Errorln("parent.config generation: TO servers had server with missing Status, skipping!")
+			warnings = append(warnings, "TO servers had server with missing Status, skipping!")
 			continue
 		} else if sv.Type == "" {
-			log.Errorln("parent.config generation: TO servers had server with missing Type, skipping!")
+			warnings = append(warnings, "TO servers had server with missing Type, skipping!")
 			continue
 		}
 		if *sv.CDNName != *server.CDNName {
@@ -405,11 +224,11 @@ func MakeParentDotConfig(
 	}
 	cgServerIDs[*server.ID] = struct{}{}
 
-	cgDSServers := FilterDSS(dss, nil, cgServerIDs)
+	cgDSServers := filterDSS(dss, nil, cgServerIDs)
 	parentServerDSes := map[int]map[int]struct{}{} // map[serverID][dsID]
 	for _, dss := range cgDSServers {
 		if dss.Server == nil || dss.DeliveryService == nil {
-			return "ERROR: getting parent.config cachegroup parent server delivery service servers: got dss with nil members!"
+			return Cfg{}, makeErr(warnings, "getting cachegroup parent server delivery service servers: got dss with nil members!")
 		}
 		if parentServerDSes[*dss.Server] == nil {
 			parentServerDSes[*dss.Server] = map[int]struct{}{}
@@ -417,28 +236,30 @@ func MakeParentDotConfig(
 		parentServerDSes[*dss.Server][*dss.DeliveryService] = struct{}{}
 	}
 
-	originServers, profileCaches, err := GetOriginServersAndProfileCaches(cgServers, parentServerDSes, profileParentConfigParams, dses, serverCapabilities, dsRequiredCapabilities)
+	originServers, profileCaches, orgProfWarns, err := getOriginServersAndProfileCaches(cgServers, parentServerDSes, profileParentConfigParams, dses, serverCapabilities, dsRequiredCapabilities)
+	warnings = append(warnings, orgProfWarns...)
 	if err != nil {
-		return "ERROR getting origin servers and profile caches: " + err.Error()
+		return Cfg{}, makeErr(warnings, "getting origin servers and profile caches: "+err.Error())
 	}
 
-	parentInfos := MakeParentInfo(serverParentCGData, serverCDNDomain, profileCaches, originServers)
+	parentInfos := makeParentInfo(serverParentCGData, serverCDNDomain, profileCaches, originServers)
 
-	dsOrigins := makeDSOrigins(dss, dses, servers)
+	dsOrigins, dsOriginWarns := makeDSOrigins(dss, dses, servers)
+	warnings = append(warnings, dsOriginWarns...)
 
 	for _, ds := range dses {
 		if ds.XMLID == nil || *ds.XMLID == "" {
-			log.Errorln("parent.config got ds with missing XMLID, skipping!")
+			warnings = append(warnings, "got ds with missing XMLID, skipping!")
 			continue
 		} else if ds.ID == nil {
-			log.Errorln("parent.config got ds with missing ID, skipping!")
+			warnings = append(warnings, "got ds with missing ID, skipping!")
 			continue
 		} else if ds.Type == nil {
-			log.Errorln("parent.config got ds with missing Type, skipping!")
+			warnings = append(warnings, "got ds with missing Type, skipping!")
 			continue
 		}
 
-		if !isTopLevelCache && ds.Topology == nil {
+		if !cacheIsTopLevel && ds.Topology == nil {
 			if _, ok := parentServerDSes[*server.ID][*ds.ID]; !ok {
 				continue // skip DSes not assigned to this server.
 			}
@@ -448,26 +269,24 @@ func MakeParentDotConfig(
 			continue // skip ANY_MAP, STEERING, etc
 		}
 		if ds.OrgServerFQDN == nil || *ds.OrgServerFQDN == "" {
-			// this check needs to be after the HTTP|DNS check, because Steering DSes without origins are ok
-			log.Errorln("ds  '" + *ds.XMLID + "' has no origin server! Skipping!")
+			// this check needs to be after the HTTP|DNS check, because Steering DSes without origins are ok'
+			warnings = append(warnings, "ds  '"+*ds.XMLID+"' has no origin server! Skipping!")
 			continue
 		}
 
 		// Note these Parameters are only used for MSO for legacy DeliveryServiceServers DeliveryServices (except QueryStringHandling which is used by all DeliveryServices).
 		//      Topology DSes use them for all DSes, MSO and non-MSO.
-		dsParams := getParentDSParams(ds, profileParentConfigParams)
-
-		log.Infoln("parent.config processing ds '" + *ds.XMLID + "'")
+		dsParams, dsParamsWarnings := getParentDSParams(ds, profileParentConfigParams)
+		warnings = append(warnings, dsParamsWarnings...)
 
 		if existingDS, ok := processedOriginsToDSNames[*ds.OrgServerFQDN]; ok {
-			log.Errorln("parent.config generation: duplicate origin! services '" + *ds.XMLID + "' and '" + string(existingDS) + "' share origin '" + *ds.OrgServerFQDN + "': skipping '" + *ds.XMLID + "'!")
+			warnings = append(warnings, "duplicate origin! services '"+*ds.XMLID+"' and '"+string(existingDS)+"' share origin '"+*ds.OrgServerFQDN+"': skipping '"+*ds.XMLID+"'!")
 			continue
 		}
 
 		// TODO put these in separate functions. No if-statement should be this long.
 		if ds.Topology != nil && *ds.Topology != "" {
-			log.Infoln("parent.config generating Topology line for ds '" + *ds.XMLID + "'")
-			txt, err := GetTopologyParentConfigLine(
+			txt, topoWarnings, err := getTopologyParentConfigLine(
 				server,
 				servers,
 				&ds,
@@ -481,24 +300,26 @@ func MakeParentDotConfig(
 				atsMajorVer,
 				dsOrigins[DeliveryServiceID(*ds.ID)],
 			)
+			warnings = append(warnings, topoWarnings...)
 			if err != nil {
-				log.Errorln(err)
+				// we don't want to fail generation with an error if one ds is malformed
+				warnings = append(warnings, err.Error()) // GetTopologyParentConfigLine includes error context
 				continue
 			}
 
 			if txt != "" { // will be empty with no error if this server isn't in the Topology, or if it doesn't have the Required Capabilities
 				textArr = append(textArr, txt)
 			}
-		} else if IsTopLevelCache(serverParentCGData) {
-			log.Infoln("parent.config generating top level line for ds '" + *ds.XMLID + "'")
+		} else if isTopLevelCache(serverParentCGData) {
 			parentQStr := "ignore"
 			if dsParams.QueryStringHandling == "" && dsParams.Algorithm == tc.AlgorithmConsistentHash && ds.QStringIgnore != nil && tc.QStringIgnore(*ds.QStringIgnore) == tc.QStringIgnoreUseInCacheKeyAndPassUp {
 				parentQStr = "consider"
 			}
 
-			orgURI, err := GetOriginURI(*ds.OrgServerFQDN)
+			orgURI, orgWarns, err := getOriginURI(*ds.OrgServerFQDN)
+			warnings = append(warnings, orgWarns...)
 			if err != nil {
-				log.Errorln("Malformed ds '" + *ds.XMLID + "' origin  URI: '" + *ds.OrgServerFQDN + "': skipping!" + err.Error())
+				warnings = append(warnings, "malformed ds '"+*ds.XMLID+"' origin  URI: '"+*ds.OrgServerFQDN+"': skipping!"+err.Error())
 				continue
 			}
 
@@ -517,28 +338,30 @@ func MakeParentDotConfig(
 
 				if len(parentInfos[OriginHost(orgURI.Hostname())]) == 0 {
 					// TODO error? emulates Perl
-					log.Warnln("ParentInfo: delivery service " + *ds.XMLID + " has no parent servers")
+					warnings = append(warnings, "delivery service "+*ds.XMLID+" has no parent servers")
 				}
 
-				parents, secondaryParents := getMSOParentStrs(&ds, parentInfos[OriginHost(orgURI.Hostname())], atsMajorVer, dsRequiredCapabilities, dsParams.Algorithm, dsParams.TryAllPrimariesBeforeSecondary)
+				parents, secondaryParents, parentWarns := getMSOParentStrs(&ds, parentInfos[OriginHost(orgURI.Hostname())], atsMajorVer, dsRequiredCapabilities, dsParams.Algorithm, dsParams.TryAllPrimariesBeforeSecondary)
+				warnings = append(warnings, parentWarns...)
 				textLine += parents + secondaryParents + ` round_robin=` + dsParams.Algorithm + ` qstring=` + parentQStr + ` go_direct=false parent_is_proxy=false`
 				textLine += getParentRetryStr(true, atsMajorVer, dsParams.ParentRetry, dsParams.UnavailableServerRetryResponses, dsParams.MaxSimpleRetries, dsParams.MaxUnavailableServerRetries)
 				textLine += "\n" // TODO remove, and join later on "\n" instead of ""?
 				textArr = append(textArr, textLine)
 			}
 		} else {
-			log.Infoln("parent.config generating non-top level line for ds '" + *ds.XMLID + "'")
 			queryStringHandling := serverParams[ParentConfigParamQStringHandling] // "qsh" in Perl
 
 			roundRobin := `round_robin=consistent_hash`
 			goDirect := `go_direct=false`
 
-			parents, secondaryParents := getParentStrs(&ds, dsRequiredCapabilities, parentInfos[DeliveryServicesAllParentsKey], atsMajorVer, dsParams.TryAllPrimariesBeforeSecondary)
+			parents, secondaryParents, parentWarns := getParentStrs(&ds, dsRequiredCapabilities, parentInfos[deliveryServicesAllParentsKey], atsMajorVer, dsParams.TryAllPrimariesBeforeSecondary)
+			warnings = append(warnings, parentWarns...)
 
 			text := ""
-			orgURI, err := GetOriginURI(*ds.OrgServerFQDN)
+			orgURI, orgWarns, err := getOriginURI(*ds.OrgServerFQDN)
+			warnings = append(warnings, orgWarns...)
 			if err != nil {
-				log.Errorln("Malformed ds '" + *ds.XMLID + "' origin  URI: '" + *ds.OrgServerFQDN + "': skipping!" + err.Error())
+				warnings = append(warnings, "malformed ds '"+*ds.XMLID+"' origin  URI: '"+*ds.OrgServerFQDN+"': skipping!"+err.Error())
 				continue
 			}
 
@@ -576,11 +399,12 @@ func MakeParentDotConfig(
 
 	// TODO determine if this is necessary. It's super-dangerous, and moreover ignores Server Capabilitites.
 	defaultDestText := ""
-	if !IsTopLevelCache(serverParentCGData) {
-		invalidDS := &tc.DeliveryServiceNullableV30{}
+	if !isTopLevelCache(serverParentCGData) {
+		invalidDS := &DeliveryService{}
 		invalidDS.ID = util.IntPtr(-1)
 		tryAllPrimariesBeforeSecondary := false
-		parents, secondaryParents := getParentStrs(invalidDS, dsRequiredCapabilities, parentInfos[DeliveryServicesAllParentsKey], atsMajorVer, tryAllPrimariesBeforeSecondary)
+		parents, secondaryParents, parentWarns := getParentStrs(invalidDS, dsRequiredCapabilities, parentInfos[deliveryServicesAllParentsKey], atsMajorVer, tryAllPrimariesBeforeSecondary)
+		warnings = append(warnings, parentWarns...)
 		defaultDestText = `dest_domain=. ` + parents
 		if serverParams[ParentConfigParamAlgorithm] == tc.AlgorithmConsistentHash {
 			defaultDestText += secondaryParents
@@ -595,10 +419,196 @@ func MakeParentDotConfig(
 
 	sort.Sort(sort.StringSlice(textArr))
 	text := hdr + strings.Join(textArr, "") + defaultDestText
-	return text
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeParentDotConfig,
+		LineComment: LineCommentParentDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type parentConfigDS struct {
+	Name                 tc.DeliveryServiceName
+	QStringIgnore        tc.QStringIgnore
+	OriginFQDN           string
+	MultiSiteOrigin      bool
+	OriginShield         string
+	Type                 tc.DSType
+	QStringHandling      string
+	RequiredCapabilities map[ServerCapability]struct{}
+	Topology             string
+}
+
+type parentConfigDSTopLevel struct {
+	parentConfigDS
+	MSOAlgorithm                       string
+	MSOParentRetry                     string
+	MSOUnavailableServerRetryResponses string
+	MSOMaxSimpleRetries                string
+	MSOMaxUnavailableServerRetries     string
+}
+
+type parentInfo struct {
+	Host            string
+	Port            int
+	Domain          string
+	Weight          string
+	UseIP           bool
+	Rank            int
+	IP              string
+	PrimaryParent   bool
+	SecondaryParent bool
+	Capabilities    map[ServerCapability]struct{}
+}
+
+func (p parentInfo) Format() string {
+	host := ""
+	if p.UseIP {
+		host = p.IP
+	} else {
+		host = p.Host + "." + p.Domain
+	}
+	return host + ":" + strconv.Itoa(p.Port) + "|" + p.Weight + ";"
 }
 
-type ParentDSParams struct {
+type parentInfos map[OriginHost]parentInfo
+
+type parentInfoSortByRank []parentInfo
+
+func (s parentInfoSortByRank) Len() int      { return len(s) }
+func (s parentInfoSortByRank) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s parentInfoSortByRank) Less(i, j int) bool {
+	if s[i].Rank != s[j].Rank {
+		return s[i].Rank < s[j].Rank
+	} else if s[i].Host != s[j].Host {
+		return s[i].Host < s[j].Host
+	} else if s[i].Domain != s[j].Domain {
+		return s[i].Domain < s[j].Domain
+	} else if s[i].Port != s[j].Port {
+		return s[i].Port < s[j].Port
+	}
+	return s[i].IP < s[j].IP
+}
+
+type serverWithParams struct {
+	Server
+	Params profileCache
+}
+
+type serversWithParamsSortByRank []serverWithParams
+
+func (ss serversWithParamsSortByRank) Len() int      { return len(ss) }
+func (ss serversWithParamsSortByRank) Swap(i, j int) { ss[i], ss[j] = ss[j], ss[i] }
+func (ss serversWithParamsSortByRank) Less(i, j int) bool {
+	if ss[i].Params.Rank != ss[j].Params.Rank {
+		return ss[i].Params.Rank < ss[j].Params.Rank
+	}
+
+	if ss[i].HostName == nil {
+		if ss[j].HostName != nil {
+			return true
+		}
+	} else if ss[j].HostName == nil {
+		return false
+	} else if ss[i].HostName != ss[j].HostName {
+		return *ss[i].HostName < *ss[j].HostName
+	}
+
+	if ss[i].DomainName == nil {
+		if ss[j].DomainName != nil {
+			return true
+		}
+	} else if ss[j].DomainName == nil {
+		return false
+	} else if ss[i].DomainName != ss[j].DomainName {
+		return *ss[i].DomainName < *ss[j].DomainName
+	}
+
+	if ss[i].Params.Port != ss[j].Params.Port {
+		return ss[i].Params.Port < ss[j].Params.Port
+	}
+
+	iIP := getServerIPAddress(&ss[i].Server)
+	jIP := getServerIPAddress(&ss[j].Server)
+
+	if iIP == nil {
+		if jIP != nil {
+			return true
+		}
+	} else if jIP == nil {
+		return false
+	}
+	return bytes.Compare(iIP, jIP) <= 0
+}
+
+type parentConfigDSTopLevelSortByName []parentConfigDSTopLevel
+
+func (s parentConfigDSTopLevelSortByName) Len() int      { return len(s) }
+func (s parentConfigDSTopLevelSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s parentConfigDSTopLevelSortByName) Less(i, j int) bool {
+	return strings.Compare(string(s[i].Name), string(s[j].Name)) < 0
+}
+
+type dsesSortByName []DeliveryService
+
+func (s dsesSortByName) Len() int      { return len(s) }
+func (s dsesSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s dsesSortByName) Less(i, j int) bool {
+	if s[i].XMLID == nil {
+		return true
+	}
+	if s[j].XMLID == nil {
+		return false
+	}
+	return *s[i].XMLID < *s[j].XMLID
+}
+
+type profileCache struct {
+	Weight     string
+	Port       int
+	UseIP      bool
+	Rank       int
+	NotAParent bool
+}
+
+func defaultProfileCache() profileCache {
+	return profileCache{
+		Weight:     "0.999",
+		Port:       0,
+		UseIP:      false,
+		Rank:       1,
+		NotAParent: false,
+	}
+}
+
+// cgServer is the server table data needed when selecting the servers assigned to a cachegroup.
+type cgServer struct {
+	ServerID       ServerID
+	ServerHost     string
+	ServerIP       string
+	ServerPort     int
+	CacheGroupID   int
+	CacheGroupName string
+	Status         int
+	Type           int
+	ProfileID      ProfileID
+	ProfileName    string
+	CDN            int
+	TypeName       string
+	Domain         string
+	Capabilities   map[ServerCapability]struct{}
+}
+
+type originURI struct {
+	Scheme string
+	Host   string
+	Port   string
+}
+
+// TODO change, this is terrible practice, using a hard-coded key. What if there were a delivery service named "all_parents" (transliterated Perl)
+const deliveryServicesAllParentsKey = "all_parents"
+
+type parentDSParams struct {
 	Algorithm                       string
 	ParentRetry                     string
 	UnavailableServerRetryResponses string
@@ -608,12 +618,13 @@ type ParentDSParams struct {
 	TryAllPrimariesBeforeSecondary  bool
 }
 
-// getDSParams returns the Delivery Service Profile Parameters used in parent.config.
+// getDSParams returns the Delivery Service Profile Parameters used in parent.config, and any warnings.
 // If Parameters don't exist, defaults are returned. Non-MSO Delivery Services default to no custom retry logic (we should reevaluate that).
 // Note these Parameters are only used for MSO for legacy DeliveryServiceServers DeliveryServices.
 //      Topology DSes use them for all DSes, MSO and non-MSO.
-func getParentDSParams(ds tc.DeliveryServiceNullableV30, profileParentConfigParams map[string]map[string]string) ParentDSParams {
-	params := ParentDSParams{}
+func getParentDSParams(ds DeliveryService, profileParentConfigParams map[string]map[string]string) (parentDSParams, []string) {
+	warnings := []string{}
+	params := parentDSParams{}
 	isMSO := ds.MultiSiteOrigin != nil && *ds.MultiSiteOrigin
 	if isMSO {
 		params.Algorithm = ParentConfigDSParamDefaultMSOAlgorithm
@@ -623,11 +634,11 @@ func getParentDSParams(ds tc.DeliveryServiceNullableV30, profileParentConfigPara
 		params.MaxUnavailableServerRetries = ParentConfigDSParamDefaultMaxUnavailableServerRetries
 	}
 	if ds.ProfileName == nil || *ds.ProfileName == "" {
-		return params
+		return params, warnings
 	}
 	dsParams, ok := profileParentConfigParams[*ds.ProfileName]
 	if !ok {
-		return params
+		return params, warnings
 	}
 
 	params.QueryStringHandling = dsParams[ParentConfigParamQStringHandling] // may be blank, no default
@@ -641,7 +652,7 @@ func getParentDSParams(ds tc.DeliveryServiceNullableV30, profileParentConfigPara
 		}
 		if v, ok := dsParams[ParentConfigParamMSOUnavailableServerRetryResponses]; ok {
 			if v != "" && !unavailableServerRetryResponsesValid(v) {
-				log.Errorln("Malformed " + ParentConfigParamMSOUnavailableServerRetryResponses + " parameter '" + v + "', not using!")
+				warnings = append(warnings, "malformed "+ParentConfigParamMSOUnavailableServerRetryResponses+" parameter '"+v+"', not using!")
 			} else if v != "" {
 				params.UnavailableServerRetryResponses = v
 			}
@@ -663,7 +674,7 @@ func getParentDSParams(ds tc.DeliveryServiceNullableV30, profileParentConfigPara
 	}
 	if v, ok := dsParams[ParentConfigParamUnavailableServerRetryResponses]; ok {
 		if v != "" && !unavailableServerRetryResponsesValid(v) {
-			log.Errorln("Malformed " + ParentConfigParamUnavailableServerRetryResponses + " parameter '" + v + "', not using!")
+			warnings = append(warnings, "malformed "+ParentConfigParamUnavailableServerRetryResponses+" parameter '"+v+"', not using!")
 		} else if v != "" {
 			params.UnavailableServerRetryResponses = v
 		}
@@ -676,64 +687,74 @@ func getParentDSParams(ds tc.DeliveryServiceNullableV30, profileParentConfigPara
 	}
 	if v, ok := dsParams[ParentConfigParamSecondaryMode]; ok {
 		if v != "" {
-			log.Errorln("parent.config generation: DS '" + *ds.XMLID + "' had Parameter " + ParentConfigParamSecondaryMode + " which is used if it exists, the value is ignored! Non-empty value '" + v + "' will be ignored!")
+			warnings = append(warnings, "DS '"+*ds.XMLID+"' had Parameter "+ParentConfigParamSecondaryMode+" which is used if it exists, the value is ignored! Non-empty value '"+v+"' will be ignored!")
 		}
 		params.TryAllPrimariesBeforeSecondary = true
 	}
 
-	return params
+	return params, warnings
 }
 
-func GetTopologyParentConfigLine(
-	server *tc.ServerNullable,
-	servers []tc.ServerNullable,
-	ds *tc.DeliveryServiceNullableV30,
+// GetTopologyParentConfigLine returns the topology parent.config line, any warnings, and any error
+func getTopologyParentConfigLine(
+	server *Server,
+	servers []Server,
+	ds *DeliveryService,
 	serverParams map[string]string,
-	parentConfigParams []ParameterWithProfilesMap, // all params with configFile parent.config
+	parentConfigParams []parameterWithProfilesMap, // all params with configFile parent.config
 	nameTopologies map[TopologyName]tc.Topology,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
 	cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable,
-	dsParams ParentDSParams,
+	dsParams parentDSParams,
 	atsMajorVer int,
 	dsOrigins map[ServerID]struct{},
-) (string, error) {
+) (string, []string, error) {
+	warnings := []string{}
 	txt := ""
 
-	if !HasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
-		return "", nil
+	if !hasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
+		return "", warnings, nil
 	}
 
-	orgURI, err := GetOriginURI(*ds.OrgServerFQDN)
+	orgURI, orgWarns, err := getOriginURI(*ds.OrgServerFQDN)
+	warnings = append(warnings, orgWarns...)
 	if err != nil {
-		return "", errors.New("Malformed ds '" + *ds.XMLID + "' origin  URI: '" + *ds.OrgServerFQDN + "': skipping!" + err.Error())
+		return "", warnings, errors.New("Malformed ds '" + *ds.XMLID + "' origin  URI: '" + *ds.OrgServerFQDN + "': skipping!" + err.Error())
 	}
 
 	topology := nameTopologies[TopologyName(*ds.Topology)]
 	if topology.Name == "" {
-		return "", errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' not found in Topologies!")
+		return "", warnings, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' not found in Topologies!")
 	}
 
 	txt += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port()
 
-	serverPlacement := getTopologyPlacement(tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups, ds)
+	serverPlacement, err := getTopologyPlacement(tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups, ds)
+	if err != nil {
+		return "", warnings, errors.New("getting topology placement: " + err.Error())
+	}
 	if !serverPlacement.InTopology {
-		return "", nil // server isn't in topology, no error
+		return "", warnings, nil // server isn't in topology, no error
 	}
 	// TODO add Topology/Capabilities to remap.config
 
-	parents, secondaryParents, err := GetTopologyParents(server, ds, servers, parentConfigParams, topology, serverPlacement.IsLastTier, serverCapabilities, dsRequiredCapabilities, dsOrigins)
+	parents, secondaryParents, parentWarnings, err := getTopologyParents(server, ds, servers, parentConfigParams, topology, serverPlacement.IsLastTier, serverCapabilities, dsRequiredCapabilities, dsOrigins)
+	warnings = append(warnings, parentWarnings...)
 	if err != nil {
-		return "", errors.New("getting topology parents for '" + *ds.XMLID + "': skipping! " + err.Error())
+		return "", warnings, errors.New("getting topology parents for '" + *ds.XMLID + "': skipping! " + err.Error())
 	}
 	if len(parents) == 0 {
-		return "", errors.New("getting topology parents for '" + *ds.XMLID + "': no parents found! skipping! (Does your Topology have a CacheGroup with no servers in it?)")
+		return "", warnings, errors.New("getting topology parents for '" + *ds.XMLID + "': no parents found! skipping! (Does your Topology have a CacheGroup with no servers in it?)")
 	}
 
 	txt += ` parent="` + strings.Join(parents, `;`) + `"`
 	if len(secondaryParents) > 0 {
 		txt += ` secondary_parent="` + strings.Join(secondaryParents, `;`) + `"`
-		txt += getSecondaryModeStr(dsParams.TryAllPrimariesBeforeSecondary, atsMajorVer, tc.DeliveryServiceName(*ds.XMLID))
+
+		secondaryModeStr, secondaryModeWarnings := getSecondaryModeStr(dsParams.TryAllPrimariesBeforeSecondary, atsMajorVer, tc.DeliveryServiceName(*ds.XMLID))
+		warnings = append(warnings, secondaryModeWarnings...)
+		txt += secondaryModeStr
 	}
 	txt += ` round_robin=` + getTopologyRoundRobin(ds, serverParams, serverPlacement.IsLastCacheTier, dsParams.Algorithm)
 	txt += ` go_direct=` + getTopologyGoDirect(ds, serverPlacement.IsLastTier)
@@ -742,7 +763,7 @@ func GetTopologyParentConfigLine(
 	txt += getParentRetryStr(serverPlacement.IsLastCacheTier, atsMajorVer, dsParams.ParentRetry, dsParams.UnavailableServerRetryResponses, dsParams.MaxSimpleRetries, dsParams.MaxUnavailableServerRetries)
 	txt += " # topology '" + *ds.Topology + "'"
 	txt += "\n"
-	return txt, nil
+	return txt, warnings, nil
 }
 
 // getParentRetryStr builds the parent retry directive(s).
@@ -774,15 +795,17 @@ func getParentRetryStr(isLastCacheTier bool, atsMajorVer int, parentRetry string
 	return txt
 }
 
-func getSecondaryModeStr(tryAllPrimariesBeforeSecondary bool, atsMajorVer int, ds tc.DeliveryServiceName) string {
+// getSecondaryModeStr returns the secondary_mode string, and any warnings.
+func getSecondaryModeStr(tryAllPrimariesBeforeSecondary bool, atsMajorVer int, ds tc.DeliveryServiceName) (string, []string) {
+	warnings := []string{}
 	if !tryAllPrimariesBeforeSecondary {
-		return ""
+		return "", warnings
 	}
 	if atsMajorVer < 8 {
-		log.Errorln("Delivery Service '" + string(ds) + "' had Parameter " + ParentConfigParamSecondaryMode + " but this cache is " + strconv.Itoa(atsMajorVer) + " and secondary_mode isn't supported in ATS until 8. Not using!")
-		return ""
+		warnings = append(warnings, "Delivery Service '"+string(ds)+"' had Parameter "+ParentConfigParamSecondaryMode+" but this cache is "+strconv.Itoa(atsMajorVer)+" and secondary_mode isn't supported in ATS until 8. Not using!")
+		return "", warnings
 	}
-	return ` secondary_mode=2` // See https://docs.trafficserver.apache.org/en/8.0.x/admin-guide/files/parent.config.en.html
+	return ` secondary_mode=2`, warnings // See https://docs.trafficserver.apache.org/en/8.0.x/admin-guide/files/parent.config.en.html
 }
 
 func getTopologyParentIsProxyStr(serverIsLastCacheTier bool) string {
@@ -793,7 +816,7 @@ func getTopologyParentIsProxyStr(serverIsLastCacheTier bool) string {
 }
 
 func getTopologyRoundRobin(
-	ds *tc.DeliveryServiceNullableV30,
+	ds *DeliveryService,
 	serverParams map[string]string,
 	serverIsLastTier bool,
 	algorithm string,
@@ -811,7 +834,7 @@ func getTopologyRoundRobin(
 	return roundRobinConsistentHash
 }
 
-func getTopologyGoDirect(ds *tc.DeliveryServiceNullableV30, serverIsLastTier bool) string {
+func getTopologyGoDirect(ds *DeliveryService, serverIsLastTier bool) string {
 	if !serverIsLastTier {
 		return "false"
 	}
@@ -825,7 +848,7 @@ func getTopologyGoDirect(ds *tc.DeliveryServiceNullableV30, serverIsLastTier boo
 }
 
 func getTopologyQueryString(
-	ds *tc.DeliveryServiceNullableV30,
+	ds *DeliveryService,
 	serverParams map[string]string,
 	serverIsLastTier bool,
 	algorithm string,
@@ -851,10 +874,11 @@ func getTopologyQueryString(
 }
 
 // serverParentageParams gets the Parameters used for parent= line, or defaults if they don't exist
-// Returns the Parameters used for parent= lines, for the given server.
-func serverParentageParams(sv *tc.ServerNullable, params []ParameterWithProfilesMap) ProfileCache {
+// Returns the Parameters used for parent= lines for the given server, and any warnings.
+func serverParentageParams(sv *Server, params []parameterWithProfilesMap) (profileCache, []string) {
+	warnings := []string{}
 	// TODO deduplicate with atstccfg/parentdotconfig.go
-	profileCache := DefaultProfileCache()
+	profileCache := defaultProfileCache()
 	if sv.TCPPort != nil {
 		profileCache.Port = *sv.TCPPort
 	}
@@ -868,7 +892,7 @@ func serverParentageParams(sv *tc.ServerNullable, params []ParameterWithProfiles
 		case ParentConfigCacheParamPort:
 			i, err := strconv.ParseInt(param.Value, 10, 64)
 			if err != nil {
-				log.Errorln("parent.config generation: port param is not an integer, skipping! : " + err.Error())
+				warnings = append(warnings, "port param is not an integer, skipping! : "+err.Error())
 			} else {
 				profileCache.Port = int(i)
 			}
@@ -877,7 +901,7 @@ func serverParentageParams(sv *tc.ServerNullable, params []ParameterWithProfiles
 		case ParentConfigCacheParamRank:
 			i, err := strconv.ParseInt(param.Value, 10, 64)
 			if err != nil {
-				log.Errorln("parent.config generation: rank param is not an integer, skipping! : " + err.Error())
+				warnings = append(warnings, "rank param is not an integer, skipping! : "+err.Error())
 			} else {
 				profileCache.Rank = int(i)
 			}
@@ -885,17 +909,17 @@ func serverParentageParams(sv *tc.ServerNullable, params []ParameterWithProfiles
 			profileCache.NotAParent = param.Value != "false"
 		}
 	}
-	return profileCache
+	return profileCache, warnings
 }
 
-func serverParentStr(sv *tc.ServerNullable, svParams ProfileCache) (string, error) {
+func serverParentStr(sv *Server, svParams profileCache) (string, error) {
 	if svParams.NotAParent {
 		return "", nil
 	}
 	host := ""
 	if svParams.UseIP {
 		// TODO get service interface here
-		ip := GetServerIPAddress(sv)
+		ip := getServerIPAddress(sv)
 		if ip == nil {
 			return "", errors.New("server params Use IP, but has no valid IPv4 Service Address")
 		}
@@ -906,25 +930,28 @@ func serverParentStr(sv *tc.ServerNullable, svParams ProfileCache) (string, erro
 	return host + ":" + strconv.Itoa(svParams.Port) + "|" + svParams.Weight, nil
 }
 
-func GetTopologyParents(
-	server *tc.ServerNullable,
-	ds *tc.DeliveryServiceNullableV30,
-	servers []tc.ServerNullable,
-	parentConfigParams []ParameterWithProfilesMap, // all params with configFile parent.confign
+// GetTopologyParents returns the parents, secondary parents, any warnings, and any error.
+func getTopologyParents(
+	server *Server,
+	ds *DeliveryService,
+	servers []Server,
+	parentConfigParams []parameterWithProfilesMap, // all params with configFile parent.confign
 	topology tc.Topology,
 	serverIsLastTier bool,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
 	dsOrigins map[ServerID]struct{}, // for Topology DSes, MSO still needs DeliveryServiceServer assignments.
-) ([]string, []string, error) {
+) ([]string, []string, []string, error) {
+	warnings := []string{}
 	// If it's the last tier, then the parent is the origin.
 	// Note this doesn't include MSO, whose final tier cachegroup points to the origin cachegroup.
 	if serverIsLastTier {
-		orgURI, err := GetOriginURI(*ds.OrgServerFQDN) // TODO pass, instead of calling again
+		orgURI, orgWarns, err := getOriginURI(*ds.OrgServerFQDN) // TODO pass, instead of calling again
+		warnings = append(warnings, orgWarns...)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, warnings, err
 		}
-		return []string{orgURI.Host}, nil, nil
+		return []string{orgURI.Host}, nil, warnings, nil
 	}
 
 	svNode := tc.TopologyNode{}
@@ -935,20 +962,20 @@ func GetTopologyParents(
 		}
 	}
 	if svNode.Cachegroup == "" {
-		return nil, nil, errors.New("This server '" + *server.HostName + "' not in DS " + *ds.XMLID + " topology, skipping")
+		return nil, nil, warnings, errors.New("This server '" + *server.HostName + "' not in DS " + *ds.XMLID + " topology, skipping")
 	}
 
 	if len(svNode.Parents) == 0 {
-		return nil, nil, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' is last tier, but NonLastTier called! Should never happen")
+		return nil, nil, warnings, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' is last tier, but NonLastTier called! Should never happen")
 	}
 	if numParents := len(svNode.Parents); numParents > 2 {
-		log.Errorln("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' has " + strconv.Itoa(numParents) + " parent nodes, but Apache Traffic Server only supports Primary and Secondary (2) lists of parents. CacheGroup nodes after the first 2 will be ignored!")
+		warnings = append(warnings, "DS "+*ds.XMLID+" topology '"+*ds.Topology+"' has "+strconv.Itoa(numParents)+" parent nodes, but Apache Traffic Server only supports Primary and Secondary (2) lists of parents. CacheGroup nodes after the first 2 will be ignored!")
 	}
 	if len(topology.Nodes) <= svNode.Parents[0] {
-		return nil, nil, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' node parent " + strconv.Itoa(svNode.Parents[0]) + " greater than number of topology nodes " + strconv.Itoa(len(topology.Nodes)) + ". Cannot create parents!")
+		return nil, nil, warnings, errors.New("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' node parent " + strconv.Itoa(svNode.Parents[0]) + " greater than number of topology nodes " + strconv.Itoa(len(topology.Nodes)) + ". Cannot create parents!")
 	}
 	if len(svNode.Parents) > 1 && len(topology.Nodes) <= svNode.Parents[1] {
-		log.Errorln("DS " + *ds.XMLID + " topology '" + *ds.Topology + "' node secondary parent " + strconv.Itoa(svNode.Parents[1]) + " greater than number of topology nodes " + strconv.Itoa(len(topology.Nodes)) + ". Secondary parent will be ignored!")
+		warnings = append(warnings, "DS "+*ds.XMLID+" topology '"+*ds.Topology+"' node secondary parent "+strconv.Itoa(svNode.Parents[1])+" greater than number of topology nodes "+strconv.Itoa(len(topology.Nodes))+". Secondary parent will be ignored!")
 	}
 
 	parentCG := topology.Nodes[svNode.Parents[0]].Cachegroup
@@ -958,33 +985,35 @@ func GetTopologyParents(
 	}
 
 	if parentCG == "" {
-		return nil, nil, errors.New("Server '" + *server.HostName + "' DS " + *ds.XMLID + " topology '" + *ds.Topology + "' cachegroup '" + *server.Cachegroup + "' topology node parent " + strconv.Itoa(svNode.Parents[0]) + " is not in the topology!")
+		return nil, nil, warnings, errors.New("Server '" + *server.HostName + "' DS " + *ds.XMLID + " topology '" + *ds.Topology + "' cachegroup '" + *server.Cachegroup + "' topology node parent " + strconv.Itoa(svNode.Parents[0]) + " is not in the topology!")
 	}
 
 	parentStrs := []string{}
 	secondaryParentStrs := []string{}
 
-	serversWithParams := []ServerWithParams{}
+	serversWithParams := []serverWithParams{}
 	for _, sv := range servers {
-		serversWithParams = append(serversWithParams, ServerWithParams{
-			ServerNullable: sv,
-			Params:         serverParentageParams(&sv, parentConfigParams),
+		serverParentParams, parentWarns := serverParentageParams(&sv, parentConfigParams)
+		warnings = append(warnings, parentWarns...)
+		serversWithParams = append(serversWithParams, serverWithParams{
+			Server: sv,
+			Params: serverParentParams,
 		})
 	}
-	sort.Sort(ServersWithParamsSortByRank(serversWithParams))
+	sort.Sort(serversWithParamsSortByRank(serversWithParams))
 
 	for _, sv := range serversWithParams {
 		if sv.ID == nil {
-			log.Errorln("TO Servers server had nil ID, skipping")
+			warnings = append(warnings, "TO Servers server had nil ID, skipping")
 			continue
 		} else if sv.Cachegroup == nil {
-			log.Errorln("TO Servers server had nil Cachegroup, skipping")
+			warnings = append(warnings, "TO Servers server had nil Cachegroup, skipping")
 			continue
 		} else if sv.CDNName == nil {
-			log.Errorln("parent.config generation: TO servers had server with missing CDNName, skipping!")
+			warnings = append(warnings, "TO servers had server with missing CDNName, skipping!")
 			continue
 		} else if sv.Status == nil || *sv.Status == "" {
-			log.Errorln("parent.config generation: TO servers had server with missing Status, skipping!")
+			warnings = append(warnings, "TO servers had server with missing Status, skipping!")
 			continue
 		}
 
@@ -1001,34 +1030,37 @@ func GetTopologyParents(
 			continue
 		}
 
-		if !HasRequiredCapabilities(serverCapabilities[*sv.ID], dsRequiredCapabilities[*ds.ID]) {
+		if !hasRequiredCapabilities(serverCapabilities[*sv.ID], dsRequiredCapabilities[*ds.ID]) {
 			continue
 		}
 		if *sv.Cachegroup == parentCG {
-			parentStr, err := serverParentStr(&sv.ServerNullable, sv.Params)
+			parentStr, err := serverParentStr(&sv.Server, sv.Params)
 			if err != nil {
-				return nil, nil, errors.New("getting server parent string: " + err.Error())
+				return nil, nil, warnings, errors.New("getting server parent string: " + err.Error())
 			}
 			if parentStr != "" { // will be empty if server is not_a_parent (possibly other reasons)
 				parentStrs = append(parentStrs, parentStr)
 			}
 		}
 		if *sv.Cachegroup == secondaryParentCG {
-			parentStr, err := serverParentStr(&sv.ServerNullable, sv.Params)
+			parentStr, err := serverParentStr(&sv.Server, sv.Params)
 			if err != nil {
-				return nil, nil, errors.New("getting server parent string: " + err.Error())
+				return nil, nil, warnings, errors.New("getting server parent string: " + err.Error())
 			}
 			secondaryParentStrs = append(secondaryParentStrs, parentStr)
 		}
 	}
 
-	return parentStrs, secondaryParentStrs, nil
+	return parentStrs, secondaryParentStrs, warnings, nil
 }
 
-func GetOriginURI(fqdn string) (*url.URL, error) {
+// getOriginURI returns the URL, any warnings, and any error.
+func getOriginURI(fqdn string) (*url.URL, []string, error) {
+	warnings := []string{}
+
 	orgURI, err := url.Parse(fqdn) // TODO verify origin is always a host:port
 	if err != nil {
-		return nil, errors.New("parsing: " + err.Error())
+		return nil, warnings, errors.New("parsing: " + err.Error())
 	}
 	if orgURI.Port() == "" {
 		if orgURI.Scheme == "http" {
@@ -1036,27 +1068,28 @@ func GetOriginURI(fqdn string) (*url.URL, error) {
 		} else if orgURI.Scheme == "https" {
 			orgURI.Host += ":443"
 		} else {
-			log.Errorln("parent.config generation non-top-level: origin '" + fqdn + "' is unknown scheme '" + orgURI.Scheme + "', but has no port! Using as-is! ")
+			warnings = append(warnings, "non-top-level: origin '"+fqdn+"' is unknown scheme '"+orgURI.Scheme+"', but has no port! Using as-is! ")
 		}
 	}
-	return orgURI, nil
+	return orgURI, warnings, nil
 }
 
-// getParentStrs returns the parents= and secondary_parents= strings for ATS parent.config lines.
+// getParentStrs returns the parents= and secondary_parents= strings for ATS parent.config lines, and any warnings.
 func getParentStrs(
-	ds *tc.DeliveryServiceNullableV30,
+	ds *DeliveryService,
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
-	parentInfos []ParentInfo,
+	parentInfos []parentInfo,
 	atsMajorVer int,
 	tryAllPrimariesBeforeSecondary bool,
-) (string, string) {
+) (string, string, []string) {
+	warnings := []string{}
 	parentInfo := []string{}
 	secondaryParentInfo := []string{}
 
-	sort.Sort(ParentInfoSortByRank(parentInfos))
+	sort.Sort(parentInfoSortByRank(parentInfos))
 
 	for _, parent := range parentInfos { // TODO fix magic key
-		if !HasRequiredCapabilities(parent.Capabilities, dsRequiredCapabilities[*ds.ID]) {
+		if !hasRequiredCapabilities(parent.Capabilities, dsRequiredCapabilities[*ds.ID]) {
 			continue
 		}
 
@@ -1089,38 +1122,41 @@ func getParentStrs(
 	if atsMajorVer >= 6 && len(secondaryParentInfo) > 0 {
 		parents = `parent="` + strings.Join(parentInfo, "") + `"`
 		secondaryParents = ` secondary_parent="` + strings.Join(secondaryParentInfo, "") + `"`
-		secondaryParents += getSecondaryModeStr(tryAllPrimariesBeforeSecondary, atsMajorVer, dsName)
+		secondaryModeStr, secondaryModeWarnings := getSecondaryModeStr(tryAllPrimariesBeforeSecondary, atsMajorVer, dsName)
+		warnings = append(warnings, secondaryModeWarnings...)
+		secondaryParents += secondaryModeStr
 	} else {
 		parents = `parent="` + strings.Join(parentInfo, "") + strings.Join(secondaryParentInfo, "") + `"`
 	}
 
-	return parents, secondaryParents
+	return parents, secondaryParents, warnings
 }
 
-// getMSOParentStrs returns the parents= and secondary_parents= strings for ATS parent.config lines, for MSO.
+// getMSOParentStrs returns the parents= and secondary_parents= strings for ATS parent.config lines for MSO, and any warnings.
 func getMSOParentStrs(
-	ds *tc.DeliveryServiceNullableV30,
-	parentInfos []ParentInfo,
+	ds *DeliveryService,
+	parentInfos []parentInfo,
 	atsMajorVer int,
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
 	msoAlgorithm string,
 	tryAllPrimariesBeforeSecondary bool,
-) (string, string) {
+) (string, string, []string) {
+	warnings := []string{}
 	// TODO determine why MSO is different, and if possible, combine with getParentAndSecondaryParentStrs.
 
-	rankedParents := ParentInfoSortByRank(parentInfos)
+	rankedParents := parentInfoSortByRank(parentInfos)
 	sort.Sort(rankedParents)
 
-	parentInfo := []string{}
+	parentInfoTxt := []string{}
 	secondaryParentInfo := []string{}
 	nullParentInfo := []string{}
-	for _, parent := range ([]ParentInfo)(rankedParents) {
-		if !HasRequiredCapabilities(parent.Capabilities, dsRequiredCapabilities[*ds.ID]) {
+	for _, parent := range ([]parentInfo)(rankedParents) {
+		if !hasRequiredCapabilities(parent.Capabilities, dsRequiredCapabilities[*ds.ID]) {
 			continue
 		}
 
 		if parent.PrimaryParent {
-			parentInfo = append(parentInfo, parent.Format())
+			parentInfoTxt = append(parentInfoTxt, parent.Format())
 		} else if parent.SecondaryParent {
 			secondaryParentInfo = append(secondaryParentInfo, parent.Format())
 		} else {
@@ -1128,20 +1164,20 @@ func getMSOParentStrs(
 		}
 	}
 
-	if len(parentInfo) == 0 {
+	if len(parentInfoTxt) == 0 {
 		// If no parents are found in the secondary parent either, then set the null parent list (parents in neither secondary or primary)
 		// as the secondary parent list and clear the null parent list.
 		if len(secondaryParentInfo) == 0 {
 			secondaryParentInfo = nullParentInfo
 			nullParentInfo = []string{}
 		}
-		parentInfo = secondaryParentInfo
+		parentInfoTxt = secondaryParentInfo
 		secondaryParentInfo = []string{} // TODO should thi be '= secondary'? Currently emulates Perl
 	}
 
 	// TODO benchmark, verify this isn't slow. if it is, it could easily be made faster
 	seen := map[string]struct{}{} // TODO change to host+port? host isn't unique
-	parentInfo, seen = util.RemoveStrDuplicates(parentInfo, seen)
+	parentInfoTxt, seen = util.RemoveStrDuplicates(parentInfoTxt, seen)
 	secondaryParentInfo, seen = util.RemoveStrDuplicates(secondaryParentInfo, seen)
 	nullParentInfo, seen = util.RemoveStrDuplicates(nullParentInfo, seen)
 
@@ -1159,22 +1195,24 @@ func getMSOParentStrs(
 	secondaryParents := ""
 
 	if atsMajorVer >= 6 && msoAlgorithm == "consistent_hash" && len(secondaryParentStr) > 0 {
-		parents = `parent="` + strings.Join(parentInfo, "") + `"`
+		parents = `parent="` + strings.Join(parentInfoTxt, "") + `"`
 		secondaryParents = ` secondary_parent="` + secondaryParentStr + `"`
-		secondaryParents += getSecondaryModeStr(tryAllPrimariesBeforeSecondary, atsMajorVer, dsName)
+		secondaryModeStr, secondaryModeWarnings := getSecondaryModeStr(tryAllPrimariesBeforeSecondary, atsMajorVer, dsName)
+		warnings = append(warnings, secondaryModeWarnings...)
+		secondaryParents += secondaryModeStr
 	} else {
-		parents = `parent="` + strings.Join(parentInfo, "") + secondaryParentStr + `"`
+		parents = `parent="` + strings.Join(parentInfoTxt, "") + secondaryParentStr + `"`
 	}
-	return parents, secondaryParents
+	return parents, secondaryParents, warnings
 }
 
-func MakeParentInfo(
-	serverParentCGData ServerParentCacheGroupData,
+func makeParentInfo(
+	serverParentCGData serverParentCacheGroupData,
 	serverDomain string, // getCDNDomainByProfileID(tx, server.ProfileID)
-	profileCaches map[ProfileID]ProfileCache, // getServerParentCacheGroupProfiles(tx, server)
-	originServers map[OriginHost][]CGServer, // getServerParentCacheGroupProfiles(tx, server)
-) map[OriginHost][]ParentInfo {
-	parentInfos := map[OriginHost][]ParentInfo{}
+	profileCaches map[ProfileID]profileCache, // getServerParentCacheGroupProfiles(tx, server)
+	originServers map[OriginHost][]cgServer, // getServerParentCacheGroupProfiles(tx, server)
+) map[OriginHost][]parentInfo {
+	parentInfos := map[OriginHost][]parentInfo{}
 
 	// note servers also contains an "all" key
 	for originHost, servers := range originServers {
@@ -1188,7 +1226,7 @@ func MakeParentInfo(
 			// 	continue
 			// }
 
-			parentInf := ParentInfo{
+			parentInf := parentInfo{
 				Host:            row.ServerHost,
 				Port:            profile.Port,
 				Domain:          row.Domain,
@@ -1219,31 +1257,23 @@ func unavailableServerRetryResponsesValid(s string) bool {
 	return re.MatchString(s)
 }
 
-// HasRequiredCapabilities returns whether the given caps has all the required capabilities in the given reqCaps.
-func HasRequiredCapabilities(caps map[ServerCapability]struct{}, reqCaps map[ServerCapability]struct{}) bool {
-	for reqCap, _ := range reqCaps {
-		if _, ok := caps[reqCap]; !ok {
-			return false
-		}
-	}
-	return true
-}
-
-func GetOriginServersAndProfileCaches(
-	cgServers map[int]tc.ServerNullable,
+// getOriginServersAndProfileCaches returns the origin servers, ProfileCaches, any warnings, and any error.
+func getOriginServersAndProfileCaches(
+	cgServers map[int]Server,
 	parentServerDSes map[int]map[int]struct{},
 	profileParentConfigParams map[string]map[string]string, // map[profileName][paramName]paramVal
-	dses []tc.DeliveryServiceNullableV30,
+	dses []DeliveryService,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
-) (map[OriginHost][]CGServer, map[ProfileID]ProfileCache, error) {
-	originServers := map[OriginHost][]CGServer{}  // "deliveryServices" in Perl
-	profileCaches := map[ProfileID]ProfileCache{} // map[profileID]ProfileCache
+) (map[OriginHost][]cgServer, map[ProfileID]profileCache, []string, error) {
+	warnings := []string{}
+	originServers := map[OriginHost][]cgServer{}  // "deliveryServices" in Perl
+	profileCaches := map[ProfileID]profileCache{} // map[profileID]ProfileCache
 
-	dsIDMap := map[int]tc.DeliveryServiceNullableV30{}
+	dsIDMap := map[int]DeliveryService{}
 	for _, ds := range dses {
 		if ds.ID == nil {
-			return nil, nil, errors.New("delivery services got nil ID!")
+			return nil, nil, warnings, errors.New("delivery services got nil ID!")
 		}
 		if !ds.Type.IsHTTP() && !ds.Type.IsDNS() {
 			continue // skip ANY_MAP, STEERING, etc
@@ -1251,14 +1281,14 @@ func GetOriginServersAndProfileCaches(
 		dsIDMap[*ds.ID] = ds
 	}
 
-	allDSMap := map[int]tc.DeliveryServiceNullableV30{} // all DSes for this server, NOT all dses in TO
+	allDSMap := map[int]DeliveryService{} // all DSes for this server, NOT all dses in TO
 	for _, dsIDs := range parentServerDSes {
 		for dsID, _ := range dsIDs {
 			if _, ok := dsIDMap[dsID]; !ok {
 				// this is normal if the TO was too old to understand our /deliveryserviceserver?servers= query param
 				// In which case, the DSS will include DSes from other CDNs, which aren't in the dsIDMap
 				// If the server was new enough to respect the params, this should never happen.
-				// log.Warnln("getting delivery services: parent server DS %v not in dsIDMap\n", dsID)
+				// warnings = append(warnings, ("getting delivery services: parent server DS %v not in dsIDMap\n", dsID)
 				continue
 			}
 			if _, ok := allDSMap[dsID]; !ok {
@@ -1267,108 +1297,112 @@ func GetOriginServersAndProfileCaches(
 		}
 	}
 
-	dsOrigins, err := GetDSOrigins(allDSMap)
+	dsOrigins, dsOriginWarns, err := getDSOrigins(allDSMap)
+	warnings = append(warnings, dsOriginWarns...)
 	if err != nil {
-		return nil, nil, errors.New("getting DS origins: " + err.Error())
+		return nil, nil, warnings, errors.New("getting DS origins: " + err.Error())
 	}
 
-	profileParams := GetParentConfigProfileParams(cgServers, profileParentConfigParams)
+	profileParams, profParamWarns := getParentConfigProfileParams(cgServers, profileParentConfigParams)
+	warnings = append(warnings, profParamWarns...)
 
-	for _, cgServer := range cgServers {
-		if cgServer.ID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil ID, skipping!")
+	for _, cgSv := range cgServers {
+		if cgSv.ID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil ID, skipping!")
 			continue
-		} else if cgServer.HostName == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil HostName, skipping!")
+		} else if cgSv.HostName == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil HostName, skipping!")
 			continue
-		} else if cgServer.TCPPort == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil TCPPort, skipping!")
+		} else if cgSv.TCPPort == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil TCPPort, skipping!")
 			continue
-		} else if cgServer.CachegroupID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil CachegroupID, skipping!")
+		} else if cgSv.CachegroupID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil CachegroupID, skipping!")
 			continue
-		} else if cgServer.StatusID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil StatusID, skipping!")
+		} else if cgSv.StatusID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil StatusID, skipping!")
 			continue
-		} else if cgServer.TypeID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil TypeID, skipping!")
+		} else if cgSv.TypeID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil TypeID, skipping!")
 			continue
-		} else if cgServer.ProfileID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil ProfileID, skipping!")
+		} else if cgSv.ProfileID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil ProfileID, skipping!")
 			continue
-		} else if cgServer.CDNID == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil CDNID, skipping!")
+		} else if cgSv.CDNID == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil CDNID, skipping!")
 			continue
-		} else if cgServer.DomainName == nil {
-			log.Errorln("parent.config getting origin servers: got server with nil DomainName, skipping!")
+		} else if cgSv.DomainName == nil {
+			warnings = append(warnings, "getting origin servers: got server with nil DomainName, skipping!")
 			continue
 		}
 
-		ipAddr := GetServerIPAddress(&cgServer)
+		ipAddr := getServerIPAddress(&cgSv)
 		if ipAddr == nil {
-			log.Errorln("parent.config getting origin servers: got server with no valid IP Address, skipping!")
+			warnings = append(warnings, "getting origin servers: got server with no valid IP Address, skipping!")
 			continue
 		}
 
-		realCGServer := CGServer{
-			ServerID:     ServerID(*cgServer.ID),
-			ServerHost:   *cgServer.HostName,
+		realCGServer := cgServer{
+			ServerID:     ServerID(*cgSv.ID),
+			ServerHost:   *cgSv.HostName,
 			ServerIP:     ipAddr.String(),
-			ServerPort:   *cgServer.TCPPort,
-			CacheGroupID: *cgServer.CachegroupID,
-			Status:       *cgServer.StatusID,
-			Type:         *cgServer.TypeID,
-			ProfileID:    ProfileID(*cgServer.ProfileID),
-			CDN:          *cgServer.CDNID,
-			TypeName:     cgServer.Type,
-			Domain:       *cgServer.DomainName,
-			Capabilities: serverCapabilities[*cgServer.ID],
-		}
-
-		if cgServer.Type == tc.OriginTypeName {
-			for dsID, _ := range parentServerDSes[*cgServer.ID] { // map[serverID][]dsID
+			ServerPort:   *cgSv.TCPPort,
+			CacheGroupID: *cgSv.CachegroupID,
+			Status:       *cgSv.StatusID,
+			Type:         *cgSv.TypeID,
+			ProfileID:    ProfileID(*cgSv.ProfileID),
+			CDN:          *cgSv.CDNID,
+			TypeName:     cgSv.Type,
+			Domain:       *cgSv.DomainName,
+			Capabilities: serverCapabilities[*cgSv.ID],
+		}
+
+		if cgSv.Type == tc.OriginTypeName {
+			for dsID, _ := range parentServerDSes[*cgSv.ID] { // map[serverID][]dsID
 				orgURI := dsOrigins[dsID]
 				if orgURI == nil {
-					// log.Warnln("ds %v has no origins! Skipping!\n", dsID) // TODO determine if this is normal
+					// warnings = append(warnings, fmt.Sprintf(("ds %v has no origins! Skipping!\n", dsID) // TODO determine if this is normal
 					continue
 				}
-				if HasRequiredCapabilities(serverCapabilities[*cgServer.ID], dsRequiredCapabilities[dsID]) {
+				if hasRequiredCapabilities(serverCapabilities[*cgSv.ID], dsRequiredCapabilities[dsID]) {
 					orgHost := OriginHost(orgURI.Host)
 					originServers[orgHost] = append(originServers[orgHost], realCGServer)
 				} else {
-					log.Errorf("ds %v server %v missing required caps, skipping!\n", dsID, orgURI.Host)
+					warnings = append(warnings, fmt.Sprintf("ds %v server %v missing required caps, skipping!\n", dsID, orgURI.Host))
 				}
 			}
 		} else {
-			originServers[DeliveryServicesAllParentsKey] = append(originServers[DeliveryServicesAllParentsKey], realCGServer)
+			originServers[deliveryServicesAllParentsKey] = append(originServers[deliveryServicesAllParentsKey], realCGServer)
 		}
 
 		if _, profileCachesHasProfile := profileCaches[realCGServer.ProfileID]; !profileCachesHasProfile {
-			if profileCache, profileParamsHasProfile := profileParams[*cgServer.Profile]; !profileParamsHasProfile {
-				log.Warnf("cachegroup has server with profile %+v but that profile has no parameters\n", *cgServer.ProfileID)
-				profileCaches[realCGServer.ProfileID] = DefaultProfileCache()
+			if profileCache, profileParamsHasProfile := profileParams[*cgSv.Profile]; !profileParamsHasProfile {
+				warnings = append(warnings, fmt.Sprintf("cachegroup has server with profile %+v but that profile has no parameters\n", *cgSv.ProfileID))
+				profileCaches[realCGServer.ProfileID] = defaultProfileCache()
 			} else {
 				profileCaches[realCGServer.ProfileID] = profileCache
 			}
 		}
 	}
 
-	return originServers, profileCaches, nil
+	return originServers, profileCaches, warnings, nil
 }
 
-func GetParentConfigProfileParams(
-	cgServers map[int]tc.ServerNullable,
+// GetParentConfigProfileParams returns the parent config profile params, and any warnings.
+func getParentConfigProfileParams(
+	cgServers map[int]Server,
 	profileParentConfigParams map[string]map[string]string, // map[profileName][paramName]paramVal
-) map[string]ProfileCache {
-	parentConfigServerCacheProfileParams := map[string]ProfileCache{} // map[profileName]ProfileCache
+) (map[string]profileCache, []string) {
+	warnings := []string{}
+	parentConfigServerCacheProfileParams := map[string]profileCache{} // map[profileName]ProfileCache
 	for _, cgServer := range cgServers {
 		if cgServer.Profile == nil {
-			log.Errorln("getting parent config profile params: server has nil profile, skipping!")
+			warnings = append(warnings, "getting parent config profile params: server has nil profile, skipping!")
 			continue
 		}
 		profileCache, ok := parentConfigServerCacheProfileParams[*cgServer.Profile]
 		if !ok {
-			profileCache = DefaultProfileCache()
+			profileCache = defaultProfileCache()
 		}
 		params, ok := profileParentConfigParams[*cgServer.Profile]
 		if !ok {
@@ -1380,7 +1414,7 @@ func GetParentConfigProfileParams(
 			case ParentConfigCacheParamWeight:
 				// f, err := strconv.ParseFloat(param.Val, 64)
 				// if err != nil {
-				// 	log.Errorln("parent.config generation: weight param is not a float, skipping! : " + err.Error())
+				// 	warnings = append(warnings, "parent.config generation: weight param is not a float, skipping! : " + err.Error())
 				// } else {
 				// 	profileCache.Weight = f
 				// }
@@ -1389,7 +1423,7 @@ func GetParentConfigProfileParams(
 			case ParentConfigCacheParamPort:
 				i, err := strconv.ParseInt(val, 10, 64)
 				if err != nil {
-					log.Errorln("parent.config generation: port param is not an integer, skipping! : " + err.Error())
+					warnings = append(warnings, "port param is not an integer, skipping! : "+err.Error())
 				} else {
 					profileCache.Port = int(i)
 				}
@@ -1398,7 +1432,7 @@ func GetParentConfigProfileParams(
 			case ParentConfigCacheParamRank:
 				i, err := strconv.ParseInt(val, 10, 64)
 				if err != nil {
-					log.Errorln("parent.config generation: rank param is not an integer, skipping! : " + err.Error())
+					warnings = append(warnings, "rank param is not an integer, skipping! : "+err.Error())
 				} else {
 					profileCache.Rank = int(i)
 				}
@@ -1408,32 +1442,33 @@ func GetParentConfigProfileParams(
 		}
 		parentConfigServerCacheProfileParams[*cgServer.Profile] = profileCache
 	}
-	return parentConfigServerCacheProfileParams
+	return parentConfigServerCacheProfileParams, warnings
 }
 
-// GetDSOrigins takes a map[deliveryServiceID]DeliveryService, and returns a map[DeliveryServiceID]OriginURI.
-func GetDSOrigins(dses map[int]tc.DeliveryServiceNullableV30) (map[int]*OriginURI, error) {
-	dsOrigins := map[int]*OriginURI{}
+// getDSOrigins takes a map[deliveryServiceID]DeliveryService, and returns a map[DeliveryServiceID]OriginURI, any warnings, and any error.
+func getDSOrigins(dses map[int]DeliveryService) (map[int]*originURI, []string, error) {
+	warnings := []string{}
+	dsOrigins := map[int]*originURI{}
 	for _, ds := range dses {
 		if ds.ID == nil {
-			return nil, errors.New("ds has nil ID")
+			return nil, warnings, errors.New("ds has nil ID")
 		}
 		if ds.XMLID == nil {
-			return nil, errors.New("ds has nil XMLID")
+			return nil, warnings, errors.New("ds has nil XMLID")
 		}
 		if ds.OrgServerFQDN == nil {
-			log.Warnf("GetDSOrigins ds %v got nil OrgServerFQDN, skipping!\n", *ds.XMLID)
+			warnings = append(warnings, fmt.Sprintf("GetDSOrigins ds %v got nil OrgServerFQDN, skipping!\n", *ds.XMLID))
 			continue
 		}
 		orgURL, err := url.Parse(*ds.OrgServerFQDN)
 		if err != nil {
-			return nil, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + err.Error())
+			return nil, warnings, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + err.Error())
 		}
 		if orgURL.Scheme == "" {
-			return nil, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + "missing scheme")
+			return nil, warnings, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + "missing scheme")
 		}
 		if orgURL.Host == "" {
-			return nil, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + "missing scheme")
+			return nil, warnings, errors.New("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + "missing scheme")
 		}
 
 		scheme := orgURL.Scheme
@@ -1445,19 +1480,21 @@ func GetDSOrigins(dses map[int]tc.DeliveryServiceNullableV30) (map[int]*OriginUR
 			} else if scheme == "https" {
 				port = "443"
 			} else {
-				log.Warnln("parsing ds '" + *ds.XMLID + "' OrgServerFQDN '" + *ds.OrgServerFQDN + "': " + "unknown scheme '" + scheme + "' and no port, leaving port empty!")
+				warnings = append(warnings, "parsing ds '"+*ds.XMLID+"' OrgServerFQDN '"+*ds.OrgServerFQDN+"': "+"unknown scheme '"+scheme+"' and no port, leaving port empty!")
 			}
 		}
-		dsOrigins[*ds.ID] = &OriginURI{Scheme: scheme, Host: host, Port: port}
+		dsOrigins[*ds.ID] = &originURI{Scheme: scheme, Host: host, Port: port}
 	}
-	return dsOrigins, nil
+	return dsOrigins, warnings, nil
 }
 
-func makeDSOrigins(dsses []tc.DeliveryServiceServer, dses []tc.DeliveryServiceNullableV30, servers []tc.ServerNullable) map[DeliveryServiceID]map[ServerID]struct{} {
+// makeDSOrigins returns the DS Origins and any warnings.
+func makeDSOrigins(dsses []tc.DeliveryServiceServer, dses []DeliveryService, servers []Server) (map[DeliveryServiceID]map[ServerID]struct{}, []string) {
+	warnings := []string{}
 	dssMap := map[DeliveryServiceID]map[ServerID]struct{}{}
 	for _, dss := range dsses {
 		if dss.Server == nil || dss.DeliveryService == nil {
-			log.Errorln("making parent.config, got deliveryserviceserver with nil values, skipping!")
+			warnings = append(warnings, "making parent.config, got deliveryserviceserver with nil values, skipping!")
 			continue
 		}
 		dsID := DeliveryServiceID(*dss.DeliveryService)
@@ -1468,10 +1505,10 @@ func makeDSOrigins(dsses []tc.DeliveryServiceServer, dses []tc.DeliveryServiceNu
 		dssMap[dsID][serverID] = struct{}{}
 	}
 
-	svMap := map[ServerID]tc.ServerNullable{}
+	svMap := map[ServerID]Server{}
 	for _, sv := range servers {
 		if sv.ID == nil {
-			log.Errorln("parent.config got server with missing ID, skipping!")
+			warnings = append(warnings, "got server with missing ID, skipping!")
 		}
 		svMap[ServerID(*sv.ID)] = sv
 	}
@@ -1479,7 +1516,7 @@ func makeDSOrigins(dsses []tc.DeliveryServiceServer, dses []tc.DeliveryServiceNu
 	dsOrigins := map[DeliveryServiceID]map[ServerID]struct{}{}
 	for _, ds := range dses {
 		if ds.ID == nil {
-			log.Errorln("parent.config got ds with missing ID, skipping!")
+			warnings = append(warnings, "got ds with missing ID, skipping!")
 			continue
 		}
 		dsID := DeliveryServiceID(*ds.ID)
@@ -1495,5 +1532,5 @@ func makeDSOrigins(dsses []tc.DeliveryServiceServer, dses []tc.DeliveryServiceNu
 			dsOrigins[dsID][svID] = struct{}{}
 		}
 	}
-	return dsOrigins
+	return dsOrigins, warnings
 }
diff --git a/lib/go-atscfg/parentdotconfig_test.go b/lib/go-atscfg/parentdotconfig_test.go
index c4656e1..123b93e 100644
--- a/lib/go-atscfg/parentdotconfig_test.go
+++ b/lib/go-atscfg/parentdotconfig_test.go
@@ -28,9 +28,7 @@ import (
 )
 
 func TestMakeParentDotConfig(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -45,7 +43,7 @@ func TestMakeParentDotConfig(t *testing.T) {
 	ds1.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
 	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -91,12 +89,28 @@ func TestMakeParentDotConfig(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{}
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
-	cgs := []tc.CacheGroupNullable{}
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG}
+
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
 			Server:          util.IntPtr(*server.ID),
@@ -112,9 +126,13 @@ func TestMakeParentDotConfig(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -128,9 +146,7 @@ func TestMakeParentDotConfig(t *testing.T) {
 }
 
 func TestMakeParentDotConfigCapabilities(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -138,7 +154,7 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 	ds0.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreUseInCacheKeyAndPassUp))
 	ds0.OrgServerFQDN = util.StrPtr("http://ds0.example.net")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0}
+	dses := []DeliveryService{*ds0}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -194,7 +210,7 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 	mid2.CachegroupID = util.IntPtr(423)
 	setIP(mid1, "192.168.2.4")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1, *mid2}
+	servers := []Server{*server, *mid0, *mid1, *mid2}
 
 	topologies := []tc.Topology{}
 	serverCapabilities := map[int]map[ServerCapability]struct{}{
@@ -232,9 +248,13 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	lines := strings.Split(txt, "\n")
 
@@ -270,9 +290,7 @@ func TestMakeParentDotConfigCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -280,7 +298,7 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 	ds0.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreUseInCacheKeyAndPassUp))
 	ds0.OrgServerFQDN = util.StrPtr("http://ds0.example.net")
 	ds0.MultiSiteOrigin = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{*ds0}
+	dses := []DeliveryService{*ds0}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -330,7 +348,7 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{}
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
@@ -371,9 +389,13 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtx := strings.Replace(txt, " ", "", -1)
 
@@ -383,9 +405,7 @@ func TestMakeParentDotConfigMSOSecondaryParent(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologies(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -401,7 +421,7 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
 	ds1.Topology = util.StrPtr("t0")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -451,7 +471,7 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -502,9 +522,13 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -519,9 +543,7 @@ func TestMakeParentDotConfigTopologies(t *testing.T) {
 
 // TestMakeParentDotConfigNotInTopologies tests when a given edge is NOT in a Topology, that it doesn't add a remap line.
 func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -537,7 +559,7 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 	ds1.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
 	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -583,7 +605,7 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -602,7 +624,23 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
-	cgs := []tc.CacheGroupNullable{}
+
+	eCG := &tc.CacheGroupNullable{}
+	eCG.Name = server.Cachegroup
+	eCG.ID = server.CachegroupID
+	eCG.ParentName = mid0.Cachegroup
+	eCG.ParentCachegroupID = mid0.CachegroupID
+	eCGType := tc.CacheGroupEdgeTypeName
+	eCG.Type = &eCGType
+
+	mCG := &tc.CacheGroupNullable{}
+	mCG.Name = mid0.Cachegroup
+	mCG.ID = mid0.CachegroupID
+	mCGType := tc.CacheGroupMidTypeName
+	mCG.Type = &mCGType
+
+	cgs := []tc.CacheGroupNullable{*eCG, *mCG}
+
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
 			Server:          util.IntPtr(*server.ID),
@@ -618,9 +656,13 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net' to NOT contain Topology DS without this edge: '%v'", txt)
@@ -631,9 +673,7 @@ func TestMakeParentDotConfigNotInTopologies(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0.ID = util.IntPtr(42)
@@ -659,7 +699,7 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 	ds2.OrgServerFQDN = util.StrPtr("http://ds2.example.net")
 	ds2.Topology = util.StrPtr("t0")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1, *ds2}
+	dses := []DeliveryService{*ds0, *ds1, *ds2}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -709,7 +749,7 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -771,9 +811,13 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net' without required capabilities: '%v'", txt)
@@ -787,9 +831,7 @@ func TestMakeParentDotConfigTopologiesCapabilities(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -805,7 +847,7 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
 	ds1.Topology = util.StrPtr("t0")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -857,7 +899,7 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -908,9 +950,13 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -928,9 +974,7 @@ func TestMakeParentDotConfigTopologiesOmitOfflineParents(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -946,7 +990,7 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 	ds1.OrgServerFQDN = util.StrPtr("http://ds1.example.net")
 	ds1.Topology = util.StrPtr("t0")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -999,7 +1043,7 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1050,9 +1094,13 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -1070,9 +1118,7 @@ func TestMakeParentDotConfigTopologiesOmitDifferentCDNParents(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1083,7 +1129,7 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 	ds1.Topology = util.StrPtr("t0")
 	ds1.MultiSiteOrigin = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{*ds1}
+	dses := []DeliveryService{*ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -1137,7 +1183,7 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 	origin1.Type = tc.OriginTypeName
 	origin1.TypeID = util.IntPtr(991)
 
-	servers := []tc.ServerNullable{*server, *origin0, *origin1}
+	servers := []Server{*server, *origin0, *origin1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1184,9 +1230,13 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1200,9 +1250,7 @@ func TestMakeParentDotConfigTopologiesMSO(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1215,7 +1263,7 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 	ds1.ProfileID = util.IntPtr(994)
 	ds1.MultiSiteOrigin = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{*ds1}
+	dses := []DeliveryService{*ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -1299,7 +1347,7 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 	origin1.Type = tc.OriginTypeName
 	origin1.TypeID = util.IntPtr(991)
 
-	servers := []tc.ServerNullable{*server, *origin0, *origin1}
+	servers := []Server{*server, *origin0, *origin1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1346,9 +1394,13 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1374,9 +1426,7 @@ func TestMakeParentDotConfigTopologiesMSOParams(t *testing.T) {
 }
 
 func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds1 := makeParentDS()
 	ds1.ID = util.IntPtr(43)
@@ -1389,7 +1439,7 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 	ds1.ProfileID = util.IntPtr(994)
 	ds1.MultiSiteOrigin = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{*ds1}
+	dses := []DeliveryService{*ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -1473,7 +1523,7 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 	origin1.Type = tc.OriginTypeName
 	origin1.TypeID = util.IntPtr(991)
 
-	servers := []tc.ServerNullable{*server, *origin0, *origin1}
+	servers := []Server{*server, *origin0, *origin1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1520,9 +1570,13 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds1.example.net', actual: '%v'", txt)
@@ -1548,9 +1602,8 @@ func TestMakeParentDotConfigTopologiesParams(t *testing.T) {
 }
 
 func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -1570,7 +1623,7 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 	ds1.ProfileID = util.IntPtr(312)
 	ds1.ProfileName = util.StrPtr("ds1Profile")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -1626,7 +1679,7 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1688,9 +1741,13 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -1707,9 +1764,7 @@ func TestMakeParentDotConfigSecondaryMode(t *testing.T) {
 }
 
 func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
-	serverName := "myserver"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
+	hdr := "myHeaderComment"
 
 	ds0 := makeParentDS()
 	ds0Type := tc.DSTypeHTTP
@@ -1729,7 +1784,7 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 	ds1.ProfileID = util.IntPtr(312)
 	ds1.ProfileName = util.StrPtr("ds1Profile")
 
-	dses := []tc.DeliveryServiceNullableV30{*ds0, *ds1}
+	dses := []DeliveryService{*ds0, *ds1}
 
 	parentConfigParams := []tc.Parameter{
 		tc.Parameter{
@@ -1779,7 +1834,7 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 	mid1.ID = util.IntPtr(46)
 	setIP(mid1, "192.168.2.3")
 
-	servers := []tc.ServerNullable{*server, *mid0, *mid1}
+	servers := []Server{*server, *mid0, *mid1}
 
 	topologies := []tc.Topology{
 		tc.Topology{
@@ -1841,9 +1896,13 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 		Name:       "my-cdn-name",
 	}
 
-	txt := MakeParentDotConfig(toolName, toURL, dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn)
+	cfg, err := MakeParentDotConfig(dses, server, servers, topologies, serverParams, parentConfigParams, serverCapabilities, dsRequiredCapabilities, cgs, dss, cdn, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, serverName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
 		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
@@ -1859,8 +1918,8 @@ func TestMakeParentDotConfigNoSecondaryMode(t *testing.T) {
 	}
 }
 
-func makeTestParentServer() *tc.ServerNullable {
-	server := &tc.ServerNullable{}
+func makeTestParentServer() *Server {
+	server := &Server{}
 	server.ProfileID = util.IntPtr(42)
 	server.CDNName = util.StrPtr("myCDN")
 	server.Cachegroup = util.StrPtr("cg0")
@@ -1882,8 +1941,8 @@ func makeTestParentServer() *tc.ServerNullable {
 	return server
 }
 
-func makeParentDS() *tc.DeliveryServiceNullableV30 {
-	ds := &tc.DeliveryServiceNullableV30{}
+func makeParentDS() *DeliveryService {
+	ds := &DeliveryService{}
 	ds.ID = util.IntPtr(42)
 	ds.XMLID = util.StrPtr("ds1")
 	ds.QStringIgnore = util.IntPtr(int(tc.QStringIgnoreDrop))
diff --git a/lib/go-atscfg/plugindotconfig.go b/lib/go-atscfg/plugindotconfig.go
index b4e45cf..4bdf7b2 100644
--- a/lib/go-atscfg/plugindotconfig.go
+++ b/lib/go-atscfg/plugindotconfig.go
@@ -19,22 +19,39 @@ package atscfg
  * under the License.
  */
 
+import (
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
 const PluginSeparator = " "
 const PluginFileName = "plugin.config"
 const ContentTypePluginDotConfig = ContentTypeTextASCII
 const LineCommentPluginDotConfig = LineCommentHash
 
 func MakePluginDotConfig(
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, StorageFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
-	txt := GenericProfileConfig(paramData, PluginSeparator)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "server profile missing")
+	}
+
+	paramData, paramWarns := paramsToMap(filterParams(serverParams, PluginFileName, "", "", "location"))
+	warnings = append(warnings, paramWarns...)
+
+	hdr := makeHdrComment(hdrComment)
+	txt := genericProfileConfig(paramData, PluginSeparator)
 	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
 	txt = hdr + txt
-	return txt
+
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentTypePluginDotConfig,
+		LineComment: LineCommentPluginDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
diff --git a/lib/go-atscfg/plugindotconfig_test.go b/lib/go-atscfg/plugindotconfig_test.go
index 42f0df4..9387054 100644
--- a/lib/go-atscfg/plugindotconfig_test.go
+++ b/lib/go-atscfg/plugindotconfig_test.go
@@ -26,17 +26,24 @@ import (
 
 func TestMakePluginDotConfig(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdr := "myHeaderComment"
+
+	paramData := makeParamsFromMap("serverProfile", PluginFileName, map[string]string{
 		"param0": "val0",
 		"param1": "val1",
 		"param2": "val2",
-	}
+	})
 
-	txt := MakePluginDotConfig(profileName, paramData, toolName, toURL)
+	server := makeGenericServer()
+	server.Profile = &profileName
+
+	cfg, err := MakePluginDotConfig(server, paramData, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "param0 val0") {
 		t.Errorf("expected config to contain paramData 'param0 val0', actual: '%v'", txt)
diff --git a/lib/go-atscfg/recordsdotconfig.go b/lib/go-atscfg/recordsdotconfig.go
index 36be1b5..784e9f8 100644
--- a/lib/go-atscfg/recordsdotconfig.go
+++ b/lib/go-atscfg/recordsdotconfig.go
@@ -22,7 +22,6 @@ package atscfg
 import (
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
@@ -32,41 +31,60 @@ const ContentTypeRecordsDotConfig = ContentTypeTextASCII
 const LineCommentRecordsDotConfig = LineCommentHash
 
 func MakeRecordsDotConfig(
-	server *tc.ServerNullable,
-	profileName string,
-	paramData map[string]string, // GetProfileParamData(tx, profile.ID, StorageFileName)
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-) string {
-	hdr := GenericHeaderComment(profileName, toToolName, toURL)
-	txt := GenericProfileConfig(paramData, RecordsSeparator)
+	server *Server,
+	serverParams []tc.Parameter,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+	if server.Profile == nil {
+		return Cfg{}, makeErr(warnings, "server profile missing")
+	}
+
+	params, paramWarns := paramsToMap(filterParams(serverParams, RecordsFileName, "", "", "location"))
+	warnings = append(warnings, paramWarns...)
+
+	hdr := makeHdrComment(hdrComment)
+	txt := genericProfileConfig(params, RecordsSeparator)
 	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
 	txt = replaceLineSuffixes(txt, "STRING __HOSTNAME__", "STRING __FULL_HOSTNAME__")
 	txt = hdr + txt
 
-	txt = addRecordsDotConfigOverrides(txt, server)
+	txt, overrideWarns := addRecordsDotConfigOverrides(txt, server)
+	warnings = append(warnings, overrideWarns...)
 
-	return txt
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentTypeRecordsDotConfig,
+		LineComment: LineCommentRecordsDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
 
-func addRecordsDotConfigOverrides(txt string, server *tc.ServerNullable) string {
-	txt = addRecordsDotConfigOutgoingIP(txt, server)
-	return txt
+// addRecordsDotConfigOverrides modifies the records.config text and adds any overrides.
+// Returns the modified text and any warnings.
+func addRecordsDotConfigOverrides(txt string, server *Server) (string, []string) {
+	warnings := []string{}
+	txt, ipWarns := addRecordsDotConfigOutgoingIP(txt, server)
+	warnings = append(warnings, ipWarns...)
+	return txt, warnings
 }
 
-func addRecordsDotConfigOutgoingIP(txt string, server *tc.ServerNullable) string {
+// addRecordsDotConfigOutgoingIP returns the outgoing IP added to the config text, and any warnings.
+func addRecordsDotConfigOutgoingIP(txt string, server *Server) (string, []string) {
+	warnings := []string{}
+
 	outgoingIPConfig := `proxy.local.outgoing_ip_to_bind`
 	if strings.Contains(txt, outgoingIPConfig) {
-		log.Warnln("records.config had a proxy.local.outgoing_ip_to_bind Parameter! Using Parameter, not setting Outgoing IP from Server")
-		return txt
+		warnings = append(warnings, "records.config had a proxy.local.outgoing_ip_to_bind Parameter! Using Parameter, not setting Outgoing IP from Server")
+		return txt, warnings
 	}
 
 	v4, v6 := getServiceAddresses(server)
 	if v4 == nil {
-		log.Errorln("Generating records.config: server had no IPv4 service address, cannot set " + outgoingIPConfig + "!")
-		return txt
+		warnings = append(warnings, "server had no IPv4 service address, cannot set "+outgoingIPConfig+"!")
+		return txt, warnings
 	}
 
 	txt = txt + `LOCAL ` + outgoingIPConfig + ` STRING ` + v4.String()
@@ -74,7 +92,7 @@ func addRecordsDotConfigOutgoingIP(txt string, server *tc.ServerNullable) string
 		txt += ` [` + v6.String() + `]`
 	}
 	txt += "\n"
-	return txt
+	return txt, warnings
 }
 
 func replaceLineSuffixes(txt string, suffix string, newSuffix string) string {
diff --git a/lib/go-atscfg/recordsdotconfig_test.go b/lib/go-atscfg/recordsdotconfig_test.go
index 15a4ef2..db34f88 100644
--- a/lib/go-atscfg/recordsdotconfig_test.go
+++ b/lib/go-atscfg/recordsdotconfig_test.go
@@ -22,26 +22,33 @@ package atscfg
 import (
 	"strings"
 	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeRecordsDotConfig(t *testing.T) {
 	profileName := "myProfile"
-	toolName := "myToolName"
-	toURL := "https://myto.example.net"
-	paramData := map[string]string{
+	hdr := "myHeaderComment"
+
+	paramData := makeParamsFromMap("serverProfile", RecordsFileName, map[string]string{
 		"param0":                    "val0",
 		"param1":                    "val1",
 		"param2":                    "val2",
 		"test-hostname-replacement": "fooSTRING __HOSTNAME__",
-	}
+	})
 
 	server := makeTestRemapServer()
 	ipStr := "192.168.2.99"
 	setIP(server, ipStr)
+	server.Profile = util.StrPtr(profileName)
 
-	txt := MakeRecordsDotConfig(server, profileName, paramData, toolName, toURL)
+	cfg, err := MakeRecordsDotConfig(server, paramData, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
-	testComment(t, txt, profileName, toolName, toURL)
+	testComment(t, txt, hdr)
 
 	if !strings.Contains(txt, "param0 val0") {
 		t.Errorf("expected config to contain paramData 'param0 val0', actual: '%v'", txt)
diff --git a/lib/go-atscfg/regexremapdotconfig.go b/lib/go-atscfg/regexremapdotconfig.go
index 4d643dd..2d7a0bd 100644
--- a/lib/go-atscfg/regexremapdotconfig.go
+++ b/lib/go-atscfg/regexremapdotconfig.go
@@ -22,32 +22,91 @@ package atscfg
 import (
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 const ContentTypeRegexRemapDotConfig = ContentTypeTextASCII
 const LineCommentRegexRemapDotConfig = LineCommentHash
 
-type CDNDS struct {
+func MakeRegexRemapDotConfig(
+	fileName string,
+	server *Server,
+	deliveryServices []DeliveryService,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "server CDNName missing")
+	}
+
+	configSuffix := `.config`
+	if !strings.HasPrefix(fileName, RegexRemapPrefix) || !strings.HasSuffix(fileName, configSuffix) {
+		return Cfg{}, makeErr(warnings, "file '"+fileName+"' not of the form 'regex_remap_*.config! Please file a bug with Traffic Control, this should never happen")
+	}
+
+	// TODO verify prefix and suffix exist, and warn if they don't? Perl doesn't
+	dsName := strings.TrimSuffix(strings.TrimPrefix(fileName, RegexRemapPrefix), configSuffix)
+	if dsName == "" {
+		return Cfg{}, makeErr(warnings, "file '"+fileName+"' has no delivery service name!")
+	}
+
+	// only send the requested DS to atscfg. The atscfg.Make will work correctly even if we send it other DSes, but this will prevent deliveryServicesToCDNDSes from logging errors about AnyMap and Steering DSes without origins.
+	ds := DeliveryService{}
+	for _, dsesDS := range deliveryServices {
+		if dsesDS.XMLID == nil {
+			continue // TODO log?
+		}
+		if *dsesDS.XMLID != dsName {
+			continue
+		}
+		ds = dsesDS
+	}
+	if ds.ID == nil {
+		return Cfg{}, makeErr(warnings, "delivery service '"+dsName+"' not found! Do you have a regex_remap_*.config location Parameter for a delivery service that doesn't exist?")
+	}
+
+	dses, dsWarns := deliveryServicesToCDNDSes([]DeliveryService{ds})
+	warnings = append(warnings, dsWarns...)
+
+	text := makeHdrComment(hdrComment)
+
+	cdnDS, ok := dses[tc.DeliveryServiceName(dsName)]
+	if !ok {
+		warnings = append(warnings, "ds '"+dsName+"' not in dses, skipping!")
+	} else {
+		text += cdnDS.RegexRemap + "\n"
+		text = strings.Replace(text, `__RETURN__`, "\n", -1)
+	}
+
+	return Cfg{
+		Text:        text,
+		ContentType: ContentTypeRegexRemapDotConfig,
+		LineComment: LineCommentRegexRemapDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type cdnDS struct {
 	OrgServerFQDN string
 	QStringIgnore int
 	CacheURL      string
 	RegexRemap    string
 }
 
-func DeliveryServicesToCDNDSes(dses []tc.DeliveryServiceNullableV30) map[tc.DeliveryServiceName]CDNDS {
-	sDSes := map[tc.DeliveryServiceName]CDNDS{}
+// deliveryServicesToCDNDSes returns the CDNDSes and any warnings.
+func deliveryServicesToCDNDSes(dses []DeliveryService) (map[tc.DeliveryServiceName]cdnDS, []string) {
+	warnings := []string{}
+	sDSes := map[tc.DeliveryServiceName]cdnDS{}
 	for _, ds := range dses {
 		if ds.OrgServerFQDN == nil || ds.QStringIgnore == nil || ds.XMLID == nil {
 			if ds.XMLID == nil {
-				log.Errorln("atscfg.DeliveryServicesToCDNDSes got unknown DS with nil values! Skipping!")
+				warnings = append(warnings, "got unknown DS with nil values! Skipping!")
 			} else {
-				log.Errorln("atscfg.DeliveryServicesToCDNDSes got DS '" + *ds.XMLID + "' with nil values! Skipping!")
+				warnings = append(warnings, "got DS '"+*ds.XMLID+"' with nil values! Skipping!")
 			}
 			continue
 		}
-		sds := CDNDS{OrgServerFQDN: *ds.OrgServerFQDN, QStringIgnore: *ds.QStringIgnore}
+		sds := cdnDS{OrgServerFQDN: *ds.OrgServerFQDN, QStringIgnore: *ds.QStringIgnore}
 		if ds.RegexRemap != nil {
 			sds.RegexRemap = *ds.RegexRemap
 		}
@@ -56,28 +115,5 @@ func DeliveryServicesToCDNDSes(dses []tc.DeliveryServiceNullableV30) map[tc.Deli
 		}
 		sDSes[tc.DeliveryServiceName(*ds.XMLID)] = sds
 	}
-	return sDSes
-}
-
-func MakeRegexRemapDotConfig(
-	cdnName tc.CDNName,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	fileName string,
-	dses map[tc.DeliveryServiceName]CDNDS,
-) string {
-	text := GenericHeaderComment(string(cdnName), toToolName, toURL)
-
-	// TODO verify prefix and suffix exist, and warn if they don't? Perl doesn't
-	dsName := tc.DeliveryServiceName(strings.TrimSuffix(strings.TrimPrefix(fileName, "regex_remap_"), ".config"))
-
-	ds, ok := dses[dsName]
-	if !ok {
-		log.Errorln("MakeRegexRemapDotConfig: ds '" + dsName + "' not in dses, skipping!")
-		return text
-	}
-
-	text += ds.RegexRemap + "\n"
-	text = strings.Replace(text, `__RETURN__`, "\n", -1)
-	return text
+	return sDSes, warnings
 }
diff --git a/lib/go-atscfg/regexremapdotconfig_test.go b/lib/go-atscfg/regexremapdotconfig_test.go
index f70f7e9..39fc59e 100644
--- a/lib/go-atscfg/regexremapdotconfig_test.go
+++ b/lib/go-atscfg/regexremapdotconfig_test.go
@@ -23,35 +23,36 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeRegexRemapDotConfig(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
+
+	dsName := "myds"
+
+	server := makeGenericServer()
+	server.CDNName = &cdnName
 
 	fileName := "regex_remap_myds.config"
 
-	dses := map[tc.DeliveryServiceName]CDNDS{
-		"myds": CDNDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-			RegexRemap:    "myregexremap",
-		},
-	}
+	ds := makeGenericDS()
+	ds.XMLID = &dsName
+	ds.OrgServerFQDN = util.StrPtr("https://myorigin.example.net") // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
+	ds.CacheURL = util.StrPtr("https://mycacheurl.net")
+	ds.RegexRemap = util.StrPtr("myregexremap")
 
-	txt := MakeRegexRemapDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	dses := []DeliveryService{*ds}
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
+	cfg, err := MakeRegexRemapDotConfig(fileName, server, dses, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
-	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment '" + hdr + "', actual: missing")
 	}
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
 		t.Errorf("expected: header comment, actual: missing")
@@ -69,37 +70,40 @@ func TestMakeRegexRemapDotConfig(t *testing.T) {
 }
 
 func TestMakeRegexRemapDotConfigUnusedDS(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
+
+	dsName := "myds"
+
+	server := makeGenericServer()
+	server.CDNName = &cdnName
 
 	fileName := "regex_remap_myds.config"
 
-	dses := map[tc.DeliveryServiceName]CDNDS{
-		"myds": CDNDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-			RegexRemap:    "myregexremap",
-		},
-		"otherds": CDNDS{
-			OrgServerFQDN: "https://otherorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://othercacheurl.net",
-			RegexRemap:    "otherregexremap",
-		},
-	}
+	ds := makeGenericDS()
+	ds.XMLID = &dsName
+	ds.OrgServerFQDN = util.StrPtr("https://myorigin.example.net") // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
+	ds.QStringIgnore = util.IntPtr(0)
+	ds.CacheURL = util.StrPtr("https://mycacheurl.net")
+	ds.RegexRemap = util.StrPtr("myregexremap")
 
-	txt := MakeRegexRemapDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	ds1 := makeGenericDS()
+	ds1.XMLID = util.StrPtr("otherds")
+	ds1.OrgServerFQDN = util.StrPtr("https://otherorigin.example.net") // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
+	ds1.QStringIgnore = util.IntPtr(0)
+	ds1.CacheURL = util.StrPtr("https://othercacheurl.net")
+	ds1.RegexRemap = util.StrPtr("otherregexremap")
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
-	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
+	dses := []DeliveryService{*ds, *ds1}
+
+	cfg, err := MakeRegexRemapDotConfig(fileName, server, dses, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment text '" + hdr + "', actual: missing")
 	}
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
 		t.Errorf("expected: header comment, actual: missing")
@@ -115,43 +119,45 @@ func TestMakeRegexRemapDotConfigUnusedDS(t *testing.T) {
 		t.Errorf("expected: regex remap to contain regex remap, actual: '%v'", txt)
 	}
 
-	if strings.Contains(txt, "mycacheurl") {
+	if strings.Contains(txt, "othercacheurl") {
 		t.Errorf("expected: regex remap to not contain other cacheurl, actual: '%v'", txt)
 	}
-	if strings.Contains(txt, "myorigin") {
+	if strings.Contains(txt, "otherorigin") {
 		t.Errorf("expected: regex remap to not contain other org server fqdn, actual: '%v'", txt)
 	}
 	if strings.Contains(txt, "otherregexremap") {
-		t.Errorf("expected: regex remap to contain other regex remap, actual: '%v'", txt)
+		t.Errorf("expected: regex remap to not contain other regex remap, actual: '%v'", txt)
 	}
 }
 
 func TestMakeRegexRemapDotConfigReplaceReturns(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
+
+	dsName := "myds"
+
+	server := makeGenericServer()
+	server.CDNName = &cdnName
 
 	fileName := "regex_remap_myds.config"
 
-	dses := map[tc.DeliveryServiceName]CDNDS{
-		"myds": CDNDS{
-			OrgServerFQDN: "https://myorigin.example.net", // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
-			QStringIgnore: 0,
-			CacheURL:      "https://mycacheurl.net",
-			RegexRemap:    "myregexremap__RETURN__mypostnewline",
-		},
-	}
+	ds := makeGenericDS()
+	ds.XMLID = &dsName
+	ds.OrgServerFQDN = util.StrPtr("https://myorigin.example.net") // DS "origin_server_fqdn" is actually a URL including the scheme, the name is wrong.
+	ds.QStringIgnore = util.IntPtr(0)
+	ds.CacheURL = util.StrPtr("https://mycacheurl.net")
+	ds.RegexRemap = util.StrPtr("myregexremap__RETURN__mypostnewline")
 
-	txt := MakeRegexRemapDotConfig(cdnName, toToolName, toURL, fileName, dses)
+	dses := []DeliveryService{*ds}
 
-	if !strings.Contains(txt, string(cdnName)) {
-		t.Errorf("expected: cdnName '" + string(cdnName) + "', actual: missing")
+	cfg, err := MakeRegexRemapDotConfig(fileName, server, dses, hdr)
+	if err != nil {
+		t.Fatal(err)
 	}
-	if !strings.Contains(txt, toToolName) {
-		t.Errorf("expected: toToolName '" + toToolName + "', actual: missing")
-	}
-	if !strings.Contains(txt, toURL) {
-		t.Errorf("expected: toURL '" + toURL + "', actual: missing")
+	txt := cfg.Text
+
+	if !strings.Contains(txt, hdr) {
+		t.Errorf("expected: header comment text '" + hdr + "', actual: missing")
 	}
 	if !strings.HasPrefix(strings.TrimSpace(txt), "#") {
 		t.Errorf("expected: header comment, actual: missing")
diff --git a/lib/go-atscfg/regexrevalidatedotconfig.go b/lib/go-atscfg/regexrevalidatedotconfig.go
index a3fc9c1..958c372 100644
--- a/lib/go-atscfg/regexrevalidatedotconfig.go
+++ b/lib/go-atscfg/regexrevalidatedotconfig.go
@@ -20,12 +20,12 @@ package atscfg
  */
 
 import (
+	"fmt"
 	"sort"
 	"strconv"
 	"strings"
 	"time"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
@@ -43,52 +43,82 @@ const RegexRevalidateMinTTL = time.Hour
 const ContentTypeRegexRevalidateDotConfig = ContentTypeTextASCII
 const LineCommentRegexRevalidateDotConfig = LineCommentHash
 
-type Job struct {
-	AssetURL string
-	PurgeEnd time.Time
-}
+func MakeRegexRevalidateDotConfig(
+	server *Server,
+	deliveryServices []DeliveryService,
+	globalParams []tc.Parameter,
+	jobs []tc.Job,
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
+
+	if server.CDNName == nil {
+		return Cfg{}, makeErr(warnings, "server CDNName missing")
+	}
 
-type Jobs []Job
+	params := paramsToMultiMap(filterParams(globalParams, RegexRevalidateFileName, "", "", ""))
 
-func (jb Jobs) Len() int      { return len(jb) }
-func (jb Jobs) Swap(i, j int) { jb[i], jb[j] = jb[j], jb[i] }
-func (jb Jobs) Less(i, j int) bool {
-	if jb[i].AssetURL == jb[j].AssetURL {
-		return jb[i].PurgeEnd.Before(jb[j].PurgeEnd)
+	dsNames := map[string]struct{}{}
+	for _, ds := range deliveryServices {
+		if ds.XMLID == nil {
+			warnings = append(warnings, "got Delivery Service from Traffic Ops with a nil xmlId! Skipping!")
+			continue
+		}
+		dsNames[*ds.XMLID] = struct{}{}
 	}
-	return strings.Compare(jb[i].AssetURL, jb[j].AssetURL) < 0
-}
 
-func MakeRegexRevalidateDotConfig(
-	cdnName tc.CDNName,
-	params map[string][]string, // params on profile GLOBAL fileName RegexRevalidateFileName
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
-	jobs []tc.Job, // jobs should be jobs on DSes on this cdn
-) string {
+	dsJobs := []tc.Job{}
+	for _, job := range jobs {
+		if _, ok := dsNames[job.DeliveryService]; !ok {
+			continue
+		}
+		dsJobs = append(dsJobs, job)
+	}
 
 	// TODO: add cdn, startTime query params to /jobs endpoint
-	err := error(nil)
 
 	maxDays := DefaultMaxRevalDurationDays
 	if maxDaysStrs := params[RegexRevalidateMaxRevalDurationDaysParamName]; len(maxDaysStrs) > 0 {
 		sort.Strings(maxDaysStrs)
+		err := error(nil)
 		if maxDays, err = strconv.Atoi(maxDaysStrs[0]); err != nil { // just use the first, if there were multiple params
-			log.Warnln("making regex revalidate config: max days param '" + maxDaysStrs[0] + "' is not an integer, using default value!")
+			warnings = append(warnings, "max days param '"+maxDaysStrs[0]+"' is not an integer, using default value!")
 			maxDays = DefaultMaxRevalDurationDays
 		}
 	}
 
 	maxReval := time.Duration(maxDays) * time.Hour * 24
 
-	cfgJobs := filterJobs(jobs, maxReval, RegexRevalidateMinTTL)
+	cfgJobs, jobWarns := filterJobs(dsJobs, maxReval, RegexRevalidateMinTTL)
+	warnings = append(warnings, jobWarns...)
 
-	txt := GenericHeaderComment(string(cdnName), toToolName, toURL)
+	txt := makeHdrComment(hdrComment)
 	for _, job := range cfgJobs {
 		txt += job.AssetURL + " " + strconv.FormatInt(job.PurgeEnd.Unix(), 10) + "\n"
 	}
 
-	return txt
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentTypeRegexRevalidateDotConfig,
+		LineComment: LineCommentRegexRevalidateDotConfig,
+		Warnings:    warnings,
+	}, nil
+}
+
+type job struct {
+	AssetURL string
+	PurgeEnd time.Time
+}
+
+type jobsSort []job
+
+func (jb jobsSort) Len() int      { return len(jb) }
+func (jb jobsSort) Swap(i, j int) { jb[i], jb[j] = jb[j], jb[i] }
+func (jb jobsSort) Less(i, j int) bool {
+	if jb[i].AssetURL == jb[j].AssetURL {
+		return jb[i].PurgeEnd.Before(jb[j].PurgeEnd)
+	}
+	return strings.Compare(jb[i].AssetURL, jb[j].AssetURL) < 0
 }
 
 // filterJobs returns only jobs which:
@@ -97,7 +127,10 @@ func MakeRegexRevalidateDotConfig(
 //   - have a start time later than (now + maxReval days). That is, we don't query jobs older than maxReval in the past.
 //   - are "purge" jobs
 //   - have a start_time+ttl > now. That is, jobs that haven't expired yet.
-func filterJobs(jobs []tc.Job, maxReval time.Duration, minTTL time.Duration) []Job {
+// Returns the filtered jobs, and any warnings.
+func filterJobs(jobs []tc.Job, maxReval time.Duration, minTTL time.Duration) ([]job, []string) {
+	warnings := []string{}
+
 	jobMap := map[string]time.Time{}
 	for _, job := range jobs {
 		if job.DeliveryService == "" {
@@ -115,7 +148,7 @@ func filterJobs(jobs []tc.Job, maxReval time.Duration, minTTL time.Duration) []J
 		ttlHoursStr = strings.TrimSuffix(ttlHoursStr, `h`)
 		ttlHours, err := strconv.Atoi(ttlHoursStr)
 		if err != nil {
-			log.Errorf("job %+v has unexpected parameters ttl format, config generation skipping!\n", job)
+			warnings = append(warnings, fmt.Sprintf("job %+v has unexpected parameters ttl format, config generation skipping!\n", job))
 			continue
 		}
 
@@ -128,7 +161,7 @@ func filterJobs(jobs []tc.Job, maxReval time.Duration, minTTL time.Duration) []J
 
 		jobStartTime, err := time.Parse(tc.JobTimeFormat, job.StartTime)
 		if err != nil {
-			log.Errorf("job %+v has unexpected time format, config generation skipping!\n", job)
+			warnings = append(warnings, fmt.Sprintf("job %+v has unexpected time format, config generation skipping!\n", job))
 			continue
 		}
 
@@ -150,11 +183,11 @@ func filterJobs(jobs []tc.Job, maxReval time.Duration, minTTL time.Duration) []J
 		}
 	}
 
-	newJobs := []Job{}
+	newJobs := []job{}
 	for assetURL, purgeEnd := range jobMap {
-		newJobs = append(newJobs, Job{AssetURL: assetURL, PurgeEnd: purgeEnd})
+		newJobs = append(newJobs, job{AssetURL: assetURL, PurgeEnd: purgeEnd})
 	}
-	sort.Sort(Jobs(newJobs))
+	sort.Sort(jobsSort(newJobs))
 
-	return newJobs
+	return newJobs, warnings
 }
diff --git a/lib/go-atscfg/regexrevalidatedotconfig_test.go b/lib/go-atscfg/regexrevalidatedotconfig_test.go
index bea9d87..7b9184f 100644
--- a/lib/go-atscfg/regexrevalidatedotconfig_test.go
+++ b/lib/go-atscfg/regexrevalidatedotconfig_test.go
@@ -25,17 +25,25 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
 )
 
 func TestMakeRegexRevalidateDotConfig(t *testing.T) {
-	cdnName := tc.CDNName("mycdn")
-	toToolName := "my-to"
-	toURL := "my-to.example.net"
+	cdnName := "mycdn"
+	hdr := "myHeaderComment"
 
-	params := map[string][]string{
+	server := makeGenericServer()
+	server.CDNName = &cdnName
+
+	ds := makeGenericDS()
+	ds.CDNName = &cdnName
+	ds.XMLID = util.StrPtr("myds")
+	dses := []DeliveryService{*ds}
+
+	params := makeParamsFromMapArr("GLOBAL", RegexRevalidateFileName, map[string][]string{
 		RegexRevalidateMaxRevalDurationDaysParamName: []string{"42"},
 		"unrelated": []string{"unrelated0", "unrelated1"},
-	}
+	})
 
 	jobs := []tc.Job{
 		tc.Job{
@@ -58,7 +66,11 @@ func TestMakeRegexRevalidateDotConfig(t *testing.T) {
 		},
 	}
 
-	txt := MakeRegexRevalidateDotConfig(cdnName, params, toToolName, toURL, jobs)
+	cfg, err := MakeRegexRevalidateDotConfig(server, dses, params, jobs, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	if !strings.Contains(txt, "assetURL0") {
 		t.Errorf("expected 'assetURL0', actual '%v'", txt)
diff --git a/lib/go-atscfg/remapdotconfig.go b/lib/go-atscfg/remapdotconfig.go
index c6c7b16..2ad3538 100644
--- a/lib/go-atscfg/remapdotconfig.go
+++ b/lib/go-atscfg/remapdotconfig.go
@@ -25,7 +25,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 )
@@ -35,73 +34,96 @@ const CacheKeyParameterConfigFile = "cachekey.config"
 const ContentTypeRemapDotConfig = ContentTypeTextASCII
 const LineCommentRemapDotConfig = LineCommentHash
 
+const RemapConfigRangeDirective = `__RANGE_DIRECTIVE__`
+
 func MakeRemapDotConfig(
-	server *tc.ServerNullable,
-	unfilteredDSes []tc.DeliveryServiceNullableV30,
+	server *Server,
+	unfilteredDSes []DeliveryService,
 	dss []tc.DeliveryServiceServer,
 	dsRegexArr []tc.DeliveryServiceRegexes,
 	serverParams []tc.Parameter,
 	cdn *tc.CDN,
-	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
-	toURL string, // tm.url global parameter (TODO: cache itself?)
 	cacheKeyParams []tc.Parameter,
 	topologies []tc.Topology,
 	cacheGroupArr []tc.CacheGroupNullable,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
-) string {
+	hdrComment string,
+) (Cfg, error) {
+	warnings := []string{}
 	if server.HostName == nil {
-		return "ERROR: server HostName missing"
+		return Cfg{}, makeErr(warnings, "server HostName missing")
 	} else if server.ID == nil {
-		return "ERROR: server ID missing"
+		return Cfg{}, makeErr(warnings, "server ID missing")
 	} else if server.Cachegroup == nil {
-		return "ERROR: server Cachegroup missing"
+		return Cfg{}, makeErr(warnings, "server Cachegroup missing")
 	} else if server.DomainName == nil {
-		return "ERROR: server DomainName missing"
+		return Cfg{}, makeErr(warnings, "server DomainName missing")
 	}
 
 	cdnDomain := cdn.DomainName
 	dsRegexes := makeDSRegexMap(dsRegexArr)
 	// Returned DSes are guaranteed to have a non-nil XMLID, Type, DSCP, ID, and Active.
-	dses := remapFilterDSes(server, dss, unfilteredDSes, cacheKeyParams)
+	dses, dsWarns := remapFilterDSes(server, dss, unfilteredDSes, cacheKeyParams)
+	warnings = append(warnings, dsWarns...)
 
-	dsProfilesCacheKeyConfigParams, err := makeDSProfilesCacheKeyConfigParams(server, dses, cacheKeyParams)
+	dsProfilesCacheKeyConfigParams, paramWarns, err := makeDSProfilesCacheKeyConfigParams(server, dses, cacheKeyParams)
+	warnings = append(warnings, paramWarns...)
 	if err != nil {
-		log.Errorln("Error making Delivery Service Cache Key Params, cache key will be missing! : " + err.Error())
+		warnings = append(warnings, "making Delivery Service Cache Key Params, cache key will be missing! : "+err.Error())
 	}
 
-	atsMajorVersion := getATSMajorVersion(serverParams)
-	serverPackageParamData := makeServerPackageParamData(server, serverParams)
-	cacheURLConfigParams := ParamsToMap(FilterParams(serverParams, CacheURLParameterConfigFile, "", "", ""))
-	cacheGroups, err := MakeCGMap(cacheGroupArr)
+	atsMajorVersion, verWarns := getATSMajorVersion(serverParams)
+	warnings = append(warnings, verWarns...)
+	serverPackageParamData, paramWarns := makeServerPackageParamData(server, serverParams)
+	warnings = append(warnings, paramWarns...)
+	cacheURLConfigParams, paramWarns := paramsToMap(filterParams(serverParams, CacheURLParameterConfigFile, "", "", ""))
+	warnings = append(warnings, paramWarns...)
+	cacheGroups, err := makeCGMap(cacheGroupArr)
 	if err != nil {
-		log.Errorln("making remap.config, config will be malformed! : " + err.Error())
+		return Cfg{}, makeErr(warnings, "making remap.config, config will be malformed! : "+err.Error())
 	}
 
-	nameTopologies := MakeTopologyNameMap(topologies)
+	nameTopologies := makeTopologyNameMap(topologies)
 
-	hdr := GenericHeaderComment(*server.HostName, toToolName, toURL)
+	hdr := makeHdrComment(hdrComment)
+	txt := ""
+	typeWarns := []string{}
 	if tc.CacheTypeFromString(server.Type) == tc.CacheTypeMid {
-		return GetServerConfigRemapDotConfigForMid(atsMajorVersion, dsProfilesCacheKeyConfigParams, dses, dsRegexes, hdr, server, nameTopologies, cacheGroups, serverCapabilities, dsRequiredCapabilities)
+		txt, typeWarns, err = getServerConfigRemapDotConfigForMid(atsMajorVersion, dsProfilesCacheKeyConfigParams, dses, dsRegexes, hdr, server, nameTopologies, cacheGroups, serverCapabilities, dsRequiredCapabilities)
+	} else {
+		txt, typeWarns, err = getServerConfigRemapDotConfigForEdge(cacheURLConfigParams, dsProfilesCacheKeyConfigParams, serverPackageParamData, dses, dsRegexes, atsMajorVersion, hdr, server, nameTopologies, cacheGroups, serverCapabilities, dsRequiredCapabilities, cdnDomain)
 	}
-	return GetServerConfigRemapDotConfigForEdge(cacheURLConfigParams, dsProfilesCacheKeyConfigParams, serverPackageParamData, dses, dsRegexes, atsMajorVersion, hdr, server, nameTopologies, cacheGroups, serverCapabilities, dsRequiredCapabilities, cdnDomain)
+	warnings = append(warnings, typeWarns...)
+	if err != nil {
+		return Cfg{}, makeErr(warnings, err.Error()) // the GetFor funcs include error context
+	}
+
+	return Cfg{
+		Text:        txt,
+		ContentType: ContentTypeRemapDotConfig,
+		LineComment: LineCommentRemapDotConfig,
+		Warnings:    warnings,
+	}, nil
 }
 
-func GetServerConfigRemapDotConfigForMid(
+// getServerConfigRemapDotConfigForMid returns the remap lines, any warnings, and any error.
+func getServerConfigRemapDotConfigForMid(
 	atsMajorVersion int,
 	profilesCacheKeyConfigParams map[int]map[string]string,
-	dses []tc.DeliveryServiceNullableV30,
+	dses []DeliveryService,
 	dsRegexes map[tc.DeliveryServiceName][]tc.DeliveryServiceRegex,
 	header string,
-	server *tc.ServerNullable,
+	server *Server,
 	nameTopologies map[TopologyName]tc.Topology,
 	cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
-) string {
+) (string, []string, error) {
+	warnings := []string{}
 	midRemaps := map[string]string{}
 	for _, ds := range dses {
-		if !HasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
+		if !hasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
 			continue
 		}
 
@@ -109,7 +131,7 @@ func GetServerConfigRemapDotConfigForMid(
 		if *ds.Topology != "" && hasTopology {
 			topoIncludesServer, err := topologyIncludesServerNullable(topology, server)
 			if err != nil {
-				return "ERROR: getting Topology Server inclusion: " + err.Error()
+				return "", warnings, errors.New("getting Topology Server inclusion: " + err.Error())
 			}
 			if !topoIncludesServer {
 				continue
@@ -120,7 +142,7 @@ func GetServerConfigRemapDotConfigForMid(
 		}
 
 		if ds.OrgServerFQDN == nil || *ds.OrgServerFQDN == "" {
-			log.Warnf("GetServerConfigRemapDotConfigForMid ds '" + *ds.XMLID + "' has no origin fqdn, skipping!") // TODO confirm - Perl uses without checking!
+			warnings = append(warnings, "ds '"+*ds.XMLID+"' has no origin fqdn, skipping!") // TODO confirm - Perl uses without checking!
 			continue
 		}
 
@@ -136,13 +158,17 @@ func GetServerConfigRemapDotConfigForMid(
 		midRemap := ""
 
 		if *ds.Topology != "" {
-			midRemap += MakeDSTopologyHeaderRewriteTxt(ds, tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups)
+			topoTxt, err := makeDSTopologyHeaderRewriteTxt(ds, tc.CacheGroupName(*server.Cachegroup), topology, cacheGroups)
+			if err != nil {
+				return "", warnings, err
+			}
+			midRemap += topoTxt
 		} else if ds.MidHeaderRewrite != nil && *ds.MidHeaderRewrite != "" {
-			midRemap += ` @plugin=header_rewrite.so @pparam=` + MidHeaderRewriteConfigFileName(*ds.XMLID)
+			midRemap += ` @plugin=header_rewrite.so @pparam=` + midHeaderRewriteConfigFileName(*ds.XMLID)
 		}
 
 		if ds.QStringIgnore != nil && *ds.QStringIgnore == tc.QueryStringIgnoreIgnoreInCacheKeyAndPassUp {
-			qstr, addedCacheURL, addedCacheKey := GetQStringIgnoreRemap(atsMajorVersion)
+			qstr, addedCacheURL, addedCacheKey := getQStringIgnoreRemap(atsMajorVersion)
 			if addedCacheURL {
 				hasCacheURL = true
 			}
@@ -153,22 +179,22 @@ func GetServerConfigRemapDotConfigForMid(
 		}
 		if ds.CacheURL != nil && *ds.CacheURL != "" {
 			if hasCacheURL {
-				log.Errorln("Making remap.config for Delivery Service '" + *ds.XMLID + "': qstring_ignore and cacheurl both add cacheurl, but ATS cacheurl doesn't work correctly with multiple entries! Adding anyway!")
+				warnings = append(warnings, "Delivery Service '"+*ds.XMLID+"': qstring_ignore and cacheurl both add cacheurl, but ATS cacheurl doesn't work correctly with multiple entries! Adding anyway!")
 			}
-			midRemap += ` @plugin=cacheurl.so @pparam=` + CacheURLConfigFileName(*ds.XMLID)
+			midRemap += ` @plugin=cacheurl.so @pparam=` + cacheURLConfigFileName(*ds.XMLID)
 		}
 
 		if ds.ProfileID != nil && len(profilesCacheKeyConfigParams[*ds.ProfileID]) > 0 {
 			if hasCacheKey {
-				log.Errorln("Making remap.config for Delivery Service '" + *ds.XMLID + "': qstring_ignore and cachekey params both add cachekey, but ATS cachekey doesn't work correctly with multiple entries! Adding anyway!")
+				warnings = append(warnings, "Delivery Service '"+*ds.XMLID+"': qstring_ignore and cachekey params both add cachekey, but ATS cachekey doesn't work correctly with multiple entries! Adding anyway!")
 			}
 			midRemap += ` @plugin=cachekey.so`
 
-			dsProfileCacheKeyParams := []KeyVal{}
+			dsProfileCacheKeyParams := []keyVal{}
 			for name, val := range profilesCacheKeyConfigParams[*ds.ProfileID] {
-				dsProfileCacheKeyParams = append(dsProfileCacheKeyParams, KeyVal{Key: name, Val: val})
+				dsProfileCacheKeyParams = append(dsProfileCacheKeyParams, keyVal{Key: name, Val: val})
 			}
-			sort.Sort(KeyVals(dsProfileCacheKeyParams))
+			sort.Sort(keyVals(dsProfileCacheKeyParams))
 			for _, nameVal := range dsProfileCacheKeyParams {
 				name := nameVal.Key
 				val := nameVal.Val
@@ -192,29 +218,31 @@ func GetServerConfigRemapDotConfigForMid(
 
 	text := header
 	text += strings.Join(textLines, "")
-	return text
+	return text, warnings, nil
 }
 
-func GetServerConfigRemapDotConfigForEdge(
+// getServerConfigRemapDotConfigForEdge returns the remap lines, any warnings, and any error.
+func getServerConfigRemapDotConfigForEdge(
 	cacheURLConfigParams map[string]string,
 	profilesCacheKeyConfigParams map[int]map[string]string,
 	serverPackageParamData map[string]string, // map[paramName]paramVal for this server, config file 'package'
-	dses []tc.DeliveryServiceNullableV30,
+	dses []DeliveryService,
 	dsRegexes map[tc.DeliveryServiceName][]tc.DeliveryServiceRegex,
 	atsMajorVersion int,
 	header string,
-	server *tc.ServerNullable,
+	server *Server,
 	nameTopologies map[TopologyName]tc.Topology,
 	cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable,
 	serverCapabilities map[int]map[ServerCapability]struct{},
 	dsRequiredCapabilities map[int]map[ServerCapability]struct{},
 	cdnDomain string,
-) string {
+) (string, []string, error) {
+	warnings := []string{}
 	textLines := []string{}
 
 	for _, ds := range dses {
 		for _, dsRegex := range dsRegexes[tc.DeliveryServiceName(*ds.XMLID)] {
-			if !HasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
+			if !hasRequiredCapabilities(serverCapabilities[*server.ID], dsRequiredCapabilities[*ds.ID]) {
 				continue
 			}
 
@@ -222,7 +250,7 @@ func GetServerConfigRemapDotConfigForEdge(
 			if *ds.Topology != "" && hasTopology {
 				topoIncludesServer, err := topologyIncludesServerNullable(topology, server)
 				if err != nil {
-					return "ERROR: getting topology server inclusion: " + err.Error()
+					return "", warnings, errors.New("getting topology server inclusion: " + err.Error())
 				}
 				if !topoIncludesServer {
 					continue
@@ -231,7 +259,7 @@ func GetServerConfigRemapDotConfigForEdge(
 			remapText := ""
 			if *ds.Type == tc.DSTypeAnyMap {
 				if ds.RemapText == nil {
-					log.Errorln("ds '" + *ds.XMLID + "' is ANY_MAP, but has no remap text - skipping")
+					warnings = append(warnings, "ds '"+*ds.XMLID+"' is ANY_MAP, but has no remap text - skipping")
 					continue
 				}
 				remapText = *ds.RemapText + "\n"
@@ -239,9 +267,9 @@ func GetServerConfigRemapDotConfigForEdge(
 				continue
 			}
 
-			remapLines, err := MakeEdgeDSDataRemapLines(ds, dsRegex, server, cdnDomain)
+			remapLines, err := makeEdgeDSDataRemapLines(ds, dsRegex, server, cdnDomain)
 			if err != nil {
-				log.Errorln("making remap lines for DS '" + *ds.XMLID + "' - skipping! : " + err.Error())
+				warnings = append(warnings, "DS '"+*ds.XMLID+"' - skipping! : "+err.Error())
 				continue
 			}
 
@@ -250,7 +278,12 @@ func GetServerConfigRemapDotConfigForEdge(
 				if ds.ProfileID != nil {
 					profilecacheKeyConfigParams = profilesCacheKeyConfigParams[*ds.ProfileID]
 				}
-				remapText = BuildEdgeRemapLine(cacheURLConfigParams, atsMajorVersion, server, serverPackageParamData, remapText, ds, dsRegex, line.From, line.To, profilecacheKeyConfigParams, cacheGroups, nameTopologies)
+				remapWarns := []string{}
+				remapText, remapWarns, err = buildEdgeRemapLine(cacheURLConfigParams, atsMajorVersion, server, serverPackageParamData, remapText, ds, dsRegex, line.From, line.To, profilecacheKeyConfigParams, cacheGroups, nameTopologies)
+				warnings = append(warnings, remapWarns...)
+				if err != nil {
+					return "", warnings, err
+				}
 				if hasTopology {
 					remapText += " # topology '" + topology.Name + "'"
 				}
@@ -263,27 +296,27 @@ func GetServerConfigRemapDotConfigForEdge(
 	text := header
 	sort.Strings(textLines)
 	text += strings.Join(textLines, "")
-	return text
+	return text, warnings, nil
 }
 
-const RemapConfigRangeDirective = `__RANGE_DIRECTIVE__`
-
-// BuildEdgeRemapLine builds the remap line for the given server and delivery service.
+// buildEdgeRemapLine builds the remap line for the given server and delivery service.
 // The cacheKeyConfigParams map may be nil, if this ds profile had no cache key config params.
-func BuildEdgeRemapLine(
+// Returns the remap line, any warnings, and any error.
+func buildEdgeRemapLine(
 	cacheURLConfigParams map[string]string,
 	atsMajorVersion int,
-	server *tc.ServerNullable,
+	server *Server,
 	pData map[string]string,
 	text string,
-	ds tc.DeliveryServiceNullableV30,
+	ds DeliveryService,
 	dsRegex tc.DeliveryServiceRegex,
 	mapFrom string,
 	mapTo string,
 	cacheKeyConfigParams map[string]string,
 	cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable,
 	nameTopologies map[TopologyName]tc.Topology,
-) string {
+) (string, []string, error) {
+	warnings := []string{}
 	// ds = 'remap' in perl
 	mapFrom = strings.Replace(mapFrom, `__http__`, *server.HostName, -1)
 
@@ -294,9 +327,13 @@ func BuildEdgeRemapLine(
 	}
 
 	if *ds.Topology != "" {
-		text += MakeDSTopologyHeaderRewriteTxt(ds, tc.CacheGroupName(*server.Cachegroup), nameTopologies[TopologyName(*ds.Topology)], cacheGroups)
+		topoTxt, err := makeDSTopologyHeaderRewriteTxt(ds, tc.CacheGroupName(*server.Cachegroup), nameTopologies[TopologyName(*ds.Topology)], cacheGroups)
+		if err != nil {
+			return "", warnings, err
+		}
+		text += topoTxt
 	} else if ds.EdgeHeaderRewrite != nil && *ds.EdgeHeaderRewrite != "" {
-		text += ` @plugin=header_rewrite.so @pparam=` + EdgeHeaderRewriteConfigFileName(*ds.XMLID)
+		text += ` @plugin=header_rewrite.so @pparam=` + edgeHeaderRewriteConfigFileName(*ds.XMLID)
 	}
 
 	if ds.SigningAlgorithm != nil && *ds.SigningAlgorithm != "" {
@@ -318,9 +355,9 @@ func BuildEdgeRemapLine(
 			text += ` @plugin=regex_remap.so @pparam=` + dqsFile
 		} else if *ds.QStringIgnore == tc.QueryStringIgnoreIgnoreInCacheKeyAndPassUp {
 			if _, globalExists := cacheURLConfigParams["location"]; globalExists {
-				log.Warnln("Making remap.config for Delivery Service '" + *ds.XMLID + "': qstring_ignore == 1, but global cacheurl.config param exists, so skipping remap rename config_file=cacheurl.config parameter")
+				warnings = append(warnings, "Delivery Service '"+*ds.XMLID+"': qstring_ignore == 1, but global cacheurl.config param exists, so skipping remap rename config_file=cacheurl.config parameter")
 			} else {
-				qstr, addedCacheURL, addedCacheKey := GetQStringIgnoreRemap(atsMajorVersion)
+				qstr, addedCacheURL, addedCacheKey := getQStringIgnoreRemap(atsMajorVersion)
 				if addedCacheURL {
 					hasCacheURL = true
 				}
@@ -334,14 +371,14 @@ func BuildEdgeRemapLine(
 
 	if ds.CacheURL != nil && *ds.CacheURL != "" {
 		if hasCacheURL {
-			log.Errorln("Making remap.config for Delivery Service '" + *ds.XMLID + "': qstring_ignore and cacheurl both add cacheurl, but ATS cacheurl doesn't work correctly with multiple entries! Adding anyway!")
+			warnings = append(warnings, "Delivery Service '"+*ds.XMLID+"': qstring_ignore and cacheurl both add cacheurl, but ATS cacheurl doesn't work correctly with multiple entries! Adding anyway!")
 		}
-		text += ` @plugin=cacheurl.so @pparam=` + CacheURLConfigFileName(*ds.XMLID)
+		text += ` @plugin=cacheurl.so @pparam=` + cacheURLConfigFileName(*ds.XMLID)
 	}
 
 	if len(cacheKeyConfigParams) > 0 {
 		if hasCacheKey {
-			log.Errorln("Making remap.config for Delivery Service '" + *ds.XMLID + "': qstring_ignore and params both add cachekey, but ATS cachekey doesn't work correctly with multiple entries! Adding anyway!")
+			warnings = append(warnings, "Delivery Service '"+*ds.XMLID+"': qstring_ignore and params both add cachekey, but ATS cachekey doesn't work correctly with multiple entries! Adding anyway!")
 		}
 		text += ` @plugin=cachekey.so`
 
@@ -390,13 +427,16 @@ func BuildEdgeRemapLine(
 	if ds.FQPacingRate != nil && *ds.FQPacingRate > 0 {
 		text += ` @plugin=fq_pacing.so @pparam=--rate=` + strconv.Itoa(*ds.FQPacingRate)
 	}
-	return text
+	return text, warnings, nil
 }
 
-// MakeDSTopologyHeaderRewriteTxt returns the appropriate header rewrite remap line text for the given DS on the given server.
+// makeDSTopologyHeaderRewriteTxt returns the appropriate header rewrite remap line text for the given DS on the given server, and any error.
 // May be empty, if the DS has no header rewrite for the server's position in the topology.
-func MakeDSTopologyHeaderRewriteTxt(ds tc.DeliveryServiceNullableV30, cg tc.CacheGroupName, topology tc.Topology, cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable) string {
-	placement := getTopologyPlacement(cg, topology, cacheGroups, &ds)
+func makeDSTopologyHeaderRewriteTxt(ds DeliveryService, cg tc.CacheGroupName, topology tc.Topology, cacheGroups map[tc.CacheGroupName]tc.CacheGroupNullable) (string, error) {
+	placement, err := getTopologyPlacement(cg, topology, cacheGroups, &ds)
+	if err != nil {
+		return "", errors.New("getting topology placement: " + err.Error())
+	}
 	txt := ""
 	const pluginTxt = ` @plugin=header_rewrite.so @pparam=`
 	if placement.IsFirstCacheTier && ds.FirstHeaderRewrite != nil && *ds.FirstHeaderRewrite != "" {
@@ -408,22 +448,22 @@ func MakeDSTopologyHeaderRewriteTxt(ds tc.DeliveryServiceNullableV30, cg tc.Cach
 	if placement.IsLastCacheTier && ds.LastHeaderRewrite != nil && *ds.LastHeaderRewrite != "" {
 		txt += pluginTxt + LastHeaderRewriteConfigFileName(*ds.XMLID) + ` `
 	}
-	return txt
+	return txt, nil
 }
 
-type RemapLine struct {
+type remapLine struct {
 	From string
 	To   string
 }
 
-// MakeEdgeDSDataRemapLines returns the remap lines for the given server and delivery service.
+// makeEdgeDSDataRemapLines returns the remap lines for the given server and delivery service.
 // Returns nil, if the given server and ds have no remap lines, i.e. the DS match is not a host regex, or has no origin FQDN.
-func MakeEdgeDSDataRemapLines(
-	ds tc.DeliveryServiceNullableV30,
+func makeEdgeDSDataRemapLines(
+	ds DeliveryService,
 	dsRegex tc.DeliveryServiceRegex,
-	server *tc.ServerNullable,
+	server *Server,
 	cdnDomain string,
-) ([]RemapLine, error) {
+) ([]remapLine, error) {
 	if tc.DSMatchType(dsRegex.Type) != tc.DSMatchTypeHostRegex || ds.OrgServerFQDN == nil || *ds.OrgServerFQDN == "" {
 		return nil, nil
 	}
@@ -437,7 +477,7 @@ func MakeEdgeDSDataRemapLines(
 		return nil, errors.New("ds missing domain")
 	}
 
-	remapLines := []RemapLine{}
+	remapLines := []remapLine{}
 	hostRegex := dsRegex.Pattern
 	mapTo := *ds.OrgServerFQDN + "/"
 
@@ -471,29 +511,29 @@ func MakeEdgeDSDataRemapLines(
 	}
 
 	if *ds.Protocol == tc.DSProtocolHTTP || *ds.Protocol == tc.DSProtocolHTTPAndHTTPS {
-		remapLines = append(remapLines, RemapLine{From: mapFromHTTP, To: mapTo})
+		remapLines = append(remapLines, remapLine{From: mapFromHTTP, To: mapTo})
 	}
 	if *ds.Protocol == tc.DSProtocolHTTPS || *ds.Protocol == tc.DSProtocolHTTPToHTTPS || *ds.Protocol == tc.DSProtocolHTTPAndHTTPS {
-		remapLines = append(remapLines, RemapLine{From: mapFromHTTPS, To: mapTo})
+		remapLines = append(remapLines, remapLine{From: mapFromHTTPS, To: mapTo})
 	}
 
 	return remapLines, nil
 }
 
-func EdgeHeaderRewriteConfigFileName(dsName string) string {
+func edgeHeaderRewriteConfigFileName(dsName string) string {
 	return "hdr_rw_" + dsName + ".config"
 }
 
-func MidHeaderRewriteConfigFileName(dsName string) string {
+func midHeaderRewriteConfigFileName(dsName string) string {
 	return "hdr_rw_mid_" + dsName + ".config"
 }
 
-func CacheURLConfigFileName(dsName string) string {
+func cacheURLConfigFileName(dsName string) string {
 	return "cacheurl_" + dsName + ".config"
 }
 
-// GetQStringIgnoreRemap returns the remap, whether cacheurl was added, and whether cachekey was added.
-func GetQStringIgnoreRemap(atsMajorVersion int) (string, bool, bool) {
+// getQStringIgnoreRemap returns the remap, whether cacheurl was added, and whether cachekey was added.
+func getQStringIgnoreRemap(atsMajorVersion int) (string, bool, bool) {
 	if atsMajorVersion >= 6 {
 		addingCacheURL := false
 		addingCacheKey := true
@@ -506,7 +546,10 @@ func GetQStringIgnoreRemap(atsMajorVersion int) (string, bool, bool) {
 }
 
 // makeServerPackageParamData returns a map[paramName]paramVal for this server, config file 'package'.
-func makeServerPackageParamData(server *tc.ServerNullable, serverParams []tc.Parameter) map[string]string {
+// Returns the param data, and any warnings
+func makeServerPackageParamData(server *Server, serverParams []tc.Parameter) (map[string]string, []string) {
+	warnings := []string{}
+
 	serverPackageParamData := map[string]string{}
 	for _, param := range serverParams {
 		if param.ConfigFile != "package" { // TODO put in const
@@ -528,21 +571,23 @@ func makeServerPackageParamData(server *tc.ServerNullable, serverParams []tc.Par
 
 		if val, ok := serverPackageParamData[paramName]; ok {
 			if val < paramValue {
-				log.Errorln("remap config generation got multiple parameters for server package name '" + paramName + "' - ignoring '" + paramValue + "'")
+				warnings = append(warnings, "got multiple parameters for server package name '"+paramName+"' - ignoring '"+paramValue+"'")
 				continue
 			} else {
-				log.Errorln("config generation got multiple parameters for server package name '" + paramName + "' - ignoring '" + val + "'")
+				warnings = append(warnings, "got multiple parameters for server package name '"+paramName+"' - ignoring '"+val+"'")
 			}
 		}
 		serverPackageParamData[paramName] = paramValue
 	}
-	return serverPackageParamData
+	return serverPackageParamData, warnings
 }
 
 // remapFilterDSes filters Delivery Services to be used to generate remap.config for the given server.
 // Returned DSes are guaranteed to have a non-nil XMLID, Type, DSCP, ID, Active, and Topology.
 // If a DS has a nil Topology, OrgServerFQDN, FirstHeaderRewrite, InnerHeaderRewrite, or LastHeaderRewrite, "" is assigned.
-func remapFilterDSes(server *tc.ServerNullable, dss []tc.DeliveryServiceServer, dses []tc.DeliveryServiceNullableV30, cacheKeyParams []tc.Parameter) []tc.DeliveryServiceNullableV30 {
+// Returns the filtered delivery services, and any warnings
+func remapFilterDSes(server *Server, dss []tc.DeliveryServiceServer, dses []DeliveryService, cacheKeyParams []tc.Parameter) ([]DeliveryService, []string) {
+	warnings := []string{}
 	isMid := strings.HasPrefix(server.Type, string(tc.CacheTypeMid))
 
 	serverIDs := map[int]struct{}{}
@@ -560,7 +605,7 @@ func remapFilterDSes(server *tc.ServerNullable, dss []tc.DeliveryServiceServer,
 		dsIDs[*ds.ID] = struct{}{}
 	}
 
-	dsServers := FilterDSS(dss, dsIDs, serverIDs)
+	dsServers := filterDSS(dss, dsIDs, serverIDs)
 
 	dssMap := map[int]map[int]struct{}{} // set of map[dsID][serverID]
 	for _, dss := range dsServers {
@@ -579,7 +624,7 @@ func remapFilterDSes(server *tc.ServerNullable, dss []tc.DeliveryServiceServer,
 		useInactive = true
 	}
 
-	filteredDSes := []tc.DeliveryServiceNullableV30{}
+	filteredDSes := []DeliveryService{}
 	for _, ds := range dses {
 		if ds.Topology == nil {
 			ds.Topology = util.StrPtr("")
@@ -597,19 +642,19 @@ func remapFilterDSes(server *tc.ServerNullable, dss []tc.DeliveryServiceServer,
 			ds.LastHeaderRewrite = util.StrPtr("")
 		}
 		if ds.XMLID == nil {
-			log.Errorln("Remap config gen got Delivery Service with nil XMLID, skipping!")
+			warnings = append(warnings, "got Delivery Service with nil XMLID, skipping!")
 			continue
 		} else if ds.Type == nil {
-			log.Errorln("Remap config gen got Delivery Service '" + *ds.XMLID + "'  with nil Type, skipping!")
+			warnings = append(warnings, "got Delivery Service '"+*ds.XMLID+"'  with nil Type, skipping!")
 			continue
 		} else if ds.DSCP == nil {
-			log.Errorln("Remap config gen got Delivery Service '" + *ds.XMLID + "'  with nil DSCP, skipping!")
+			warnings = append(warnings, "got Delivery Service '"+*ds.XMLID+"'  with nil DSCP, skipping!")
 			continue
 		} else if ds.ID == nil {
-			log.Errorln("Remap config gen got Delivery Service '" + *ds.XMLID + "'  with nil ID, skipping!")
+			warnings = append(warnings, "got Delivery Service '"+*ds.XMLID+"'  with nil ID, skipping!")
 			continue
 		} else if ds.Active == nil {
-			log.Errorln("Remap config gen got Delivery Service '" + *ds.XMLID + "'  with nil Active, skipping!")
+			warnings = append(warnings, "got Delivery Service '"+*ds.XMLID+"'  with nil Active, skipping!")
 			continue
 		} else if _, ok := dssMap[*ds.ID]; !ok && *ds.Topology == "" {
 			continue // normal, not an error, this DS just isn't assigned to our Cache
@@ -618,45 +663,19 @@ func remapFilterDSes(server *tc.ServerNullable, dss []tc.DeliveryServiceServer,
 		}
 		filteredDSes = append(filteredDSes, ds)
 	}
-	return filteredDSes
-}
-
-// getATSMajorVersion returns the ATS major version from the config_file 'package' name 'trafficserver' Parameter on the given Server Profile Parameters.
-// If no Parameter is found, or the value is malformed, a warning or error is logged and DefaultATSVersion is returned.
-func getATSMajorVersion(serverParams []tc.Parameter) int {
-	atsVersionParam := ""
-	for _, param := range serverParams {
-		if param.ConfigFile != "package" || param.Name != "trafficserver" {
-			continue
-		}
-		atsVersionParam = param.Value
-		break
-	}
-	if atsVersionParam == "" {
-		log.Warnln("ATS version Parameter (config_file 'package' name 'trafficserver') not found on Server Profile, using default")
-		atsVersionParam = DefaultATSVersion
-	}
-
-	atsMajorVer, err := GetATSMajorVersionFromATSVersion(atsVersionParam)
-	if err != nil {
-		log.Errorln("getting ATS major version from server Profile Parameter, using default: " + err.Error())
-		atsMajorVer, err = GetATSMajorVersionFromATSVersion(DefaultATSVersion)
-		if err != nil {
-			// should never happen
-			log.Errorln("getting ATS major version from default version! Should never happen! Using 0, config will be malformed! : " + err.Error())
-		}
-	}
-	return atsMajorVer
+	return filteredDSes, warnings
 }
 
 // makeDSProfilesCacheKeyConfigParams returns a map[ProfileID][ParamName]ParamValue for the cache key params for each profile.
-func makeDSProfilesCacheKeyConfigParams(server *tc.ServerNullable, dses []tc.DeliveryServiceNullableV30, cacheKeyParams []tc.Parameter) (map[int]map[string]string, error) {
-	cacheKeyParamsWithProfiles, err := TCParamsToParamsWithProfiles(cacheKeyParams)
+// Returns the params, any warnings, and any error.
+func makeDSProfilesCacheKeyConfigParams(server *Server, dses []DeliveryService, cacheKeyParams []tc.Parameter) (map[int]map[string]string, []string, error) {
+	warnings := []string{}
+	cacheKeyParamsWithProfiles, err := tcParamsToParamsWithProfiles(cacheKeyParams)
 	if err != nil {
-		return nil, errors.New("decoding cache key parameter profiles: " + err.Error())
+		return nil, warnings, errors.New("decoding cache key parameter profiles: " + err.Error())
 	}
 
-	cacheKeyParamsWithProfilesMap := ParameterWithProfilesToMap(cacheKeyParamsWithProfiles)
+	cacheKeyParamsWithProfilesMap := parameterWithProfilesToMap(cacheKeyParamsWithProfiles)
 
 	dsProfileNamesToIDs := map[string]int{}
 	for _, ds := range dses {
@@ -675,49 +694,49 @@ func makeDSProfilesCacheKeyConfigParams(server *tc.ServerNullable, dses []tc.Del
 				}
 				if val, ok := dsProfilesCacheKeyConfigParams[dsProfileID][param.Name]; ok {
 					if val < param.Value {
-						log.Errorln("remap config generation got multiple parameters for name '" + param.Name + "' - ignoring '" + param.Value + "'")
+						warnings = append(warnings, "got multiple parameters for name '"+param.Name+"' - ignoring '"+param.Value+"'")
 						continue
 					} else {
-						log.Errorln("remap config generation got multiple parameters for name '" + param.Name + "' - ignoring '" + val + "'")
+						warnings = append(warnings, "got multiple parameters for name '"+param.Name+"' - ignoring '"+val+"'")
 					}
 				}
 				dsProfilesCacheKeyConfigParams[dsProfileID][param.Name] = param.Value
 			}
 		}
 	}
-	return dsProfilesCacheKeyConfigParams, nil
+	return dsProfilesCacheKeyConfigParams, warnings, nil
 }
 
-type DeliveryServiceRegexesSortByTypeThenSetNum []tc.DeliveryServiceRegex
+type deliveryServiceRegexesSortByTypeThenSetNum []tc.DeliveryServiceRegex
 
-func (r DeliveryServiceRegexesSortByTypeThenSetNum) Len() int { return len(r) }
-func (r DeliveryServiceRegexesSortByTypeThenSetNum) Less(i, j int) bool {
+func (r deliveryServiceRegexesSortByTypeThenSetNum) Len() int { return len(r) }
+func (r deliveryServiceRegexesSortByTypeThenSetNum) Less(i, j int) bool {
 	if rc := strings.Compare(r[i].Type, r[j].Type); rc != 0 {
 		return rc < 0
 	}
 	return r[i].SetNumber < r[j].SetNumber
 }
-func (r DeliveryServiceRegexesSortByTypeThenSetNum) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
+func (r deliveryServiceRegexesSortByTypeThenSetNum) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
 
 func makeDSRegexMap(regexes []tc.DeliveryServiceRegexes) map[tc.DeliveryServiceName][]tc.DeliveryServiceRegex {
 	dsRegexMap := map[tc.DeliveryServiceName][]tc.DeliveryServiceRegex{}
 	for _, dsRegex := range regexes {
-		sort.Sort(DeliveryServiceRegexesSortByTypeThenSetNum(dsRegex.Regexes))
+		sort.Sort(deliveryServiceRegexesSortByTypeThenSetNum(dsRegex.Regexes))
 		dsRegexMap[tc.DeliveryServiceName(dsRegex.DSName)] = dsRegex.Regexes
 	}
 	return dsRegexMap
 }
 
-type KeyVal struct {
+type keyVal struct {
 	Key string
 	Val string
 }
 
-type KeyVals []KeyVal
+type keyVals []keyVal
 
-func (ks KeyVals) Len() int      { return len(ks) }
-func (ks KeyVals) Swap(i, j int) { ks[i], ks[j] = ks[j], ks[i] }
-func (ks KeyVals) Less(i, j int) bool {
+func (ks keyVals) Len() int      { return len(ks) }
+func (ks keyVals) Swap(i, j int) { ks[i], ks[j] = ks[j], ks[i] }
+func (ks keyVals) Less(i, j int) bool {
 	if ks[i].Key != ks[j].Key {
 		return ks[i].Key < ks[j].Key
 	}
diff --git a/lib/go-atscfg/remapdotconfig_test.go b/lib/go-atscfg/remapdotconfig_test.go
index b1c8f0f..8b1c60c 100644
--- a/lib/go-atscfg/remapdotconfig_test.go
+++ b/lib/go-atscfg/remapdotconfig_test.go
@@ -28,14 +28,12 @@ import (
 )
 
 func TestMakeRemapDotConfig(t *testing.T) {
-	serverName := tc.CacheName("server0")
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 	server.Type = "EDGE"
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE")
 	ds.Type = &dsType
@@ -58,7 +56,7 @@ func TestMakeRemapDotConfig(t *testing.T) {
 	ds.Protocol = util.IntPtr(0)
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -120,16 +118,21 @@ func TestMakeRemapDotConfig(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, string(serverName), toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
 	if len(txtLines) != 2 {
-		t.Errorf("expected one line for each remap plus a comment, actual: '%v' count %v", txt, len(txtLines))
+		t.Log(cfg.Warnings)
+		t.Fatalf("expected one line for each remap plus a comment, actual: '%v' count %v", txt, len(txtLines))
 	}
 
 	remapLine := txtLines[1]
@@ -148,12 +151,11 @@ func TestMakeRemapDotConfig(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidLiveLocalExcluded(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE")
 	ds.Type = &dsType
@@ -176,7 +178,7 @@ func TestMakeRemapDotConfigMidLiveLocalExcluded(t *testing.T) {
 	ds.Protocol = util.IntPtr(0)
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -244,11 +246,15 @@ func TestMakeRemapDotConfigMidLiveLocalExcluded(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -258,12 +264,11 @@ func TestMakeRemapDotConfigMidLiveLocalExcluded(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMid(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -286,7 +291,7 @@ func TestMakeRemapDotConfigMid(t *testing.T) {
 	ds.Protocol = util.IntPtr(0)
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -354,11 +359,15 @@ func TestMakeRemapDotConfigMid(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -382,12 +391,11 @@ func TestMakeRemapDotConfigMid(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigNilOrigin(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -410,7 +418,7 @@ func TestMakeRemapDotConfigNilOrigin(t *testing.T) {
 	ds.Protocol = util.IntPtr(0)
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -478,11 +486,15 @@ func TestMakeRemapDotConfigNilOrigin(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -492,12 +504,11 @@ func TestMakeRemapDotConfigNilOrigin(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigEmptyOrigin(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -520,7 +531,7 @@ func TestMakeRemapDotConfigEmptyOrigin(t *testing.T) {
 	ds.Protocol = util.IntPtr(0)
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -588,11 +599,15 @@ func TestMakeRemapDotConfigEmptyOrigin(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -602,12 +617,11 @@ func TestMakeRemapDotConfigEmptyOrigin(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigDuplicateOrigins(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -631,7 +645,7 @@ func TestMakeRemapDotConfigDuplicateOrigins(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	ds2 := tc.DeliveryServiceNullableV30{}
+	ds2 := DeliveryService{}
 	ds2.ID = util.IntPtr(49)
 	dsType2 := tc.DSType("HTTP_LIVE_NATNL")
 	ds2.Type = &dsType2
@@ -655,7 +669,7 @@ func TestMakeRemapDotConfigDuplicateOrigins(t *testing.T) {
 	ds2.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds2.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds, ds2}
+	dses := []DeliveryService{ds, ds2}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -737,11 +751,15 @@ func TestMakeRemapDotConfigDuplicateOrigins(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -751,12 +769,11 @@ func TestMakeRemapDotConfigDuplicateOrigins(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigNilMidRewrite(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -780,7 +797,7 @@ func TestMakeRemapDotConfigNilMidRewrite(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -848,11 +865,15 @@ func TestMakeRemapDotConfigNilMidRewrite(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -885,12 +906,11 @@ func TestMakeRemapDotConfigNilMidRewrite(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidHasNoEdgeRewrite(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -914,7 +934,7 @@ func TestMakeRemapDotConfigMidHasNoEdgeRewrite(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -982,11 +1002,15 @@ func TestMakeRemapDotConfigMidHasNoEdgeRewrite(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1010,12 +1034,11 @@ func TestMakeRemapDotConfigMidHasNoEdgeRewrite(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidQStringPassUpATS7CacheKey(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1039,7 +1062,7 @@ func TestMakeRemapDotConfigMidQStringPassUpATS7CacheKey(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1107,11 +1130,15 @@ func TestMakeRemapDotConfigMidQStringPassUpATS7CacheKey(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1142,12 +1169,11 @@ func TestMakeRemapDotConfigMidQStringPassUpATS7CacheKey(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidQStringPassUpATS5CacheURL(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1171,7 +1197,7 @@ func TestMakeRemapDotConfigMidQStringPassUpATS5CacheURL(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1239,11 +1265,15 @@ func TestMakeRemapDotConfigMidQStringPassUpATS5CacheURL(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1274,12 +1304,11 @@ func TestMakeRemapDotConfigMidQStringPassUpATS5CacheURL(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidProfileCacheKey(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1304,7 +1333,7 @@ func TestMakeRemapDotConfigMidProfileCacheKey(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1384,11 +1413,15 @@ func TestMakeRemapDotConfigMidProfileCacheKey(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1420,12 +1453,11 @@ func TestMakeRemapDotConfigMidProfileCacheKey(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidRangeRequestHandling(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1450,7 +1482,7 @@ func TestMakeRemapDotConfigMidRangeRequestHandling(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1530,11 +1562,15 @@ func TestMakeRemapDotConfigMidRangeRequestHandling(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1558,12 +1594,11 @@ func TestMakeRemapDotConfigMidRangeRequestHandling(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigMidSlicePluginRangeRequestHandling(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1588,7 +1623,7 @@ func TestMakeRemapDotConfigMidSlicePluginRangeRequestHandling(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds}
+	dses := []DeliveryService{ds}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1668,11 +1703,15 @@ func TestMakeRemapDotConfigMidSlicePluginRangeRequestHandling(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1700,12 +1739,11 @@ func TestMakeRemapDotConfigMidSlicePluginRangeRequestHandling(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigFirstExcludedSecondIncluded(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("HTTP_LIVE_NATNL")
 	ds.Type = &dsType
@@ -1730,7 +1768,7 @@ func TestMakeRemapDotConfigFirstExcludedSecondIncluded(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	ds2 := tc.DeliveryServiceNullableV30{}
+	ds2 := DeliveryService{}
 	ds2.ID = util.IntPtr(48)
 	dsType2 := tc.DSType("HTTP_LIVE_NATNL")
 	ds2.Type = &dsType2
@@ -1755,7 +1793,7 @@ func TestMakeRemapDotConfigFirstExcludedSecondIncluded(t *testing.T) {
 	ds2.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds2.Active = util.BoolPtr(true)
 
-	dses := []tc.DeliveryServiceNullableV30{ds, ds2}
+	dses := []DeliveryService{ds, ds2}
 
 	dss := []tc.DeliveryServiceServer{
 		tc.DeliveryServiceServer{
@@ -1835,11 +1873,15 @@ func TestMakeRemapDotConfigFirstExcludedSecondIncluded(t *testing.T) {
 	serverCapabilities := map[int]map[ServerCapability]struct{}{}
 	dsRequiredCapabilities := map[int]map[ServerCapability]struct{}{}
 
-	txt := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, toToolName, toURL, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities)
+	cfg, err := MakeRemapDotConfig(server, dses, dss, dsRegexes, serverParams, cdn, cacheKeyParams, topologies, cgs, serverCapabilities, dsRequiredCapabilities, hdr)
+	if err != nil {
+		t.Fatal(err)
+	}
+	txt := cfg.Text
 
 	txt = strings.TrimSpace(txt)
 
-	testComment(t, txt, *server.HostName, toToolName, toURL)
+	testComment(t, txt, hdr)
 
 	txtLines := strings.Split(txt, "\n")
 
@@ -1849,13 +1891,12 @@ func TestMakeRemapDotConfigFirstExcludedSecondIncluded(t *testing.T) {
 }
 
 func TestMakeRemapDotConfigAnyMap(t *testing.T) {
-	toToolName := "to0"
-	toURL := "trafficops.example.net"
+	hdr := "myHeaderComment"
 
 	server := makeTestRemapServer()
 	server.Type = "EDGE"
 
-	ds := tc.DeliveryServiceNullableV30{}
+	ds := DeliveryService{}
 	ds.ID = util.IntPtr(48)
 	dsType := tc.DSType("ANY_MAP")
 	ds.Type = &dsType
@@ -1880,7 +1921,7 @@ func TestMakeRemapDotConfigAnyMap(t *testing.T) {
 	ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	ds.Active = util.BoolPtr(true)
 
-	ds2 := tc.DeliveryServiceNullableV30{}
+	ds2 := DeliveryService{}
 	ds2.ID = util.IntPtr(49)
... 15783 lines suppressed ...