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:22 UTC

[trafficcontrol] branch 5.0.x updated (a3158d5 -> 3d0a90f)

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

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


    from a3158d5  Update Go version to 1.15.5 (#5278)
     new a73b193  Fix an issue with API v1/2 tests not cleaning up certain v3 structures (topologies) (#5282)
     new 5adfc79  Fix LetsEncryptDnsChallengeWatcher config location (#5280)
     new 5da7a39  fixed federations/all IMS (#5269)
     new c93bc76  Update CDN in a Box to CentOS 8 (#5252)
     new 7f4b8ca  Remove unnecessary CDN in a Box waits (#5283)
     new 1e3aa0d  Change ORT/atstccfg to use standard TC objects (#5247)
     new 7181282  Add PUSH and PURGE denial to mid tier caches. (#5292)
     new 8c4617c  Fix merge non-conflict error (#5300)
     new 3d0a90f  Add validation to topology updates and server updates/deletions (#5299)

The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .github/actions/to-integration-tests/entrypoint.sh |   2 +-
 .github/workflows/ciab.yaml                        |   4 +-
 .github/workflows/traffic ops.yml                  |  16 +-
 CHANGELOG.md                                       |   5 +
 docs/source/admin/quick_howto/ciab.rst             |  21 +-
 docs/source/admin/traffic_monitor.rst              |   2 +-
 docs/source/admin/traffic_ops.rst                  |   2 +-
 docs/source/admin/traffic_router.rst               |   2 +-
 docs/source/development/traffic_ops.rst            |   3 +
 infrastructure/cdn-in-a-box/Makefile               |   2 +-
 infrastructure/cdn-in-a-box/README.md              |   2 +-
 .../docker-compose.traffic-ops-test.yml            |   7 +-
 .../docker-compose.traffic-portal-test.yml         |   6 +
 infrastructure/cdn-in-a-box/docker-compose.yml     |  10 +
 infrastructure/cdn-in-a-box/edge/Dockerfile        |  89 +-
 infrastructure/cdn-in-a-box/mid/Dockerfile         |  89 +-
 .../optional/docker-compose.socksproxy.yml         |   2 +
 .../cdn-in-a-box/optional/docker-compose.vnc.yml   |   1 +
 .../cdn-in-a-box/optional/socksproxy/Dockerfile    |  16 +-
 .../cdn-in-a-box/optional/vnc/Dockerfile           |  15 +-
 infrastructure/cdn-in-a-box/origin/Dockerfile      |   1 -
 .../cdn-in-a-box/ort/traffic_ops_ort/packaging.py  |   4 +-
 .../cdn-in-a-box/traffic_monitor/Dockerfile        |  16 +-
 .../cdn-in-a-box/traffic_monitor/Dockerfile-debug  |  15 +-
 infrastructure/cdn-in-a-box/traffic_ops/Dockerfile |  41 +-
 .../cdn-in-a-box/traffic_ops/Dockerfile-debug      |   2 +-
 .../cdn-in-a-box/traffic_ops/Dockerfile-go         |  30 +-
 .../cdn-in-a-box/traffic_ops/Dockerfile-go-debug   |  15 +-
 infrastructure/cdn-in-a-box/traffic_ops/run.sh     |   6 +-
 .../traffic_ops/set-to-ips-from-dns.sh             |   1 -
 .../cdn-in-a-box/traffic_ops/trafficops-init.sh    |  10 +-
 .../cdn-in-a-box/traffic_portal/Dockerfile         |  16 +-
 .../traffic_portal_integration_test/Dockerfile     |  29 +-
 .../cdn-in-a-box/traffic_router/Dockerfile         |  19 +-
 .../cdn-in-a-box/traffic_stats/Dockerfile          |  14 +-
 .../cdn-in-a-box/traffic_stats/Dockerfile-debug    |  15 +-
 .../poststart.d/02-add-search-schema.sh            |   2 +
 infrastructure/cdn-in-a-box/traffic_vault/run.sh   |   2 -
 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                  | 185 ++--
 lib/go-atscfg/ipallowdotconfig_test.go             | 101 ++-
 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/testing/api/v1/todb_test.go            |   1 +
 traffic_ops/testing/api/v2/todb_test.go            |   1 +
 traffic_ops/testing/api/v3/servers_test.go         |  63 +-
 traffic_ops/testing/api/v3/tc-fixtures.json        | 335 ++++++++
 traffic_ops/testing/api/v3/topologies_test.go      |  66 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  45 +
 .../federations/allfederations.go                  |  10 +-
 traffic_ops/traffic_ops_golang/server/servers.go   |  64 +-
 .../traffic_ops_golang/topology/topologies.go      |  95 +-
 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 +-
 .../core/ds/LetsEncryptDnsChallengeWatcher.java    |  26 +-
 .../src/main/webapp/WEB-INF/applicationContext.xml |  15 +-
 225 files changed, 6327 insertions(+), 13258 deletions(-)
 delete mode 100644 lib/go-atscfg/unknownconfig.go
 delete mode 100644 lib/go-atscfg/unknownconfig_test.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/astatsdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/atsdotrules.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/bgfetchdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/cachedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/cacheurldotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/chkconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/dropqstringdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/facts.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/headerrewritedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/headerrewritemiddotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/hostingdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/ipallowdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/loggingdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/loggingdotyaml.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/logsxmldotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/meta.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/packages.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/parentdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/plugindotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/recordsdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/regexremapdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/regexrevalidatedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/remapdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/servercachedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/serverunknownconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/setdscpdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/sslmulticertdotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/storagedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/sysctldotconf.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/topologyheaderrewritedotconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/unknownconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/urisigningconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/urisigningconfig_test.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/urlsigconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/urlsigconfig_test.go
 delete mode 100644 traffic_ops_ort/atstccfg/cfgfile/volumedotconfig.go
 create mode 100644 traffic_ops_ort/atstccfg/cfgfile/wrappers.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/LICENSE
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/VERSION
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/changeset.txt
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/README.md
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/about.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/asn.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/atsconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/cachegroup.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/cachegroup_parameters.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/cdn.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/cdn_domains.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/cdnfederations.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/coordinate.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/crconfig.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryservice.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryservice_endpoints.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryservice_request_comments.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryservice_requests.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryservices_required_capabilities.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/deliveryserviceserver.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/division.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/dsuser.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/endpoints.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/federation.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/federation_resolver.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/hardware.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/iso.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/job.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/log.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/origin.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/parameter.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/phys_location.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/ping.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/profile.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/profile_parameter.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/region.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/role.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/server.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/server_server_capabilities.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/server_update_status.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/servercapability.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/servercheck.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/serversstatus.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/session.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/staticdnsentry.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/stats_summary.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/status.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/steering.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/steeringtarget.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/tenant.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/tenant_endpoints.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/toextension.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/traffic_monitor.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/traffic_stats.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/type.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/update.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/user.go
 delete mode 100644 traffic_ops_ort/atstccfg/toreq/vendor/github.com/apache/trafficcontrol/traffic_ops/client/util.go


[trafficcontrol] 02/09: Fix LetsEncryptDnsChallengeWatcher config location (#5280)

Posted by oc...@apache.org.
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 5adfc79c5873f2ec8232ac2362785f75dcee58e6
Author: Steve Hamrick <sh...@users.noreply.github.com>
AuthorDate: Fri Nov 13 10:56:47 2020 -0700

    Fix LetsEncryptDnsChallengeWatcher config location (#5280)
    
    * Fix LE Watcher
    
    * Forgot Changelog
    
    * Use tabs
    
    Co-authored-by: Steve Hamrick <st...@comcast.com>
    (cherry picked from commit bf100338b405cf3f5e5d0d318f8f7c4772f16115)
---
 CHANGELOG.md                                       |  2 ++
 .../core/ds/LetsEncryptDnsChallengeWatcher.java    | 26 ++++++++++++++++------
 .../src/main/webapp/WEB-INF/applicationContext.xml | 15 +++++++------
 3 files changed, 29 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b14c8e2..a5fc170 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -127,6 +127,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Fixed #5237 - /isos API endpoint rejecting valid IPv6 addresses with CIDR-notation network prefixes.
 - Fixed an issue with Traffic Monitor to fix peer polling to work as expected
 - Fixed #5274 - CDN in a Box's Traffic Vault image failed to build due to Basho's repo responding with 402 Payment Required. The repo has been removed from the image.
+- #5069 - For LetsEncryptDnsChallengerWatcher in Traffic Router, the cr-config location is configurable instead of only looking at `/opt/traffic_router/db/cr-config.json`
+
 
 ### Changed
 - Changed some Traffic Ops Go Client methods to use `DeliveryServiceNullable` inputs and outputs.
diff --git a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/ds/LetsEncryptDnsChallengeWatcher.java b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/ds/LetsEncryptDnsChallengeWatcher.java
index d568085..f009f6a 100644
--- a/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/ds/LetsEncryptDnsChallengeWatcher.java
+++ b/traffic_router/core/src/main/java/com/comcast/cdn/traffic_control/traffic_router/core/ds/LetsEncryptDnsChallengeWatcher.java
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.log4j.Logger;
 
 import java.io.*;
+import java.nio.file.Path;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.List;
@@ -36,13 +37,10 @@ import java.util.List;
 public class LetsEncryptDnsChallengeWatcher extends AbstractResourceWatcher {
     private static final Logger LOGGER = Logger.getLogger(LetsEncryptDnsChallengeWatcher.class);
     public static final String DEFAULT_LE_DNS_CHALLENGE_URL = "https://${toHostname}/api/2.0/letsencrypt/dnsrecords/";
-    private static final String configFile = "/opt/traffic_router/db/cr-config.json";
 
+    private String configFile;
     private ConfigHandler configHandler;
-
-    public void setConfigHandler(final ConfigHandler configHandler) {
-        this.configHandler = configHandler;
-    }
+    private Path databasesDirectory;
 
     public LetsEncryptDnsChallengeWatcher() {
         setDatabaseUrl(DEFAULT_LE_DNS_CHALLENGE_URL);
@@ -134,7 +132,7 @@ public class LetsEncryptDnsChallengeWatcher extends AbstractResourceWatcher {
 
     private String readConfigFile() {
         try {
-            final InputStream is = new FileInputStream(configFile);
+            final InputStream is = new FileInputStream(databasesDirectory.resolve(configFile).toString());
             final BufferedReader buf = new BufferedReader(new InputStreamReader(is));
             String line = buf.readLine();
             final StringBuilder sb = new StringBuilder();
@@ -144,7 +142,7 @@ public class LetsEncryptDnsChallengeWatcher extends AbstractResourceWatcher {
             }
             return sb.toString();
         } catch (Exception e) {
-            LOGGER.error("Could not read cr-config file.");
+            LOGGER.error("Could not read cr-config file " + configFile + ".");
             return null;
         }
     }
@@ -178,4 +176,18 @@ public class LetsEncryptDnsChallengeWatcher extends AbstractResourceWatcher {
         return newStaticDnsEntriesNode;
     }
 
+    public void setConfigHandler(final ConfigHandler configHandler) {
+        this.configHandler = configHandler;
+    }
+    public ConfigHandler getConfigHandler() {
+        return this.configHandler;
+    }
+
+    public void setDatabasesDirectory(final Path databasesDirectory) {
+        this.databasesDirectory = databasesDirectory;
+    }
+
+    public void setConfigFile(final String configFile) {
+        this.configFile = configFile;
+    }
 }
diff --git a/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml b/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml
index 544cf55..55ef754 100644
--- a/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml
+++ b/traffic_router/core/src/main/webapp/WEB-INF/applicationContext.xml
@@ -89,14 +89,15 @@
 		<property name="steeringRegistry" ref="steeringRegistry" />
 	</bean>
 
-    <bean id="letsEncryptDnsChallengeWatcher" class="com.comcast.cdn.traffic_control.traffic_router.core.ds.LetsEncryptDnsChallengeWatcher">
-        <property name="executorService" ref="ScheduledExecutorService" />
-        <property name="databasesDirectory" ref="databasesDir" />
-        <property name="databaseName" value="$[cache.letsencrypt.database:letsencrypt.json]" />
-        <property name="trafficOpsUtils" ref="trafficOpsUtils" />
-        <property name="trafficRouterManager" ref="trafficRouterManager" />
+	<bean id="letsEncryptDnsChallengeWatcher" class="com.comcast.cdn.traffic_control.traffic_router.core.ds.LetsEncryptDnsChallengeWatcher">
+		<property name="executorService" ref="ScheduledExecutorService" />
+		<property name="databasesDirectory" ref="databasesDir" />
+		<property name="databaseName" value="$[cache.letsencrypt.database:letsencrypt.json]" />
+		<property name="trafficOpsUtils" ref="trafficOpsUtils" />
+		<property name="trafficRouterManager" ref="trafficRouterManager" />
 		<property name="pollingInterval" value="60000" />
-        <property name="configHandler" ref="ConfigHandler" />
+		<property name="configFile" value="$[cache.config.json:cr-config.json]" />
+		<property name="configHandler" ref="ConfigHandler" />
 	</bean>
 
 	<bean id="certificatesQueue" class="java.util.concurrent.ArrayBlockingQueue" >


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

Posted by oc...@apache.org.
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 ...


[trafficcontrol] 04/09: Update CDN in a Box to CentOS 8 (#5252)

Posted by oc...@apache.org.
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 c93bc76b03f6244fdbe7a3bb0a6d79ead644d4a5
Author: Zach Hoffman <zr...@apache.org>
AuthorDate: Mon Nov 16 17:21:32 2020 -0700

    Update CDN in a Box to CentOS 8 (#5252)
    
    * Use CentOS 8 for CDN-in-a-Box images by default
    
    * Use dnf instead of yum
    
    * If on CentOS 8, enable additional repositories before trying to install
    packages
    
    * Install python3 packages, not python36
    
    * Specify additional packages to install that would have been
    automatically installed on CentOS 7
    
    * Use Traffic Server built for CentOS 7 on CentOS 8
    
    * Update for openssl 1.1: Open permissions on generated certs
    
    * We only need certs to be world-readable, not world-writable
    
    * Alpine Linux is not based on CentOS
    
    * Strip potential minor version from CENTOS_VERSION before using in repo RPM URL
    
    * Make Traffic Portal UI tests Dockerfile CentOS 8-compatible
    
    * More reliable way of deriving major version from CENTOS_VERSION that
    will work with CentOS 10
    
    (cherry picked from commit a4a87000c5b4a5ec26dc5ec1fd911dc58d1e0cd8)
---
 .github/workflows/ciab.yaml                        |  4 +-
 CHANGELOG.md                                       |  1 +
 docs/source/admin/quick_howto/ciab.rst             | 17 +++--
 docs/source/admin/traffic_monitor.rst              |  2 +-
 docs/source/admin/traffic_ops.rst                  |  2 +-
 docs/source/admin/traffic_router.rst               |  2 +-
 infrastructure/cdn-in-a-box/Makefile               |  2 +-
 infrastructure/cdn-in-a-box/README.md              |  2 +-
 .../docker-compose.traffic-portal-test.yml         |  2 +
 infrastructure/cdn-in-a-box/docker-compose.yml     |  7 ++
 infrastructure/cdn-in-a-box/edge/Dockerfile        | 89 ++++++++++++++++++++--
 infrastructure/cdn-in-a-box/mid/Dockerfile         | 89 ++++++++++++++++++++--
 .../optional/docker-compose.socksproxy.yml         |  2 +
 .../cdn-in-a-box/optional/docker-compose.vnc.yml   |  1 +
 .../cdn-in-a-box/optional/socksproxy/Dockerfile    | 16 ++--
 .../cdn-in-a-box/optional/vnc/Dockerfile           | 15 +++-
 infrastructure/cdn-in-a-box/origin/Dockerfile      |  1 -
 .../cdn-in-a-box/ort/traffic_ops_ort/packaging.py  |  4 +-
 .../cdn-in-a-box/traffic_monitor/Dockerfile        | 16 ++--
 .../cdn-in-a-box/traffic_monitor/Dockerfile-debug  | 15 +++-
 infrastructure/cdn-in-a-box/traffic_ops/Dockerfile | 41 +++++++---
 .../cdn-in-a-box/traffic_ops/Dockerfile-debug      |  2 +-
 .../cdn-in-a-box/traffic_ops/Dockerfile-go         | 30 +++++---
 .../cdn-in-a-box/traffic_ops/Dockerfile-go-debug   | 15 +++-
 infrastructure/cdn-in-a-box/traffic_ops/run.sh     |  6 +-
 .../cdn-in-a-box/traffic_portal/Dockerfile         | 16 ++--
 .../traffic_portal_integration_test/Dockerfile     | 29 +++++--
 .../cdn-in-a-box/traffic_router/Dockerfile         | 19 +++--
 .../cdn-in-a-box/traffic_stats/Dockerfile          | 14 +++-
 .../cdn-in-a-box/traffic_stats/Dockerfile-debug    | 15 +++-
 30 files changed, 376 insertions(+), 100 deletions(-)

diff --git a/.github/workflows/ciab.yaml b/.github/workflows/ciab.yaml
index d8dc16e..aab60b3 100644
--- a/.github/workflows/ciab.yaml
+++ b/.github/workflows/ciab.yaml
@@ -212,7 +212,7 @@ jobs:
           path: ${{ github.workspace }}/dist/
       - name: Cache Perl modules
         env:
-          CENTOS_VERSION: 7
+          CENTOS_VERSION: 8
         uses: actions/cache@v2
         with:
           path: ${{ github.workspace }}/infrastructure/cdn-in-a-box/traffic_ops/local
@@ -223,7 +223,7 @@ jobs:
             ${{ runner.os }}-cpan-centos-${{ env.CENTOS_VERSION }}-
       - name: Build CDN-in-a-Box images
         env:
-          CENTOS_VERSION: 7
+          CENTOS_VERSION: 8
         uses: ./.github/actions/build-ciab
       - name: Start CDN-in-a-Box
         uses: ./.github/actions/run-ciab
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d14fd9..07861c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -158,6 +158,7 @@ will be returned indicating that overlap exists.
 - Changed certificate loading code in Traffic Router to use Bouncy Castle instead of deprecated Sun libraries.
 - Changed deprecated AsyncHttpClient Java dependency to use new active mirror and updated to version 2.12.1.
 - Changed Traffic Portal to use the more performant and powerful ag-grid for the delivery service request (DSR) table.
+- Updated CDN in a Box to CentOS 8 and added `CENTOS_VERSION` Docker build arg so CDN in a Box can be built for CentOS 7, if desired
 
 ### Deprecated
 - Deprecated the non-nullable `DeliveryService` Go struct and other structs that use it. `DeliveryServiceNullable` structs should be used instead.
diff --git a/docs/source/admin/quick_howto/ciab.rst b/docs/source/admin/quick_howto/ciab.rst
index 8368c72..0f135ca 100644
--- a/docs/source/admin/quick_howto/ciab.rst
+++ b/docs/source/admin/quick_howto/ciab.rst
@@ -38,12 +38,19 @@ The CDN in a Box directory is found within the Traffic Control repository at :fi
 
 .. note:: These can also be specified via the ``RPM`` variable to a direct Docker build of the component - with the exception of Traffic Router, which instead accepts ``JDK8_RPM`` to specify a Java Development Kit RPM,  ``TRAFFIC_ROUTER_RPM`` to specify a Traffic Router RPM, and  ``TOMCAT_RPM`` to specify an Apache Tomcat RPM.
 
-These can all be supplied manually via the steps in :ref:`dev-building` (for Traffic Control component RPMs) or via some external source. Alternatively, the :file:`infrastructure/cdn-in-a-box/Makefile` file contains recipes to build all of these - simply run :manpage:`make(1)` from the :file:`infrastructure/cdn-in-a-box/` directory. Once all RPM dependencies have been satisfied, run ``docker-compose build`` from the :file:`infrastructure/cdn-in-a-box/` directory to construct the images n [...]
+These can all be supplied manually via the steps in :ref:`dev-building` (for Traffic Control component RPMs) or via some external source. Alternatively, the :file:`infrastructure/cdn-in-a-box/Makefile` file contains recipes to build all of these - simply run :manpage:`make(1)` from the :file:`infrastructure/cdn-in-a-box/` directory. Once all RPM dependencies have been satisfied, run ``docker-compose build --parallel`` from the :file:`infrastructure/cdn-in-a-box/` directory to construct t [...]
 
 .. tip:: If you have gone through the steps to :ref:`dev-building-natively`, you can run ``make native`` instead of ``make`` to build the RPMs quickly. Another option is running ``make -j4`` to build 4 components at once, if your computer can handle it.
 
 .. tip:: When updating CDN-in-a-Box, there is no need to remove old images before building new ones. Docker detects which files are updated and only reuses cached layers that have not changed.
 
+By default, CDN in a Box will be based on CentOS 8. To base CDN in a Box on CentOS 7, set the ``CENTOS_VERSION`` `build arg <https://docs.docker.com/engine/reference/builder/#arg>`_ to ``7`` (it defaults to ``8``):
+
+.. code-block:: shell
+	:caption: Building CDN in a Box to run CentOS 7 instead of CentOS 8
+
+	docker-compose build --parallel --build-arg CENTOS_VERSION=7
+
 The image that takes the takes the longest to build is the ``trafficops-perl`` image. In order to avoid needing to download, build, and test 239 Perl CPAN modules each time you rebuild the image from scratch, you can run the following command while running CDN in a Box in order to skip building the Perl modules next time:
 
 .. code-block:: shell
@@ -200,14 +207,14 @@ Importing the :abbr:`CA (Certificate Authority)` certificate on Windows
 #. Import the CIAB intermediate :abbr:`CA (Certificate Authority)` certificate into :menuselection:`Trusted Root Certification Authorities --> Certificates`.
 #. Restart all HTTPS clients (browsers, etc).
 
-Importing the :abbr:`CA (Certificate Authority)` certificate on Linux/Centos7
------------------------------------------------------------------------------
+Importing the :abbr:`CA (Certificate Authority)` certificate on CentOS 8 (Linux)
+--------------------------------------------------------------------------------
 #. Copy the CIAB full chain :abbr:`CA (Certificate Authority)` certificate bundle from :file:`infrastructure/cdn-in-a-box/traffic_ops/ca/CIAB-CA-fullchain.crt` to path :file:`/etc/pki/ca-trust/source/anchors/`.
 #. Run ``update-ca-trust-extract`` as the root user or with :manpage:`sudo(8)`.
 #. Restart all HTTPS clients (browsers, etc).
 
-Importing the :abbr:`CA (Certificate Authority)` certificate on Linux/Ubuntu
-----------------------------------------------------------------------------
+Importing the :abbr:`CA (Certificate Authority)` certificate on Ubuntu (Linux)
+------------------------------------------------------------------------------
 #. Copy the CIAB full chain :abbr:`CA (Certificate Authority)` certificate bundle from :file:`infrastructure/cdn-in-a-box/traffic_ops/ca/CIAB-CA-fullchain.crt` to path :file:`/usr/local/share/ca-certificates/`.
 #. Run ``update-ca-certificates`` as the root user or with :manpage:`sudo(8)`.
 #. Restart all HTTPS clients (browsers, etc).
diff --git a/docs/source/admin/traffic_monitor.rst b/docs/source/admin/traffic_monitor.rst
index 832eece..a349107 100644
--- a/docs/source/admin/traffic_monitor.rst
+++ b/docs/source/admin/traffic_monitor.rst
@@ -23,7 +23,7 @@ Installing Traffic Monitor
 ==========================
 The following are hard requirements requirements for Traffic Monitor to operate:
 
-* CentOS 7+
+* CentOS 7 or later
 * Successful install of Traffic Ops (usually on a separate machine)
 * Administrative access to the Traffic Ops (usually on a separate machine)
 
diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst
index 613018f..9f2ff3b 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -30,7 +30,7 @@ System Requirements
 -------------------
 The user must have the following for a successful minimal install:
 
-- CentOS 7+
+- CentOS 7 or later
 - Two machines - physical or virtual -, each with at least two (v)CPUs, 4GB of RAM, and 20 GB of disk space
 - Access to CentOS Base and EPEL :manpage:`yum(8)` repositories
 - Access to `The Comprehensive Perl Archive Network (CPAN) <http://www.cpan.org/>`_
diff --git a/docs/source/admin/traffic_router.rst b/docs/source/admin/traffic_router.rst
index a4b67af..95f081c 100644
--- a/docs/source/admin/traffic_router.rst
+++ b/docs/source/admin/traffic_router.rst
@@ -21,7 +21,7 @@ Traffic Router Administration
 
 Requirements
 ============
-* CentOS 7
+* CentOS 7 or later
 * 4 CPUs
 * 8GB of RAM
 * Successful install of Traffic Ops (usually on another machine)
diff --git a/infrastructure/cdn-in-a-box/Makefile b/infrastructure/cdn-in-a-box/Makefile
index 176aa87..94d3a8c 100644
--- a/infrastructure/cdn-in-a-box/Makefile
+++ b/infrastructure/cdn-in-a-box/Makefile
@@ -18,7 +18,7 @@
 ############################################################
 # Dockerfile to build Edge-Tier Cache container images for
 # Apache Traffic Control
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
 # Check for proper invocation
diff --git a/infrastructure/cdn-in-a-box/README.md b/infrastructure/cdn-in-a-box/README.md
index b16ba09..6623c0e 100644
--- a/infrastructure/cdn-in-a-box/README.md
+++ b/infrastructure/cdn-in-a-box/README.md
@@ -30,7 +30,7 @@ minimal CDN for full system testing.
 The containers run on Docker, and require Docker (tested v17.05.0-ce) and Docker
 Compose (tested v1.9.0) to build and run. On most 'nix systems these can be installed
 via the distribution's package manager under the names `docker-ce` and
-`docker-compose`, respectively (e.g. `sudo yum install docker-ce`).
+`docker-compose`, respectively (e.g. `sudo dnf install docker-ce`).
 
 Each container (except the origin) requires an `.rpm` file to install the Traffic Control
 component for which it is responsible. You can download these `*.rpm` files from an archive
diff --git a/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml b/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
index 89b0c03..f285029 100644
--- a/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
+++ b/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
@@ -31,6 +31,8 @@ services:
     build:
       context: ../..
       dockerfile: infrastructure/cdn-in-a-box/traffic_portal_integration_test/Dockerfile
+      args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
     env_file:
       - variables.env
     hostname: portal-integration
diff --git a/infrastructure/cdn-in-a-box/docker-compose.yml b/infrastructure/cdn-in-a-box/docker-compose.yml
index 9fd186c..a78c52b 100644
--- a/infrastructure/cdn-in-a-box/docker-compose.yml
+++ b/infrastructure/cdn-in-a-box/docker-compose.yml
@@ -59,6 +59,7 @@ services:
       context: .
       dockerfile: traffic_ops/Dockerfile-go
       args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
         TRAFFIC_OPS_RPM: traffic_ops/traffic_ops.rpm
     depends_on:
       - db
@@ -80,6 +81,7 @@ services:
       context: ../..
       dockerfile: infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
       args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
         TRAFFIC_OPS_RPM: infrastructure/cdn-in-a-box/traffic_ops/traffic_ops.rpm
     depends_on:
       - db
@@ -101,6 +103,7 @@ services:
       context: .
       dockerfile: traffic_portal/Dockerfile
       args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
         TRAFFIC_PORTAL_RPM: traffic_portal/traffic_portal.rpm
     depends_on:
       - enroller
@@ -118,6 +121,7 @@ services:
       context: .
       dockerfile: traffic_monitor/Dockerfile
       args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
         TRAFFIC_MONITOR_RPM: traffic_monitor/traffic_monitor.rpm
     depends_on:
       - enroller
@@ -135,6 +139,7 @@ services:
       context: .
       dockerfile: traffic_router/Dockerfile
       args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
         TRAFFIC_ROUTER_RPM: traffic_router/traffic_router.rpm
         TOMCAT_RPM: traffic_router/tomcat.rpm
     depends_on:
@@ -151,6 +156,8 @@ services:
     build:
       context: .
       dockerfile: traffic_stats/Dockerfile
+      args:
+        CENTOS_VERSION: ${CENTOS_VERSION:-8}
     image: trafficstats
     depends_on:
       - enroller
diff --git a/infrastructure/cdn-in-a-box/edge/Dockerfile b/infrastructure/cdn-in-a-box/edge/Dockerfile
index fc0f798..9ab0579 100644
--- a/infrastructure/cdn-in-a-box/edge/Dockerfile
+++ b/infrastructure/cdn-in-a-box/edge/Dockerfile
@@ -18,21 +18,98 @@
 ############################################################
 # Dockerfile to build Edge-Tier Cache container images for
 # Apache Traffic Control
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
-FROM centos:7 AS common-cache-server-layers
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} AS common-cache-server-layers
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 EXPOSE 80
 
+RUN dnf -y install epel-release && \
+    if [[ "${CENTOS_VERSION%%.*}" -ge 8 ]]; then \
+        additional_packages='compat-openssl10 pkgconf-pkg-config' || \
+        exit 1; \
+    else \
+        additional_packages=openssl || \
+        exit 1; \
+    fi && \
+    dnf -y install              \
+        GeoIP                   \
+        groff-base              \
+        hwloc                   \
+        hwloc-libs              \
+        kyotocabinet-libs       \
+        libtool-ltdl            \
+        libunwind               \
+        lzo                     \
+        make                    \
+        numactl-libs            \
+        perl                    \
+        perl-Carp               \
+        perl-constant           \
+        perl-Data-Dumper        \
+        perl-Encode             \
+        perl-Exporter           \
+        perl-File-Path          \
+        perl-File-Temp          \
+        perl-Filter             \
+        perl-Getopt-Long        \
+        perl-HTTP-Tiny          \
+        perl-libs               \
+        perl-macros             \
+        perl-parent             \
+        perl-PathTools          \
+        perl-Pod-Escapes        \
+        perl-podlators          \
+        perl-Pod-Perldoc        \
+        perl-Pod-Simple         \
+        perl-Pod-Usage          \
+        perl-Scalar-List-Utils  \
+        perl-Socket             \
+        perl-Storable           \
+        perl-Text-ParseWords    \
+        perl-threads            \
+        perl-threads-shared     \
+        perl-Time-HiRes         \
+        perl-Time-Local         \
+        perl-URI                \
+        tcl                     \
+        $additional_packages && \
+    if [[ "${CENTOS_VERSION%%.*}" -eq 8 ]]; then \
+        set -- \
+            # Pretend that we have the right library versions.
+            # TODO: Use a proper CentOS 7 or 8 RPM once trafficserver
+            # is in EPEL again (see apache/trafficserver#6855)
+            libtcl8.6.so        libtcl8.5.so     \
+            libncursesw.so.6    libncursesw.so.5 \
+            libtinfo.so.6       libtinfo.so.5    \
+            || exit 1; \
+    fi && \
+    cd /usr/lib64 && \
+    while [[ $# -gt 0 ]]; do \
+        source="$1" && \
+        shift && \
+        target="$1" && \
+        shift && \
+        ln -s "$source" "$target" || exit 1; \
+    done
 
 ADD https://ci.trafficserver.apache.org/RPMS/CentOS7/trafficserver-7.1.4-2.el7.x86_64.rpm /trafficserver.rpm
 ADD https://ci.trafficserver.apache.org/RPMS/CentOS7/trafficserver-devel-7.1.4-2.el7.x86_64.rpm /trafficserver-devel.rpm
 
-RUN yum install -y bind-utils kyotocabinet-libs epel-release initscripts iproute net-tools nmap-ncat gettext autoconf automake libtool gcc-c++ cronie glibc-devel openssl-devel
-RUN yum install -y /trafficserver.rpm /trafficserver-devel.rpm jq python36-psutil python36-typing python36-setuptools python36-pip logrotate && yum clean all
-RUN python3 -m pip install --upgrade pip && python3 -m pip install requests urllib3 distro
+RUN rpm -Uvh --nodeps /trafficserver.rpm /trafficserver-devel.rpm && \
+    dnf install -y jq python3-psutil python3-setuptools python3-pip logrotate && \
+    dnf clean all
 
+RUN dnf install -y bind-utils kyotocabinet-libs initscripts iproute net-tools nmap-ncat gettext autoconf automake libtool gcc-c++ cronie glibc-devel openssl-devel
+
+RUN python3 -m pip install --upgrade pip && python3 -m pip install requests urllib3 distro
 
 ADD traffic_server/plugins/astats_over_http/astats_over_http.c traffic_server/plugins/astats_over_http/Makefile.am /
 
@@ -41,7 +118,7 @@ RUN tsxs -v -c astats_over_http.c -o astats_over_http.so
 # The symbolic link here is a shim for broken atstccfg behavior - remove when it's fixed.
 RUN mkdir -p /usr/libexec/trafficserver /opt/ort /opt/trafficserver/etc/trafficserver/ /opt/init.d && ln -s /opt/trafficserver/etc/trafficserver/ssl /etc/trafficserver/ssl && tsxs -v -o astats_over_http.so -i
 
-RUN yum remove -y gcc-c++ glibc-devel autoconf automake libtool && rm -f /astats_over_http.c /Makefile.am
+RUN dnf remove -y gcc-c++ glibc-devel autoconf automake libtool && rm -f /astats_over_http.c /Makefile.am
 
 # You need to do this because the RPM in the ATS archives is just all kinds of messed-up
 RUN chmod 755 /usr/lib64/trafficserver /etc/trafficserver/body_factory /etc/trafficserver/body_factory/default
diff --git a/infrastructure/cdn-in-a-box/mid/Dockerfile b/infrastructure/cdn-in-a-box/mid/Dockerfile
index 046321a..d72f5ad 100644
--- a/infrastructure/cdn-in-a-box/mid/Dockerfile
+++ b/infrastructure/cdn-in-a-box/mid/Dockerfile
@@ -18,21 +18,98 @@
 ############################################################
 # Dockerfile to build Edge-Tier Cache container images for
 # Apache Traffic Control
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
-FROM centos:7 AS common-cache-server-layers
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} AS common-cache-server-layers
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 EXPOSE 80
 
+RUN dnf -y install epel-release && \
+    if [[ "${CENTOS_VERSION%%.*}" -ge 8 ]]; then \
+        additional_packages='compat-openssl10 pkgconf-pkg-config' || \
+        exit 1; \
+    else \
+        additional_packages=openssl || \
+        exit 1; \
+    fi && \
+    dnf -y install              \
+        GeoIP                   \
+        groff-base              \
+        hwloc                   \
+        hwloc-libs              \
+        kyotocabinet-libs       \
+        libtool-ltdl            \
+        libunwind               \
+        lzo                     \
+        make                    \
+        numactl-libs            \
+        perl                    \
+        perl-Carp               \
+        perl-constant           \
+        perl-Data-Dumper        \
+        perl-Encode             \
+        perl-Exporter           \
+        perl-File-Path          \
+        perl-File-Temp          \
+        perl-Filter             \
+        perl-Getopt-Long        \
+        perl-HTTP-Tiny          \
+        perl-libs               \
+        perl-macros             \
+        perl-parent             \
+        perl-PathTools          \
+        perl-Pod-Escapes        \
+        perl-podlators          \
+        perl-Pod-Perldoc        \
+        perl-Pod-Simple         \
+        perl-Pod-Usage          \
+        perl-Scalar-List-Utils  \
+        perl-Socket             \
+        perl-Storable           \
+        perl-Text-ParseWords    \
+        perl-threads            \
+        perl-threads-shared     \
+        perl-Time-HiRes         \
+        perl-Time-Local         \
+        perl-URI                \
+        tcl                     \
+        $additional_packages && \
+    if [[ "${CENTOS_VERSION%%.*}" -eq 8 ]]; then \
+        set -- \
+            # Pretend that we have the right library versions.
+            # TODO: Use a proper CentOS 7 or 8 RPM once trafficserver
+            # is in EPEL again (see apache/trafficserver#6855)
+            libtcl8.6.so        libtcl8.5.so     \
+            libncursesw.so.6    libncursesw.so.5 \
+            libtinfo.so.6       libtinfo.so.5    \
+            || exit 1; \
+    fi && \
+    cd /usr/lib64 && \
+    while [[ $# -gt 0 ]]; do \
+        source="$1" && \
+        shift && \
+        target="$1" && \
+        shift && \
+        ln -s "$source" "$target" || exit 1; \
+    done
 
 ADD https://ci.trafficserver.apache.org/RPMS/CentOS7/trafficserver-7.1.4-2.el7.x86_64.rpm /trafficserver.rpm
 ADD https://ci.trafficserver.apache.org/RPMS/CentOS7/trafficserver-devel-7.1.4-2.el7.x86_64.rpm /trafficserver-devel.rpm
 
-RUN yum install -y bind-utils kyotocabinet-libs epel-release initscripts iproute net-tools nmap-ncat gettext autoconf automake libtool gcc-c++ cronie glibc-devel openssl-devel
-RUN yum install -y /trafficserver.rpm /trafficserver-devel.rpm jq python36-psutil python36-typing python36-setuptools python36-pip logrotate && yum clean all
-RUN python3 -m pip install --upgrade pip && python3 -m pip install requests urllib3 distro
+RUN rpm -Uvh --nodeps /trafficserver.rpm /trafficserver-devel.rpm && \
+    dnf install -y jq python3-psutil python3-setuptools python3-pip logrotate && \
+    dnf clean all
 
+RUN dnf install -y bind-utils kyotocabinet-libs initscripts iproute net-tools nmap-ncat gettext autoconf automake libtool gcc-c++ cronie glibc-devel openssl-devel
+
+RUN python3 -m pip install --upgrade pip && python3 -m pip install requests urllib3 distro
 
 ADD traffic_server/plugins/astats_over_http/astats_over_http.c traffic_server/plugins/astats_over_http/Makefile.am /
 
@@ -41,7 +118,7 @@ RUN tsxs -v -c astats_over_http.c -o astats_over_http.so
 # The symbolic link here is a shim for broken atstccfg behavior - remove when it's fixed.
 RUN mkdir -p /usr/libexec/trafficserver /opt/ort /opt/trafficserver/etc/trafficserver/ /opt/init.d && ln -s /opt/trafficserver/etc/trafficserver/ssl /etc/trafficserver/ssl && tsxs -v -o astats_over_http.so -i
 
-RUN yum remove -y gcc-c++ glibc-devel autoconf automake libtool && rm -f /astats_over_http.c /Makefile.am
+RUN dnf remove -y gcc-c++ glibc-devel autoconf automake libtool && rm -f /astats_over_http.c /Makefile.am
 
 # You need to do this because the RPM in the ATS archives is just all kinds of messed-up
 RUN chmod 755 /usr/lib64/trafficserver /etc/trafficserver/body_factory /etc/trafficserver/body_factory/default
diff --git a/infrastructure/cdn-in-a-box/optional/docker-compose.socksproxy.yml b/infrastructure/cdn-in-a-box/optional/docker-compose.socksproxy.yml
index f98a04b..11d15de 100644
--- a/infrastructure/cdn-in-a-box/optional/docker-compose.socksproxy.yml
+++ b/infrastructure/cdn-in-a-box/optional/docker-compose.socksproxy.yml
@@ -44,6 +44,8 @@ services:
     build:
       context: .
       dockerfile: optional/socksproxy/Dockerfile
+      args:
+        CENTOS_VERSION: 8
     hostname: socksproxy
     domainname: infra.ciab.test
     volumes:
diff --git a/infrastructure/cdn-in-a-box/optional/docker-compose.vnc.yml b/infrastructure/cdn-in-a-box/optional/docker-compose.vnc.yml
index 5bc395f..0b26682 100644
--- a/infrastructure/cdn-in-a-box/optional/docker-compose.vnc.yml
+++ b/infrastructure/cdn-in-a-box/optional/docker-compose.vnc.yml
@@ -48,6 +48,7 @@ services:
       context: .
       dockerfile: optional/vnc/Dockerfile
       args:
+        CENTOS_VERSION: 8
         VNC_BUILD_USER: "ciabuser"
     depends_on:
       - dns
diff --git a/infrastructure/cdn-in-a-box/optional/socksproxy/Dockerfile b/infrastructure/cdn-in-a-box/optional/socksproxy/Dockerfile
index e0d91e8..01e21d4 100644
--- a/infrastructure/cdn-in-a-box/optional/socksproxy/Dockerfile
+++ b/infrastructure/cdn-in-a-box/optional/socksproxy/Dockerfile
@@ -17,13 +17,19 @@
 
 ############################################################
 # Dockerfile to build optional CiaB Socks Proxy
-# Based on CentOS 7
+# Based on CentOS 8
 ############################################################
-FROM centos:7
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 ARG DANTES_SRC=https://www.inet.no/dante/files/dante-1.4.2.tar.gz
 
-RUN yum install -y net-tools bind-utils iproute wget curl automake autoconf gcc make && \
+RUN dnf install -y net-tools bind-utils iproute wget curl automake autoconf gcc make && \
     curl -Ls -o /tmp/dante.tar.gz $DANTES_SRC && \
     tar -C /usr/src -zxvpf $(find /tmp -type f -name dante\*) && \
     cd $(find /usr/src -type d -name dante\*) && \
@@ -32,8 +38,8 @@ RUN yum install -y net-tools bind-utils iproute wget curl automake autoconf gcc
     make install && \
     groupadd -g 8062 sockd  && \
     useradd -m -u 8062 -g sockd sockd && \
-    yum remove -y automake autoconf gcc make && \
-    yum clean all && \
+    dnf remove -y automake autoconf gcc make && \
+    dnf clean all && \
     rm -rf /tmp/*  
 
 COPY optional/socksproxy/sockd.conf /etc
diff --git a/infrastructure/cdn-in-a-box/optional/vnc/Dockerfile b/infrastructure/cdn-in-a-box/optional/vnc/Dockerfile
index fa294d0..a446a8b 100644
--- a/infrastructure/cdn-in-a-box/optional/vnc/Dockerfile
+++ b/infrastructure/cdn-in-a-box/optional/vnc/Dockerfile
@@ -14,15 +14,22 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-FROM docker.io/centos:7
+
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 ARG VNC_BUILD_USER
 ENV VNC_USER=$VNC_BUILD_USER
 
-RUN yum -y install https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm epel-release && \
-    yum -y install xterm firefox git tigervnc-server sudo bind-utils net-tools which passwd which \
+RUN dnf -y install https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm epel-release && \
+    dnf -y install xterm firefox git tigervnc-server sudo bind-utils net-tools which passwd which \
                    fluxbox webcore-fonts terminus-fonts vnc mplayer wget openssl curl nc && \
-    yum -y clean all && rm -rf /var/cache/yum
+    dnf -y clean all && rm -rf /var/cache/dnf
 
 RUN useradd -m $VNC_USER && \
     echo "$VNC_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
diff --git a/infrastructure/cdn-in-a-box/origin/Dockerfile b/infrastructure/cdn-in-a-box/origin/Dockerfile
index 8b8f4e4..b95fee7 100644
--- a/infrastructure/cdn-in-a-box/origin/Dockerfile
+++ b/infrastructure/cdn-in-a-box/origin/Dockerfile
@@ -18,7 +18,6 @@
 ############################################################
 # Dockerfile to build Mid-Tier Cache container images for
 # Apache Traffic Control
-# Based on CentOS 7.2
 ############################################################
 
 FROM alpine:latest
diff --git a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/packaging.py b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/packaging.py
index 7d15dbc..a570103 100644
--- a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/packaging.py
+++ b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/packaging.py
@@ -48,8 +48,8 @@ class _MetaPackage(type):
 			                                                     stdout=subprocess.PIPE)
 			                                              .communicate()[0].decode().splitlines()
 			                                    if not p.endswith("is not installed")]
-			pack.installArgs = ["/bin/yum", "install", "-y"]
-			pack.uninstallArgs = ["/bin/yum", "remove", "-y"]
+			pack.installArgs = ["/bin/dnf", "install", "-y"]
+			pack.uninstallArgs = ["/bin/dnf", "remove", "-y"]
 
 		elif DISTRO in {'ubuntu', 'linuxmint', 'debian'}:
 			concat = '='
diff --git a/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile b/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile
index 8c84ddd..8f37220 100644
--- a/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile
@@ -19,13 +19,19 @@
 # Based on CentOS
 ############################################################
 
-FROM centos/systemd
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 # Default values for RPM -- override with `docker build --build-arg RPM=...'
 ARG RPM=traffic_monitor/traffic_monitor.rpm
 
-RUN yum install -y epel-release && \
-    yum install -y \
+RUN dnf install -y epel-release && \
+    dnf install -y \
         jq \
         nmap-ncat \
         iproute \
@@ -34,10 +40,10 @@ RUN yum install -y epel-release && \
         bind-utils \
         openssl \
         initscripts && \
-    yum clean all
+    dnf clean all
 
 ADD $RPM /
-RUN yum install -y  /$(basename $RPM) && \
+RUN rpm -Uvh  /$(basename $RPM) && \
     rm /$(basename $RPM)
 
 RUN mkdir -p /opt/traffic_monitor/conf
diff --git a/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile-debug b/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile-debug
index eba37e3..948c174 100644
--- a/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile-debug
+++ b/infrastructure/cdn-in-a-box/traffic_monitor/Dockerfile-debug
@@ -20,10 +20,17 @@
 # Based on CentOS
 ############################################################
 
-FROM centos/systemd as build-delve
-RUN yum -y install epel-release && \
-    yum -y install golang && \
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as get-delve
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+RUN dnf -y install epel-release && \
+    dnf -y install golang git && \
     go get -u github.com/go-delve/delve/cmd/dlv
 
 FROM trafficmonitor
-COPY --from=build-delve /root/go/bin /usr/bin
+COPY --from=get-delve /root/go/bin /usr/bin
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
index d2154de..a675f4c 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile
@@ -17,18 +17,33 @@
 
 ############################################################
 # Dockerfile to build Traffic Ops container images
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
 # Keep the trafficops-common-deps in Dockerfile the same as
 # trafficops-common-deps in Dockerfile-go to cache the same
 # layer.
-FROM centos:7 as trafficops-common-deps
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as trafficops-common-deps
+ARG CENTOS_VERSION=8
+# Makes CENTOS_VERSION available in later layers without needing to specify it again
+ENV CENTOS_VERSION=$CENTOS_VERSION
 
-RUN mkdir /etc/cron.d && \
-    yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm && \
-    yum -y install epel-release && \
-    yum -y install      \
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+RUN set -o nounset -o errexit && \
+    mkdir -p /etc/cron.d; \
+    if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        use_repo=''; \
+    else \
+        use_repo='--repo=pgdg96'; \
+    fi; \
+    dnf -y install "https://download.postgresql.org/pub/repos/yum/reporpms/EL-${CENTOS_VERSION%%.*}-x86_64/pgdg-redhat-repo-latest.noarch.rpm"; \
+    dnf -y $use_repo -- install postgresql96; \
+    dnf -y install epel-release; \
+    dnf -y install      \
         jq              \
         bind-utils      \
         net-tools       \
@@ -38,15 +53,19 @@ RUN mkdir /etc/cron.d && \
         isomd5sum       \
         nmap-ncat       \
         openssl         \
-        postgresql96 && \
-    yum clean all
+        # Used to copy certs in "Shared SSL certificate generation" step
+        rsync;          \
+    dnf clean all
 
 FROM trafficops-common-deps as trafficops-perl-deps
 
 EXPOSE 443
 ENV MOJO_MODE production
 
-RUN yum install -y          \
+RUN if [[ "${CENTOS_VERSION%%.*}" -ge 8 ]]; then \
+        enable_repo='--enablerepo=PowerTools' || exit 1; \
+    fi && \
+    dnf -y --allowerasing $enable_repo install \
         cpanminus           \
         expat-devel         \
         gcc-c++             \
@@ -67,13 +86,13 @@ RUN yum install -y          \
         perl-Digest-SHA1    \
         perl-JSON           \
         perl-libwww-perl    \
+        perl-Net-Pcap       \
         perl-TermReadKey    \
         perl-Test-CPAN-Meta \
         perl-WWW-Curl       \
         postgresql96-devel  \
-        postgresql96-libs   \
         tar &&              \
-    yum -y clean all && \
+    dnf -y clean all && \
     mkdir -p /opt/traffic_ops/app/public && \
     cpanm Carton
 
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-debug b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-debug
index e62b474..6f6c7e0 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-debug
+++ b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-debug
@@ -17,7 +17,7 @@
 
 ############################################################
 # Dockerfile to build Traffic Ops container images
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
 FROM trafficops-perl
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go
index ff01b91..2101902 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go
+++ b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go
@@ -17,18 +17,31 @@
 
 ############################################################
 # Dockerfile to build Traffic Ops container images
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
 # Keep the trafficops-common-deps in Dockerfile-go the same
 # as trafficops-common-deps in Dockerfile to cache the same
 # layer.
-FROM centos:7 as trafficops-common-deps
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as trafficops-common-deps
+ARG CENTOS_VERSION=8
 
-RUN mkdir /etc/cron.d && \
-    yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm && \
-    yum -y install epel-release && \
-    yum -y install      \
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+RUN set -o nounset -o errexit && \
+    mkdir -p /etc/cron.d; \
+    if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+	    include_repo=''; \
+    else \
+	    include_repo='--repo=pgdg96'; \
+    fi; \
+    dnf -y install "https://download.postgresql.org/pub/repos/yum/reporpms/EL-${CENTOS_VERSION%%.*}-x86_64/pgdg-redhat-repo-latest.noarch.rpm"; \
+    dnf -y $include_repo -- install postgresql96; \
+    dnf -y install epel-release; \
+    dnf -y install      \
         jq              \
         bind-utils      \
         net-tools       \
@@ -37,9 +50,8 @@ RUN mkdir /etc/cron.d && \
         mkisofs         \
         isomd5sum       \
         nmap-ncat       \
-        openssl         \
-        postgresql96 && \
-    yum clean all
+        openssl;        \
+    dnf clean all
 
 FROM    trafficops-common-deps
 
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go-debug b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go-debug
index 9f9d81d..be661d0 100644
--- a/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go-debug
+++ b/infrastructure/cdn-in-a-box/traffic_ops/Dockerfile-go-debug
@@ -17,12 +17,19 @@
 
 ############################################################
 # Dockerfile to build Traffic Ops debugging container image
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
-FROM centos:7 as get-delve
-RUN yum -y install epel-release && \
-    yum -y install golang && \
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as get-delve
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+RUN dnf -y install epel-release && \
+    dnf -y install golang git && \
     go get -u github.com/go-delve/delve/cmd/dlv
 
 FROM trafficops-go
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/run.sh b/infrastructure/cdn-in-a-box/traffic_ops/run.sh
index 1af0666..1e107a6 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/run.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/run.sh
@@ -53,7 +53,7 @@ source /generate-certs.sh
 # copy contents of /ca to /export/ssl
 # update the permissions 
 mkdir -p "$X509_CA_PERSIST_DIR" && chmod 777 "$X509_CA_PERSIST_DIR"
-chmod -R a+rw "$X509_CA_PERSIST_DIR"
+chmod -R a+r "$X509_CA_PERSIST_DIR"
 
 if [ -r "$X509_CA_PERSIST_ENV_FILE" ] ; then
   umask $X509_CA_UMASK 
@@ -74,14 +74,14 @@ elif x509v3_init; then
 		x509v3_dump_env
     # Save newly generated certs for future restarts.
     rsync -av "$X509_CA_DIR/" "$X509_CA_PERSIST_DIR/"
-    chmod 777 "$X509_CA_PERSIST_DIR"
+    chmod -R a+r "$X509_CA_DIR/" "$X509_CA_PERSIST_DIR"
     sync
     echo "GENERATE CERTS FROM $X509_CA_DIR to $X509_CA_PERSIST_DIR"
     sleep 4
 fi
 
 chown -R trafops:trafops "$X509_CA_PERSIST_DIR"
-chmod -R a+rw "$X509_CA_PERSIST_DIR"
+chmod -R a+r "$X509_CA_PERSIST_DIR"
 
 # Write config files
 set -x
diff --git a/infrastructure/cdn-in-a-box/traffic_portal/Dockerfile b/infrastructure/cdn-in-a-box/traffic_portal/Dockerfile
index b44c035..07a9c52 100644
--- a/infrastructure/cdn-in-a-box/traffic_portal/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_portal/Dockerfile
@@ -17,10 +17,16 @@
 
 ############################################################
 # Dockerfile to build Traffic Portal container images
-# Based on CentOS 7.2
+# Based on CentOS 8
 ############################################################
 
-FROM centos:7
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 RUN curl -sL https://rpm.nodesource.com/setup_12.x | bash -
 
@@ -30,16 +36,16 @@ ARG TRAFFIC_PORTAL_RPM=traffic_portal/traffic_portal.rpm
 ARG TO_HOST=$TO_HOST
 
 # Install and delete the TRAFFIC_PORTAL_RPM when finished
-RUN yum install -y \
+RUN dnf install -y \
     epel-release && \
-    yum install -y \
+    dnf install -y \
       jq \
       nodejs \
       openssl \
       gettext \
       bind-utils \
       net-tools && \
-    yum clean all || \
+    dnf clean all || \
     echo "ERROR INSTALLING PACKAGES"
 
 ADD $TRAFFIC_PORTAL_RPM /
diff --git a/infrastructure/cdn-in-a-box/traffic_portal_integration_test/Dockerfile b/infrastructure/cdn-in-a-box/traffic_portal_integration_test/Dockerfile
index 1910f6f..39c1cde 100644
--- a/infrastructure/cdn-in-a-box/traffic_portal_integration_test/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_portal_integration_test/Dockerfile
@@ -15,21 +15,34 @@
 # specific language governing permissions and limitations
 # under the License.
 
-FROM centos:7 as os-dependencies
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as os-dependencies
+ARG CENTOS_VERSION=8
 
-# Installs the Google Chrome yum repo
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+# Installs the Google Chrome dnf repo
 COPY infrastructure/cdn-in-a-box/traffic_portal_integration_test/etc etc
 
-RUN yum install -y \
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        utils_package=yum-utils; \
+    else \
+        utils_package=dnf-utils; \
+    fi && \
+    dnf install -y \
         bind-utils \
         # jq is in EPEL
         epel-release \
+        GConf2 \
         git \
         google-chrome-stable \
         java-1.8.0-openjdk \
-        net-tools && \
-    yum -y install jq && \
-    yum -y clean all
+        net-tools \
+        $utils_package && \
+    dnf -y install jq && \
+    dnf -y clean all
 
 FROM os-dependencies AS node-dependencies
 # Download and install node
@@ -64,8 +77,8 @@ RUN jq ' \
     mv conf.json.tmp conf.json
 
 RUN webdriver-manager clean && \
-	repoquery --installed --qf='%{version}' google-chrome-stable | \
-		xargs webdriver-manager update --versions.chrome
+    repoquery --installed --qf='%{version}' google-chrome-stable | \
+        xargs webdriver-manager update --versions.chrome
 
 COPY infrastructure/cdn-in-a-box/traffic_ops/to-access.sh \
      infrastructure/cdn-in-a-box/traffic_portal_integration_test/run.sh \
diff --git a/infrastructure/cdn-in-a-box/traffic_router/Dockerfile b/infrastructure/cdn-in-a-box/traffic_router/Dockerfile
index 0eff38b..6d8bca7 100644
--- a/infrastructure/cdn-in-a-box/traffic_router/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_router/Dockerfile
@@ -16,22 +16,29 @@
 # under the License.
 ############################################################
 # Dockerfile to build Traffic Router 3.0
-# Based on CentOS 7.x
+# Based on CentOS 8
 ############################################################
 
-FROM centos:7
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
 MAINTAINER dev@trafficcontrol.apache.org
 
 # Default values for TOMCAT RPM and RPM -- override with `docker build --build-arg JDK=...'
 ARG TRAFFIC_ROUTER_RPM=traffic_router/traffic_router.rpm
 ARG TOMCAT_RPM=traffic_router/tomcat.rpm
 
-RUN yum -y install epel-release && \
-    yum -y install jq git rpm-build net-tools iproute nc wget tar unzip \
-          perl-JSON perl-WWWCurl which make autoconf automake gcc gcc-c++ apr apr-devel \
+RUN dnf -y install epel-release && \
+    dnf -y install jq git rpm-build net-tools iproute nc wget tar unzip \
+          perl-JSON perl-WWW-Curl which make autoconf automake gcc gcc-c++ apr apr-devel \
           openssl openssl-devel bind-utils net-tools perl-JSON-PP gettext \
           java-1.8.0-openjdk-headless java-1.8.0-openjdk-devel tomcat-native && \
-    yum -y clean all && \
+    dnf -y clean all && \
     ln -sfv $(realpath /usr/lib/jvm/java-1.8.0) /opt/java
 
 ADD $TRAFFIC_ROUTER_RPM /traffic_router.rpm
diff --git a/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile b/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile
index 60a4622..a9d6c06 100644
--- a/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile
+++ b/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile
@@ -19,20 +19,26 @@
 # Based on CentOS
 ############################################################
 
-FROM centos:7
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION}
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
 
 # Default values for RPM -- override with `docker build --build-arg RPM=...'
 ARG TRAFFIC_TS_RPM=traffic_stats/traffic_stats.rpm
 
-RUN yum install -y epel-release && \
-    yum install -y \
+RUN dnf install -y epel-release && \
+    dnf install -y \
         jq \
         nmap-ncat \
         net-tools \
         gettext \
         bind-utils \
         openssl && \
-    yum clean all
+    dnf clean all
 
 ADD $TRAFFIC_TS_RPM /
 RUN rpm -Uvh /$(basename $TRAFFIC_TS_RPM) && \
diff --git a/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile-debug b/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile-debug
index a5ad931..9c49f6f 100644
--- a/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile-debug
+++ b/infrastructure/cdn-in-a-box/traffic_stats/Dockerfile-debug
@@ -20,10 +20,17 @@
 # Based on CentOS
 ############################################################
 
-FROM centos/systemd as build-delve
-RUN yum -y install epel-release && \
-    yum -y install golang && \
+ARG CENTOS_VERSION=8
+FROM centos:${CENTOS_VERSION} as get-delve
+ARG CENTOS_VERSION=8
+
+RUN if [[ "${CENTOS_VERSION%%.*}" -eq 7 ]]; then \
+        yum -y install dnf || exit 1; \
+    fi
+
+RUN dnf -y install epel-release && \
+    dnf -y install golang git && \
     go get -u github.com/go-delve/delve/cmd/dlv
 
 FROM trafficstats
-COPY --from=build-delve /root/go/bin /usr/bin
+COPY --from=get-delve /root/go/bin /usr/bin


[trafficcontrol] 01/09: Fix an issue with API v1/2 tests not cleaning up certain v3 structures (topologies) (#5282)

Posted by oc...@apache.org.
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 a73b193fecebc7cdaba0f72408c60dc1160a6209
Author: ocket8888 <oc...@apache.org>
AuthorDate: Fri Nov 13 10:53:11 2020 -0700

    Fix an issue with API v1/2 tests not cleaning up certain v3 structures (topologies) (#5282)
    
    (cherry picked from commit 087a221d19d0e5f34afb668a3a3c5b6f23705936)
---
 traffic_ops/testing/api/v1/todb_test.go | 1 +
 traffic_ops/testing/api/v2/todb_test.go | 1 +
 2 files changed, 2 insertions(+)

diff --git a/traffic_ops/testing/api/v1/todb_test.go b/traffic_ops/testing/api/v1/todb_test.go
index cba904d..c85564c 100644
--- a/traffic_ops/testing/api/v1/todb_test.go
+++ b/traffic_ops/testing/api/v1/todb_test.go
@@ -333,6 +333,7 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM profile;
 	DELETE FROM parameter;
 	DELETE FROM profile_parameter;
+	DELETE FROM topology;
 	DELETE FROM cachegroup;
 	DELETE FROM coordinate;
 	DELETE FROM type;
diff --git a/traffic_ops/testing/api/v2/todb_test.go b/traffic_ops/testing/api/v2/todb_test.go
index 95e3228..370187d 100644
--- a/traffic_ops/testing/api/v2/todb_test.go
+++ b/traffic_ops/testing/api/v2/todb_test.go
@@ -333,6 +333,7 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM profile;
 	DELETE FROM parameter;
 	DELETE FROM profile_parameter;
+	DELETE FROM topology;
 	DELETE FROM cachegroup;
 	DELETE FROM coordinate;
 	DELETE FROM type;


[trafficcontrol] 07/09: Add PUSH and PURGE denial to mid tier caches. (#5292)

Posted by oc...@apache.org.
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 71812820de47dcbb471d15232e6c960bf0e5bdc3
Author: alficles <al...@gmail.com>
AuthorDate: Tue Nov 17 16:51:45 2020 -0700

    Add PUSH and PURGE denial to mid tier caches. (#5292)
    
    (cherry picked from commit 97382c971d2e98cc4922f331ebb870ffa744895e)
---
 lib/go-atscfg/ipallowdotconfig.go      | 16 ++++++++++++++++
 lib/go-atscfg/ipallowdotconfig_test.go | 20 ++++++++++++++++++++
 2 files changed, 36 insertions(+)

diff --git a/lib/go-atscfg/ipallowdotconfig.go b/lib/go-atscfg/ipallowdotconfig.go
index 246fb6c..f3f59ba 100644
--- a/lib/go-atscfg/ipallowdotconfig.go
+++ b/lib/go-atscfg/ipallowdotconfig.go
@@ -268,6 +268,22 @@ func MakeIPAllowDotConfig(
 		// order matters, so sort before adding the denys
 		sort.Sort(ipAllowDatas(ipAllowDat))
 
+		// start with a deny for PUSH and PURGE - TODO CDL: parameterize
+		if isMid { // Edges already deny PUSH and PURGE
+			ipAllowData = append([]IPAllowData{
+				{
+					Src:    `0.0.0.0-255.255.255.255`,
+					Action: ActionDeny,
+					Method: `PUSH|PURGE`,
+				},
+				{
+					Src:    `::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff`,
+					Action: ActionDeny,
+					Method: `PUSH|PURGE`,
+				},
+			}, ipAllowData...)
+		}
+
 		// end with a deny
 		ipAllowDat = append(ipAllowDat, ipAllowData{
 			Src:    `0.0.0.0-255.255.255.255`,
diff --git a/lib/go-atscfg/ipallowdotconfig_test.go b/lib/go-atscfg/ipallowdotconfig_test.go
index 9a1c8fa..ed6dc0a 100644
--- a/lib/go-atscfg/ipallowdotconfig_test.go
+++ b/lib/go-atscfg/ipallowdotconfig_test.go
@@ -99,6 +99,26 @@ func TestMakeIPAllowDotConfig(t *testing.T) {
 
 	lines = lines[1:] // remove comment line
 
+	/* Test that PUSH and PURGE are denied ere the allowance of anything else. */
+	{
+		ip4deny := false
+		ip6deny := false
+	eachLine:
+		for i, line := range lines {
+			switch {
+			case strings.Contains(line, `0.0.0.0-255.255.255.255`) && strings.Contains(line, `ip_deny`) && strings.Contains(line, `PUSH`) && strings.Contains(line, `PURGE`):
+				ip4deny = true
+			case strings.Contains(line, `::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff`) && strings.Contains(line, `ip_deny`) && strings.Contains(line, `PUSH`) && strings.Contains(line, `PURGE`):
+				ip6deny = true
+			case strings.Contains(line, `ip_allow`):
+				if !(ip4deny && ip6deny) {
+					t.Errorf("Expected denies for PUSH and PURGE before any ips are allowed; pre-denial allowance on line %d.", i+1)
+				}
+				break eachLine
+			}
+		}
+	}
+
 	for _, expected := range expecteds {
 		if !strings.Contains(txt, expected) {
 			t.Errorf("expected %+v actual '%v'\n", expected, txt)


[trafficcontrol] 03/09: fixed federations/all IMS (#5269)

Posted by oc...@apache.org.
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 5da7a3998df822d199d6984bea2164a8ffb33224
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Fri Nov 13 13:19:25 2020 -0700

    fixed federations/all IMS (#5269)
    
    * fixed federations/all IMS
    
    * updated changelog
    
    * missed issue number
    
    * updated my changelog to match format on others
    
    (cherry picked from commit f6a805651310c2efcba6290ab16761fd50ad81e2)
---
 CHANGELOG.md                                                 |  1 +
 traffic_ops/traffic_ops_golang/federations/allfederations.go | 10 +++++++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5fc170..5d14fd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -128,6 +128,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Fixed an issue with Traffic Monitor to fix peer polling to work as expected
 - Fixed #5274 - CDN in a Box's Traffic Vault image failed to build due to Basho's repo responding with 402 Payment Required. The repo has been removed from the image.
 - #5069 - For LetsEncryptDnsChallengerWatcher in Traffic Router, the cr-config location is configurable instead of only looking at `/opt/traffic_router/db/cr-config.json`
+- #5191 - Error from IMS requests to /federations/all
 
 
 ### Changed
diff --git a/traffic_ops/traffic_ops_golang/federations/allfederations.go b/traffic_ops/traffic_ops_golang/federations/allfederations.go
index cf5539b..20cfc08 100644
--- a/traffic_ops/traffic_ops_golang/federations/allfederations.go
+++ b/traffic_ops/traffic_ops_golang/federations/allfederations.go
@@ -237,7 +237,15 @@ func tryIfModifiedSinceQuery(header http.Header, tx *sql.Tx, param string, imsQu
 		return runSecond, max
 	}
 
-	rows, err := tx.Query(imsQuery, param)
+	var rows *sql.Rows
+	var err error
+
+	if param == "" {
+		rows, err = tx.Query(imsQuery)
+	} else {
+		rows, err = tx.Query(imsQuery, param)
+	}
+
 	if err != nil {
 		log.Warnf("Couldn't get the max last updated time: %v", err)
 		return runSecond, max


[trafficcontrol] 08/09: Fix merge non-conflict error (#5300)

Posted by oc...@apache.org.
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 8c4617c7202ac00c155cf86f5b1339f340632d86
Author: Robert O Butts <ro...@users.noreply.github.com>
AuthorDate: Tue Nov 17 18:44:29 2020 -0700

    Fix merge non-conflict error (#5300)
    
    (cherry picked from commit 0d3b849776819ed854bb7fbf759fa4101a47ccf0)
---
 lib/go-atscfg/ipallowdotconfig.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/go-atscfg/ipallowdotconfig.go b/lib/go-atscfg/ipallowdotconfig.go
index f3f59ba..40fc5e2 100644
--- a/lib/go-atscfg/ipallowdotconfig.go
+++ b/lib/go-atscfg/ipallowdotconfig.go
@@ -270,7 +270,7 @@ func MakeIPAllowDotConfig(
 
 		// start with a deny for PUSH and PURGE - TODO CDL: parameterize
 		if isMid { // Edges already deny PUSH and PURGE
-			ipAllowData = append([]IPAllowData{
+			ipAllowDat = append([]ipAllowData{
 				{
 					Src:    `0.0.0.0-255.255.255.255`,
 					Action: ActionDeny,
@@ -281,7 +281,7 @@ func MakeIPAllowDotConfig(
 					Action: ActionDeny,
 					Method: `PUSH|PURGE`,
 				},
-			}, ipAllowData...)
+			}, ipAllowDat...)
 		}
 
 		// end with a deny


[trafficcontrol] 05/09: Remove unnecessary CDN in a Box waits (#5283)

Posted by oc...@apache.org.
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 7f4b8ca79a05e605cfa631e9963e757e670225be
Author: Zach Hoffman <zr...@apache.org>
AuthorDate: Tue Nov 17 13:00:04 2020 -0700

    Remove unnecessary CDN in a Box waits (#5283)
    
    * Skip enroller initial data load when LOAD_TRAFFIC_OPS_DATA is not equal
    to true
    
    * Make DIG_IP_RETRY an inherited environment variable
    
    * Enroll Traffic Vault after Riak finishes starting up
    
    * Do not attempt to enroll Traffic Vault when running the Traffic Ops
    integration tests GitHub action
    
    * Add workflow-related files to Traffic Ops Go client/API integration
    tests paths filters
    
    * Add a period
    
    * Specify LOAD_TRAFFIC_OPS_DATA and DIG_IP_RETRY for trafficops-perl service only
    
    (cherry picked from commit 2b3e9cb71cf692e8531cb88f4354e9362f9a886b)
---
 .github/actions/to-integration-tests/entrypoint.sh       |  2 +-
 .github/workflows/traffic ops.yml                        | 16 ++++++++++++----
 docs/source/admin/quick_howto/ciab.rst                   |  4 ++++
 docs/source/development/traffic_ops.rst                  |  3 +++
 .../cdn-in-a-box/docker-compose.traffic-ops-test.yml     |  7 ++++++-
 .../cdn-in-a-box/docker-compose.traffic-portal-test.yml  |  4 ++++
 infrastructure/cdn-in-a-box/docker-compose.yml           |  3 +++
 .../cdn-in-a-box/traffic_ops/set-to-ips-from-dns.sh      |  1 -
 .../cdn-in-a-box/traffic_ops/trafficops-init.sh          | 10 +++++++---
 .../traffic_vault/poststart.d/02-add-search-schema.sh    |  2 ++
 infrastructure/cdn-in-a-box/traffic_vault/run.sh         |  2 --
 11 files changed, 42 insertions(+), 12 deletions(-)

diff --git a/.github/actions/to-integration-tests/entrypoint.sh b/.github/actions/to-integration-tests/entrypoint.sh
index d3c4ff2..443e7f4 100755
--- a/.github/actions/to-integration-tests/entrypoint.sh
+++ b/.github/actions/to-integration-tests/entrypoint.sh
@@ -72,7 +72,7 @@ start_traffic_vault() {
 		sed -i '0,/^update-ca-certificates/d' /etc/riak/prestart.d/00-config.sh;
 
 		# Do not try to source to-access.sh
-		sed -i '/to-access\.sh/d' /etc/riak/{prestart.d,poststart.d}/*
+		sed -i '/to-access\.sh\|^to-enroll/d' /etc/riak/{prestart.d,poststart.d}/*
 	BASH_LINES
 
 	DOCKER_BUILDKIT=1 docker build "$ciab_dir" -f "${ciab_dir}/traffic_vault/Dockerfile" -t "$trafficvault" 2>&1 |
diff --git a/.github/workflows/traffic ops.yml b/.github/workflows/traffic ops.yml
index 80d674e..1a51795 100644
--- a/.github/workflows/traffic ops.yml	
+++ b/.github/workflows/traffic ops.yml	
@@ -20,17 +20,25 @@ name: Traffic Ops Go client/API integration tests
 on:
   push:
     paths:
+      - .github/actions/todb-init/**
+      - .github/actions/to-integration-tests/**
+      - '.github/workflows/traffic ops.yml'
       - GO_VERSION
-      - traffic_ops/traffic_ops_golang/**.go
-      - traffic_ops/testing/api/**.go
+      - infrastructure/cdn-in-a-box/traffic_vault/**
       - traffic_ops/*client/**.go
+      - traffic_ops/testing/api/**.go
+      - traffic_ops/traffic_ops_golang/**.go
   create:
   pull_request:
     paths:
+      - .github/actions/todb-init/**
+      - .github/actions/to-integration-tests/**
+      - '.github/workflows/traffic ops.yml'
       - GO_VERSION
-      - traffic_ops/traffic_ops_golang/**.go
-      - traffic_ops/testing/api/**.go
+      - infrastructure/cdn-in-a-box/traffic_vault/**
       - traffic_ops/*client/**.go
+      - traffic_ops/testing/api/**.go
+      - traffic_ops/traffic_ops_golang/**.go
     types: [opened, reopened, ready_for_review, synchronize]
 
 jobs:
diff --git a/docs/source/admin/quick_howto/ciab.rst b/docs/source/admin/quick_howto/ciab.rst
index 0f135ca..6a100d2 100644
--- a/docs/source/admin/quick_howto/ciab.rst
+++ b/docs/source/admin/quick_howto/ciab.rst
@@ -140,6 +140,8 @@ There also exist TP and TO integration tests containers. Both of these container
 
 	sudo docker-compose -f docker-compose.traffic-ops-test.yml up
 
+.. note:: If all CDN in a Box containers are started at once (example: ``docker-compose -f docker-compose.yml -f docker-compose.traffic-ops-test.yml up integration``), the :ref:`Enroller <ciab-enroller>` initial data load is skipped to prevent data conflicts with the :ref:`Traffic Ops API tests fixtures <dev-traffic-ops-fixtures>`.
+
 variables.env
 """""""""""""
 .. literalinclude:: ../../../../infrastructure/cdn-in-a-box/variables.env
@@ -226,6 +228,8 @@ Advanced Usage
 ==============
 This section will be amended as functionality is added to the CDN in a Box project.
 
+.. _ciab-enroller:
+
 The Enroller
 ------------
 The "enroller" began as an efficient way for Traffic Ops to be populated with data as CDN in a Box starts up. It connects to Traffic Ops as the "admin" user and processes files places in the docker volume shared between the containers. The enroller watches each directory within the ``/shared/enroller`` directory for new :file:`{filename}.json` files to be created there. These files must follow the format outlined in the API guide for the ``POST`` method for each data type,  (e.g. for a ` [...]
diff --git a/docs/source/development/traffic_ops.rst b/docs/source/development/traffic_ops.rst
index bd3b7b6..38c9bb8 100644
--- a/docs/source/development/traffic_ops.rst
+++ b/docs/source/development/traffic_ops.rst
@@ -336,6 +336,9 @@ The integration tests are run using :manpage:`go-test(1)`, with two configuratio
 	Specify the path to the `Test Configuration File`_. If not specified, it will attempt to read a file named ``traffic-ops-test.conf`` in the working directory.
 
 	.. seealso:: `Configuring the Integration Tests`_ for a detailed explanation of the format of this configuration file.
+
+.. _dev-traffic-ops-fixtures:
+
 .. option:: --fixtures FIXTURES
 
 	Specify the path to a file containing static data for the tests to use. This should almost never be used, because many of the tests depend on the data having a certain content and structure. If not specified, it will attempt to read a file named ``tc-fixtures.json`` in the working directory.
diff --git a/infrastructure/cdn-in-a-box/docker-compose.traffic-ops-test.yml b/infrastructure/cdn-in-a-box/docker-compose.traffic-ops-test.yml
index 520df5f..2093e8a 100644
--- a/infrastructure/cdn-in-a-box/docker-compose.traffic-ops-test.yml
+++ b/infrastructure/cdn-in-a-box/docker-compose.traffic-ops-test.yml
@@ -38,8 +38,13 @@ services:
     volumes:
       - shared:/shared
       - ../../junit:/junit
+
+  trafficops-perl:
+    environment:
+      DIG_IP_RETRY: ${DIG_IP_RETRY:-0}
+      LOAD_TRAFFIC_OPS_DATA: ${LOAD_TRAFFIC_OPS_DATA:-false}
+
 volumes:
   junit:
   shared:
     external: false
- 
diff --git a/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml b/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
index f285029..59ad76b 100644
--- a/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
+++ b/infrastructure/cdn-in-a-box/docker-compose.traffic-portal-test.yml
@@ -41,6 +41,10 @@ services:
       - shared:/shared
       - ../../junit:/junit
 
+  trafficops-perl:
+    environment:
+      DIG_IP_RETRY: ${DIG_IP_RETRY:-0}
+
 volumes:
   shared:
     external: false
diff --git a/infrastructure/cdn-in-a-box/docker-compose.yml b/infrastructure/cdn-in-a-box/docker-compose.yml
index a78c52b..943ee4a 100644
--- a/infrastructure/cdn-in-a-box/docker-compose.yml
+++ b/infrastructure/cdn-in-a-box/docker-compose.yml
@@ -89,6 +89,9 @@ services:
     domainname: infra.ciab.test
     env_file:
       - variables.env
+    environment:
+      DIG_IP_RETRY: ${DIG_IP_RETRY:-10}
+      LOAD_TRAFFIC_OPS_DATA: ${LOAD_TRAFFIC_OPS_DATA:-true}
     hostname: trafficops-perl
     image: trafficops-perl
     # TODO: change to expose: "60443" to limit to containers
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/set-to-ips-from-dns.sh b/infrastructure/cdn-in-a-box/traffic_ops/set-to-ips-from-dns.sh
index 1df6a8f..dc73ee9 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/set-to-ips-from-dns.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/set-to-ips-from-dns.sh
@@ -40,7 +40,6 @@ service_ips="${gateway_ip}"
 service_ip6s="${gateway_ip6}"
 INTERFACE=$(ip link | awk '/\<UP\>/ && !/LOOPBACK/ {sub(/@.*/, "", $2); print $2}')
 NETMASK=$(route | awk -v INTERFACE=$INTERFACE '$8 ~ INTERFACE && $1 !~ "default"  {print $3}')
-DIG_IP_RETRY=10
 
 for service_name in $service_names; do
 	service_fqdn="${service_name}.${service_domain}"
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/trafficops-init.sh b/infrastructure/cdn-in-a-box/traffic_ops/trafficops-init.sh
index b0dc2b4..11deb9b 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/trafficops-init.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/trafficops-init.sh
@@ -141,7 +141,11 @@ traffic_router_zonemanager_timeout() {
   mv "$modified_crconfig" $crconfig_path;
 }
 
-traffic_router_zonemanager_timeout
+if [[ "$LOAD_TRAFFIC_OPS_DATA" == true ]]; then
+	traffic_router_zonemanager_timeout
 
-# Load required data at the top level
-load_data_from /traffic_ops_data
+	# Load required data at the top level
+	load_data_from /traffic_ops_data
+else
+	touch "$ENROLLER_DIR/initial-load-done"
+fi
diff --git a/infrastructure/cdn-in-a-box/traffic_vault/poststart.d/02-add-search-schema.sh b/infrastructure/cdn-in-a-box/traffic_vault/poststart.d/02-add-search-schema.sh
index 512f479..dcf2037 100644
--- a/infrastructure/cdn-in-a-box/traffic_vault/poststart.d/02-add-search-schema.sh
+++ b/infrastructure/cdn-in-a-box/traffic_vault/poststart.d/02-add-search-schema.sh
@@ -21,3 +21,5 @@ curl -kvs -XPUT -H 'Content-Type:application/xml' "https://$TV_ADMIN_USER:$TV_AD
 curl -kvs -XPUT -H 'Content-Type:application/json' "https://$TV_ADMIN_USER:$TV_ADMIN_PASSWORD@$TV_FQDN:$TV_HTTPS_PORT/search/index/sslkeys" -d '{"schema":"sslkeys"}'
 
 curl -kvs -XPUT -H 'Content-Type:application/json' "https://$TV_ADMIN_USER:$TV_ADMIN_PASSWORD@$TV_FQDN:$TV_HTTPS_PORT/buckets/ssl/props" -d'{"props":{"search_index":"sslkeys"}}'
+
+to-enroll "tv" ALL "" "8088" "8088" || (while true; do echo "enroll failed."; sleep 3 ; done)
diff --git a/infrastructure/cdn-in-a-box/traffic_vault/run.sh b/infrastructure/cdn-in-a-box/traffic_vault/run.sh
index a7667e0..5585d0c 100755
--- a/infrastructure/cdn-in-a-box/traffic_vault/run.sh
+++ b/infrastructure/cdn-in-a-box/traffic_vault/run.sh
@@ -26,6 +26,4 @@ TO_URL=https://${TO_FQDN}:${TO_PORT}
 TO_USER=$TV_USER
 TO_PASSWORD=$TV_PASSWORD
 
-to-enroll "tv" ALL "" "8088" "8088" || (while true; do echo "enroll failed."; sleep 3 ; done)
-
 ${RIAK_HOME}/riak-cluster.sh


[trafficcontrol] 09/09: Add validation to topology updates and server updates/deletions (#5299)

Posted by oc...@apache.org.
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 3d0a90f9918dfa1a0158a3e04da3a2440197555c
Author: Rawlin Peters <ra...@apache.org>
AuthorDate: Wed Nov 18 06:47:14 2020 -0700

    Add validation to topology updates and server updates/deletions (#5299)
    
    Topologies must contain at least 1 server per cachegroup in each of the
    CDNs of any assigned delivery services. Otherwise, a delivery service's
    topology is not truly representative of reality.
    
    (cherry picked from commit be43e76bef2e60b2a5df2049ab8633f035ecd995)
---
 CHANGELOG.md                                       |   1 +
 traffic_ops/testing/api/v3/servers_test.go         |  63 +++-
 traffic_ops/testing/api/v3/tc-fixtures.json        | 335 +++++++++++++++++++++
 traffic_ops/testing/api/v3/topologies_test.go      |  66 +++-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  45 +++
 traffic_ops/traffic_ops_golang/server/servers.go   |  64 ++--
 .../traffic_ops_golang/topology/topologies.go      |  95 ++++--
 7 files changed, 602 insertions(+), 67 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07861c4..1d6f591 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -67,6 +67,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Added default sort logic to GET API calls using Read()
 - Traffic Ops: added validation for assigning ORG servers to topology-based delivery services
 - Added locationByDeepCoverageZone to the `crs/stats/ip/{ip}` endpoint in the Traffic Router API
+- Traffic Ops: added validation for topology updates and server updates/deletions to ensure that topologies have at least one server per cachegroup in each CDN of any assigned delivery services
 
 ### Fixed
 - Fixed #5188 - DSR (delivery service request) incorrectly marked as complete and error message not displaying when DSR fulfilled and DS update fails in Traffic Portal. [Related Github issue](https://github.com/apache/trafficcontrol/issues/5188)
diff --git a/traffic_ops/testing/api/v3/servers_test.go b/traffic_ops/testing/api/v3/servers_test.go
index 1af5300..9d0d4d1 100644
--- a/traffic_ops/testing/api/v3/servers_test.go
+++ b/traffic_ops/testing/api/v3/servers_test.go
@@ -60,16 +60,23 @@ func LastServerInTopologyCacheGroup(t *testing.T) {
 	const cacheGroupName = "topology-mid-cg-01"
 	const moveToCacheGroup = "topology-mid-cg-02"
 	const topologyName = "forked-topology"
+	const cdnName = "cdn2"
 	const expectedLength = 1
+	cdns, _, err := TOSession.GetCDNByNameWithHdr(cdnName, nil)
+	if err != nil {
+		t.Fatalf("unable to GET CDN: %v", err)
+	}
+	cdnID := cdns[0].ID
 	params := url.Values{}
 	params.Add("cachegroupName", cacheGroupName)
 	params.Add("topology", topologyName)
+	params.Add("cdn", strconv.Itoa(cdnID))
 	servers, _, err := TOSession.GetServersWithHdr(&params, nil)
 	if err != nil {
-		t.Fatalf("getting server from cachegroup %s in topology %s: %s", cacheGroupName, topologyName, err.Error())
+		t.Fatalf("getting server from cdn %s from cachegroup %s in topology %s: %s", cdnName, cacheGroupName, topologyName, err.Error())
 	}
 	if len(servers.Response) != expectedLength {
-		t.Fatalf("expected to get %d server from cachegroup %s in topology %s, got %d servers", expectedLength, cacheGroupName, topologyName, len(servers.Response))
+		t.Fatalf("expected to get %d server from cdn %s from cachegroup %s in topology %s, got %d servers", expectedLength, cdnName, cacheGroupName, topologyName, len(servers.Response))
 	}
 	server := servers.Response[0]
 	_, reqInf, err := TOSession.DeleteServerByID(*server.ID)
@@ -80,6 +87,28 @@ func LastServerInTopologyCacheGroup(t *testing.T) {
 		t.Fatalf("expected a 400-level error deleting server with id %d, got status code %d: %s", *server.ID, reqInf.StatusCode, err.Error())
 	}
 
+	// attempt to move it to another CDN while it's the last server in the cachegroup in its CDN
+	cdns, _, err = TOSession.GetCDNByNameWithHdr("cdn1", nil)
+	if err != nil {
+		t.Fatalf("unable to GET CDN: %v", err)
+	}
+	newCDNID := cdns[0].ID
+	oldCDNID := *server.CDNID
+	server.CDNID = &newCDNID
+	profiles, _, err := TOSession.GetProfileByNameWithHdr("MID1", nil)
+	if err != nil {
+		t.Fatalf("unable to GET profile: %v", err)
+	}
+	newProfile := profiles[0].ID
+	oldProfile := *server.ProfileID
+	server.ProfileID = &newProfile
+	_, _, err = TOSession.UpdateServerByIDWithHdr(*server.ID, server, nil)
+	if err == nil {
+		t.Fatalf("changing the CDN of the last server (%s) in a CDN in a cachegroup used by a topology assigned to a delivery service(s) in that CDN - expected: error, actual: nil", *server.HostName)
+	}
+	server.CDNID = &oldCDNID
+	server.ProfileID = &oldProfile
+
 	params = url.Values{}
 	params.Add("name", moveToCacheGroup)
 	cgs, _, err := TOSession.GetCacheGroupsByQueryParamsWithHdr(params, nil)
@@ -548,10 +577,11 @@ func GetTestServersQueryParameters(t *testing.T) {
 
 	params.Set("dsId", strconv.Itoa(*ds.ID))
 	expectedHostnames := map[string]bool{
-		"edge1-cdn1-cg3":    false,
-		"edge2-cdn1-cg3":    false,
-		"atlanta-mid-16":    false,
-		"edgeInCachegroup3": false,
+		"edge1-cdn1-cg3":                 false,
+		"edge2-cdn1-cg3":                 false,
+		"atlanta-mid-16":                 false,
+		"edgeInCachegroup3":              false,
+		"midInSecondaryCachegroupInCDN1": false,
 	}
 	response, _, err := TOSession.GetServersWithHdr(&params, nil)
 	if err != nil {
@@ -562,7 +592,7 @@ func GetTestServersQueryParameters(t *testing.T) {
 	}
 	for _, server := range response.Response {
 		if _, exists := expectedHostnames[*server.HostName]; !exists {
-			t.Fatalf("expected hostnames %v, actual %s actual: ", expectedHostnames, *server.HostName)
+			t.Fatalf("expected hostnames %v, actual %s", expectedHostnames, *server.HostName)
 		}
 		expectedHostnames[*server.HostName] = true
 	}
@@ -600,14 +630,15 @@ func GetTestServersQueryParameters(t *testing.T) {
 	params.Del("dsId")
 	params.Add("topology", topology)
 	expectedHostnames = map[string]bool{
-		originHostname:             false,
-		"edge1-cdn1-cg3":           false,
-		"edge2-cdn1-cg3":           false,
-		"atlanta-mid-16":           false,
-		"atlanta-mid-17":           false,
-		"edgeInCachegroup3":        false,
-		"midInParentCachegroup":    false,
-		"midInSecondaryCachegroup": false,
+		originHostname:                   false,
+		"edge1-cdn1-cg3":                 false,
+		"edge2-cdn1-cg3":                 false,
+		"atlanta-mid-16":                 false,
+		"atlanta-mid-17":                 false,
+		"edgeInCachegroup3":              false,
+		"midInParentCachegroup":          false,
+		"midInSecondaryCachegroup":       false,
+		"midInSecondaryCachegroupInCDN1": false,
 	}
 	response, _, err = TOSession.GetServersWithHdr(&params, nil)
 	if err != nil {
@@ -618,7 +649,7 @@ func GetTestServersQueryParameters(t *testing.T) {
 	}
 	for _, server := range response.Response {
 		if _, exists := expectedHostnames[*server.HostName]; !exists {
-			t.Fatalf("expected hostnames %v, actual %s actual: ", expectedHostnames, *server.HostName)
+			t.Fatalf("expected hostnames %v, actual %s", expectedHostnames, *server.HostName)
 		}
 		expectedHostnames[*server.HostName] = true
 	}
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json b/traffic_ops/testing/api/v3/tc-fixtures.json
index 30c811f..796c630 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -222,6 +222,13 @@
             "name": "dtrc3",
             "shortName": "dtrc3",
             "typeName": "EDGE_LOC"
+        },
+        {
+            "latitude": 0,
+            "longitude": 0,
+            "name": "cdn1-only",
+            "shortName": "cdn1-only",
+            "typeName": "EDGE_LOC"
         }
     ],
     "cdns": [
@@ -1066,6 +1073,198 @@
             "type": "CLIENT_STEERING",
             "xmlId": "ds-client-steering",
             "anonymousBlockingEnabled": false
+        },
+        {
+            "active": true,
+            "cdnName": "cdn2",
+            "cacheurl": "",
+            "ccrDnsTtl": 3600,
+            "checkPath": "",
+            "consistentHashQueryParams": [],
+            "deepCachingType": "NEVER",
+            "displayName": "ds-forked-topology",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": null,
+            "fqPacingRate": 0,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "lastUpdated": "2018-04-06 16:48:51+00",
+            "logsEnabled": false,
+            "longDesc": "",
+            "longDesc1": "",
+            "longDesc2": "",
+            "matchList": [
+                {
+                    "pattern": ".*\\.ds-forked-topology\\..*",
+                    "setNumber": 0,
+                    "type": "HOST_REGEXP"
+                }
+            ],
+            "maxDnsAnswers": 0,
+            "midHeaderRewrite": null,
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://example.org",
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 0,
+            "qstringIgnore": 0,
+            "rangeRequestHandling": 0,
+            "regexRemap": null,
+            "regionalGeoBlocking": false,
+            "remapText": null,
+            "routingName": "cdn",
+            "signed": false,
+            "signingAlgorithm": null,
+            "sslKeyVersion": 0,
+            "tenant": "tenant1",
+            "tenantName": "tenant1",
+            "topology": "forked-topology",
+            "type": "HTTP",
+            "xmlId": "ds-forked-topology",
+            "anonymousBlockingEnabled": false
+        },
+        {
+            "active": true,
+            "cdnName": "cdn1",
+            "cacheurl": "",
+            "ccrDnsTtl": 3600,
+            "checkPath": "",
+            "consistentHashQueryParams": [],
+            "deepCachingType": "NEVER",
+            "displayName": "top-ds-in-cdn1",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": null,
+            "fqPacingRate": 0,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "lastUpdated": "2018-04-06 16:48:51+00",
+            "logsEnabled": false,
+            "longDesc": "",
+            "longDesc1": "",
+            "longDesc2": "",
+            "matchList": [
+                {
+                    "pattern": ".*\\.top-ds-in-cdn1\\..*",
+                    "setNumber": 0,
+                    "type": "HOST_REGEXP"
+                }
+            ],
+            "maxDnsAnswers": 0,
+            "midHeaderRewrite": null,
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://example.org",
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 0,
+            "qstringIgnore": 0,
+            "rangeRequestHandling": 0,
+            "regexRemap": null,
+            "regionalGeoBlocking": false,
+            "remapText": null,
+            "routingName": "cdn",
+            "signed": false,
+            "signingAlgorithm": null,
+            "sslKeyVersion": 0,
+            "tenant": "tenant1",
+            "tenantName": "tenant1",
+            "topology": "top-used-by-cdn1-and-cdn2",
+            "type": "HTTP",
+            "xmlId": "top-ds-in-cdn1",
+            "anonymousBlockingEnabled": false
+        },
+        {
+            "active": true,
+            "cdnName": "cdn2",
+            "cacheurl": "",
+            "ccrDnsTtl": 3600,
+            "checkPath": "",
+            "consistentHashQueryParams": [],
+            "deepCachingType": "NEVER",
+            "displayName": "top-ds-in-cdn2",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": null,
+            "fqPacingRate": 0,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "lastUpdated": "2018-04-06 16:48:51+00",
+            "logsEnabled": false,
+            "longDesc": "",
+            "longDesc1": "",
+            "longDesc2": "",
+            "matchList": [
+                {
+                    "pattern": ".*\\.top-ds-in-cdn2\\..*",
+                    "setNumber": 0,
+                    "type": "HOST_REGEXP"
+                }
+            ],
+            "maxDnsAnswers": 0,
+            "midHeaderRewrite": null,
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://example.org",
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 0,
+            "qstringIgnore": 0,
+            "rangeRequestHandling": 0,
+            "regexRemap": null,
+            "regionalGeoBlocking": false,
+            "remapText": null,
+            "routingName": "cdn",
+            "signed": false,
+            "signingAlgorithm": null,
+            "sslKeyVersion": 0,
+            "tenant": "tenant1",
+            "tenantName": "tenant1",
+            "topology": "top-used-by-cdn1-and-cdn2",
+            "type": "HTTP",
+            "xmlId": "top-ds-in-cdn2",
+            "anonymousBlockingEnabled": false
         }
     ],
     "deliveryServicesRegexes": [
@@ -3426,6 +3625,90 @@
             "updPending": false
         },
         {
+            "cachegroup": "secondaryCachegroup",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "midInSecondaryCachegroupInCDN1",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:deed:beef::12/64",
+                            "gateway": "2001:db8:deed:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.7.15/24",
+                            "gateway": "192.0.7.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "MID1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "MID",
+            "updPending": false
+        },
+        {
+            "cachegroup": "cdn1-only",
+            "cdnName": "cdn1",
+            "domainName": "foo.kabletown.net",
+            "guid": null,
+            "hostName": "edge-in-cdn1-only",
+            "httpsPort": 443,
+            "iloIpAddress": "",
+            "iloIpGateway": "",
+            "iloIpNetmask": "",
+            "iloPassword": "",
+            "iloUsername": "",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "192.0.2.88/24",
+                            "gateway": "192.0.2.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2001:db8:f33d:beef::2/64",
+                            "gateway": "2001:db8:f33d:beef::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
+            "mgmtIpAddress": "",
+            "mgmtIpGateway": "",
+            "mgmtIpNetmask": "",
+            "offlineReason": null,
+            "physLocation": "Denver",
+            "profile": "EDGE1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "routerHostName": "",
+            "routerPortName": "",
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "EDGE",
+            "updPending": false,
+            "xmppId": "",
+            "xmppPasswd": ""
+        },
+        {
             "cachegroup": "fallback1",
             "cdnName": "cdn2",
             "domainName": "kabletown.net",
@@ -3630,6 +3913,40 @@
             "updPending": false
         },
         {
+            "cachegroup": "topology-mid-cg-01",
+            "cdnName": "cdn1",
+            "domainName": "kabletown.net",
+            "hostName": "midInTopologyMidCg01InCDN1",
+            "httpsPort": 443,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2001:db8:de4d:beef::12/64",
+                            "gateway": "2001:db8:de4d:beef::1",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "192.0.12.21/24",
+                            "gateway": "192.0.12.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
+            "physLocation": "Denver",
+            "profile": "MID1",
+            "rack": "RR 119.02",
+            "revalPending": false,
+            "status": "REPORTED",
+            "tcpPort": 80,
+            "type": "MID",
+            "updPending": false
+        },
+        {
             "cachegroup": "topology-mid-cg-02",
             "cdnName": "cdn2",
             "domainName": "kabletown.net",
@@ -4230,6 +4547,24 @@
                     "parents": [1]
                 }
             ]
+        },
+        {
+            "name": "top-used-by-cdn1-and-cdn2",
+            "description": "a topology",
+            "nodes": [
+                {
+                    "cachegroup": "dtrc1",
+                    "parents": []
+                },
+                {
+                    "cachegroup": "dtrc2",
+                    "parents": [0]
+                },
+                {
+                    "cachegroup": "dtrc3",
+                    "parents": [0]
+                }
+            ]
         }
     ],
     "types": [
diff --git a/traffic_ops/testing/api/v3/topologies_test.go b/traffic_ops/testing/api/v3/topologies_test.go
index 2b43a31..4350c8e 100644
--- a/traffic_ops/testing/api/v3/topologies_test.go
+++ b/traffic_ops/testing/api/v3/topologies_test.go
@@ -21,7 +21,9 @@ package v3
 
 import (
 	"fmt"
+	"net/url"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -164,14 +166,6 @@ func updateSingleTopology(topology tc.Topology) error {
 }
 
 func UpdateTestTopologies(t *testing.T) {
-	firstTopName := testData.Topologies[0].Name
-	for _, top := range testData.Topologies {
-		top.Name = firstTopName
-		if err := updateSingleTopology(top); err != nil {
-			t.Fatalf(err.Error())
-		}
-	}
-	// Revert test topologies
 	for _, topology := range testData.Topologies {
 		if err := updateSingleTopology(topology); err != nil {
 			t.Fatalf(err.Error())
@@ -188,6 +182,62 @@ func UpdateTestTopologies(t *testing.T) {
 	if err == nil {
 		t.Errorf("making invalid update to topology - expected: error, actual: nil")
 	}
+
+	// attempt to add a cachegroup that only has caches in one CDN while the topology is assigned to DSes from multiple CDNs
+	top, _, err = TOSession.GetTopologyWithHdr("top-used-by-cdn1-and-cdn2", nil)
+	if err != nil {
+		t.Fatalf("cannot GET topology: %v", err)
+	}
+	params := url.Values{}
+	params.Add("topology", "top-used-by-cdn1-and-cdn2")
+	dses, _, err := TOSession.GetDeliveryServicesV30WithHdr(nil, params)
+	if err != nil {
+		t.Fatalf("cannot GET delivery services: %v", err)
+	}
+	if len(dses) < 2 {
+		t.Fatalf("expected at least 2 delivery services assigned to topology top-used-by-cdn1-and-cdn2, actual: %d", len(dses))
+	}
+	foundCDN1 := false
+	foundCDN2 := false
+	for _, ds := range dses {
+		if *ds.CDNName == "cdn1" {
+			foundCDN1 = true
+		} else if *ds.CDNName == "cdn2" {
+			foundCDN2 = true
+		}
+	}
+	if !foundCDN1 || !foundCDN2 {
+		t.Fatalf("expected delivery services assigned to topology top-used-by-cdn1-and-cdn2 to be assigned to cdn1 and cdn2")
+	}
+	cgs, _, err := TOSession.GetCacheGroupNullableByNameWithHdr("cdn1-only", nil)
+	if err != nil {
+		t.Fatalf("unable to GET cachegroup by name: %v", err)
+	}
+	if len(cgs) != 1 {
+		t.Fatalf("expected: to get 1 cachegroup named 'cdn1-only', actual: got %d", len(cgs))
+	}
+	params = url.Values{}
+	params.Add("cachegroup", strconv.Itoa(*cgs[0].ID))
+	servers, _, err := TOSession.GetServersWithHdr(&params, nil)
+	if err != nil {
+		t.Fatalf("unable to GET servers by cachegroup: %v", err)
+	}
+	for _, s := range servers.Response {
+		if *s.Cachegroup != "cdn1-only" {
+			t.Fatalf("GET servers by cachegroup 'cdn1-only' - expected: only servers in cachegroup 'cdn1-only', actual: got server in %s", *s.Cachegroup)
+		}
+		if *s.CDNName != "cdn1" {
+			t.Fatalf("expected: servers in cachegroup 'cdn1-only' to only be in cdn1, actual: servers in cdn %s", *s.CDNName)
+		}
+	}
+	top.Nodes = append(top.Nodes, tc.TopologyNode{
+		Cachegroup: "cdn1-only",
+		Parents:    []int{0},
+	})
+	_, _, err = TOSession.UpdateTopology(top.Name, *top)
+	if err == nil {
+		t.Errorf("making invalid update to topology (cachegroup contains only servers from cdn1 while the topology is assigned to delivery services in cdn1 and cdn2) - expected: error, actual: nil")
+	}
 }
 
 func DeleteTestTopologies(t *testing.T) {
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 03ac0a8..53ce367 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -881,6 +881,51 @@ WHERE
 	return dses, nil
 }
 
+// GetDeliveryServiceCDNsByTopology returns a slice of CDN IDs for all delivery services
+// assigned to the given topology.
+func GetDeliveryServiceCDNsByTopology(tx *sql.Tx, topology string) ([]int, error) {
+	q := `
+SELECT
+  COALESCE(ARRAY_AGG(DISTINCT d.cdn_id), '{}'::BIGINT[])
+FROM
+  deliveryservice d
+WHERE
+  d.topology = $1
+`
+	cdnIDs := []int64{}
+	if err := tx.QueryRow(q, topology).Scan(pq.Array(&cdnIDs)); err != nil {
+		return nil, fmt.Errorf("in GetDeliveryServiceCDNsByTopology: querying deliveryservices by topology '%s': %v", topology, err)
+	}
+	res := make([]int, len(cdnIDs))
+	for i, id := range cdnIDs {
+		res[i] = int(id)
+	}
+	return res, nil
+}
+
+// CheckCachegroupHasTopologyBasedDeliveryServicesOnCDN returns true if the given cachegroup is assigned to
+// any topologies with delivery services assigned on the given CDN.
+func CachegroupHasTopologyBasedDeliveryServicesOnCDN(tx *sql.Tx, cachegroupID int, CDNID int) (bool, error) {
+	q := `
+SELECT EXISTS(
+  SELECT
+    1
+  FROM cachegroup c
+  JOIN topology_cachegroup tc on c.name = tc.cachegroup
+  JOIN topology t ON tc.topology = t.name
+  JOIN deliveryservice d on t.name = d.topology
+  WHERE
+    c.id = $1
+    AND d.cdn_id = $2
+)
+`
+	res := false
+	if err := tx.QueryRow(q, cachegroupID, CDNID).Scan(&res); err != nil {
+		return false, fmt.Errorf("in CachegroupHasTopologyBasedDeliveryServicesOnCDN: %v", err)
+	}
+	return res, nil
+}
+
 // GetFederationIDForUserIDByXMLID retrieves the ID of the Federation assigned to the user defined by
 // userID on the Delivery Service identified by xmlid. If no such federation exists, the boolean
 // returned will be 'false', while the error indicates unexpected errors that occurred when querying.
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index 8a0a2b1..369dee9 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -26,7 +26,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/topology"
 	"net"
 	"net/http"
 	"strconv"
@@ -43,6 +42,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/topology"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
 
 	validation "github.com/go-ozzo/ozzo-validation"
@@ -1194,14 +1194,15 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("the server doesn't exist, cannot update"), nil)
 		return
 	}
+	origServer := origSer[0]
 	originalXMPPID := ""
 	originalStatusID := 0
 	changeXMPPID := false
-	if origSer[0].XMPPID != nil {
-		originalXMPPID = *origSer[0].XMPPID
+	if origServer.XMPPID != nil {
+		originalXMPPID = *origServer.XMPPID
 	}
-	if origSer[0].Status != nil {
-		originalStatusID = *origSer[0].StatusID
+	if origServer.Status != nil {
+		originalStatusID = *origServer.StatusID
 	}
 
 	var server tc.ServerNullableV2
@@ -1220,7 +1221,7 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		if newServer.StatusID != nil && *newServer.StatusID != originalStatusID {
 			newServer.StatusLastUpdated = &currentTime
 		} else {
-			newServer.StatusLastUpdated = origSer[0].StatusLastUpdated
+			newServer.StatusLastUpdated = origServer.StatusLastUpdated
 		}
 		serviceInterface, err := validateV3(&newServer, tx)
 		if err != nil {
@@ -1228,15 +1229,6 @@ func Update(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		cacheGroupIds := []int{*origSer[0].CachegroupID}
-		serverIds := []int{*origSer[0].ID}
-		if *origSer[0].CachegroupID != *newServer.CachegroupID {
-			if err = topology.CheckForEmptyCacheGroups(inf.Tx, cacheGroupIds, true, serverIds); err != nil {
-				api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("server is the last one in its cachegroup, which is used by a topology, so it cannot be moved to another cachegroup: "+err.Error()), nil)
-				return
-			}
-		}
-
 		server, err = newServer.ToServerV2()
 		if err != nil {
 			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("converting v3 server to v2 for update: %v", err))
@@ -1291,6 +1283,24 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	if *origServer.CachegroupID != *server.CachegroupID || *origServer.CDNID != *server.CDNID {
+		hasDSOnCDN, err := dbhelpers.CachegroupHasTopologyBasedDeliveryServicesOnCDN(inf.Tx.Tx, *origServer.CachegroupID, *origServer.CDNID)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+			return
+		}
+		CDNIDs := []int{}
+		if hasDSOnCDN {
+			CDNIDs = append(CDNIDs, *origServer.CDNID)
+		}
+		cacheGroupIds := []int{*origServer.CachegroupID}
+		serverIds := []int{*origServer.ID}
+		if err = topology.CheckForEmptyCacheGroups(inf.Tx, cacheGroupIds, CDNIDs, true, serverIds); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("server is the last one in its cachegroup, which is used by a topology, so it cannot be moved to another cachegroup: "+err.Error()), nil)
+			return
+		}
+	}
+
 	server.ID = new(int)
 	*server.ID = inf.IntParams["id"]
 
@@ -1624,13 +1634,21 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("there are somehow two servers with id %d - cannot delete", id))
 		return
 	}
-	if version.Major >= 3 {
-		cacheGroupIds := []int{*servers[0].CachegroupID}
-		serverIds := []int{*servers[0].ID}
-		if err := topology.CheckForEmptyCacheGroups(inf.Tx, cacheGroupIds, true, serverIds); err != nil {
-			api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("server is the last one in its cachegroup, which is used by a topology: "+err.Error()), nil)
-			return
-		}
+	server := servers[0]
+	cacheGroupIds := []int{*server.CachegroupID}
+	serverIds := []int{*server.ID}
+	hasDSOnCDN, err := dbhelpers.CachegroupHasTopologyBasedDeliveryServicesOnCDN(inf.Tx.Tx, *server.CachegroupID, *server.CDNID)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	CDNIDs := []int{}
+	if hasDSOnCDN {
+		CDNIDs = append(CDNIDs, *server.CDNID)
+	}
+	if err := topology.CheckForEmptyCacheGroups(inf.Tx, cacheGroupIds, CDNIDs, true, serverIds); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("server is the last one in its cachegroup, which is used by a topology: "+err.Error()), nil)
+		return
 	}
 
 	userErr, sysErr, errCode = deleteInterfaces(id, tx)
@@ -1652,8 +1670,6 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	server := servers[0]
-
 	if inf.Version.Major >= 3 {
 		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", server)
 	} else {
diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go
index bf77b01..57a9745 100644
--- a/traffic_ops/traffic_ops_golang/topology/topologies.go
+++ b/traffic_ops/traffic_ops_golang/topology/topologies.go
@@ -142,7 +142,12 @@ func (topology *TOTopology) Validate() error {
 	for index, cacheGroup := range cacheGroups {
 		cacheGroupIds[index] = *cacheGroup.ID
 	}
-	rules["empty cachegroups"] = CheckForEmptyCacheGroups(topology.ReqInfo.Tx, cacheGroupIds, false, nil)
+	dsCDNs, err := dbhelpers.GetDeliveryServiceCDNsByTopology(topology.ReqInfo.Tx.Tx, topology.Name)
+	if err != nil {
+		log.Errorf("validating topology: %v", err)
+		return errors.New("unable to validate topology")
+	}
+	rules["empty cachegroups"] = CheckForEmptyCacheGroups(topology.ReqInfo.Tx, cacheGroupIds, dsCDNs, false, nil)
 	rules["required capabilities"] = topology.validateDSRequiredCapabilities()
 
 	/* Only perform further checks if everything so far is valid */
@@ -159,7 +164,10 @@ func (topology *TOTopology) Validate() error {
 	return util.JoinErrs(tovalidate.ToErrors(rules))
 }
 
-func CheckForEmptyCacheGroups(tx *sqlx.Tx, cacheGroupIds []int, cachegroupsInTopology bool, excludeServerIds []int) error {
+// CheckForEmptyCacheGroups checks if the cachegroups are empty (altogether) or empty in any of the given CDN IDs.
+// If cachegroupsInTopology is true, it will only check cachegroups that are used in a topology. Any server IDs in
+// excludeServerIds will not be counted.
+func CheckForEmptyCacheGroups(tx *sqlx.Tx, cacheGroupIds []int, CDNIDs []int, cachegroupsInTopology bool, excludeServerIds []int) error {
 	if excludeServerIds == nil {
 		excludeServerIds = []int{}
 	}
@@ -180,14 +188,16 @@ func CheckForEmptyCacheGroups(tx *sqlx.Tx, cacheGroupIds []int, cachegroupsInTop
 	}
 
 	var (
-		serverCount int
-		cacheGroup  string
-		cacheGroups []string
-		topologies  []string
+		serverCountByCDN int
+		cacheGroup       string
+		cdnID            *int
 	)
+	cgServerCountsByCDN := make(map[int]map[string]int)
+	cgServerCounts := make(map[string]int)
+	topologySetByCachegroup := make(map[string]map[string]struct{})
 	defer log.Close(rows, "unable to close DB connection when checking for cachegroups with no servers")
 	for rows.Next() {
-		var scanTo = []interface{}{&cacheGroup, &serverCount}
+		var scanTo = []interface{}{&cacheGroup, &cdnID, &serverCountByCDN}
 		var topologiesForRow []string
 		if cachegroupsInTopology {
 			scanTo = append(scanTo, pq.Array(&topologiesForRow))
@@ -196,23 +206,70 @@ func CheckForEmptyCacheGroups(tx *sqlx.Tx, cacheGroupIds []int, cachegroupsInTop
 			log.Errorf(systemError, err.Error())
 			return baseError
 		}
-		if serverCount != 0 {
-			break
+		if cdnID != nil {
+			if _, ok := cgServerCountsByCDN[*cdnID]; !ok {
+				cgServerCountsByCDN[*cdnID] = make(map[string]int)
+			}
+			cgServerCountsByCDN[*cdnID][cacheGroup] = serverCountByCDN
 		}
-		cacheGroups = append(cacheGroups, cacheGroup)
+		cgServerCounts[cacheGroup] += serverCountByCDN
+
 		if cachegroupsInTopology {
-			topologies = append(topologies, topologiesForRow...)
+			if _, ok := topologySetByCachegroup[cacheGroup]; !ok {
+				topologySetByCachegroup[cacheGroup] = make(map[string]struct{})
+			}
+			for _, topology := range topologiesForRow {
+				topologySetByCachegroup[cacheGroup][topology] = struct{}{}
+			}
+		}
+	}
+	topologiesByCachegroup := make(map[string][]string, len(topologySetByCachegroup))
+	for cg, topologySet := range topologySetByCachegroup {
+		for topology := range topologySet {
+			topologiesByCachegroup[cg] = append(topologiesByCachegroup[cg], topology)
+		}
+	}
+	emptyCachegroups := []string{}
+	for cg, count := range cgServerCounts {
+		if count == 0 {
+			messageEntry := cg
+			if cachegroupsInTopology {
+				messageEntry += " (in topologies: " + strings.Join(topologiesByCachegroup[cg], ", ") + ")"
+			}
+			emptyCachegroups = append(emptyCachegroups, messageEntry)
 		}
 	}
 
-	if len(cacheGroups) > 0 {
-		errMessage := "cachegroups with no servers in them: " + strings.Join(cacheGroups, ", ")
-		if cachegroupsInTopology {
-			errMessage += " in topologies: " + strings.Join(topologies, ", ")
+	if len(emptyCachegroups) > 0 {
+		errMessage := "cachegroups with no servers in them: " + strings.Join(emptyCachegroups, ", ")
+		return errors.New(errMessage)
+	}
+
+	errMessage := []string{}
+	for _, cdnID := range CDNIDs {
+		if _, ok := cgServerCountsByCDN[cdnID]; !ok {
+			return fmt.Errorf("topology is assigned to delivery service on CDN %d, but that CDN has no servers", cdnID)
+		}
+		emptyCachegroupsByCDN := []string{}
+		for cg, serverCount := range cgServerCountsByCDN[cdnID] {
+			if serverCount == 0 {
+				emptyCachegroupsByCDN = append(emptyCachegroupsByCDN, cg)
+			}
+		}
+		// check that this CDN has a count for all given cachegroups
+		for cg := range cgServerCounts {
+			if _, ok := cgServerCountsByCDN[cdnID][cg]; !ok {
+				emptyCachegroupsByCDN = append(emptyCachegroupsByCDN, cg)
+			}
+		}
+		if len(emptyCachegroupsByCDN) > 0 {
+			errMessage = append(errMessage, fmt.Sprintf("topology is assigned to delivery service(s) on CDN %d, but the following cachegroups have no servers in CDN %d: %s", cdnID, cdnID, strings.Join(emptyCachegroupsByCDN, ", ")))
 		}
-		err = errors.New(errMessage)
 	}
-	return err
+	if len(errMessage) > 0 {
+		return errors.New(strings.Join(errMessage, "; "))
+	}
+	return nil
 }
 
 func (topology *TOTopology) nodesInOtherTopologies() ([]tc.TopologyNode, map[string][]string, error) {
@@ -769,6 +826,7 @@ func selectEmptyCacheGroupsQuery(cachegroupsInTopology bool) string {
 	query := fmt.Sprintf(`
 		SELECT
 			c."name",
+			s.cdn_id,
 			COUNT(*) FILTER (
 			    WHERE s.id IS NOT NULL
 			    AND NOT(s."id" = ANY(CAST(:exclude_server_ids AS INT[])))
@@ -777,8 +835,7 @@ func selectEmptyCacheGroupsQuery(cachegroupsInTopology bool) string {
 		%s
 		LEFT JOIN "server" s ON c.id = s.cachegroup
 		WHERE c."id" = ANY(CAST(:cachegroup_ids AS BIGINT[]))
-		GROUP BY c."name"
-		ORDER BY server_count
+		GROUP BY c."name", s.cdn_id
 	`, topologyNames, joinTopologyCachegroups)
 	return query
 }