trafficcontrol-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mitchell...@apache.org
Subject [trafficcontrol] branch master updated: Add Cache-Side Config Generator (#3762)
Date Wed, 25 Sep 2019 00:42:21 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new dc9bdf5  Add Cache-Side Config Generator (#3762)
dc9bdf5 is described below

commit dc9bdf5c13a1a30ffcd74e19f87b7d1e5a470b39
Author: Robert Butts <rob05c@users.noreply.github.com>
AuthorDate: Tue Sep 24 18:42:15 2019 -0600

    Add Cache-Side Config Generator (#3762)
    
    * Add client-side ATS config gen
    
    Adds a client-side ATS config gen interceptor to ORT, with an initial
    parent.config gen and passing everything else to TO. Plan is to
    add all configs to be client-side generated.
    
    * Add TO deliveryserviceserver ids query params
    
    Also adds a client function to specify server and DS IDs, as well as
    a missing client func for the existing deliveryservice?cdn param.
    
    * Add ORT atstccfg to ort build scripts
    
    * Add ORT args to override hostname, OS check
    
    For debugging, or emergencies.
    
    * Add ORT using atstccfg instead of TO directly
    
    Also changes atstccfg to do things ORT needed:
    
     - changed to return the HTTP code as the exit code, on error
     - added CLI option to return which configs are generated (vs proxied)
     - added retry num option, for failed TO attempts
     - changed to use lib/tc-log, and take args for where to log
     - fixed missing license headers
     - added integrity check via SHA512 or Content-length headers
    
    * Add atstccfg insecure, timeout, cache age flags
    
    * Fix merge conflict
    
    * Add TO ORT gen profile configs
    
    * Add atstccfg changelog
    
    * Add atstccfg docs
    
    * Add license for vendored pflag
    
    * Remove old/wrong comments
    
    * Remove unused symbols
    
    * Removed docs comma
    
    * Fix license files
---
 .dependency_license                                |   1 +
 CHANGELOG.md                                       |   1 +
 LICENSE                                            |   8 +
 docs/source/admin/traffic_server.rst               |  15 +
 .../astats.go => lib/go-atscfg/astatsdotconfig.go  |  29 +-
 lib/go-atscfg/atscfg.go                            | 111 ++++
 lib/go-atscfg/atscfg_test.go                       |  52 ++
 .../atsprofile => lib/go-atscfg}/atsdotrules.go    |  32 +-
 lib/go-atscfg/atsdotrules_test.go                  |  52 ++
 .../cache.go => lib/go-atscfg/cachedotconfig.go    |  48 +-
 lib/go-atscfg/cachedotconfig_test.go               |  56 ++
 .../go-atscfg/dropqstringdotconfig.go              |  31 +-
 .../go-atscfg/dropqstringdotconfig_test.go         |  28 +-
 .../ats/atsprofile => lib/go-atscfg}/facts.go      |  26 +-
 .../facts.go => lib/go-atscfg/facts_test.go        |  32 +-
 .../go-atscfg/loggingdotconfig.go                  |  66 +--
 lib/go-atscfg/loggingdotconfig_test.go             |  79 +++
 .../go-atscfg/loggingdotyaml.go                    |  59 +-
 lib/go-atscfg/loggingdotyaml_test.go               |  59 ++
 .../logsxml.go => lib/go-atscfg/logsdotxml.go      |  54 +-
 lib/go-atscfg/logsdotxml_test.go                   |  79 +++
 lib/go-atscfg/parentdotconfig.go                   | 461 +++++++++++++++
 lib/go-atscfg/parentdotconfig_test.go              | 111 ++++
 .../plugin.go => lib/go-atscfg/plugindotconfig.go  |  29 +-
 .../go-atscfg/plugindotconfig_test.go              |  44 +-
 .../go-atscfg/recordsdotconfig.go                  |  31 +-
 lib/go-atscfg/recordsdotconfig_test.go             |  54 ++
 .../go-atscfg/storagedotconfig.go                  |  37 +-
 lib/go-atscfg/storagedotconfig_test.go             |  76 +++
 .../sysctl.go => lib/go-atscfg/sysctldotconf.go    |  30 +-
 .../go-atscfg/sysctldotconf_test.go                |  47 +-
 .../facts.go => lib/go-atscfg/unknownconfig.go     |  35 +-
 lib/go-atscfg/unknownconfig_test.go                |  71 +++
 .../facts.go => lib/go-atscfg/urisigningconfig.go  |  21 +-
 .../go-atscfg/urisigningconfig_test.go             |  22 +-
 .../sysctl.go => lib/go-atscfg/urlsigconfig.go     |  38 +-
 lib/go-atscfg/urlsigconfig_test.go                 |  70 +++
 .../volume.go => lib/go-atscfg/volumedotconfig.go  |  30 +-
 lib/go-atscfg/volumedotconfig_test.go              |  77 +++
 lib/go-tc/enum.go                                  |   2 +
 licenses/BSD-pflag                                 |  28 +
 traffic_ops/build/build_rpm.sh                     |   7 +-
 traffic_ops/build/traffic_ops_ort.spec             |  55 ++
 traffic_ops/client/deliveryservice.go              |  33 ++
 traffic_ops/client/deliveryserviceserver.go        |  28 +
 traffic_ops/ort/atstccfg/astatsdotconfig.go        |  62 ++
 traffic_ops/ort/atstccfg/atsdotrules.go            |  64 ++
 traffic_ops/ort/atstccfg/atstccfg.go               | 114 ++++
 traffic_ops/ort/atstccfg/cachedotconfig.go         | 101 ++++
 traffic_ops/ort/atstccfg/caching.go                | 177 ++++++
 traffic_ops/ort/atstccfg/config.go                 | 193 +++++++
 traffic_ops/ort/atstccfg/dropqstringdotconfig.go   |  63 ++
 .../ats/atsprofile => ort/atstccfg}/facts.go       |  24 +-
 traffic_ops/ort/atstccfg/loggingdotconfig.go       |  62 ++
 traffic_ops/ort/atstccfg/loggingdotyaml.go         |  62 ++
 traffic_ops/ort/atstccfg/logsxmldotconfig.go       |  62 ++
 traffic_ops/ort/atstccfg/parentdotconfig.go        | 562 ++++++++++++++++++
 traffic_ops/ort/atstccfg/plugindotconfig.go        |  62 ++
 .../facts.go => ort/atstccfg/profile.go}           |  29 +-
 traffic_ops/ort/atstccfg/recordsdotconfig.go       |  62 ++
 traffic_ops/ort/atstccfg/routing.go                | 159 +++++
 traffic_ops/ort/atstccfg/storagedotconfig.go       |  64 ++
 traffic_ops/ort/atstccfg/sysctldotconf.go          |  62 ++
 traffic_ops/ort/atstccfg/toreq.go                  | 345 +++++++++++
 traffic_ops/ort/atstccfg/trafficops.go             | 284 +++++++++
 traffic_ops/ort/atstccfg/unknownconfig.go          |  83 +++
 traffic_ops/ort/atstccfg/urisigningconfig.go       |  54 ++
 .../atstccfg/urisigningconfig_test.go}             |  37 +-
 traffic_ops/ort/atstccfg/urlsigconfig.go           |  86 +++
 .../atstccfg/urlsigconfig_test.go}                 |  37 +-
 traffic_ops/ort/atstccfg/volumedotconfig.go        |  64 ++
 traffic_ops/{bin => ort}/supermicro_udev_mapper.pl |   0
 traffic_ops/{bin => ort}/traffic_ops_ort.pl        | 128 ++--
 .../traffic_ops_golang/ats/atsprofile/astats.go    |  20 +-
 .../ats/atsprofile/atsdotrules.go                  |  33 +-
 .../traffic_ops_golang/ats/atsprofile/cache.go     |  56 +-
 .../ats/atsprofile/dropqstring.go                  |  21 +-
 .../traffic_ops_golang/ats/atsprofile/facts.go     |  13 +-
 .../traffic_ops_golang/ats/atsprofile/logging.go   | 102 +---
 .../ats/atsprofile/loggingyaml.go                  | 102 +---
 .../traffic_ops_golang/ats/atsprofile/logsxml.go   |  73 +--
 .../traffic_ops_golang/ats/atsprofile/plugin.go    |  20 +-
 .../traffic_ops_golang/ats/atsprofile/profile.go   |  61 +-
 .../traffic_ops_golang/ats/atsprofile/records.go   |  34 +-
 .../traffic_ops_golang/ats/atsprofile/storage.go   |  57 +-
 .../traffic_ops_golang/ats/atsprofile/sysctl.go    |  16 +-
 .../traffic_ops_golang/ats/atsprofile/unknown.go   |  94 ++-
 .../ats/atsprofile/urisigning.go                   |   9 +-
 .../traffic_ops_golang/ats/atsprofile/urlsig.go    |  22 +-
 .../traffic_ops_golang/ats/atsprofile/volume.go    |  47 +-
 traffic_ops/traffic_ops_golang/ats/config.go       |  21 +-
 traffic_ops/traffic_ops_golang/ats/db.go           |  49 +-
 .../traffic_ops_golang/ats/parentdotconfig.go      | 642 ++++-----------------
 .../deliveryservice/servers/servers.go             |  73 ++-
 traffic_ops/traffic_ops_golang/routing/routes.go   |   1 +
 vendor/github.com/ogier/pflag/.travis.yml          |   1 +
 vendor/github.com/ogier/pflag/LICENSE              |  28 +
 vendor/github.com/ogier/pflag/README.md            | 157 +++++
 vendor/github.com/ogier/pflag/bool.go              |  79 +++
 vendor/github.com/ogier/pflag/bool_test.go         | 164 ++++++
 vendor/github.com/ogier/pflag/duration.go          |  74 +++
 vendor/github.com/ogier/pflag/example_test.go      |  73 +++
 vendor/github.com/ogier/pflag/export_test.go       |  29 +
 vendor/github.com/ogier/pflag/flag.go              | 624 ++++++++++++++++++++
 vendor/github.com/ogier/pflag/flag_test.go         | 350 +++++++++++
 vendor/github.com/ogier/pflag/float32.go           |  70 +++
 vendor/github.com/ogier/pflag/float64.go           |  70 +++
 vendor/github.com/ogier/pflag/int.go               |  70 +++
 vendor/github.com/ogier/pflag/int32.go             |  70 +++
 vendor/github.com/ogier/pflag/int64.go             |  70 +++
 vendor/github.com/ogier/pflag/int8.go              |  70 +++
 vendor/github.com/ogier/pflag/ip.go                |  75 +++
 vendor/github.com/ogier/pflag/ipmask.go            |  85 +++
 vendor/github.com/ogier/pflag/revision.txt         |   1 +
 vendor/github.com/ogier/pflag/string.go            |  66 +++
 vendor/github.com/ogier/pflag/uint.go              |  70 +++
 vendor/github.com/ogier/pflag/uint16.go            |  71 +++
 vendor/github.com/ogier/pflag/uint32.go            |  71 +++
 vendor/github.com/ogier/pflag/uint64.go            |  70 +++
 vendor/github.com/ogier/pflag/uint8.go             |  70 +++
 vendor/github.com/pkg/errors/.travis.yml           |  10 +
 vendor/github.com/pkg/errors/LICENSE               |  23 +
 vendor/github.com/pkg/errors/Makefile              |  44 ++
 vendor/github.com/pkg/errors/README.md             |  59 ++
 vendor/github.com/pkg/errors/appveyor.yml          |  32 +
 vendor/github.com/pkg/errors/bench_test.go         | 110 ++++
 vendor/github.com/pkg/errors/commit.txt            |   1 +
 vendor/github.com/pkg/errors/errors.go             | 282 +++++++++
 vendor/github.com/pkg/errors/errors_test.go        | 251 ++++++++
 vendor/github.com/pkg/errors/example_test.go       | 205 +++++++
 vendor/github.com/pkg/errors/format_test.go        | 560 ++++++++++++++++++
 vendor/github.com/pkg/errors/json_test.go          |  51 ++
 vendor/github.com/pkg/errors/stack.go              | 177 ++++++
 vendor/github.com/pkg/errors/stack_test.go         | 250 ++++++++
 134 files changed, 10008 insertions(+), 1648 deletions(-)

diff --git a/.dependency_license b/.dependency_license
index 12b48e8..da8a968 100644
--- a/.dependency_license
+++ b/.dependency_license
@@ -101,6 +101,7 @@ jquery\.tree\.min\.css$, MIT
 jquery\.dataTables\..*\.(css|js)$, MIT
 github\.com/basho/backoff/.*, MIT
 github\.com/dchest/siphash/.*, CC0
+github\.com/pkg/errors\..*, BSD
 traffic_portal/app/src/assets/js/chartjs/angular-chart\..*, BSD
 traffic_portal/app/src/assets/css/jsonformatter\..*, Apache
 traffic_portal/app/src/assets/js/jsonformatter\..*, Apache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0bcff7..94eecc1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Added pagination support to some Traffic Ops endpoints via three new query parameters, limit and offset/page
 - Traffic Ops now supports a "sortOrder" query parameter on some endpoints to return API responses in descending order
 - Traffic Ops now uses a consistent format for audit logs across all Go endpoints
+- Added cache-side config generator, atstccfg, installed with ORT. Currently includes parent.config and all profile configs, proxies others to Traffic Ops.
 
 ### Changed
 - Traffic Router:  TR will now allow steering DSs and steering target DSs to have RGB enabled. (fixes #3910)
diff --git a/LICENSE b/LICENSE
index 4e351e7..f74f345 100644
--- a/LICENSE
+++ b/LICENSE
@@ -440,3 +440,11 @@ For the lestrrat-go/jwx (commit e35178a) component:
 For the dgrijalva/jwt-go (commit 5e25c22) component:
 @traffic_ops/traffic_ops_golang/vendor/github.com/dgrijalva/jwt-go/*
 ./traffic_ops/traffic_ops_golang/vendor/github.com/dgrijalva/jwt-go/LICENSE
+
+For the ogier/pflag (commit 45c278a) component:
+@vendor/github.com/ogier/pflag/*
+./licenses/BSD-pflag
+
+For the errors (commit 27936f6) component:
+@vendor/github.com/pkg/errors/*
+./vendor/github.com/pkg/errors/LICENSE
diff --git a/docs/source/admin/traffic_server.rst b/docs/source/admin/traffic_server.rst
index fae57ee..051d696 100644
--- a/docs/source/admin/traffic_server.rst
+++ b/docs/source/admin/traffic_server.rst
@@ -80,6 +80,21 @@ Configuring Traffic Server
 ==========================
 All of the :abbr:`ATS (Apache Traffic Server)` application configuration files are generated by Traffic Ops and installed by :term:`ORT`. The :file:`traffic_ops_ort.pl` file should be installed on all :term:`cache server` s (See `Installing the ORT Script`_), usually in ``/opt/ort``. It is used to do the initial install of the configuration files when the :term:`cache server` is being deployed, and to keep the configuration files up-to-date when the :term:`cache server` is already in service.
 
+.. _config-generation:
+
+ORT Config File Generation
+-------------------------
+
+In the past, ATS config files were generated by Traffic Ops. Traffic Control is in the process of moving ATS config file generation to a library for generic use, and to an application which uses the library and resides on the cache.
+
+The library, ``lib/go-atscfg``, allows users to write their own applications and servers, if they wish to generate ATS configuration files and deploy them to caches via other means. For example, if you wish to generate config files with an additional service, or continue generating config files on Traffic Ops itself via a plugin or local service.
+
+The app, ``atstccfg``, is installed by the traffic_ops_ort RPM alongside the ORT script. This app makes standard API calls to Traffic Ops, and uses their data to build the ATS config files. The ORT script now requests all config through the app, which generates config files it has, and requests directly from Traffic Ops the files it doesn't recognize.
+
+This provides several benefits. Primarily, reduces the overhead and risk of the monolithic Traffic Ops installation and upgrade process, and allows operators to canary-test config changes one cache at a time, and in the event of an error, only rolling back a few canary caches rather than the entire Traffic Ops instance.
+
+In order to see which config files are generated by a given ``ORT`` or ``atstccfg`` version, run ``/opt/ort/atstccfg --print-generated-files``.
+
 .. _installing-ort:
 
 Installing the ORT Script
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go b/lib/go-atscfg/astatsdotconfig.go
similarity index 65%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
copy to lib/go-atscfg/astatsdotconfig.go
index bbc302e..910f048 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
+++ b/lib/go-atscfg/astatsdotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,25 +19,20 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
 const AstatsSeparator = "="
 const AstatsFileName = "astats.config"
 
-func GetAstats(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeAstats)
-}
-
-func makeAstats(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, AstatsFileName, AstatsSeparator)
-	if err == nil && txt == "" {
+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)
+	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return txt, err
+	txt = hdr + txt
+	return txt
 }
diff --git a/lib/go-atscfg/atscfg.go b/lib/go-atscfg/atscfg.go
new file mode 100644
index 0000000..4209b8f
--- /dev/null
+++ b/lib/go-atscfg/atscfg.go
@@ -0,0 +1,111 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+const HeaderCommentDateFormat = "Mon Jan 2 15:04:05 MST 2006"
+
+type ServerInfo struct {
+	CacheGroupID                  int
+	CDN                           tc.CDNName
+	CDNID                         int
+	DomainName                    string
+	HostName                      string
+	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 HeaderCommentWithTOVersionStr(name string, nameVersionStr string) string {
+	return "# DO NOT EDIT - Generated for " + name + " by " + nameVersionStr + " on " + time.Now().Format(HeaderCommentDateFormat) + "\n"
+}
+
+func GetNameVersionStringFromToolNameAndURL(toolName string, url string) string {
+	return toolName + " (" + url + ")"
+}
+
+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.
+// 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 not a number.
+func GetATSMajorVersionFromATSVersion(atsVersion string) (int, error) {
+	if len(atsVersion) == 0 {
+		return 0, errors.New("ats version missing")
+	}
+	atsMajorVer, err := strconv.Atoi(atsVersion[:1])
+	if err != nil {
+		return 0, errors.New("ats version parameter '" + atsVersion + "' is not a number")
+	}
+	return atsMajorVer, 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.
+// 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(
+	paramData map[string]string, // GetProfileParamData(tx, profileID, fileName)
+	separator string,
+) string {
+	text := ""
+	for name, val := range paramData {
+		name = trimParamUnderscoreNumSuffix(name)
+		text += name + separator + val + "\n"
+	}
+	return text
+}
+
+// trimParamUnderscoreNumSuffix removes any trailing "__[0-9]+" and returns the trimmed string.
+func trimParamUnderscoreNumSuffix(paramName string) string {
+	underscorePos := strings.LastIndex(paramName, `__`)
+	if underscorePos == -1 {
+		return paramName
+	}
+	if _, err := strconv.ParseFloat(paramName[underscorePos+2:], 64); err != nil {
+		return paramName
+	}
+	return paramName[:underscorePos]
+}
diff --git a/lib/go-atscfg/atscfg_test.go b/lib/go-atscfg/atscfg_test.go
new file mode 100644
index 0000000..3c45437
--- /dev/null
+++ b/lib/go-atscfg/atscfg_test.go
@@ -0,0 +1,52 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestGenericHeaderComment(t *testing.T) {
+	objName := "foo"
+	toolName := "bar"
+	toURL := "url"
+
+	txt := GenericHeaderComment(objName, toolName, toURL)
+
+	testComment(t, txt, objName, toolName, toURL)
+}
+
+func testComment(t *testing.T, txt string, objName string, toolName string, toURL 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 + "'")
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go b/lib/go-atscfg/atsdotrules.go
similarity index 63%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
copy to lib/go-atscfg/atsdotrules.go
index 45eb470..e2b46ab 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
+++ b/lib/go-atscfg/atsdotrules.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,30 +20,20 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strings"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-func GetATSDotRules(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeATSDotRules)
-}
-
-func makeATSDotRules(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	// TODO add more efficient db func to only get drive params?
-	paramData, err := ats.GetProfileParamData(tx, profile.ID, "storage.config") // ats.rules is based on the storage.config params
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
+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)
 
 	drivePrefix := strings.TrimPrefix(paramData["Drive_Prefix"], `/dev/`)
 	drivePostfix := strings.Split(paramData["Drive_Letters"], ",")
 
-	text := ""
 	for _, l := range drivePostfix {
 		l = strings.TrimSpace(l)
 		if l == "" {
@@ -58,8 +48,6 @@ func makeATSDotRules(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, file
 			text += `KERNEL=="` + ramPrefix + l + `", OWNER="ats"` + "\n"
 		}
 	}
-	if text == "" {
-		text = "\n" // prevents it being flagged as "not found"
-	}
-	return text, nil
+
+	return text
 }
diff --git a/lib/go-atscfg/atsdotrules_test.go b/lib/go-atscfg/atsdotrules_test.go
new file mode 100644
index 0000000..8c6c87b
--- /dev/null
+++ b/lib/go-atscfg/atsdotrules_test.go
@@ -0,0 +1,52 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+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",
+	}
+
+	txt := MakeATSDotRules(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	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)
+	}
+
+	if !strings.Contains(txt, "sda") {
+		t.Errorf("expected sda for drive letter, actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "rah") {
+		t.Errorf("expected sda for drive letter, actual: '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go b/lib/go-atscfg/cachedotconfig.go
similarity index 62%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
copy to lib/go-atscfg/cachedotconfig.go
index affd050..ce7919c 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
+++ b/lib/go-atscfg/cachedotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,28 +20,26 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-func GetCache(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeCache)
+type ProfileDS struct {
+	Type       tc.DSType
+	OriginFQDN *string
 }
 
-func makeCache(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
+// 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 {
 	lines := map[string]struct{}{} // use a "set" for lines, to avoid duplicates, since we're looking up by profile
-	profileDSes, err := ats.GetProfileDS(tx, profile.ID)
-	if err != nil {
-		return "", errors.New("getting profile delivery services: " + err.Error())
-	}
-
 	for _, ds := range profileDSes {
 		if ds.Type != tc.DSTypeHTTPNoCache {
 			continue
@@ -67,7 +65,27 @@ func makeCache(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string)
 	if text == "" {
 		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return text, nil
+	hdr := GenericHeaderComment(profileName, toToolName, toURL)
+	text = hdr + text
+	return text
+}
+
+// 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{}
+	for _, ds := range dses {
+		pds := ProfileDS{}
+		if ds.Type != nil {
+			pds.Type = *ds.Type
+		}
+		if ds.OrgServerFQDN != nil && *ds.OrgServerFQDN != "" {
+			org := *ds.OrgServerFQDN
+			pds.OriginFQDN = &org
+		}
+		pdses = append(pdses, pds)
+	}
+	return pdses
 }
 
 func getHostPortFromURI(uriStr string) (string, string) {
diff --git a/lib/go-atscfg/cachedotconfig_test.go b/lib/go-atscfg/cachedotconfig_test.go
new file mode 100644
index 0000000..b2d6a4d
--- /dev/null
+++ b/lib/go-atscfg/cachedotconfig_test.go
@@ -0,0 +1,56 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+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},
+	}
+
+	txt := MakeCacheDotConfig(profileName, profileDSes, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if strings.Contains(txt, "my.fqdn.example.net") {
+		t.Errorf("expected cached DS type 'my.fqdn.example.net' omitted, actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "my.other.fqdn.example.net") {
+		t.Errorf("expected cached DS type 'my.fqdn.example.net' omitted, actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "nocache-fqn-should-not-exist.example.net") {
+		t.Errorf("expected config include NoCache DS origin 'nocache-fqn-should-not-exist.example.net', actual: '%v'", txt)
+	}
+
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/dropqstringdotconfig.go
similarity index 56%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/dropqstringdotconfig.go
index 6b94a55..b3efa7c 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/dropqstringdotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,19 +19,20 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
+const DropQStringDotConfigFileName = "drop_qstring.config"
+const DropQStringDotConfigParamName = "content"
 
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
-
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+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)
+	if dropQStringVal != nil {
+		text += *dropQStringVal + "\n"
+	} else {
+		text += `/([^?]+) $s://$t/$1` + "\n"
+	}
+	return text
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/dropqstringdotconfig_test.go
similarity index 62%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/dropqstringdotconfig_test.go
index 6b94a55..d49cc68 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/dropqstringdotconfig_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,22 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"strings"
+	"testing"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
+func TestMakeDropQStringDotConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	dropQStringVal := "myDropQStringVal"
+
+	txt := MakeDropQStringDotConfig(profileName, toolName, toURL, &dropQStringVal)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, dropQStringVal) {
+		t.Errorf("expected dropQStringVal '"+dropQStringVal+"' actual comment, actual: '%v'", txt)
+	}
 
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/facts.go
similarity index 63%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/facts.go
index 6b94a55..b8413c0 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/facts.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,19 +19,13 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
-
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+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)
+	txt := hdr
+	txt += "profile:" + profileName + "\n"
+	return txt
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/facts_test.go
similarity index 55%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/facts_test.go
index 6b94a55..4c575ab 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/facts_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,26 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"strings"
+	"testing"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
+func TestMake12MFacts(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+
+	txt := Make12MFacts(profileName, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	lines := strings.SplitN(txt, "\n", 2) // SplitN always returns at least 1 element, no need to check len before indexing
+	if len(lines) < 2 {
+		t.Fatalf("expected at least one line after the comment, found: 0")
+	}
+	afterCommentLines := lines[1]
 
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+	if !strings.Contains(afterCommentLines, profileName) {
+		t.Errorf("expected profile name '"+profileName+"' in config, actual: '%v'", txt)
+	}
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go b/lib/go-atscfg/loggingdotconfig.go
similarity index 61%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
copy to lib/go-atscfg/loggingdotconfig.go
index 2fe0b38..f0f5dd7 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
+++ b/lib/go-atscfg/loggingdotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,36 +20,26 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strconv"
 	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const LoggingFileName = "logging.config"
-
-func GetLogging(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	WithProfileDataHdr(w, r, addHdr, tc.ContentTypeTextPlain, makeLogging) // TODO change to Content-Type text/x-lua? Perl uses text/plain.
-}
+const MaxLogObjects = 10
 
-func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LoggingFileName)
+const LoggingFileName = "logging.config"
 
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
+// 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 {
 
-	hdrComment, err := ats.HeaderComment(tx, profile.Name)
-	if err != nil {
-		return "", errors.New("getting header comment: " + err.Error())
-	}
+	hdrComment := GenericHeaderComment(profileName, toToolName, toURL)
 	// 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)
@@ -60,11 +50,11 @@ func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 		if i > 0 {
 			logFormatField += strconv.Itoa(i)
 		}
-		if logFormatName := profileParamData[logFormatField+".Name"]; logFormatName != "" {
-			format := profileParamData[logFormatField+".Format"]
+		if logFormatName := paramData[logFormatField+".Name"]; logFormatName != "" {
+			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", profile.Name, logFormatField)
+				log.Errorf("Profile '%v' has logging.config format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", profileName, logFormatField)
 			}
 			format = strings.Replace(format, `"`, `\"`, -1)
 			text += logFormatName + " = format {\n"
@@ -79,17 +69,17 @@ func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 			logFilterField += strconv.Itoa(i)
 		}
 
-		if logFilterName := profileParamData[logFilterField+".Name"]; logFilterName != "" {
-			filter := profileParamData[logFilterField+".Filter"]
+		if logFilterName := paramData[logFilterField+".Name"]; logFilterName != "" {
+			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", profile.Name, logFilterField)
+				log.Errorf("Profile '%v' has logging.config format '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", profileName, logFilterField)
 			}
 
 			filter = strings.Replace(filter, `\`, `\\`, -1)
 			filter = strings.Replace(filter, `'`, `\'`, -1)
 
-			logFilterType := profileParamData[logFilterField+".Type"]
+			logFilterType := paramData[logFilterField+".Type"]
 			if logFilterType == "" {
 				logFilterType = "accept"
 			}
@@ -103,17 +93,17 @@ func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 			logObjectField += strconv.Itoa(i)
 		}
 
-		if logObjectFilename := profileParamData[logObjectField+".Filename"]; logObjectFilename != "" {
-			logObjectType := profileParamData[logObjectField+".Type"]
+		if logObjectFilename := paramData[logObjectField+".Filename"]; logObjectFilename != "" {
+			logObjectType := paramData[logObjectField+".Type"]
 			if logObjectType == "" {
 				logObjectType = "ascii"
 			}
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectFilters := profileParamData[logObjectField+".Filters"]
+			logObjectFormat := paramData[logObjectField+".Format"]
+			logObjectRollingEnabled := paramData[logObjectField+".RollingEnabled"]
+			logObjectRollingIntervalSec := paramData[logObjectField+".RollingIntervalSec"]
+			logObjectRollingOffsetHr := paramData[logObjectField+".RollingOffsetHr"]
+			logObjectRollingSizeMb := paramData[logObjectField+".RollingSizeMb"]
+			logObjectFilters := paramData[logObjectField+".Filters"]
 
 			text += "\nlog." + logObjectType + " {\n"
 			text += "  Format = " + logObjectFormat + ",\n"
@@ -132,5 +122,5 @@ func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 		}
 	}
 
-	return text, nil
+	return text
 }
diff --git a/lib/go-atscfg/loggingdotconfig_test.go b/lib/go-atscfg/loggingdotconfig_test.go
new file mode 100644
index 0000000..3e3144a
--- /dev/null
+++ b/lib/go-atscfg/loggingdotconfig_test.go
@@ -0,0 +1,79 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeLoggingDotConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := 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)
+
+	testLuaComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "myFormatName") {
+		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFormat") {
+		t.Errorf("expected config to contain LogFormat.Format 'myFormat', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFilename") {
+		t.Errorf("expected config to contain LogFormat.Filename 'myFilename', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myRollingEnabled") {
+		t.Errorf("expected config to contain LogFormat.RollingEnabled 'myRollingEnabled', actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "ShouldNotBeHere") {
+		t.Errorf("expected config to omit unknown config 'ShouldNotBeHere', actual: '%v'", txt)
+	}
+}
+
+func testLuaComment(t *testing.T, txt string, objName string, toolName string, toURL 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.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 + "'")
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go b/lib/go-atscfg/loggingdotyaml.go
similarity index 65%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
copy to lib/go-atscfg/loggingdotyaml.go
index fe302f0..6f9ffae 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
+++ b/lib/go-atscfg/loggingdotyaml.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,44 +20,38 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strconv"
 	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 const LoggingYAMLFileName = "logging.yaml"
 
-func GetLoggingYAML(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeLoggingYAML) // TODO change to Content-Type text/yaml? Perl uses text/plain.
-}
-
-func makeLoggingYAML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LoggingYAMLFileName)
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
+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)
 
 	// 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
 
-	text := "\nformats: \n"
+	text := hdr
+	text += "\nformats: \n"
 	for i := 0; i < maxLogObjects; i++ {
 		logFormatField := "LogFormat"
 		if i > 0 {
 			logFormatField += strconv.Itoa(i)
 		}
-		logFormatName := profileParamData[logFormatField+".Name"]
+		logFormatName := paramData[logFormatField+".Name"]
 		if logFormatName != "" {
-			format := profileParamData[logFormatField+".Format"]
+			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", profile.Name, logFormatField)
+				log.Errorf("Profile '%v' has logging.yaml format '%v' Name Parameter but no Format Parameter. Setting blank Format!\n", profileName, logFormatField)
 			}
 			text += " - name: " + logFormatName + " \n"
 			text += "   format: '" + format + "'\n"
@@ -70,13 +64,13 @@ func makeLoggingYAML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ st
 		if i > 0 {
 			logFilterField += strconv.Itoa(i)
 		}
-		if logFilterName := profileParamData[logFilterField+".Name"]; logFilterName != "" {
-			filter := profileParamData[logFilterField+".Filter"]
+		if logFilterName := paramData[logFilterField+".Name"]; logFilterName != "" {
+			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", profile.Name, logFilterField)
+				log.Errorf("Profile '%v' has logging.yaml filter '%v' Name Parameter but no Filter Parameter. Setting blank Filter!\n", profileName, logFilterField)
 			}
-			logFilterType := profileParamData[logFilterField+".Type"]
+			logFilterType := paramData[logFilterField+".Type"]
 			if logFilterType == "" {
 				logFilterType = "accept"
 			}
@@ -92,17 +86,17 @@ func makeLoggingYAML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ st
 			logObjectField += strconv.Itoa(i)
 		}
 
-		if logObjectFilename := profileParamData[logObjectField+".Filename"]; logObjectFilename != "" {
-			logObjectType := profileParamData[logObjectField+".Type"]
+		if logObjectFilename := paramData[logObjectField+".Filename"]; logObjectFilename != "" {
+			logObjectType := paramData[logObjectField+".Type"]
 			if logObjectType == "" {
 				logObjectType = "ascii"
 			}
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectFilters := profileParamData[logObjectField+".Filters"]
+			logObjectFormat := paramData[logObjectField+".Format"]
+			logObjectRollingEnabled := paramData[logObjectField+".RollingEnabled"]
+			logObjectRollingIntervalSec := paramData[logObjectField+".RollingIntervalSec"]
+			logObjectRollingOffsetHr := paramData[logObjectField+".RollingOffsetHr"]
+			logObjectRollingSizeMb := paramData[logObjectField+".RollingSizeMb"]
+			logObjectFilters := paramData[logObjectField+".Filters"]
 
 			text += "\nlogs:\n"
 			text += "- mode: " + logObjectType + "\n"
@@ -129,5 +123,6 @@ func makeLoggingYAML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ st
 			}
 		}
 	}
-	return text, nil
+
+	return text
 }
diff --git a/lib/go-atscfg/loggingdotyaml_test.go b/lib/go-atscfg/loggingdotyaml_test.go
new file mode 100644
index 0000000..9d2707a
--- /dev/null
+++ b/lib/go-atscfg/loggingdotyaml_test.go
@@ -0,0 +1,59 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeLoggingDotYAML(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := 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)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "myFormatName") {
+		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFormat") {
+		t.Errorf("expected config to contain LogFormat.Format 'myFormat', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFilename") {
+		t.Errorf("expected config to contain LogFormat.Filename 'myFilename', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myRollingEnabled") {
+		t.Errorf("expected config to contain LogFormat.RollingEnabled 'myRollingEnabled', actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "ShouldNotBeHere") {
+		t.Errorf("expected config to omit unknown config 'ShouldNotBeHere', actual: '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go b/lib/go-atscfg/logsdotxml.go
similarity index 55%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
copy to lib/go-atscfg/logsdotxml.go
index 8d033ce..681fa69 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
+++ b/lib/go-atscfg/logsdotxml.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,36 +20,19 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strconv"
 	"strings"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 const LogsXMLFileName = "logs_xml.config"
 
-const MaxLogObjects = 10
-
-func GetLogsXML(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	WithProfileDataHdr(w, r, addHdr, tc.ContentTypeTextPlain, makeLogsXML) // TODO change to Content-Type text/xml? Perl uses text/plain.
-}
-
-func makeLogsXML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LogsXMLFileName)
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
-
-	hdrComment, err := ats.HeaderComment(tx, profile.Name)
-	if err != nil {
-		return "", errors.New("getting header comment: " + err.Error())
-	}
+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 {
+	hdrComment := GenericHeaderComment(profileName, toToolName, toURL)
 	hdrComment = strings.Replace(hdrComment, `# `, ``, -1)
 	hdrComment = strings.Replace(hdrComment, "\n", ``, -1)
 	text := "<!-- " + hdrComment + " -->\n"
@@ -63,9 +46,9 @@ func makeLogsXML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 			logObjectField += iStr
 		}
 
-		logFormatName := profileParamData[logFormatField+".Name"]
+		logFormatName := paramData[logFormatField+".Name"]
 		if logFormatName != "" {
-			format := profileParamData[logFormatField+".Format"]
+			format := paramData[logFormatField+".Format"]
 			format = strings.Replace(format, `"`, `\"`, -1)
 
 			text += `<LogFormat>
@@ -75,14 +58,13 @@ func makeLogsXML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 `
 		}
 
-		logObjectFileName := profileParamData[logObjectField+".Filename"]
-		if logObjectFileName != "" {
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectHeader := profileParamData[logObjectField+".Header"]
+		if logObjectFileName := paramData[logObjectField+".Filename"]; logObjectFileName != "" {
+			logObjectFormat := paramData[logObjectField+".Format"]
+			logObjectRollingEnabled := paramData[logObjectField+".RollingEnabled"]
+			logObjectRollingIntervalSec := paramData[logObjectField+".RollingIntervalSec"]
+			logObjectRollingOffsetHr := paramData[logObjectField+".RollingOffsetHr"]
+			logObjectRollingSizeMb := paramData[logObjectField+".RollingSizeMb"]
+			logObjectHeader := paramData[logObjectField+".Header"]
 
 			text += `<LogObject>
   <Format = "` + logObjectFormat + `"/>
@@ -104,5 +86,5 @@ func makeLogsXML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 `
 		}
 	}
-	return text, nil
+	return text
 }
diff --git a/lib/go-atscfg/logsdotxml_test.go b/lib/go-atscfg/logsdotxml_test.go
new file mode 100644
index 0000000..58e130f
--- /dev/null
+++ b/lib/go-atscfg/logsdotxml_test.go
@@ -0,0 +1,79 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeLogsXMLDotConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"LogFormat.Name":           "myFormatName",
+		"LogFormat.Format":         "myFormat",
+		"LogObject.Filename":       "myFilename",
+		"LogObject.RollingEnabled": "myRollingEnabled",
+		"LogFormat.Invalid":        "ShouldNotBeHere",
+		"LogObject.Invalid":        "ShouldNotBeHere",
+	}
+
+	txt := MakeLogsXMLDotConfig(profileName, paramData, toolName, toURL)
+
+	testXMLComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "myFormatName") {
+		t.Errorf("expected config to contain LogFormat.Name 'myFormatName', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFormat") {
+		t.Errorf("expected config to contain LogFormat.Format 'myFormat', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myFilename") {
+		t.Errorf("expected config to contain LogFormat.Filename 'myFilename', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "myRollingEnabled") {
+		t.Errorf("expected config to contain LogFormat.RollingEnabled 'myRollingEnabled', actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "ShouldNotBeHere") {
+		t.Errorf("expected config to omit unknown config 'ShouldNotBeHere', actual: '%v'", txt)
+	}
+}
+
+func testXMLComment(t *testing.T, txt string, objName string, toolName string, toURL 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.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 + "'")
+	}
+}
diff --git a/lib/go-atscfg/parentdotconfig.go b/lib/go-atscfg/parentdotconfig.go
new file mode 100644
index 0000000..e39b4b1
--- /dev/null
+++ b/lib/go-atscfg/parentdotconfig.go
@@ -0,0 +1,461 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"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"
+)
+
+const InvalidID = -1
+
+const ParentConfigParamQStringHandling = "psel.qstring_handling"
+const ParentConfigParamMSOAlgorithm = "mso.algorithm"
+const ParentConfigParamMSOParentRetry = "mso.parent_retry"
+const ParentConfigParamUnavailableServerRetryResponses = "mso.unavailable_server_retry_responses"
+const ParentConfigParamMaxSimpleRetries = "mso.max_simple_retries"
+const ParentConfigParamMaxUnavailableServerRetries = "mso.max_unavailable_server_retries"
+const ParentConfigParamAlgorithm = "algorithm"
+const ParentConfigParamQString = "qstring"
+
+const ParentConfigDSParamDefaultMSOAlgorithm = "consistent_hash"
+const ParentConfigDSParamDefaultMSOParentRetry = "both"
+const ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses = ""
+const ParentConfigDSParamDefaultMaxSimpleRetries = "1"
+const ParentConfigDSParamDefaultMaxUnavailableServerRetries = "1"
+
+const ParentConfigCacheParamWeight = "weight"
+const ParentConfigCacheParamPort = "port"
+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"
+
+const DefaultATSVersion = "5" // TODO Emulates Perl; change to 6? ATC no longer officially supports ATS 5.
+
+type ParentConfigDS struct {
+	Name            tc.DeliveryServiceName
+	QStringIgnore   tc.QStringIgnore
+	OriginFQDN      string
+	MultiSiteOrigin bool
+	OriginShield    string
+	Type            tc.DSType
+	QStringHandling 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
+}
+
+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 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 { return s[i].Rank < s[j].Rank }
+
+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 {
+	// TODO make this match the Perl sort "foreach my $ds ( sort @{ $data->{dslist} } )" ?
+	return strings.Compare(string(s[i].Name), string(s[j].Name)) < 0
+}
+
+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
+	Status       int
+	Type         int
+	ProfileID    ProfileID
+	CDN          int
+	TypeName     string
+	Domain       string
+}
+
+type OriginURI struct {
+	Scheme string
+	Host   string
+	Port   string
+}
+
+func MakeParentDotConfig(
+	serverInfo *ServerInfo, // getServerInfoByHost OR getServerInfoByID
+	atsMajorVer int, // GetATSMajorVersion (TODO: determine if the cache itself [ORT via Yum] should produce this data, rather than asking TO?)
+	toToolName string, // tm.toolname global parameter (TODO: cache itself?)
+	toURL string, // tm.url global parameter (TODO: cache itself?)
+	parentConfigDSes []ParentConfigDSTopLevel, // getParentConfigDSTopLevel(cdn) OR getParentConfigDS(server) (TODO determine how to handle non-top missing MSO?)
+	serverParams map[string]string, // getParentConfigServerProfileParams(serverID)
+	parentInfos map[string][]ParentInfo, // getParentInfo(profileID, parentCachegroupID, secondaryParentCachegroupID)
+) string {
+
+	// parentInfos := makeParentInfo(serverInfo)
+
+	nameVersionStr := GetNameVersionStringFromToolNameAndURL(toToolName, toURL)
+	hdr := HeaderCommentWithTOVersionStr(serverInfo.HostName, nameVersionStr)
+
+	textArr := []string{}
+	text := ""
+	// TODO put these in separate functions. No if-statement should be this long.
+	if serverInfo.IsTopLevelCache() {
+		uniqueOrigins := map[string]struct{}{}
+
+		for _, ds := range parentConfigDSes {
+			parentQStr := "ignore"
+			if ds.QStringHandling == "" && ds.MSOAlgorithm == tc.AlgorithmConsistentHash && ds.QStringIgnore == tc.QStringIgnoreUseInCacheKeyAndPassUp {
+				parentQStr = "consider"
+			}
+
+			orgURIStr := ds.OriginFQDN
+			orgURI, err := url.Parse(orgURIStr) // TODO verify origin is always a host:port
+			if err != nil {
+				log.Errorln("Malformed ds '" + string(ds.Name) + "' origin  URI: '" + orgURIStr + "', skipping! : " + err.Error())
+				continue
+			}
+			// TODO put in function, to remove duplication
+			if orgURI.Port() == "" {
+				if orgURI.Scheme == "http" {
+					orgURI.Host += ":80"
+				} else if orgURI.Scheme == "https" {
+					orgURI.Host += ":443"
+				} else {
+					log.Errorln("parent.config generation: delivery service '" + string(ds.Name) + "' origin  URI: '" + orgURIStr + "' is unknown scheme '" + orgURI.Scheme + "', but has no port! Using as-is! ")
+				}
+			}
+
+			if _, ok := uniqueOrigins[ds.OriginFQDN]; ok {
+				continue // TODO warn?
+			}
+			uniqueOrigins[ds.OriginFQDN] = struct{}{}
+
+			textLine := ""
+
+			if ds.OriginShield != "" {
+				algorithm := ""
+				if parentSelectAlg := serverParams[ParentConfigParamAlgorithm]; strings.TrimSpace(parentSelectAlg) != "" {
+					algorithm = "round_robin=" + parentSelectAlg
+				}
+				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " parent=" + ds.OriginShield + " " + algorithm + " go_direct=true\n"
+			} else if ds.MultiSiteOrigin {
+				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " "
+				if len(parentInfos) == 0 {
+				}
+
+				if len(parentInfos[orgURI.Hostname()]) == 0 {
+					// TODO error? emulates Perl
+					log.Warnln("ParentInfo: delivery service " + ds.Name + " has no parent servers")
+				}
+
+				rankedParents := ParentInfoSortByRank(parentInfos[orgURI.Hostname()])
+				sort.Sort(rankedParents)
+
+				parentInfo := []string{}
+				secondaryParentInfo := []string{}
+				nullParentInfo := []string{}
+				for _, parent := range ([]ParentInfo)(rankedParents) {
+					if parent.PrimaryParent {
+						parentInfo = append(parentInfo, parent.Format())
+					} else if parent.SecondaryParent {
+						secondaryParentInfo = append(secondaryParentInfo, parent.Format())
+					} else {
+						nullParentInfo = append(nullParentInfo, parent.Format())
+					}
+				}
+
+				if len(parentInfo) == 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
+					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)
+				secondaryParentInfo, seen = util.RemoveStrDuplicates(secondaryParentInfo, seen)
+				nullParentInfo, seen = util.RemoveStrDuplicates(nullParentInfo, seen)
+
+				// If the ats version supports it and the algorithm is consistent hash, put secondary and non-primary parents into secondary parent group.
+				// This will ensure that secondary and tertiary parents will be unused unless all hosts in the primary group are unavailable.
+
+				parents := ""
+
+				if atsMajorVer >= 6 && ds.MSOAlgorithm == "consistent_hash" && (len(secondaryParentInfo) > 0 || len(nullParentInfo) > 0) {
+					parents = `parent="` + strings.Join(parentInfo, "") + `" secondary_parent="` + strings.Join(secondaryParentInfo, "") + strings.Join(nullParentInfo, "") + `"`
+				} else {
+					parents = `parent="` + strings.Join(parentInfo, "") + strings.Join(secondaryParentInfo, "") + strings.Join(nullParentInfo, "") + `"`
+				}
+				textLine += parents + ` round_robin=` + ds.MSOAlgorithm + ` qstring=` + parentQStr + ` go_direct=false parent_is_proxy=false`
+
+				parentRetry := ds.MSOParentRetry
+				if atsMajorVer >= 6 && parentRetry != "" {
+					if unavailableServerRetryResponsesValid(ds.MSOUnavailableServerRetryResponses) {
+						textLine += ` parent_retry=` + parentRetry + ` unavailable_server_retry_responses=` + ds.MSOUnavailableServerRetryResponses
+					} else {
+						if ds.MSOUnavailableServerRetryResponses != "" {
+							log.Errorln("Malformed unavailable_server_retry_responses parameter '" + ds.MSOUnavailableServerRetryResponses + "', not using!")
+						}
+						textLine += ` parent_retry=` + parentRetry
+					}
+					textLine += ` max_simple_retries=` + ds.MSOMaxSimpleRetries + ` max_unavailable_server_retries=` + ds.MSOMaxUnavailableServerRetries
+				}
+				textLine += "\n" // TODO remove, and join later on "\n" instead of ""?
+				textArr = append(textArr, textLine)
+			}
+		}
+		sort.Sort(sort.StringSlice(textArr))
+		text = hdr + strings.Join(textArr, "")
+	} else {
+		processedOriginsToDSNames := map[string]tc.DeliveryServiceName{}
+
+		queryStringHandling := serverParams[ParentConfigParamQStringHandling] // "qsh" in Perl
+		parentInfo := []string{}
+		secondaryParentInfo := []string{}
+
+		parentInfosAllParents := parentInfos[DeliveryServicesAllParentsKey]
+		sort.Sort(ParentInfoSortByRank(parentInfosAllParents))
+
+		for _, parent := range parentInfosAllParents { // TODO fix magic key
+			pTxt := parent.Format()
+			if parent.PrimaryParent {
+				parentInfo = append(parentInfo, pTxt)
+			} else if parent.SecondaryParent {
+				secondaryParentInfo = append(secondaryParentInfo, pTxt)
+			}
+		}
+
+		if len(parentInfo) == 0 {
+			parentInfo = secondaryParentInfo
+			secondaryParentInfo = []string{}
+		}
+
+		// TODO remove duplicate code with top level if block
+		seen := map[string]struct{}{} // TODO change to host+port? host isn't unique
+		parentInfo, seen = util.RemoveStrDuplicates(parentInfo, seen)
+		secondaryParentInfo, seen = util.RemoveStrDuplicates(secondaryParentInfo, seen)
+
+		parents := ""
+		secondaryParents := "" // "secparents" in Perl
+		sort.Sort(sort.StringSlice(parentInfo))
+		sort.Sort(sort.StringSlice(secondaryParentInfo))
+		if atsMajorVer >= 6 && len(secondaryParentInfo) > 0 {
+			parents = `parent="` + strings.Join(parentInfo, "") + `"`
+			secondaryParents = ` secondary_parent="` + strings.Join(secondaryParentInfo, "") + `"`
+		} else {
+			parents = `parent="` + strings.Join(parentInfo, "") + strings.Join(secondaryParentInfo, "") + `"`
+		}
+
+		roundRobin := `round_robin=consistent_hash`
+		goDirect := `go_direct=false`
+
+		sort.Sort(ParentConfigDSTopLevelSortByName(parentConfigDSes))
+		for _, ds := range parentConfigDSes {
+			text := ""
+			originFQDN := ds.OriginFQDN
+			if originFQDN == "" {
+				continue // TODO warn? (Perl doesn't)
+			}
+
+			orgURI, err := url.Parse(originFQDN) // TODO verify
+			if err != nil {
+				log.Errorln("Malformed ds '" + string(ds.Name) + "' origin  URI: '" + originFQDN + "': skipping!" + err.Error())
+				continue
+			}
+
+			if existingDS, ok := processedOriginsToDSNames[originFQDN]; ok {
+				log.Errorln("parent.config generation: duplicate origin! services '" + string(ds.Name) + "' and '" + string(existingDS) + "' share origin '" + orgURI.Host + "': skipping '" + string(ds.Name) + "'!")
+				continue
+			}
+
+			// TODO put in function, to remove duplication
+			if orgURI.Port() == "" {
+				if orgURI.Scheme == "http" {
+					orgURI.Host += ":80"
+				} else if orgURI.Scheme == "https" {
+					orgURI.Host += ":443"
+				} else {
+					log.Errorln("parent.config generation non-top-level: ds '" + string(ds.Name) + "' origin  URI: '" + originFQDN + "' is unknown scheme '" + orgURI.Scheme + "', but has no port! Using as-is! ")
+				}
+			}
+
+			// TODO encode this in a DSType func, IsGoDirect() ?
+			if dsType := tc.DSType(ds.Type); dsType == tc.DSTypeHTTPNoCache || dsType == tc.DSTypeHTTPLive || dsType == tc.DSTypeDNSLive {
+				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` go_direct=true` + "\n"
+			} else {
+
+				// check for profile psel.qstring_handling.  If this parameter is assigned to the server profile,
+				// then edges will use the qstring handling value specified in the parameter for all profiles.
+
+				// If there is no defined parameter in the profile, then check the delivery service profile.
+				// If psel.qstring_handling exists in the DS profile, then we use that value for the specified DS only.
+				// This is used only if not overridden by a server profile qstring handling parameter.
+
+				// TODO refactor this logic, hard to understand (transliterated from Perl)
+				dsQSH := queryStringHandling
+				if dsQSH == "" {
+					dsQSH = ds.QStringHandling
+				}
+				parentQStr := dsQSH
+				if parentQStr == "" {
+					parentQStr = "ignore"
+				}
+				if ds.QStringIgnore == tc.QStringIgnoreUseInCacheKeyAndPassUp && dsQSH == "" {
+					parentQStr = "consider"
+				}
+
+				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` ` + parents + ` ` + secondaryParents + ` ` + roundRobin + ` ` + goDirect + ` qstring=` + parentQStr + "\n"
+			}
+			textArr = append(textArr, text)
+			processedOriginsToDSNames[originFQDN] = ds.Name
+		}
+
+		defaultDestText := `dest_domain=. ` + parents
+		if serverParams[ParentConfigParamAlgorithm] == tc.AlgorithmConsistentHash {
+			defaultDestText += secondaryParents
+		}
+		defaultDestText += ` round_robin=consistent_hash go_direct=false`
+
+		if qStr := serverParams[ParentConfigParamQString]; qStr != "" {
+			defaultDestText += ` qstring=` + qStr
+		}
+		defaultDestText += "\n"
+
+		sort.Sort(sort.StringSlice(textArr))
+		text = hdr + strings.Join(textArr, "") + defaultDestText
+	}
+	return text
+}
+
+func MakeParentInfo(
+	server *ServerInfo,
+	serverDomain string, // getCDNDomainByProfileID(tx, server.ProfileID)
+	profileCaches map[ProfileID]ProfileCache, // getServerParentCacheGroupProfiles(tx, server)
+	originServers map[string][]CGServer, // getServerParentCacheGroupProfiles(tx, server)
+) map[string][]ParentInfo {
+	parentInfos := map[string][]ParentInfo{}
+
+	// note servers also contains an "all" key
+	// originFQDN is "prefix" in Perl; ds is not really a "ds", that's what it's named in Perl
+	for originFQDN, servers := range originServers {
+		for _, row := range servers {
+			profile := profileCaches[row.ProfileID]
+			if profile.NotAParent {
+				continue
+			}
+			// Perl has this check, but we only select servers ("deliveryServices" in Perl) with the right CDN in the first place
+			// if profile.Domain != serverDomain {
+			// 	continue
+			// }
+
+			parentInf := ParentInfo{
+				Host:            row.ServerHost,
+				Port:            profile.Port,
+				Domain:          row.Domain,
+				Weight:          profile.Weight,
+				UseIP:           profile.UseIP,
+				Rank:            profile.Rank,
+				IP:              row.ServerIP,
+				PrimaryParent:   server.ParentCacheGroupID == row.CacheGroupID,
+				SecondaryParent: server.SecondaryParentCacheGroupID == row.CacheGroupID,
+			}
+			if parentInf.Port < 1 {
+				parentInf.Port = row.ServerPort
+			}
+			parentInfos[originFQDN] = append(parentInfos[originFQDN], parentInf)
+		}
+	}
+	return parentInfos
+}
+
+// unavailableServerRetryResponsesValid returns whether a unavailable_server_retry_responses parameter is valid for an ATS parent rule.
+func unavailableServerRetryResponsesValid(s string) bool {
+	// optimization if param is empty
+	if s == "" {
+		return false
+	}
+	re := regexp.MustCompile(`^"(:?\d{3},)+\d{3}"\s*$`) // TODO benchmark, cache if performance matters
+	return re.MatchString(s)
+}
diff --git a/lib/go-atscfg/parentdotconfig_test.go b/lib/go-atscfg/parentdotconfig_test.go
new file mode 100644
index 0000000..81a1372
--- /dev/null
+++ b/lib/go-atscfg/parentdotconfig_test.go
@@ -0,0 +1,111 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+func TestMakeParentDotConfig(t *testing.T) {
+	atsMajorVer := 7
+	serverName := "myserver"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+
+	parentConfigDSes := []ParentConfigDSTopLevel{
+		ParentConfigDSTopLevel{
+			ParentConfigDS: ParentConfigDS{
+				Name:            "ds0",
+				QStringIgnore:   tc.QStringIgnoreUseInCacheKeyAndPassUp,
+				OriginFQDN:      "http://ds0.example.net",
+				MultiSiteOrigin: false,
+				Type:            tc.DSTypeHTTP,
+				QStringHandling: "ds0qstringhandling",
+			},
+		},
+		ParentConfigDSTopLevel{
+			ParentConfigDS: ParentConfigDS{
+				Name:            "ds1",
+				QStringIgnore:   tc.QStringIgnoreDrop,
+				OriginFQDN:      "http://ds1.example.net",
+				MultiSiteOrigin: false,
+				Type:            tc.DSTypeDNS,
+				QStringHandling: "ds1qstringhandling",
+			},
+		},
+	}
+
+	serverInfo := &ServerInfo{
+		CacheGroupID:                  42,
+		CDN:                           "myCDN",
+		CDNID:                         43,
+		DomainName:                    "serverdomain.example.net",
+		HostName:                      "myserver",
+		ID:                            44,
+		IP:                            "192.168.2.1",
+		ParentCacheGroupID:            45,
+		ParentCacheGroupType:          "myParentCGType",
+		ProfileID:                     46,
+		ProfileName:                   "MyProfileName",
+		Port:                          80,
+		SecondaryParentCacheGroupID:   47,
+		SecondaryParentCacheGroupType: "MySecondaryParentCGType",
+		Type:                          "EDGE",
+	}
+
+	serverParams := map[string]string{
+		ParentConfigParamQStringHandling: "myQStringHandlingParam",
+		ParentConfigParamAlgorithm:       tc.AlgorithmConsistentHash,
+		ParentConfigParamQString:         "myQstringParam",
+	}
+
+	parentInfos := map[string][]ParentInfo{
+		"ds1.example.net": []ParentInfo{
+			ParentInfo{
+				Host:            "my-parent-0",
+				Port:            80,
+				Domain:          "my-parent-0-domain",
+				Weight:          "1",
+				UseIP:           false,
+				Rank:            1,
+				IP:              "192.168.2.2",
+				PrimaryParent:   true,
+				SecondaryParent: true,
+			},
+		},
+	}
+
+	txt := MakeParentDotConfig(serverInfo, atsMajorVer, toolName, toURL, parentConfigDSes, serverParams, parentInfos)
+
+	testComment(t, txt, serverName, toolName, toURL)
+
+	if !strings.Contains(txt, "dest_domain=ds0.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "dest_domain=ds1.example.net") {
+		t.Errorf("expected parent 'dest_domain=ds0.example.net', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "qstring=myQStringHandlingParam") {
+		t.Errorf("expected qstring from param 'qstring=myQStringHandlingParam', actual: '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go b/lib/go-atscfg/plugindotconfig.go
similarity index 65%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
copy to lib/go-atscfg/plugindotconfig.go
index 8731508..9a1ba52 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
+++ b/lib/go-atscfg/plugindotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,25 +19,20 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
 const PluginSeparator = " "
 const PluginFileName = "plugin.config"
 
-func GetPlugin(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makePlugin)
-}
-
-func makePlugin(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, PluginFileName, PluginSeparator)
-	if err == nil && txt == "" {
+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)
+	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return txt, err
+	txt = hdr + txt
+	return txt
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go b/lib/go-atscfg/plugindotconfig_test.go
similarity index 51%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
copy to lib/go-atscfg/plugindotconfig_test.go
index 4909766..42f0df4 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
+++ b/lib/go-atscfg/plugindotconfig_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,29 +20,31 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"strings"
+	"testing"
 )
 
-func GetDropQString(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeDropQString)
-}
-
-func makeDropQString(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	dropQStringVal, hasDropQStringParam, err := ats.GetProfileParamValue(tx, profile.ID, "drop_qstring.config", "content")
-	if err != nil {
-		return "", errors.New("getting profile param val: " + err.Error())
+func TestMakePluginDotConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"param0": "val0",
+		"param1": "val1",
+		"param2": "val2",
 	}
 
-	text := ""
-	if hasDropQStringParam {
-		text += dropQStringVal + "\n"
-	} else {
-		text += `/([^?]+) $s://$t/$1` + "\n"
+	txt := MakePluginDotConfig(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "param0 val0") {
+		t.Errorf("expected config to contain paramData 'param0 val0', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "param1 val1") {
+		t.Errorf("expected config to contain paramData 'param1 val1', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "param2 val2") {
+		t.Errorf("expected config to contain paramData 'param2 val2', actual: '%v'", txt)
 	}
-	return text, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go b/lib/go-atscfg/recordsdotconfig.go
similarity index 67%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
copy to lib/go-atscfg/recordsdotconfig.go
index 8e43600..b00e992 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
+++ b/lib/go-atscfg/recordsdotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,34 +20,29 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
 	"strings"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 const RecordsSeparator = " "
 const RecordsFileName = "records.config"
 
-func GetRecords(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeRecords)
-}
-
-func makeRecords(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, RecordsFileName, RecordsSeparator)
-	if err != nil {
-		return "", nil
-	}
+func MakeRecordsDotConfig(
+	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)
 	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__")
-	return txt, nil
+	txt = replaceLineSuffixes(txt, "STRING __HOSTNAME__", "STRING __FULL_HOSTNAME__")
+	txt = hdr + txt
+	return txt
 }
 
-func ReplaceLineSuffixes(txt string, suffix string, newSuffix string) string {
+func replaceLineSuffixes(txt string, suffix string, newSuffix string) string {
 	lines := strings.Split(txt, "\n")
 	newLines := make([]string, 0, len(lines))
 	for _, line := range lines {
diff --git a/lib/go-atscfg/recordsdotconfig_test.go b/lib/go-atscfg/recordsdotconfig_test.go
new file mode 100644
index 0000000..bcb259e
--- /dev/null
+++ b/lib/go-atscfg/recordsdotconfig_test.go
@@ -0,0 +1,54 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeRecordsDotConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"param0":                    "val0",
+		"param1":                    "val1",
+		"param2":                    "val2",
+		"test-hostname-replacement": "fooSTRING __HOSTNAME__",
+	}
+
+	txt := MakeRecordsDotConfig(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "param0 val0") {
+		t.Errorf("expected config to contain paramData 'param0 val0', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "param1 val1") {
+		t.Errorf("expected config to contain paramData 'param1 val1', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "param2 val2") {
+		t.Errorf("expected config to contain paramData 'param2 val2', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "test-hostname-replacement fooSTRING __FULL_HOSTNAME__") {
+		t.Errorf("expected config to replace 'STRING __HOSTNAME__' with 'STRING __FULL_HOSTNAME__', actual: '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go b/lib/go-atscfg/storagedotconfig.go
similarity index 75%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
copy to lib/go-atscfg/storagedotconfig.go
index 1a3d687..ab6f1bd 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
+++ b/lib/go-atscfg/storagedotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,36 +20,27 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strconv"
 	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const StorageFileName = "storage.config"
-
-func GetStorage(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeStorage)
-}
-
-func makeStorage(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
+// 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 MakeStorageDotConfig(
+	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 := ""
 
-	paramData, err := ats.GetProfileParamData(tx, profile.ID, StorageFileName) // ats.rules is based on the storage.config params
-	if err != nil {
-		return "", errors.New("profile param data: " + err.Error())
-	}
-
 	nextVolume := 1
 	if drivePrefix := paramData["Drive_Prefix"]; drivePrefix != "" {
 		driveLetters := strings.TrimSpace(paramData["Drive_Letters"])
 		if driveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has Drive_Prefix parameter, but no Drive_Letters; creating anyway", profile.Name)
+			log.Warnf("Creating storage.config: profile %+v has Drive_Prefix parameter, but no Drive_Letters; creating anyway", profileName)
 		}
 		text += makeStorageVolumeText(drivePrefix, driveLetters, nextVolume)
 		nextVolume++
@@ -58,7 +49,7 @@ func makeStorage(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 	if ramDrivePrefix := paramData["RAM_Drive_Prefix"]; ramDrivePrefix != "" {
 		ramDriveLetters := strings.TrimSpace(paramData["RAM_Drive_Letters"])
 		if ramDriveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has RAM_Drive_Prefix parameter, but no RAM_Drive_Letters; creating anyway", profile.Name)
+			log.Warnf("Creating storage.config: profile %+v has RAM_Drive_Prefix parameter, but no RAM_Drive_Letters; creating anyway", profileName)
 		}
 		text += makeStorageVolumeText(ramDrivePrefix, ramDriveLetters, nextVolume)
 		nextVolume++
@@ -67,7 +58,7 @@ func makeStorage(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 	if ssdDrivePrefix := paramData["SSD_Drive_Prefix"]; ssdDrivePrefix != "" {
 		ssdDriveLetters := strings.TrimSpace(paramData["SSD_Drive_Letters"])
 		if ssdDriveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has SSD_Drive_Prefix parameter, but no SSD_Drive_Letters; creating anyway", profile.Name)
+			log.Warnf("Creating storage.config: profile %+v has SSD_Drive_Prefix parameter, but no SSD_Drive_Letters; creating anyway", profileName)
 		}
 		text += makeStorageVolumeText(ssdDrivePrefix, ssdDriveLetters, nextVolume)
 		nextVolume++
@@ -76,7 +67,9 @@ func makeStorage(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string
 	if text == "" {
 		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return text, nil
+	hdr := GenericHeaderComment(profileName, toToolName, toURL)
+	text = hdr + text
+	return text
 }
 
 func makeStorageVolumeText(prefix string, letters string, volume int) string {
diff --git a/lib/go-atscfg/storagedotconfig_test.go b/lib/go-atscfg/storagedotconfig_test.go
new file mode 100644
index 0000000..bfaac74
--- /dev/null
+++ b/lib/go-atscfg/storagedotconfig_test.go
@@ -0,0 +1,76 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeStorageDotConfig(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",
+		"SSD_Drive_Prefix":  "/dev/ss",
+		"SSD_Drive_Letters": "i,j,k",
+	}
+
+	/*
+	   # DO NOT EDIT - Generated for myProfile by myToolName (https://myto.example.net) on Thu
+	   Aug 8 08:58:54 MDT 2019
+	           /dev/sda volume=1
+	           /dev/sdb volume=1
+	           /dev/sdc volume=1
+	           /dev/sdd volume=1
+	           /dev/sde volume=1
+	           /dev/raf volume=2
+	           /dev/rag volume=2
+	           /dev/rah volume=2
+	           /dev/ssi volume=3
+	           /dev/ssj volume=3
+	   	/dev/ssk volume=3
+	*/
+
+	txt := MakeStorageDotConfig(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if count := strings.Count(txt, "\n"); count != 12 { // one line for each drive letter, plus the comment
+		t.Errorf("expected one line for each drive letter plus a comment, actual: '"+txt+"' count %v", count)
+	}
+
+	if !strings.Contains(txt, paramData["Drive_Prefix"]) {
+		t.Errorf("expected to contain Drive_Prefix '" + paramData["Drive_Prefix"] + "', actual: '" + txt + "'")
+	}
+	if !strings.Contains(txt, paramData["Ram_Drive_Prefix"]) {
+		t.Errorf("expected to contain Ram_Drive_Prefix '" + paramData["Ram_Drive_Prefix"] + "', actual: '" + txt + "'")
+	}
+	if !strings.Contains(txt, paramData["SSD_Drive_Prefix"]) {
+		t.Errorf("expected to contain SSD_Drive_Prefix '" + paramData["SSD_Drive_Prefix"] + "', actual: '" + txt + "'")
+	}
+	if !strings.Contains(txt, paramData["SSD_Drive_Prefix"]) {
+		t.Errorf("expected to contain SSD_Drive_Prefix '" + paramData["SSD_Drive_Prefix"] + "', actual: '" + txt + "'")
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go b/lib/go-atscfg/sysctldotconf.go
similarity index 65%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
copy to lib/go-atscfg/sysctldotconf.go
index 16e5267..fa83280 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
+++ b/lib/go-atscfg/sysctldotconf.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,28 +19,20 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
 const SysctlSeparator = " = "
 const SysctlFileName = "sysctl.conf"
 
-func GetSysctl(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeSysctl)
-}
-
-func makeSysctl(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, SysctlFileName, SysctlSeparator)
-	if err != nil {
-		return "", err
-	}
+func MakeSysCtlDotConf(
+	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, SysctlSeparator)
 	if txt == "" {
 		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return txt, nil
+	txt = hdr + txt
+	return txt
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go b/lib/go-atscfg/sysctldotconf_test.go
similarity index 50%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
copy to lib/go-atscfg/sysctldotconf_test.go
index 4909766..17eec16 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
+++ b/lib/go-atscfg/sysctldotconf_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,29 +20,34 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"strings"
+	"testing"
 )
 
-func GetDropQString(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeDropQString)
-}
-
-func makeDropQString(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	dropQStringVal, hasDropQStringParam, err := ats.GetProfileParamValue(tx, profile.ID, "drop_qstring.config", "content")
-	if err != nil {
-		return "", errors.New("getting profile param val: " + err.Error())
+func TestMakeSysCtlDotConf(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"param0": "val0",
+		"param1": "val1",
+		"param2": "val2",
 	}
 
-	text := ""
-	if hasDropQStringParam {
-		text += dropQStringVal + "\n"
-	} else {
-		text += `/([^?]+) $s://$t/$1` + "\n"
+	txt := MakeSysCtlDotConf(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	txt = strings.Replace(txt, " ", "", -1)
+
+	if !strings.Contains(txt, "param0=val0") {
+		t.Errorf("expected config to contain paramData 'param0=val0', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "param1=val1") {
+		t.Errorf("expected config to contain paramData 'param1=val1', actual: '%v'", txt)
 	}
-	return text, nil
+	if !strings.Contains(txt, "param2=val2") {
+		t.Errorf("expected config to contain paramData 'param2=val2', actual: '%v'", txt)
+	}
+
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/unknownconfig.go
similarity index 55%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/unknownconfig.go
index 6b94a55..b7a85de 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/unknownconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,29 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"strings"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
+func MakeUnknownConfig(
+	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)
 
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+	text := ""
+	for paramName, paramVal := range paramData {
+		if paramName == "header" {
+			if paramVal == "none" {
+				hdr = ""
+			} else {
+				hdr = paramVal + "\n"
+			}
+		} else {
+			text += paramVal + "\n"
+		}
+	}
+	text = strings.Replace(text, "__RETURN__", "\n", -1)
+	return hdr + text
 }
diff --git a/lib/go-atscfg/unknownconfig_test.go b/lib/go-atscfg/unknownconfig_test.go
new file mode 100644
index 0000000..c44ab3c
--- /dev/null
+++ b/lib/go-atscfg/unknownconfig_test.go
@@ -0,0 +1,71 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeUnknownConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"param0": "val0",
+		"param1": "val1",
+		"param2": "val2",
+	}
+
+	txt := MakeUnknownConfig(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if !strings.Contains(txt, "val0") {
+		t.Errorf("expected config to contain paramData value 'val0', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "val1") {
+		t.Errorf("expected config to contain paramData value 'val1', actual: '%v'", txt)
+	}
+	if !strings.Contains(txt, "val2") {
+		t.Errorf("expected config to contain paramData value 'val2', actual: '%v'", txt)
+	}
+	if strings.Contains(txt, "param0") {
+		t.Errorf("expected config to NOT contain paramData name 'param0', actual: '%v'", txt)
+	}
+
+	paramData["header"] = "none"
+
+	txt = MakeUnknownConfig(profileName, paramData, toolName, toURL)
+
+	firstLine := strings.TrimSpace(strings.SplitN(txt, "\n", 2)[0]) // SplitN always returns at least 1 element, no need to check len before indexing
+	if strings.HasPrefix(firstLine, "#") {
+		t.Errorf("expected config with 'header=none' to NOT contain header line, actual: '%v'", txt)
+	}
+
+	paramData["header"] = "foobar"
+
+	txt = MakeUnknownConfig(profileName, paramData, toolName, toURL)
+
+	firstLine = strings.TrimSpace(strings.SplitN(txt, "\n", 2)[0]) // SplitN always returns at least 1 element, no need to check len before indexing
+	if firstLine != "foobar" {
+		t.Errorf("expected config with 'header=foobar' to contain header 'foobar', actual: '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/urisigningconfig.go
similarity index 63%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/urisigningconfig.go
index 6b94a55..a312def 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/urisigningconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,19 +19,8 @@ package atsprofile
  * under the License.
  */
 
-import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
-)
-
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
-
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+func MakeURISigningConfig(
+	uriSigningKeysBts []byte,
+) string {
+	return string(uriSigningKeysBts)
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/lib/go-atscfg/urisigningconfig_test.go
similarity index 64%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to lib/go-atscfg/urisigningconfig_test.go
index 6b94a55..a94a4d0 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/lib/go-atscfg/urisigningconfig_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,16 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"testing"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
+func TestMakeURISigningConfig(t *testing.T) {
+	keyBts := []byte("anything")
+
+	txt := MakeURISigningConfig(keyBts)
 
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+	// URI Signing config is the verbatim bytes from Riak.
+	if txt != string(keyBts) {
+		t.Errorf("expected URI signing config to match input bytes, actual '%v'", txt)
+	}
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go b/lib/go-atscfg/urlsigconfig.go
similarity index 53%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
copy to lib/go-atscfg/urlsigconfig.go
index 16e5267..938414a 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
+++ b/lib/go-atscfg/urlsigconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,27 +20,31 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
+	"strings"
 
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
-const SysctlSeparator = " = "
-const SysctlFileName = "sysctl.conf"
+func MakeURLSigConfig(
+	profileName string,
+	urlSigKeys tc.URLSigKeys,
+	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)
 
-func GetSysctl(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeSysctl)
-}
+	sep := " = "
 
-func makeSysctl(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, SysctlFileName, SysctlSeparator)
-	if err != nil {
-		return "", err
+	text := hdr
+	for paramName, paramVal := range paramData {
+		if len(urlSigKeys) == 0 || !strings.HasPrefix(paramName, "key") {
+			text += paramName + sep + paramVal + "\n"
+		}
 	}
-	if txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+
+	for key, val := range urlSigKeys {
+		text += key + sep + val + "\n"
 	}
-	return txt, nil
+	return text
 }
diff --git a/lib/go-atscfg/urlsigconfig_test.go b/lib/go-atscfg/urlsigconfig_test.go
new file mode 100644
index 0000000..1b49ab2
--- /dev/null
+++ b/lib/go-atscfg/urlsigconfig_test.go
@@ -0,0 +1,70 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+func TestMakeURLSigConfig(t *testing.T) {
+	profileName := "myProfile"
+	toolName := "myToolName"
+	toURL := "https://myto.example.net"
+	paramData := map[string]string{
+		"key1": "foo",
+		"key2": "bar",
+		//todo
+	}
+	urlSigKeys := tc.URLSigKeys{}
+
+	txt := MakeURLSigConfig(profileName, urlSigKeys, paramData, toolName, toURL)
+
+	txt = strings.Replace(txt, " ", "", -1)
+
+	if !strings.Contains(txt, "key1=foo") {
+		t.Errorf("expected param key key1=foo, actual '%v'", txt)
+	}
+	if !strings.Contains(txt, "key2=bar") {
+		t.Errorf("expected param key key1=foo, actual '%v'", txt)
+	}
+
+	urlSigKeys["urlsigkeys-1"] = "urlsigkeys-val-1"
+	urlSigKeys["urlsigkeys-2"] = "urlsigkeys-val-2"
+
+	txt = MakeURLSigConfig(profileName, urlSigKeys, paramData, toolName, toURL)
+
+	txt = strings.Replace(txt, " ", "", -1)
+
+	if !strings.Contains(txt, "urlsigkeys-1=urlsigkeys-val-1") {
+		t.Errorf("expected param key key1=foo, actual '%v'", txt)
+	}
+	if !strings.Contains(txt, "urlsigkeys-2=urlsigkeys-val-2") {
+		t.Errorf("expected param key key1=foo, actual '%v'", txt)
+	}
+	if strings.Contains(txt, "key1=foo") {
+		t.Errorf("expected config to NOT contain param data keys 'key1=foo' if urlsig keys exist, actual '%v'", txt)
+	}
+	if strings.Contains(txt, "key2=bar") {
+		t.Errorf("expected config to NOT contain param data keys 'key2=bar' if urlsig keys exist, actual '%v'", txt)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go b/lib/go-atscfg/volumedotconfig.go
similarity index 73%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
copy to lib/go-atscfg/volumedotconfig.go
index bb177d5..22f7261 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
+++ b/lib/go-atscfg/volumedotconfig.go
@@ -1,4 +1,4 @@
-package atsprofile
+package atscfg
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,28 +20,22 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
 	"strconv"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-func GetVolume(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeVolume)
-}
-
-func makeVolume(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	paramData, err := ats.GetProfileParamData(tx, profile.ID, StorageFileName) // volume.config is based on the storage.config params
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
+// MakeVolumeDotConfig creates volume.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 MakeVolumeDotConfig(
+	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)
 
 	numVolumes := getNumVolumes(paramData)
 
-	text := "# TRAFFIC OPS NOTE: This is running with forced volumes - the size is irrelevant\n"
+	text += "# TRAFFIC OPS NOTE: This is running with forced volumes - the size is irrelevant\n"
 	nextVolume := 1
 	if drivePrefix := paramData["Drive_Prefix"]; drivePrefix != "" {
 		text += volumeText(strconv.Itoa(nextVolume), numVolumes)
@@ -59,7 +53,7 @@ func makeVolume(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string)
 	if text == "" {
 		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
 	}
-	return text, nil
+	return text
 }
 
 func volumeText(volume string, numVolumes int) string {
diff --git a/lib/go-atscfg/volumedotconfig_test.go b/lib/go-atscfg/volumedotconfig_test.go
new file mode 100644
index 0000000..3c52d53
--- /dev/null
+++ b/lib/go-atscfg/volumedotconfig_test.go
@@ -0,0 +1,77 @@
+package atscfg
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestMakeVolumeDotConfig(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",
+		"SSD_Drive_Prefix":  "/dev/ss",
+		"SSD_Drive_Letters": "i,j,k",
+	}
+
+	txt := MakeVolumeDotConfig(profileName, paramData, toolName, toURL)
+
+	testComment(t, txt, profileName, toolName, toURL)
+
+	if count := strings.Count(txt, "\n"); count != 5 { // one line for each volume, plus 2 comments
+		t.Errorf("expected one line for each drive letter plus a comment, actual: '%v' count %v", txt, count)
+	}
+
+	if !strings.Contains(txt, "size=33%") {
+		t.Errorf("expected size=33%% for three volumes, actual: '%v'", txt)
+	}
+
+	delete(paramData, "SSD_Drive_Prefix")
+	delete(paramData, "SSD_Drive_Letters")
+
+	txt = MakeVolumeDotConfig(profileName, paramData, toolName, toURL)
+
+	if count := strings.Count(txt, "\n"); count != 4 { // one line for each volume, plus 2 comments
+		t.Errorf("expected one line for each drive letter plus a comment, actual: '%v' count %v", txt, count)
+	}
+
+	if !strings.Contains(txt, "size=50%") {
+		t.Errorf("expected size=50%% for two volumes, actual: '%v'", txt)
+	}
+
+	delete(paramData, "RAM_Drive_Prefix")
+	delete(paramData, "RAM_Drive_Letters")
+
+	txt = MakeVolumeDotConfig(profileName, paramData, toolName, toURL)
+
+	if count := strings.Count(txt, "\n"); count != 3 { // one line for each volume, plus 2 comments
+		t.Errorf("expected one line for each drive letter plus a comment, actual: '%v' count %v", txt, count)
+	}
+
+	if !strings.Contains(txt, "size=100%") {
+		t.Errorf("expected size=100%% for one volume, actual: '%v'", txt)
+	}
+}
diff --git a/lib/go-tc/enum.go b/lib/go-tc/enum.go
index 7a95400..44c1a00 100644
--- a/lib/go-tc/enum.go
+++ b/lib/go-tc/enum.go
@@ -64,6 +64,8 @@ const (
 	CacheTypeInvalid = CacheType("")
 )
 
+const AlgorithmConsistentHash = "consistent_hash"
+
 const MonitorTypeName = "RASCAL"
 const MonitorProfilePrefix = "RASCAL"
 const RouterTypeName = "CCR"
diff --git a/licenses/BSD-pflag b/licenses/BSD-pflag
new file mode 100644
index 0000000..63ed1cf
--- /dev/null
+++ b/licenses/BSD-pflag
@@ -0,0 +1,28 @@
+Copyright (c) 2012 Alex Ogier. All rights reserved.
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/traffic_ops/build/build_rpm.sh b/traffic_ops/build/build_rpm.sh
index b7cf92b..011efe8 100755
--- a/traffic_ops/build/build_rpm.sh
+++ b/traffic_ops/build/build_rpm.sh
@@ -48,8 +48,11 @@ function initBuildArea() {
 
 	# Create traffic_ops_ort source area
 	to_ort_dest=$(createSourceDir traffic_ops_ort)
-	cp -p bin/traffic_ops_ort.pl "$to_ort_dest"
-	cp -p bin/supermicro_udev_mapper.pl "$to_ort_dest"
+	cp -p ort/traffic_ops_ort.pl "$to_ort_dest"
+	cp -p ort/supermicro_udev_mapper.pl "$to_ort_dest"
+	mkdir -p "${to_ort_dest}/atstccfg"
+	cp -R -p ort/atstccfg/* "${to_ort_dest}/atstccfg"
+
 	tar -czvf "$to_ort_dest".tgz -C "$RPMBUILD"/SOURCES $(basename "$to_ort_dest") || \
 		 { echo "Could not create tar archive $to_ort_dest: $?"; exit 1; }
 
diff --git a/traffic_ops/build/traffic_ops_ort.spec b/traffic_ops/build/traffic_ops_ort.spec
index f00af71..cd37553 100644
--- a/traffic_ops/build/traffic_ops_ort.spec
+++ b/traffic_ops/build/traffic_ops_ort.spec
@@ -38,6 +38,57 @@ tar xvf %{SOURCE0} -C $RPM_SOURCE_DIR
 
 
 %build
+export GOPATH=$(pwd)
+# Create build area with proper gopath structure
+mkdir -p src pkg bin || { echo "Could not create directories in $(pwd): $!"; exit 1; }
+
+go_get_version() {
+  local src=$1
+  local version=$2
+  (
+   cd $src && \
+   git checkout $version && \
+   go get -v \
+  )
+}
+
+# build all internal go dependencies (expects package being built as argument)
+build_dependencies () {
+    IFS=$'\n'
+    array=($(go list -f '{{ join .Deps "\n" }}' | grep trafficcontrol | grep -v $1))
+    echo "array: AA${array}AA";
+
+    prefix=github.com/apache/trafficcontrol
+    for (( i=0; i<${#array[@]}; i++ )); do
+        curPkg=${array[i]};
+        curPkgShort=${curPkg#$prefix};
+        echo "checking $curPkg";
+        godir=$GOPATH/src/$curPkg;
+        if [ ! -d "$godir" ]; then
+          ( echo "building $curPkg" && \
+            mkdir -p "$godir" && \
+            cd "$godir" && \
+            cp -r "$TC_DIR$curPkgShort"/* . && \
+            build_dependencies "$curPkgShort" && \
+            go get -v && \
+            echo "go building $curPkgShort at $(pwd)" && \
+            go build \
+          ) || { echo "Could not build go $curPkgShort at $(pwd): $!"; exit 1; };
+        fi
+     done
+}
+
+#build atstccfg binary
+godir=src/github.com/apache/trafficcontrol/traffic_ops/ort/atstccfg
+oldpwd=$(pwd)
+( mkdir -p "$godir" && \
+  cd "$godir" && \
+  cp -r "$TC_DIR"/traffic_ops/ort/atstccfg/* . && \
+  build_dependencies atstccfg  && \
+  #with proper vendoring go get would be  unneeded.
+  go get -d -v && \
+  go build -ldflags "-X main.GitRevision=`git rev-parse HEAD` -X main.BuildTimestamp=`date +'%Y-%M-%dT%H:%M:%s'` -X main.Version=%{traffic_control_version}"
+) || { echo "Could not build go program at $(pwd): $!"; exit 1; }
 
 
 %install
@@ -45,6 +96,9 @@ mkdir -p ${RPM_BUILD_ROOT}/opt/ort
 cp -p ${RPM_SOURCE_DIR}/traffic_ops_ort-%{version}/traffic_ops_ort.pl ${RPM_BUILD_ROOT}/opt/ort
 cp -p ${RPM_SOURCE_DIR}/traffic_ops_ort-%{version}/supermicro_udev_mapper.pl ${RPM_BUILD_ROOT}/opt/ort
 
+src=src/github.com/apache/trafficcontrol/traffic_ops/ort/atstccfg
+cp -p "$src"/atstccfg ${RPM_BUILD_ROOT}/opt/ort
+
 %clean
 rm -rf ${RPM_BUILD_ROOT}
 
@@ -54,5 +108,6 @@ rm -rf ${RPM_BUILD_ROOT}
 %attr(755, root, root)
 /opt/ort/traffic_ops_ort.pl
 /opt/ort/supermicro_udev_mapper.pl
+/opt/ort/atstccfg
 
 %changelog
diff --git a/traffic_ops/client/deliveryservice.go b/traffic_ops/client/deliveryservice.go
index 0b63baf..76e2aea 100644
--- a/traffic_ops/client/deliveryservice.go
+++ b/traffic_ops/client/deliveryservice.go
@@ -98,6 +98,17 @@ func (to *Session) GetDeliveryServicesNullable() ([]tc.DeliveryServiceNullable,
 	return data.Response, reqInf, nil
 }
 
+func (to *Session) GetDeliveryServicesByCDNID(cdnID int) ([]tc.DeliveryServiceNullable, ReqInf, error) {
+	data := struct {
+		Response []tc.DeliveryServiceNullable `json:"response"`
+	}{}
+	reqInf, err := get(to, apiBase+dsPath+"?cdn="+strconv.Itoa(cdnID), &data)
+	if err != nil {
+		return nil, reqInf, err
+	}
+	return data.Response, reqInf, nil
+}
+
 func (to *Session) GetDeliveryServiceNullable(id string) (*tc.DeliveryServiceNullable, ReqInf, error) {
 	data := struct {
 		Response []tc.DeliveryServiceNullable `json:"response"`
@@ -421,3 +432,25 @@ func (to *Session) GetDeliveryServicesEligible(dsID int) ([]tc.DSServer, ReqInf,
 	}
 	return resp.Response, reqInf, nil
 }
+
+func (to *Session) GetDeliveryServiceURLSigKeys(dsName string) (tc.URLSigKeys, ReqInf, error) {
+	data := struct {
+		Response tc.URLSigKeys `json:"response"`
+	}{}
+	path := apiBase + `/deliveryservices/xmlId/` + dsName + `/urlkeys.json`
+	reqInf, err := get(to, path, &data)
+	if err != nil {
+		return tc.URLSigKeys{}, reqInf, err
+	}
+	return data.Response, reqInf, nil
+}
+
+func (to *Session) GetDeliveryServiceURISigningKeys(dsName string) ([]byte, ReqInf, error) {
+	path := apiBase + `/deliveryservices/` + dsName + `/urisignkeys`
+	data := json.RawMessage{}
+	reqInf, err := get(to, path, &data)
+	if err != nil {
+		return []byte{}, reqInf, err
+	}
+	return []byte(data), reqInf, nil
+}
diff --git a/traffic_ops/client/deliveryserviceserver.go b/traffic_ops/client/deliveryserviceserver.go
index 642e5fd..d446fb3 100644
--- a/traffic_ops/client/deliveryserviceserver.go
+++ b/traffic_ops/client/deliveryserviceserver.go
@@ -21,6 +21,7 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
@@ -72,6 +73,33 @@ func (to *Session) GetDeliveryServiceServersN(n int) (tc.DeliveryServiceServerRe
 	return to.getDeliveryServiceServers(url.Values{"limit": []string{strconv.Itoa(n)}})
 }
 
+// GetDeliveryServiceServersWithLimits gets all delivery service servers, allowing specifying the limit of mappings to return, the delivery services to return, and the servers to return.
+// The limit may be 0, in which case the default limit will be applied. The deliveryServiceIDs and serverIDs may be nil or empty, in which case all delivery services and/or servers will be returned.
+func (to *Session) GetDeliveryServiceServersWithLimits(limit int, deliveryServiceIDs []int, serverIDs []int) (tc.DeliveryServiceServerResponse, ReqInf, error) {
+	vals := url.Values{}
+	if limit != 0 {
+		vals.Set("limit", strconv.Itoa(limit))
+	}
+
+	if len(deliveryServiceIDs) != 0 {
+		dsIDStrs := []string{}
+		for _, dsID := range deliveryServiceIDs {
+			dsIDStrs = append(dsIDStrs, strconv.Itoa(dsID))
+		}
+		vals.Set("deliveryserviceids", strings.Join(dsIDStrs, ","))
+	}
+
+	if len(serverIDs) != 0 {
+		serverIDStrs := []string{}
+		for _, serverID := range serverIDs {
+			serverIDStrs = append(serverIDStrs, strconv.Itoa(serverID))
+		}
+		vals.Set("serverids", strings.Join(serverIDStrs, ","))
+	}
+
+	return to.getDeliveryServiceServers(vals)
+}
+
 func (to *Session) getDeliveryServiceServers(urlQuery url.Values) (tc.DeliveryServiceServerResponse, ReqInf, error) {
 	route := apiBase + `/deliveryserviceserver`
 	if qry := urlQuery.Encode(); qry != "" {
diff --git a/traffic_ops/ort/atstccfg/astatsdotconfig.go b/traffic_ops/ort/atstccfg/astatsdotconfig.go
new file mode 100644
index 0000000..4d9c177
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/astatsdotconfig.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileAstatsDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.AstatsFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeAStatsDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/atsdotrules.go b/traffic_ops/ort/atstccfg/atsdotrules.go
new file mode 100644
index 0000000..5292ef8
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/atsdotrules.go
@@ -0,0 +1,64 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+const ATSDotRulesFileName = StorageFileName
+
+func GetConfigFileProfileATSDotRules(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != ATSDotRulesFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeATSDotRules(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/atstccfg.go b/traffic_ops/ort/atstccfg/atstccfg.go
new file mode 100644
index 0000000..1885b25
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/atstccfg.go
@@ -0,0 +1,114 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	toclient "github.com/apache/trafficcontrol/traffic_ops/client"
+)
+
+const AppName = "atstccfg"
+const Version = "0.1"
+const UserAgent = AppName + "/" + Version
+
+const APIVersion = "1.2"
+const TempSubdir = AppName + "_cache"
+const TempCookieFileName = "cookies"
+const TOCookieName = "mojolicious"
+
+const ExitCodeSuccess = 0
+const ExitCodeErrGeneric = 1
+const ExitCodeNotFound = 104
+const ExitCodeBadRequest = 100
+
+type TCCfg struct {
+	Cfg
+	TOClient **toclient.Session
+}
+
+func main() {
+	cfg, err := GetCfg()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Getting config: "+err.Error()+"\n")
+		os.Exit(ExitCodeErrGeneric)
+	}
+
+	if cfg.PrintGeneratedFiles {
+		fmt.Println(strings.Join(GetGeneratedFilesList(), "\n"))
+		os.Exit(ExitCodeSuccess)
+	}
+
+	log.Infoln("URL: '" + cfg.TOURL.String() + "' User: '" + cfg.TOUser + "' Pass len: '" + strconv.Itoa(len(cfg.TOPass)) + "'")
+	log.Infoln("TempDir: '" + cfg.TempDir + "'")
+
+	toFQDN := cfg.TOURL.Scheme + "://" + cfg.TOURL.Host
+	log.Infoln("TO FQDN: '" + toFQDN + "'")
+	log.Infoln("TO URL: '" + cfg.TOURL.String() + "'")
+
+	toClient, err := GetClient(toFQDN, cfg.TOUser, cfg.TOPass, cfg.TempDir, cfg.CacheFileMaxAge, cfg.TOTimeout, cfg.TOInsecure)
+	if err != nil {
+		log.Errorln("Logging in to Traffic Ops: " + err.Error())
+		os.Exit(ExitCodeErrGeneric)
+	}
+
+	tccfg := TCCfg{Cfg: cfg, TOClient: &toClient}
+
+	cfgFile, code, err := GetConfigFile(tccfg)
+	log.Infof("GetConfigFile returned %v %v\n", code, err)
+	if err != nil {
+		log.Errorln("Getting config file '" + cfg.TOURL.String() + "': " + err.Error())
+		if code == 0 {
+			code = ExitCodeErrGeneric
+		}
+		log.Infof("GetConfigFile exiting with code %v\n", code)
+		os.Exit(code)
+	}
+	fmt.Println(cfgFile)
+	os.Exit(ExitCodeSuccess)
+}
+
+func GetGeneratedFilesList() []string {
+	names := []string{}
+	for scope, fileFuncs := range ConfigFileFuncs() {
+		for cfgFile, _ := range fileFuncs {
+			names = append(names, scope+"/"+cfgFile)
+		}
+	}
+
+	names = append(names, "profiles/url_sig_*.config")     // url_sig configs are generated, but not in the funcs because they're not a literal match
+	names = append(names, "profiles/uri_signing_*.config") // uri_signing configs are generated, but not in the funcs because they're not a literal match
+	names = append(names, "profiles/*")                    // unknown profiles configs are generated, a.k.a. "take-and-bake"
+
+	return names
+}
+
+func HTTPCodeToExitCode(httpCode int) int {
+	switch httpCode {
+	case http.StatusNotFound:
+		return ExitCodeNotFound
+	}
+	return ExitCodeErrGeneric
+}
diff --git a/traffic_ops/ort/atstccfg/cachedotconfig.go b/traffic_ops/ort/atstccfg/cachedotconfig.go
new file mode 100644
index 0000000..87e6554
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/cachedotconfig.go
@@ -0,0 +1,101 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+func GetConfigFileProfileCacheDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	servers, err := GetServers(cfg)
+	if err != nil {
+		return "", errors.New("getting servers: " + err.Error())
+	}
+
+	profileServerIDs := []int{}
+	profileServerIDsMap := map[int]struct{}{}
+	profileServers := []tc.Server{}
+	for _, sv := range servers {
+		if sv.Profile != profileName {
+			continue
+		}
+		profileServers = append(profileServers, sv)
+		profileServerIDs = append(profileServerIDs, sv.ID)
+		profileServerIDsMap[sv.ID] = struct{}{}
+	}
+
+	dsServers, err := GetDeliveryServiceServers(cfg, profileServerIDs)
+	if err != nil {
+		return "", errors.New("getting parent.config cachegroup parent server delivery service servers: " + err.Error())
+	}
+
+	profile, err := GetProfileByName(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileNameOrID + "': " + err.Error())
+	}
+
+	dses, err := GetCDNDeliveryServices(cfg, profile.CDNID)
+	if err != nil {
+		return "", errors.New("getting delivery services: " + err.Error())
+	}
+
+	dsIDs := map[int]struct{}{}
+	for _, dss := range dsServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			continue // TODO warn? err?
+		}
+		if _, ok := profileServerIDsMap[*dss.Server]; !ok {
+			continue
+		}
+		dsIDs[*dss.DeliveryService] = struct{}{}
+	}
+
+	profileDSes := []atscfg.ProfileDS{}
+	for _, ds := range dses {
+		if ds.ID == nil || ds.Type == nil || ds.OrgServerFQDN == nil {
+			continue // TODO warn? err?
+		}
+		if *ds.Type == tc.DSTypeInvalid {
+			continue // TODO warn? err?
+		}
+		if *ds.OrgServerFQDN == "" {
+			continue // TODO warn? err?
+		}
+		if _, ok := dsIDs[*ds.ID]; !ok {
+			continue
+		}
+		origin := *ds.OrgServerFQDN
+		profileDSes = append(profileDSes, atscfg.ProfileDS{Type: *ds.Type, OriginFQDN: &origin})
+	}
+	return atscfg.MakeCacheDotConfig(profileName, profileDSes, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/caching.go b/traffic_ops/ort/atstccfg/caching.go
new file mode 100644
index 0000000..e83e6cb
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/caching.go
@@ -0,0 +1,177 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+)
+
+// GetCachedJSON attempts to get the given object from tempDir/cacheFileName.
+// If the cache file doesn't exist, is too old, or is malformed, it uses getter to get the object, and stores it in cacheFileName.
+// The object is placed in obj (which must be a pointer to the type of object to decode from JSON), and the error from getter is returned.
+func GetCachedJSON(cfg TCCfg, cacheFileName string, obj interface{}, getter func(obj interface{}) error) error {
+	err := GetJSONObjFromFile(cfg.TempDir, cacheFileName, cfg.CacheFileMaxAge, obj)
+	if err == nil {
+		return nil
+	}
+
+	log.Infoln("GetCachedJSON failed to get object from '" + cfg.TempDir + "/" + cacheFileName + "', calling getter: " + err.Error())
+
+	currentRetry := 0
+	for {
+		err := getter(obj)
+		if err == nil {
+			break
+		}
+
+		if currentRetry == cfg.NumRetries {
+			return errors.New("getting uncached: " + err.Error())
+		}
+
+		sleepSeconds := RetryBackoffSeconds(currentRetry)
+		log.Warnf("getting '%v', sleeping for %v seconds: %v\n", cacheFileName, sleepSeconds, err)
+		currentRetry++
+		time.Sleep(time.Second * time.Duration(sleepSeconds)) // TODO make backoff configurable?
+	}
+
+	WriteCacheJSON(cfg.TempDir, cacheFileName, obj)
+	return nil
+}
+
+// WriteCacheJSON attempts to write obj to tempDir/cacheFileName.
+// If there is an error, it is written to stderr but not returned.
+func WriteCacheJSON(tempDir string, cacheFileName string, obj interface{}) {
+	objBts, err := json.Marshal(obj)
+	if err != nil {
+		log.Errorln("serializing object '" + cacheFileName + "' to JSON: " + err.Error())
+		return
+	}
+
+	objPath := filepath.Join(tempDir, cacheFileName)
+	// Use os.OpenFile, not os.Create, in order to set perms to 0600 - cookies allow login, therefore the file MUST only allow access by the current user, for security reasons
+	objFile, err := os.OpenFile(objPath, os.O_RDWR|os.O_CREATE, 0600)
+	if err != nil {
+		log.Errorln("creating object cache file '" + objPath + "': " + err.Error())
+		return
+	}
+	defer objFile.Close()
+
+	if _, err := objFile.Write(objBts); err != nil {
+		log.Errorln("writing object cache file '" + objPath + "': " + err.Error())
+		return
+	}
+}
+
+// GetJSONObjFromFile gets obj from tempDir/cacheFileName, if it exists and isn't older than CacheFileMaxAge.
+// Just like with json.Unmarshal, obj must be a non-nil pointer to the object to decode into.
+func GetJSONObjFromFile(tempDir string, cacheFileName string, cacheFileMaxAge time.Duration, obj interface{}) error {
+	objPath := filepath.Join(tempDir, cacheFileName)
+
+	objFile, err := os.Open(objPath)
+	if err != nil {
+		return errors.New("opening object file '" + objPath + "':" + err.Error())
+	}
+	defer objFile.Close()
+
+	objFileInfo, err := objFile.Stat()
+	if err != nil {
+		return errors.New("getting object file info '" + objPath + "':" + err.Error())
+	}
+
+	if objFileAge := time.Now().Sub(objFileInfo.ModTime()); objFileAge > cacheFileMaxAge {
+		return fmt.Errorf("object file too old, max age %dms less than file age %dms", int(cacheFileMaxAge/time.Millisecond), int(objFileAge/time.Millisecond))
+	}
+
+	bts, err := ioutil.ReadAll(objFile)
+	if err != nil {
+		return errors.New("reading object from file '" + objPath + "':" + err.Error())
+	}
+
+	if err := json.Unmarshal(bts, obj); err != nil {
+		return errors.New("unmarshalling object from file '" + objPath + "':" + err.Error())
+	}
+
+	return nil
+}
+
+func StringToCookies(cookiesStr string) []*http.Cookie {
+	hdr := http.Header{}
+	hdr.Add("Cookie", cookiesStr)
+	req := http.Request{Header: hdr}
+	return req.Cookies()
+}
+
+func CookiesToString(cookies []*http.Cookie) string {
+	strs := []string{}
+	for _, cookie := range cookies {
+		strs = append(strs, cookie.String())
+	}
+	return strings.Join(strs, "; ")
+}
+
+// WriteCookiesFile writes the given cookies to the temp file. On error, returns nothing, but writes to stderr.
+func WriteCookiesToFile(cookiesStr string, tempDir string) {
+	cookiePath := filepath.Join(tempDir, TempCookieFileName)
+	// Use os.OpenFile, not os.Create, in order to set perms to 0600 - cookies allow login, therefore the file MUST only allow access by the current user, for security reasons
+	if cookieFile, err := os.OpenFile(cookiePath, os.O_RDWR|os.O_CREATE, 0600); err != nil {
+		log.Errorln("creating cookie file '" + cookiePath + "': " + err.Error())
+	} else {
+		defer cookieFile.Close()
+		if _, err := cookieFile.WriteString(cookiesStr + "\n"); err != nil {
+			log.Errorln("writing cookie file '" + cookiePath + "': " + err.Error())
+		}
+	}
+}
+
+func GetCookiesFromFile(tempDir string, cacheFileMaxAge time.Duration) (string, error) {
+	cookiePath := filepath.Join(tempDir, TempCookieFileName)
+
+	cookieFile, err := os.Open(cookiePath)
+	if err != nil {
+		return "", errors.New("opening cookie file '" + cookiePath + "':" + err.Error())
+	}
+	defer cookieFile.Close()
+
+	cookieFileInfo, err := cookieFile.Stat()
+	if err != nil {
+		return "", errors.New("getting cookie file info '" + cookiePath + "':" + err.Error())
+	}
+
+	cookieFileAge := time.Now().Sub(cookieFileInfo.ModTime())
+	if cookieFileAge > cacheFileMaxAge {
+		return "", fmt.Errorf("cookie file too old, max age %dms less than file age %dms", int(cacheFileMaxAge/time.Millisecond), int(cookieFileAge/time.Millisecond))
+	}
+
+	bts, err := ioutil.ReadAll(cookieFile)
+	if err != nil {
+		return "", errors.New("reading cookies from file '" + cookiePath + "':" + err.Error())
+	}
+	return string(bts), nil
+}
diff --git a/traffic_ops/ort/atstccfg/config.go b/traffic_ops/ort/atstccfg/config.go
new file mode 100644
index 0000000..5d61614
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/config.go
@@ -0,0 +1,193 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"math"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+
+	flag "github.com/ogier/pflag"
+)
+
+type Cfg struct {
+	CacheFileMaxAge     time.Duration
+	LogLocationErr      string
+	LogLocationInfo     string
+	LogLocationWarn     string
+	NumRetries          int
+	TempDir             string
+	TOInsecure          bool
+	TOPass              string
+	TOTimeout           time.Duration
+	TOURL               *url.URL
+	TOUser              string
+	PrintGeneratedFiles bool
+}
+
+func (cfg Cfg) ErrorLog() log.LogLocation   { return log.LogLocation(cfg.LogLocationErr) }
+func (cfg Cfg) WarningLog() log.LogLocation { return log.LogLocation(cfg.LogLocationWarn) }
+func (cfg Cfg) InfoLog() log.LogLocation    { return log.LogLocation(cfg.LogLocationInfo) }
+func (cfg Cfg) DebugLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // atstccfg doesn't use the debug logger, use Info instead.
+func (cfg Cfg) EventLog() log.LogLocation   { return log.LogLocation(log.LogLocationNull) } // atstccfg doesn't use the event logger.
+
+// GetCfg gets the application configuration, from arguments and environment variables.
+// Note if PrintGeneratedFiles is configured, the config will be returned with PrintGeneratedFiles true and all other values set to their defaults. This is because other values may have requirements and return errors, where if PrintGeneratedFiles is set by the user, no other setting should be considered.
+func GetCfg() (Cfg, error) {
+	toURLPtr := flag.StringP("traffic-ops-url", "u", "", "Traffic Ops URL. Must be the full URL, including the scheme. Required. May also be set with the environment variable TO_URL.")
+	toUserPtr := flag.StringP("traffic-ops-user", "U", "", "Traffic Ops username. Required. May also be set with the environment variable TO_USER.")
+	toPassPtr := flag.StringP("traffic-ops-password", "P", "", "Traffic Ops password. Required. May also be set with the environment variable TO_PASS.")
+	noCachePtr := flag.BoolP("no-cache", "n", false, "Whether not to use existing cache files. Optional. Cache files will still be created, existing ones just won't be used.")
+	numRetriesPtr := flag.IntP("num-retries", "r", 5, "The number of times to retry getting a file if it fails.")
+	logLocationErrPtr := flag.StringP("log-location-error", "e", "stderr", "Where to log errors. May be a file path, stdout, stderr, or null.")
+	logLocationWarnPtr := flag.StringP("log-location-warning", "w", "stderr", "Where to log warnings. May be a file path, stdout, stderr, or null.")
+	logLocationInfoPtr := flag.StringP("log-location-info", "i", "stderr", "Where to log information messages. May be a file path, stdout, stderr, or null.")
+	printGeneratedFilesPtr := flag.BoolP("print-generated-files", "g", false, "Whether to print a list of files which are generated (and not proxied to Traffic Ops).")
+	toInsecurePtr := flag.BoolP("traffic-ops-insecure", "s", false, "Whether to ignore HTTPS certificate errors from Traffic Ops. It is HIGHLY RECOMMENDED to never use this in a production environment, but only for debugging.")
+	toTimeoutMSPtr := flag.IntP("traffic-ops-timeout-milliseconds", "t", 10000, "Timeout in seconds for Traffic Ops requests.")
+	cacheFileMaxAgeSecondsPtr := flag.IntP("cache-file-max-age-seconds", "a", 60, "Maximum age to use cached files.")
+	flag.Parse()
+
+	if *printGeneratedFilesPtr {
+		return Cfg{PrintGeneratedFiles: true}, nil
+	}
+
+	toURL := *toURLPtr
+	toUser := *toUserPtr
+	toPass := *toPassPtr
+	noCache := *noCachePtr
+	numRetries := *numRetriesPtr
+	logLocationErr := *logLocationErrPtr
+	logLocationWarn := *logLocationWarnPtr
+	logLocationInfo := *logLocationInfoPtr
+	toInsecure := *toInsecurePtr
+	toTimeout := time.Millisecond * time.Duration(*toTimeoutMSPtr)
+	cacheFileMaxAge := time.Second * time.Duration(*cacheFileMaxAgeSecondsPtr)
+
+	urlSourceStr := "argument" // for error messages
+	if toURL == "" {
+		urlSourceStr = "environment variable"
+		toURL = os.Getenv("TO_URL")
+	}
+	if toUser == "" {
+		toUser = os.Getenv("TO_USER")
+	}
+	if toPass == "" {
+		toPass = os.Getenv("TO_PASS")
+	}
+
+	if strings.TrimSpace(toURL) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-url or TO_URL environment variable. Usage: ./" + AppName + " --traffic-ops-url myurl --traffic-ops-user myuser --traffic-ops-password mypass")
+	}
+	if strings.TrimSpace(toUser) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-user or TO_USER environment variable. Usage: ./" + AppName + " --traffic-ops-url myurl --traffic-ops-user myuser --traffic-ops-password mypass")
+	}
+	if strings.TrimSpace(toPass) == "" {
+		return Cfg{}, errors.New("Missing required argument --traffic-ops-password or TO_PASS environment variable. Usage: ./" + AppName + " --traffic-ops-url myurl --traffic-ops-user myuser --traffic-ops-password mypass")
+	}
+
+	toURLParsed, err := url.Parse(toURL)
+	if err != nil {
+		return Cfg{}, errors.New("parsing Traffic Ops URL from " + urlSourceStr + " '" + toURL + "': " + err.Error())
+	} else if err := ValidateURL(toURLParsed); err != nil {
+		return Cfg{}, errors.New("invalid Traffic Ops URL from " + urlSourceStr + " '" + toURL + "': " + err.Error())
+	}
+
+	tmpDir := os.TempDir()
+	tmpDir = filepath.Join(tmpDir, TempSubdir)
+
+	cfg := Cfg{
+		CacheFileMaxAge: cacheFileMaxAge,
+		LogLocationErr:  logLocationErr,
+		LogLocationWarn: logLocationWarn,
+		LogLocationInfo: logLocationInfo,
+		NumRetries:      numRetries,
+		TempDir:         tmpDir,
+		TOInsecure:      toInsecure,
+		TOPass:          toPass,
+		TOTimeout:       toTimeout,
+		TOURL:           toURLParsed,
+		TOUser:          toUser,
+	}
+
+	if err := log.InitCfg(cfg); err != nil {
+		return Cfg{}, errors.New("Initializing loggers: " + err.Error() + "\n")
+	}
+
+	if noCache {
+		if err := os.RemoveAll(tmpDir); err != nil {
+			log.Errorln("deleting cache directory '" + tmpDir + "': " + err.Error())
+		}
+	}
+
+	if err := os.MkdirAll(tmpDir, 0700); err != nil {
+		return Cfg{}, errors.New("creating temp directory '" + tmpDir + "': " + err.Error())
+	}
+	if err := ValidateDirWriteable(tmpDir); err != nil {
+		return Cfg{}, errors.New("validating temp directory is writeable '" + tmpDir + "': " + err.Error())
+	}
+
+	return cfg, nil
+}
+
+func ValidateURL(u *url.URL) error {
+	if u == nil {
+		return errors.New("nil url")
+	}
+	if u.Scheme != "http" && u.Scheme != "https" {
+		return errors.New("scheme expected 'http' or 'https', actual '" + u.Scheme + "'")
+	}
+	if strings.TrimSpace(u.Host) == "" {
+		return errors.New("no host")
+	}
+	return nil
+}
+
+func ValidateDirWriteable(dir string) error {
+	testFileName := "testwrite.txt"
+	testFilePath := filepath.Join(dir, testFileName)
+	if err := os.RemoveAll(testFilePath); err != nil {
+		// TODO don't log? This can be normal
+		log.Infoln("error removing temp test file '" + testFilePath + "' (ok if it didn't exist): " + err.Error())
+	}
+
+	fl, err := os.Create(testFilePath)
+	if err != nil {
+		return errors.New("creating temp test file '" + testFilePath + "': " + err.Error())
+	}
+	defer fl.Close()
+
+	if _, err := fl.WriteString("test"); err != nil {
+		return errors.New("writing to temp test file '" + testFilePath + "': " + err.Error())
+	}
+
+	return nil
+}
+
+func RetryBackoffSeconds(currentRetry int) int {
+	// TODO make configurable?
+	return int(math.Pow(2.0, float64(currentRetry)))
+}
diff --git a/traffic_ops/ort/atstccfg/dropqstringdotconfig.go b/traffic_ops/ort/atstccfg/dropqstringdotconfig.go
new file mode 100644
index 0000000..3d5f63d
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/dropqstringdotconfig.go
@@ -0,0 +1,63 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileDropQStringDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	dropQStringVal := (*string)(nil)
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.DropQStringDotConfigFileName {
+			continue
+		}
+		if param.Name != atscfg.DropQStringDotConfigParamName {
+			continue
+		}
+		dropQStringVal = &param.Value
+		break
+	}
+
+	return atscfg.MakeDropQStringDotConfig(profileName, toToolName, toURL, dropQStringVal), nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/traffic_ops/ort/atstccfg/facts.go
similarity index 59%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to traffic_ops/ort/atstccfg/facts.go
index 6b94a55..ddbfbc9 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/traffic_ops/ort/atstccfg/facts.go
@@ -1,4 +1,4 @@
-package atsprofile
+package main
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,20 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
+	"errors"
 
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
+func GetConfigFileProfile12MFacts(cfg TCCfg, profileNameOrID string) (string, error) {
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
 
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+	return atscfg.Make12MFacts(profileName, toToolName, toURL), nil
 }
diff --git a/traffic_ops/ort/atstccfg/loggingdotconfig.go b/traffic_ops/ort/atstccfg/loggingdotconfig.go
new file mode 100644
index 0000000..765854b
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/loggingdotconfig.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileLoggingDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.LoggingFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeLoggingDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/loggingdotyaml.go b/traffic_ops/ort/atstccfg/loggingdotyaml.go
new file mode 100644
index 0000000..5f60e8a
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/loggingdotyaml.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileLoggingDotYAML(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.LoggingYAMLFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeLoggingDotYAML(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/logsxmldotconfig.go b/traffic_ops/ort/atstccfg/logsxmldotconfig.go
new file mode 100644
index 0000000..cd441d5
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/logsxmldotconfig.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileLogsXMLDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.LogsXMLFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeLogsXMLDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/parentdotconfig.go b/traffic_ops/ort/atstccfg/parentdotconfig.go
new file mode 100644
index 0000000..b44a50a
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/parentdotconfig.go
@@ -0,0 +1,562 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+const GlobalProfileName = "GLOBAL"
+
+func GetConfigFileServerParentDotConfig(cfg TCCfg, serverNameOrID string) (string, error) {
+	// TODO TOAPI add /servers?cdn=1 query param
+	servers, err := GetServers(cfg)
+	if err != nil {
+		return "", errors.New("getting servers: " + err.Error())
+	}
+
+	server := tc.Server{ID: atscfg.InvalidID}
+	if serverID, err := strconv.Atoi(serverNameOrID); err == nil {
+		for _, toServer := range servers {
+			if toServer.ID == serverID {
+				server = toServer
+				break
+			}
+		}
+	} else {
+		serverName := serverNameOrID
+		for _, toServer := range servers {
+			if toServer.HostName == serverName {
+				server = toServer
+				break
+			}
+		}
+	}
+	if server.ID == atscfg.InvalidID {
+		return "", errors.New("server '" + serverNameOrID + " not found in servers")
+	}
+
+	cacheGroups, err := GetCacheGroups(cfg)
+	if err != nil {
+		return "", errors.New("getting cachegroups: " + err.Error())
+	}
+
+	cgMap := map[string]tc.CacheGroupNullable{}
+	for _, cg := range cacheGroups {
+		if cg.Name == nil {
+			return "", errors.New("got cachegroup with nil name!'")
+		}
+		cgMap[*cg.Name] = cg
+	}
+
+	serverCG, ok := cgMap[server.Cachegroup]
+	if !ok {
+		return "", errors.New("server '" + serverNameOrID + "' cachegroup '" + server.Cachegroup + "' not found in CacheGroups")
+	}
+
+	parentCGID := -1
+	parentCGType := ""
+	if serverCG.ParentName != nil && *serverCG.ParentName != "" {
+		parentCG, ok := cgMap[*serverCG.ParentName]
+		if !ok {
+			return "", errors.New("server '" + serverNameOrID + "' cachegroup '" + server.Cachegroup + "' parent '" + *serverCG.ParentName + "' not found in CacheGroups")
+		}
+		if parentCG.ID == nil {
+			return "", errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
+		}
+		parentCGID = *parentCG.ID
+
+		if parentCG.Type == nil {
+			return "", errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
+		}
+		parentCGType = *parentCG.Type
+	}
+
+	secondaryParentCGID := -1
+	secondaryParentCGType := ""
+	if serverCG.SecondaryParentName != nil && *serverCG.SecondaryParentName != "" {
+		parentCG, ok := cgMap[*serverCG.SecondaryParentName]
+		if !ok {
+			return "", errors.New("server '" + serverNameOrID + "' cachegroup '" + server.Cachegroup + "' secondary parent '" + *serverCG.SecondaryParentName + "' not found in CacheGroups")
+		}
+
+		if parentCG.ID == nil {
+			return "", errors.New("got cachegroup '" + *parentCG.Name + "' with nil ID!'")
+		}
+		secondaryParentCGID = *parentCG.ID
+		if parentCG.Type == nil {
+			return "", errors.New("got cachegroup '" + *parentCG.Name + "' with nil Type!'")
+		}
+
+		secondaryParentCGType = *parentCG.Type
+	}
+
+	serverInfo := atscfg.ServerInfo{
+		CacheGroupID:                  server.CachegroupID,
+		CDN:                           tc.CDNName(server.CDNName),
+		CDNID:                         server.CDNID,
+		DomainName:                    server.DomainName,
+		HostName:                      server.HostName,
+		ID:                            server.ID,
+		IP:                            server.IPAddress,
+		ParentCacheGroupID:            parentCGID,
+		ParentCacheGroupType:          parentCGType,
+		ProfileID:                     atscfg.ProfileID(server.ProfileID),
+		ProfileName:                   server.Profile,
+		Port:                          server.TCPPort,
+		SecondaryParentCacheGroupID:   secondaryParentCGID,
+		SecondaryParentCacheGroupType: secondaryParentCGType,
+		Type:                          server.Type,
+	}
+
+	parentCacheGroups := map[string]struct{}{}
+	if serverInfo.IsTopLevelCache() {
+		log.Infoln("This cache Is Top Level!")
+		for _, cg := range cacheGroups {
+			if cg.Type == nil {
+				return "", errors.New("cachegroup type is nil!")
+			}
+			if cg.Name == nil {
+				return "", errors.New("cachegroup type is nil!")
+			}
+
+			if *cg.Type != tc.CacheGroupOriginTypeName {
+				continue
+			}
+			parentCacheGroups[*cg.Name] = struct{}{}
+		}
+	} else {
+		if server.Cachegroup == "" {
+			return "", errors.New("server cachegroup is nil!")
+		}
+		for _, cg := range cacheGroups {
+			if cg.Type == nil {
+				return "", errors.New("cachegroup type is nil!")
+			}
+			if cg.Name == nil {
+				return "", errors.New("cachegroup type is nil!")
+			}
+
+			if *cg.Name == server.Cachegroup {
+				if cg.ParentName != nil && *cg.ParentName != "" {
+					parentCacheGroups[*cg.ParentName] = struct{}{}
+				}
+				if cg.SecondaryParentName != nil && *cg.SecondaryParentName != "" {
+					parentCacheGroups[*cg.SecondaryParentName] = struct{}{}
+				}
+				break
+			}
+		}
+	}
+
+	cgServers := map[int]tc.Server{} // map[serverID]server
+	for _, sv := range servers {
+		if sv.CDNName != server.CDNName {
+			continue
+		}
+		if _, ok := parentCacheGroups[sv.Cachegroup]; !ok {
+			continue
+		}
+		if sv.Type != tc.OriginTypeName &&
+			!strings.HasPrefix(sv.Type, tc.EdgeTypePrefix) &&
+			!strings.HasPrefix(sv.Type, tc.MidTypePrefix) {
+			continue
+		}
+		if sv.Status != string(tc.CacheStatusReported) && sv.Status != string(tc.CacheStatusOnline) {
+			continue
+		}
+		cgServers[sv.ID] = sv
+	}
+
+	cgServerIDs := []int{}
+	for serverID, _ := range cgServers {
+		cgServerIDs = append(cgServerIDs, serverID)
+	}
+	cgServerIDs = append(cgServerIDs, server.ID)
+
+	cgDSServers, err := GetDeliveryServiceServers(cfg, cgServerIDs)
+	if err != nil {
+		return "", errors.New("getting parent.config cachegroup parent server delivery service servers: " + err.Error())
+	}
+
+	parentServerDSes := map[int]map[int]struct{}{} // map[serverID][dsID] // cgServerDSes
+	for _, dss := range cgDSServers {
+		if dss.Server == nil || dss.DeliveryService == nil {
+			return "", errors.New("getting parent.config cachegroup parent server delivery service servers: got dss with nil members!" + err.Error())
+		}
+		if parentServerDSes[*dss.Server] == nil {
+			parentServerDSes[*dss.Server] = map[int]struct{}{}
+		}
+		parentServerDSes[*dss.Server][*dss.DeliveryService] = struct{}{}
+	}
+
+	serverProfileParameters, err := GetServerProfileParameters(cfg, server.Profile)
+	if err != nil {
+		return "", errors.New("getting server profile '" + server.Profile + "' parameters: " + err.Error())
+	}
+
+	atsVersionParam := ""
+	for _, param := range serverProfileParameters {
+		if param.ConfigFile != "package" || param.Name != "trafficserver" {
+			continue
+		}
+		atsVersionParam = param.Value
+		break
+	}
+	if atsVersionParam == "" {
+		atsVersionParam = atscfg.DefaultATSVersion
+	}
+
+	atsMajorVer, err := atscfg.GetATSMajorVersionFromATSVersion(atsVersionParam)
+	if err != nil {
+		return "", errors.New("getting ATS major version from version parameter (profile '" + server.Profile + "' configFile 'package' name 'trafficserver'): " + err.Error())
+	}
+
+	globalParams, err := GetGlobalParameters(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	toToolName := ""
+	toURL := ""
+	for _, param := range globalParams {
+		if param.Name == "tm.toolname" {
+			toToolName = param.Value
+		} else if param.Name == "tm.url" {
+			toURL = param.Value
+		}
+		if toToolName != "" && toURL != "" {
+			break
+		}
+	}
+
+	deliveryServices, err := GetCDNDeliveryServices(cfg, server.CDNID)
+	if err != nil {
+		return "", errors.New("getting delivery services: " + err.Error())
+	}
+
+	parentConfigParams, err := GetConfigFileParameters(cfg, "parent.config")
+	if err != nil {
+		return "", errors.New("getting parent.config parameters: " + err.Error())
+	}
+
+	parentConfigParamsWithProfiles, err := TCParamsToParamsWithProfiles(parentConfigParams)
+	if err != nil {
+		return "", errors.New("unmarshalling parent.config parameters profiles: " + err.Error())
+	}
+
+	// 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
+	for _, param := range parentConfigParamsWithProfiles {
+		for _, profile := range param.ProfileNames {
+			if _, ok := profileParentConfigParams[profile]; !ok {
+				profileParentConfigParams[profile] = map[string]string{}
+			}
+			profileParentConfigParams[profile][param.Name] = param.Value
+		}
+	}
+
+	parentConfigDSes := []atscfg.ParentConfigDSTopLevel{}
+	for _, tcDS := range deliveryServices {
+		if tcDS.ID == nil {
+			continue // TODO warn?
+		}
+
+		if !serverInfo.IsTopLevelCache() {
+			if _, ok := parentServerDSes[server.ID][*tcDS.ID]; !ok {
+				continue // skip DSes not assigned to this server.
+			}
+		}
+
+		if !tcDS.Type.IsHTTP() && !tcDS.Type.IsDNS() {
+			continue // skip ANY_MAP, STEERING, etc
+		}
+		if tcDS.XMLID == nil || *tcDS.XMLID == "" {
+			log.Errorln("got delivery service with no XMLID! Skipping!")
+			continue
+		}
+		if tcDS.OrgServerFQDN == nil || *tcDS.OrgServerFQDN == "" {
+			log.Errorln("ds  '" + *tcDS.XMLID + "' has no origin server! Skipping!")
+			continue
+		}
+
+		xmlID := tc.DeliveryServiceName(*tcDS.XMLID)
+		originFQDN := *tcDS.OrgServerFQDN
+		qStringIgnore := 0
+		multiSiteOrigin := false
+		originShield := ""
+		dsType := tc.DSTypeFromString("")
+		if tcDS.QStringIgnore != nil {
+			qStringIgnore = *tcDS.QStringIgnore
+		}
+		if tcDS.MultiSiteOrigin != nil {
+			multiSiteOrigin = *tcDS.MultiSiteOrigin
+		}
+		if tcDS.OriginShield != nil {
+			originShield = *tcDS.OriginShield
+		}
+		if tcDS.Type != nil {
+			dsType = *tcDS.Type
+		}
+
+		ds := atscfg.ParentConfigDSTopLevel{
+			ParentConfigDS: atscfg.ParentConfigDS{
+				Name:            xmlID,
+				QStringIgnore:   tc.QStringIgnore(qStringIgnore),
+				OriginFQDN:      originFQDN,
+				MultiSiteOrigin: multiSiteOrigin,
+				OriginShield:    originShield,
+				Type:            dsType,
+			},
+		}
+
+		ds.MSOAlgorithm = atscfg.ParentConfigDSParamDefaultMSOAlgorithm
+		ds.MSOParentRetry = atscfg.ParentConfigDSParamDefaultMSOParentRetry
+		ds.MSOUnavailableServerRetryResponses = atscfg.ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses
+		ds.MSOMaxSimpleRetries = atscfg.ParentConfigDSParamDefaultMaxSimpleRetries
+		ds.MSOMaxUnavailableServerRetries = atscfg.ParentConfigDSParamDefaultMaxUnavailableServerRetries
+
+		if tcDS.ProfileName != nil && *tcDS.ProfileName != "" {
+			if dsParams, ok := profileParentConfigParams[*tcDS.ProfileName]; ok {
+				ds.QStringHandling = dsParams[atscfg.ParentConfigParamQStringHandling] // may be blank, no default
+				if v, ok := dsParams[atscfg.ParentConfigParamMSOAlgorithm]; ok && strings.TrimSpace(v) != "" {
+					ds.MSOAlgorithm = v
+				}
+				if v, ok := dsParams[atscfg.ParentConfigParamMSOParentRetry]; ok {
+					ds.MSOParentRetry = v
+				}
+				if v, ok := dsParams[atscfg.ParentConfigParamUnavailableServerRetryResponses]; ok {
+					ds.MSOUnavailableServerRetryResponses = v
+				}
+				if v, ok := dsParams[atscfg.ParentConfigParamMaxSimpleRetries]; ok {
+					ds.MSOMaxSimpleRetries = v
+				}
+				if v, ok := dsParams[atscfg.ParentConfigParamMaxUnavailableServerRetries]; ok {
+					ds.MSOMaxUnavailableServerRetries = v
+				}
+			}
+		}
+		parentConfigDSes = append(parentConfigDSes, ds)
+	}
+
+	serverParams := map[string]string{}
+	if serverInfo.ProfileName != "" { // TODO warn/error if false? Servers requires profiles.
+		for name, val := range profileParentConfigParams[serverInfo.ProfileName] {
+			if name == atscfg.ParentConfigParamQStringHandling ||
+				name == atscfg.ParentConfigParamAlgorithm ||
+				name == atscfg.ParentConfigParamQString {
+				serverParams[name] = val
+			}
+		}
+	}
+
+	cdn, err := GetCDN(cfg, serverInfo.CDN)
+	if err != nil {
+		return "", errors.New("getting cdn '" + string(serverInfo.CDN) + "': " + err.Error())
+	}
+
+	serverCDNDomain := cdn.DomainName
+
+	parentConfigServerCacheProfileParams := map[string]atscfg.ProfileCache{} // map[profileName]ProfileCache
+	for _, cgServer := range cgServers {
+		profileCache, ok := parentConfigServerCacheProfileParams[cgServer.Profile]
+		if !ok {
+			profileCache = atscfg.DefaultProfileCache()
+		}
+		params, ok := profileParentConfigParams[cgServer.Profile]
+		if !ok {
+			parentConfigServerCacheProfileParams[cgServer.Profile] = profileCache
+			continue
+		}
+		for name, val := range params {
+			switch name {
+			case atscfg.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())
+				// } else {
+				// 	profileCache.Weight = f
+				// }
+				// TODO validate float?
+				profileCache.Weight = val
+			case atscfg.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())
+				} else {
+					profileCache.Port = int(i)
+				}
+			case atscfg.ParentConfigCacheParamUseIP:
+				profileCache.UseIP = val == "1"
+			case atscfg.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())
+				} else {
+					profileCache.Rank = int(i)
+				}
+			case atscfg.ParentConfigCacheParamNotAParent:
+				profileCache.NotAParent = val != "false"
+			}
+		}
+		parentConfigServerCacheProfileParams[cgServer.Profile] = profileCache
+	}
+
+	dsIDMap := map[int]tc.DeliveryServiceNullable{}
+	for _, ds := range deliveryServices {
+		if ds.ID == nil {
+			log.Errorln("delivery services got nil ID!")
+			os.Exit(1)
+		}
+
+		if !ds.Type.IsHTTP() && !ds.Type.IsDNS() {
+			continue // skip ANY_MAP, STEERING, etc
+		}
+
+		dsIDMap[*ds.ID] = ds
+	}
+
+	allDSMap := map[int]tc.DeliveryServiceNullable{} // 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)
+				continue
+			}
+			if _, ok := allDSMap[dsID]; !ok {
+				allDSMap[dsID] = dsIDMap[dsID]
+			}
+		}
+	}
+
+	log.Infof("len(parentServerDSes) %v!\n", len(parentServerDSes))
+	log.Infof("len(dsIDMap) %v!\n", len(dsIDMap))
+	log.Infof("len(allDSMap) %v!\n", len(allDSMap))
+
+	dsOrigins, err := GetDSOrigins(allDSMap)
+	if err != nil {
+		log.Errorln("getting delivery service origins: " + err.Error())
+		os.Exit(1)
+	}
+
+	log.Infof("len(dsOrigins) %v!\n", len(dsOrigins))
+
+	profileParams := parentConfigServerCacheProfileParams
+
+	originServers := map[string][]atscfg.CGServer{}             // "deliveryServices" in Perl
+	profileCaches := map[atscfg.ProfileID]atscfg.ProfileCache{} // map[profileID]ProfileCache
+
+	for _, cgServer := range cgServers {
+		realCGServer := atscfg.CGServer{
+			ServerID:     atscfg.ServerID(cgServer.ID),
+			ServerHost:   cgServer.HostName,
+			ServerIP:     cgServer.IPAddress,
+			ServerPort:   cgServer.TCPPort,
+			CacheGroupID: cgServer.CachegroupID,
+			Status:       cgServer.StatusID,
+			Type:         cgServer.TypeID,
+			ProfileID:    atscfg.ProfileID(cgServer.ProfileID),
+			CDN:          cgServer.CDNID,
+			TypeName:     cgServer.Type,
+			Domain:       cgServer.DomainName,
+		}
+
+		if cgServer.Type == tc.OriginTypeName {
+			for dsID, _ := range parentServerDSes[cgServer.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
+					continue
+				}
+				originServers[orgURI.Host] = append(originServers[orgURI.Host], realCGServer)
+			}
+		} else {
+			originServers[atscfg.DeliveryServicesAllParentsKey] = append(originServers[atscfg.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] = atscfg.DefaultProfileCache()
+			} else {
+				profileCaches[realCGServer.ProfileID] = profileCache
+			}
+		}
+	}
+
+	parentInfos := atscfg.MakeParentInfo(&serverInfo, serverCDNDomain, profileCaches, originServers)
+
+	return atscfg.MakeParentDotConfig(&serverInfo, atsMajorVer, toToolName, toURL, parentConfigDSes, serverParams, parentInfos), nil
+
+}
+
+// GetDSOrigins takes a map[deliveryServiceID]DeliveryService, and returns a map[DeliveryServiceID]OriginURI.
+func GetDSOrigins(dses map[int]tc.DeliveryServiceNullable) (map[int]*atscfg.OriginURI, error) {
+	dsOrigins := map[int]*atscfg.OriginURI{}
+	for _, ds := range dses {
+		if ds.ID == nil {
+			return nil, errors.New("ds has nil ID")
+		}
+		if ds.XMLID == nil {
+			return nil, fmt.Errorf("ds id %v has nil XMLID", *ds.ID)
+		}
+		if ds.OrgServerFQDN == nil {
+			log.Warnf("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())
+		}
+		if orgURL.Scheme == "" {
+			return nil, 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")
+		}
+
+		scheme := orgURL.Scheme
+		host := orgURL.Hostname()
+		port := orgURL.Port()
+		if port == "" {
+			if scheme == "http" {
+				port = "80"
+			} 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!")
+			}
+		}
+		dsOrigins[*ds.ID] = &atscfg.OriginURI{Scheme: scheme, Host: host, Port: port}
+	}
+	return dsOrigins, nil
+}
diff --git a/traffic_ops/ort/atstccfg/plugindotconfig.go b/traffic_ops/ort/atstccfg/plugindotconfig.go
new file mode 100644
index 0000000..ddb5861
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/plugindotconfig.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfilePluginDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.PluginFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakePluginDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/traffic_ops/ort/atstccfg/profile.go
similarity index 59%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
copy to traffic_ops/ort/atstccfg/profile.go
index 6b94a55..1d563fa 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/traffic_ops/ort/atstccfg/profile.go
@@ -1,4 +1,4 @@
-package atsprofile
+package main
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,18 +20,21 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"errors"
+	"strconv"
 )
 
-func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
-}
-
-func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+func GetProfileNameFromProfileNameOrID(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName := profileNameOrID
+	if profileID, err := strconv.Atoi(profileNameOrID); err == nil {
+		profile, err := GetProfile(cfg, profileID)
+		if err != nil {
+			return "", errors.New("getting profile '" + profileNameOrID + "': " + err.Error())
+		}
+		if profile.Name == "" {
+			return "", errors.New("getting profile '" + profileNameOrID + "': got profile with empty name")
+		}
+		profileName = profile.Name
+	}
+	return profileName, nil
 }
diff --git a/traffic_ops/ort/atstccfg/recordsdotconfig.go b/traffic_ops/ort/atstccfg/recordsdotconfig.go
new file mode 100644
index 0000000..43434ea
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/recordsdotconfig.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileRecordsDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.RecordsFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeRecordsDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/routing.go b/traffic_ops/ort/atstccfg/routing.go
new file mode 100644
index 0000000..5d67a42
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/routing.go
@@ -0,0 +1,159 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+)
+
+var scopeConfigFileFuncs = map[string]func(cfg TCCfg, resource string, fileName string) (string, int, error){
+	"cdns":     GetConfigFileCDN,
+	"servers":  GetConfigFileServer,
+	"profiles": GetConfigFileProfile,
+}
+
+var ErrNotFound = errors.New("not found")
+var ErrBadRequest = errors.New("bad request")
+
+func GetConfigFile(cfg TCCfg) (string, int, error) {
+	pathParts := strings.Split(cfg.TOURL.Path, "/")
+
+	log.Infof("GetConfigFile pathParts %++v\n", pathParts)
+
+	if len(pathParts) < 8 {
+		log.Infoln("GetConfigFile pathParts < 7, calling TO")
+		return GetConfigFileFromTrafficOps(cfg)
+	}
+	scope := pathParts[3]
+	resource := pathParts[4]
+	fileName := pathParts[7]
+
+	log.Infoln("GetConfigFile scope '" + scope + "' resource '" + resource + "' fileName '" + fileName + "'")
+
+	if scopeConfigFileFunc, ok := scopeConfigFileFuncs[scope]; ok {
+		return scopeConfigFileFunc(cfg, resource, fileName)
+	}
+
+	log.Infoln("GetConfigFile unknown scope, calling TO")
+	return GetConfigFileFromTrafficOps(cfg)
+}
+
+func GetConfigFileCDN(cfg TCCfg, cdnNameOrID string, fileName string) (string, int, error) {
+	log.Infoln("GetConfigFileCDN cdn '" + cdnNameOrID + "' fileName '" + fileName + "'")
+	return GetConfigFileFromTrafficOps(cfg)
+}
+
+func GetConfigFileProfile(cfg TCCfg, profileNameOrID string, fileName string) (string, int, error) {
+	log.Infoln("GetConfigFileProfile profile '" + profileNameOrID + "' fileName '" + fileName + "'")
+
+	txt := ""
+	err := error(nil)
+	if getCfgFunc, ok := ProfileConfigFileFuncs()[fileName]; ok {
+		txt, err = getCfgFunc(cfg, profileNameOrID)
+	} else if strings.HasPrefix(fileName, "url_sig_") && strings.HasSuffix(fileName, ".config") && len(fileName) > len("url_sig_")+len(".config") {
+		txt, err = GetConfigFileProfileURLSigConfig(cfg, profileNameOrID, fileName)
+	} else if strings.HasPrefix(fileName, "uri_signing_") && strings.HasSuffix(fileName, ".config") && len(fileName) > len("uri_signing")+len(".config") {
+		txt, err = GetConfigFileProfileURISigningConfig(cfg, profileNameOrID, fileName)
+	} else {
+		txt, err = GetConfigFileProfileUnknownConfig(cfg, profileNameOrID, fileName)
+	}
+
+	if err != nil {
+		code := ExitCodeErrGeneric
+		if err == ErrNotFound {
+			code = ExitCodeNotFound
+		} else if err == ErrBadRequest {
+			code = ExitCodeBadRequest
+		}
+		return "", code, err
+	}
+	return txt, ExitCodeSuccess, nil
+}
+
+// ConfigFileFuncs returns a map[scope][configFile]configFileFunc.
+func ConfigFileFuncs() map[string]map[string]func(cfg TCCfg, serverNameOrID string) (string, error) {
+	return map[string]map[string]func(cfg TCCfg, serverNameOrID string) (string, error){
+		"cdns":     CDNConfigFileFuncs(),
+		"servers":  ServerConfigFileFuncs(),
+		"profiles": ProfileConfigFileFuncs(),
+	}
+}
+
+func CDNConfigFileFuncs() map[string]func(cfg TCCfg, serverNameOrID string) (string, error) {
+	return map[string]func(cfg TCCfg, serverNameOrID string) (string, error){}
+}
+
+func ProfileConfigFileFuncs() map[string]func(cfg TCCfg, serverNameOrID string) (string, error) {
+	return map[string]func(cfg TCCfg, serverNameOrID string) (string, error){
+		"12M_facts":           GetConfigFileProfile12MFacts,
+		"50-ats.rules":        GetConfigFileProfileATSDotRules,
+		"astats.config":       GetConfigFileProfileAstatsDotConfig,
+		"cache.config":        GetConfigFileProfileCacheDotConfig,
+		"drop_qstring.config": GetConfigFileProfileDropQStringDotConfig,
+		"logging.config":      GetConfigFileProfileLoggingDotConfig,
+		"logging.yaml":        GetConfigFileProfileLoggingDotYAML,
+		"logs_xml.config":     GetConfigFileProfileLogsXMLDotConfig,
+		"plugin.config":       GetConfigFileProfilePluginDotConfig,
+		"records.config":      GetConfigFileProfileRecordsDotConfig,
+		"storage.config":      GetConfigFileProfileStorageDotConfig,
+		"sysctl.conf":         GetConfigFileProfileSysCtlDotConf,
+		"volume.config":       GetConfigFileProfileVolumeDotConfig,
+	}
+}
+
+func ServerConfigFileFuncs() map[string]func(cfg TCCfg, serverNameOrID string) (string, error) {
+	return map[string]func(cfg TCCfg, serverNameOrID string) (string, error){
+		"parent.config": GetConfigFileServerParentDotConfig,
+	}
+}
+
+func GetConfigFileServer(cfg TCCfg, serverNameOrID string, fileName string) (string, int, error) {
+	log.Infoln("GetConfigFileServer server '" + serverNameOrID + "' fileName '" + fileName + "'")
+	if getCfgFunc, ok := ServerConfigFileFuncs()[fileName]; ok {
+		txt, err := getCfgFunc(cfg, serverNameOrID)
+		if err != nil {
+			return "", ExitCodeErrGeneric, err
+		}
+		return txt, ExitCodeSuccess, nil
+	}
+	return GetConfigFileFromTrafficOps(cfg)
+}
+
+func GetConfigFileFromTrafficOps(cfg TCCfg) (string, int, error) {
+	path := cfg.TOURL.Path
+	if cfg.TOURL.RawQuery != "" {
+		path += "?" + cfg.TOURL.RawQuery
+	}
+	log.Infoln("GetConfigFile path '" + path + "' not generated locally, requesting from Traffic Ops")
+	log.Infoln("GetConfigFile url '" + cfg.TOURL.String() + "'")
+
+	body, code, err := TrafficOpsRequest(cfg, http.MethodGet, cfg.TOURL.String(), nil)
+	if err != nil {
+		return "", code, errors.New("Requesting path '" + path + "': " + err.Error())
+	}
+
+	WriteCookiesToFile(CookiesToString((*cfg.TOClient).Client.Jar.Cookies(cfg.TOURL)), cfg.TempDir)
+
+	return string(body), HTTPCodeToExitCode(code), nil
+}
diff --git a/traffic_ops/ort/atstccfg/storagedotconfig.go b/traffic_ops/ort/atstccfg/storagedotconfig.go
new file mode 100644
index 0000000..1e78334
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/storagedotconfig.go
@@ -0,0 +1,64 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+const StorageFileName = "storage.config"
+
+func GetConfigFileProfileStorageDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != StorageFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeStorageDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/sysctldotconf.go b/traffic_ops/ort/atstccfg/sysctldotconf.go
new file mode 100644
index 0000000..2481d53
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/sysctldotconf.go
@@ -0,0 +1,62 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileSysCtlDotConf(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != atscfg.SysctlFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeSysCtlDotConf(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/toreq.go b/traffic_ops/ort/atstccfg/toreq.go
new file mode 100644
index 0000000..56f50e0
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/toreq.go
@@ -0,0 +1,345 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+)
+
+func GetProfile(cfg TCCfg, profileID int) (tc.Profile, error) {
+	profile := tc.Profile{}
+	err := GetCachedJSON(cfg, "profile_"+strconv.Itoa(profileID)+".json", &profile, func(obj interface{}) error {
+		toProfiles, reqInf, err := (*cfg.TOClient).GetProfileByID(profileID)
+		if err != nil {
+			return errors.New("getting profile '" + strconv.Itoa(profileID) + "' from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		if len(toProfiles) != 1 {
+			return errors.New("getting profile '" + strconv.Itoa(profileID) + "'from Traffic Ops '" + MaybeIPStr(reqInf) + "': expected 1 Profile, got " + strconv.Itoa(len(toProfiles)))
+		}
+
+		profile := obj.(*tc.Profile)
+		*profile = toProfiles[0]
+		return nil
+	})
+	if err != nil {
+		return tc.Profile{}, errors.New("getting profile '" + strconv.Itoa(profileID) + "': " + err.Error())
+	}
+	return profile, nil
+}
+
+func GetProfileByName(cfg TCCfg, profileName string) (tc.Profile, error) {
+	profile := tc.Profile{}
+	err := GetCachedJSON(cfg, "profile_"+profileName+".json", &profile, func(obj interface{}) error {
+		toProfiles, reqInf, err := (*cfg.TOClient).GetProfileByName(profileName)
+		if err != nil {
+			return errors.New("getting profile '" + profileName + "' from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		if len(toProfiles) != 1 {
+			return errors.New("getting profile '" + profileName + "'from Traffic Ops '" + MaybeIPStr(reqInf) + "': expected 1 Profile, got " + strconv.Itoa(len(toProfiles)))
+		}
+
+		profile := obj.(*tc.Profile)
+		*profile = toProfiles[0]
+		return nil
+	})
+	if err != nil {
+		return tc.Profile{}, errors.New("getting profile '" + profileName + "': " + err.Error())
+	}
+	return profile, nil
+}
+
+func GetProfileParameters(cfg TCCfg, profileName string) ([]tc.Parameter, error) {
+	profileParameters := []tc.Parameter{}
+	err := GetCachedJSON(cfg, "profile_"+profileName+"_parameters.json", &profileParameters, func(obj interface{}) error {
+		toParams, reqInf, err := (*cfg.TOClient).GetParametersByProfileName(profileName)
+		if err != nil {
+			return errors.New("getting profile '" + profileName + "' parameters from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		params := obj.(*[]tc.Parameter)
+		*params = toParams
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	return profileParameters, nil
+}
+
+func GetGlobalParameters(cfg TCCfg) ([]tc.Parameter, error) {
+	globalParams := []tc.Parameter{}
+	err := GetCachedJSON(cfg, "profile_global_parameters.json", &globalParams, func(obj interface{}) error {
+		toParams, reqInf, err := (*cfg.TOClient).GetParametersByProfileName(GlobalProfileName)
+		if err != nil {
+			return errors.New("getting global profile '" + GlobalProfileName + "' parameters from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		params := obj.(*[]tc.Parameter)
+		*params = toParams
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting global profile '" + GlobalProfileName + "' parameters: " + err.Error())
+	}
+	return globalParams, nil
+}
+
+func GetTOToolNameAndURL(globalParams []tc.Parameter) (string, string) {
+	toToolName := ""
+	toURL := ""
+	for _, param := range globalParams {
+		if param.Name == "tm.toolname" {
+			toToolName = param.Value
+		} else if param.Name == "tm.url" {
+			toURL = param.Value
+		}
+		if toToolName != "" && toURL != "" {
+			break
+		}
+	}
+	// TODO error here? Perl doesn't.
+	if toToolName == "" {
+		log.Warnln("Global Parameter tm.toolname not found, config may not be constructed properly!")
+	}
+	if toURL == "" {
+		log.Warnln("Global Parameter tm.url not found, config may not be constructed properly!")
+	}
+	return toToolName, toURL
+}
+
+func GetTOToolNameAndURLFromTO(cfg TCCfg) (string, string, error) {
+	globalParams, err := GetGlobalParameters(cfg)
+	if err != nil {
+		return "", "", err
+	}
+	toToolName, toURL := GetTOToolNameAndURL(globalParams)
+	return toToolName, toURL, nil
+}
+
+func GetServers(cfg TCCfg) ([]tc.Server, error) {
+	servers := []tc.Server{}
+	err := GetCachedJSON(cfg, "servers.json", &servers, func(obj interface{}) error {
+		toServers, reqInf, err := (*cfg.TOClient).GetServers()
+		if err != nil {
+			return errors.New("getting servers from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		servers := obj.(*[]tc.Server)
+		*servers = toServers
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting servers: " + err.Error())
+	}
+	return servers, nil
+}
+
+func GetCacheGroups(cfg TCCfg) ([]tc.CacheGroupNullable, error) {
+	cacheGroups := []tc.CacheGroupNullable{}
+	err := GetCachedJSON(cfg, "cachegroups.json", &cacheGroups, func(obj interface{}) error {
+		toCacheGroups, reqInf, err := (*cfg.TOClient).GetCacheGroupsNullable()
+		if err != nil {
+			return errors.New("getting cachegroups from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		cacheGroups := obj.(*[]tc.CacheGroupNullable)
+		*cacheGroups = toCacheGroups
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting cachegroups: " + err.Error())
+	}
+	return cacheGroups, nil
+}
+
+func GetDeliveryServiceServers(cfg TCCfg, serverIDs []int) ([]tc.DeliveryServiceServer, error) {
+	serverIDsSorted := make([]int, 0, len(serverIDs))
+	for _, id := range serverIDs {
+		serverIDsSorted = append(serverIDsSorted, id)
+	}
+	sort.Ints(serverIDsSorted)
+
+	serverIDStrs := []string{}
+	for _, id := range serverIDs {
+		serverIDStrs = append(serverIDStrs, strconv.Itoa(id))
+	}
+	serverIDsStr := strings.Join(serverIDStrs, "-")
+
+	dsServers := []tc.DeliveryServiceServer{}
+	// TODO make this filename shorter (but still unique) somehow. The filename is almost always too long.
+	err := GetCachedJSON(cfg, "deliveryservice_servers_"+serverIDsStr+".json", &dsServers, func(obj interface{}) error {
+		const noLimit = 999999 // TODO add "no limit" param to DSS endpoint
+		toDSS, reqInf, err := (*cfg.TOClient).GetDeliveryServiceServersWithLimits(noLimit, nil, serverIDs)
+		if err != nil {
+			return errors.New("getting delivery service servers from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+
+		serverIDsMap := map[int]struct{}{}
+		for _, id := range serverIDs {
+			serverIDsMap[id] = struct{}{}
+		}
+
+		// Older TO's may ignore the server ID list, so we need to filter them out manually to be sure.
+		filteredDSServers := []tc.DeliveryServiceServer{}
+		for _, dsServer := range toDSS.Response {
+			if dsServer.Server == nil || dsServer.DeliveryService == nil {
+				continue // TODO warn? error?
+			}
+			if _, ok := serverIDsMap[*dsServer.Server]; !ok {
+				continue
+			}
+			filteredDSServers = append(filteredDSServers, dsServer)
+		}
+
+		dss := obj.(*[]tc.DeliveryServiceServer)
+		*dss = filteredDSServers
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting delivery service servers: " + err.Error())
+	}
+
+	return dsServers, nil
+}
+
+func GetServerProfileParameters(cfg TCCfg, profileName string) ([]tc.Parameter, error) {
+	serverProfileParameters := []tc.Parameter{}
+	err := GetCachedJSON(cfg, "profile_"+profileName+"_parameters.json", &serverProfileParameters, func(obj interface{}) error {
+		toParams, reqInf, err := (*cfg.TOClient).GetParametersByProfileName(profileName)
+		if err != nil {
+			return errors.New("getting server profile '" + profileName + "' parameters from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		params := obj.(*[]tc.Parameter)
+		*params = toParams
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting server profile '" + profileName + "' parameters: " + err.Error())
+	}
+	return serverProfileParameters, nil
+}
+
+func GetCDNDeliveryServices(cfg TCCfg, cdnID int) ([]tc.DeliveryServiceNullable, error) {
+	deliveryServices := []tc.DeliveryServiceNullable{}
+	err := GetCachedJSON(cfg, "cdn_"+strconv.Itoa(cdnID)+"_deliveryservices"+".json", &deliveryServices, func(obj interface{}) error {
+		toDSes, reqInf, err := (*cfg.TOClient).GetDeliveryServicesByCDNID(cdnID)
+		if err != nil {
+			return errors.New("getting delivery services from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		dses := obj.(*[]tc.DeliveryServiceNullable)
+		*dses = toDSes
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting delivery services: " + err.Error())
+	}
+	return deliveryServices, nil
+}
+
+func GetConfigFileParameters(cfg TCCfg, configFile string) ([]tc.Parameter, error) {
+	params := []tc.Parameter{}
+	err := GetCachedJSON(cfg, "config_file_"+configFile+"_parameters"+".json", &params, func(obj interface{}) error {
+		toParams, reqInf, err := (*cfg.TOClient).GetParameterByConfigFile(configFile)
+		if err != nil {
+			return errors.New("getting delivery services from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		params := obj.(*[]tc.Parameter)
+		*params = toParams
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting parent.config parameters: " + err.Error())
+	}
+	return params, nil
+}
+
+func GetCDN(cfg TCCfg, cdnName tc.CDNName) (tc.CDN, error) {
+	cdn := tc.CDN{}
+	err := GetCachedJSON(cfg, "cdn_"+string(cdnName)+".json", &cdn, func(obj interface{}) error {
+		toCDNs, reqInf, err := (*cfg.TOClient).GetCDNByName(string(cdnName))
+		if err != nil {
+			return errors.New("getting cdn from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		if len(toCDNs) != 1 {
+			return errors.New("getting cdn from Traffic Ops '" + MaybeIPStr(reqInf) + "': expected 1 CDN, got " + strconv.Itoa(len(toCDNs)))
+		}
+		cdn := obj.(*tc.CDN)
+		*cdn = toCDNs[0]
+		return nil
+	})
+	if err != nil {
+		return tc.CDN{}, errors.New("getting cdn: " + err.Error())
+	}
+	return cdn, nil
+}
+
+func GetURLSigKeys(cfg TCCfg, dsName string) (tc.URLSigKeys, error) {
+	keys := tc.URLSigKeys{}
+	err := GetCachedJSON(cfg, "urlsigkeys_"+string(dsName)+".json", &keys, func(obj interface{}) error {
+		toKeys, reqInf, err := (*cfg.TOClient).GetDeliveryServiceURLSigKeys(dsName)
+		if err != nil {
+			return errors.New("getting url sig keys from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		keys := obj.(*tc.URLSigKeys)
+		*keys = toKeys
+		return nil
+	})
+	if err != nil {
+		return tc.URLSigKeys{}, errors.New("getting url sig keys: " + err.Error())
+	}
+	return keys, nil
+}
+
+func GetURISigningKeys(cfg TCCfg, dsName string) ([]byte, error) {
+	keys := []byte{}
+	err := GetCachedJSON(cfg, "urisigningkeys_"+string(dsName)+".json", &keys, func(obj interface{}) error {
+		toKeys, reqInf, err := (*cfg.TOClient).GetDeliveryServiceURISigningKeys(dsName)
+		if err != nil {
+			return errors.New("getting url sig keys from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+
+		keys := obj.(*[]byte)
+		*keys = toKeys
+		return nil
+	})
+	if err != nil {
+		return []byte{}, errors.New("getting url sig keys: " + err.Error())
+	}
+	return keys, nil
+}
+
+func GetParametersByName(cfg TCCfg, paramName string) ([]tc.Parameter, error) {
+	params := []tc.Parameter{}
+	err := GetCachedJSON(cfg, "parameters_name_"+paramName+".json", &params, func(obj interface{}) error {
+		toParams, reqInf, err := (*cfg.TOClient).GetParameterByName(paramName)
+		if err != nil {
+			return errors.New("getting parameters name '" + paramName + "' from Traffic Ops '" + MaybeIPStr(reqInf) + "': " + err.Error())
+		}
+		params := obj.(*[]tc.Parameter)
+		*params = toParams
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("getting params name '" + paramName + "': " + err.Error())
+	}
+	return params, nil
+}
diff --git a/traffic_ops/ort/atstccfg/trafficops.go b/traffic_ops/ort/atstccfg/trafficops.go
new file mode 100644
index 0000000..a503b37
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/trafficops.go
@@ -0,0 +1,284 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"bytes"
+	"crypto/sha512"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"io"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/cookiejar"
+	"net/http/httptrace"
+	"net/url"
+	"strconv"
+	"time"
+
+	"golang.org/x/net/publicsuffix"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	toclient "github.com/apache/trafficcontrol/traffic_ops/client"
+)
+
+// GetClient returns a TO Client, using a cached cookie if it exists, or logging in otherwise
+func GetClient(toURL string, toUser string, toPass string, tempDir string, cacheFileMaxAge time.Duration, toTimeout time.Duration, toInsecure bool) (*toclient.Session, error) {
+	cookies, err := GetCookiesFromFile(tempDir, cacheFileMaxAge)
+	if err != nil {
+		log.Infoln("failed to get cookies from cache file (trying real TO): " + err.Error())
+		cookies = ""
+	}
+
+	if cookies == "" {
+		err := error(nil)
+		cookies, err = GetCookiesFromTO(toURL, toUser, toPass, tempDir, toTimeout, toInsecure)
+		if err != nil {
+			return nil, errors.New("getting cookies from Traffic Ops: " + err.Error())
+		}
+		log.Infoln("using cookies from TO")
+	} else {
+		log.Infoln("using cookies from cache file")
+	}
+
+	useCache := false
+	toClient := toclient.NewNoAuthSession(toURL, toInsecure, UserAgent, useCache, toTimeout)
+	toClient.UserName = toUser
+	toClient.Password = toPass
+
+	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+	if err != nil {
+		return nil, errors.New("making cookie jar: " + err.Error())
+	}
+	toClient.Client.Jar = jar
+
+	toURLParsed, err := url.Parse(toURL)
+	if err != nil {
+		return nil, errors.New("parsing Traffic Ops URL '" + toURL + "': " + err.Error())
+	}
+
+	toClient.Client.Jar.SetCookies(toURLParsed, StringToCookies(cookies))
+	return toClient, nil
+}
+
+// GetCookies gets the cookies from logging in to Traffic Ops.
+// If this succeeds, it also writes the cookies to TempSubdir/TempCookieFileName.
+func GetCookiesFromTO(toURL string, toUser string, toPass string, tempDir string, toTimeout time.Duration, toInsecure bool) (string, error) {
+	toURLParsed, err := url.Parse(toURL)
+	if err != nil {
+		return "", errors.New("parsing Traffic Ops URL '" + toURL + "': " + err.Error())
+	}
+
+	toUseCache := false
+	toClient, toIP, err := toclient.LoginWithAgent(toURL, toUser, toPass, toInsecure, UserAgent, toUseCache, toTimeout)
+	if err != nil {
+		toIPStr := ""
+		if toIP != nil {
+			toIPStr = toIP.String()
+		}
+		return "", errors.New("logging in to Traffic Ops IP '" + toIPStr + "': " + err.Error())
+	}
+
+	cookiesStr := CookiesToString(toClient.Client.Jar.Cookies(toURLParsed))
+	WriteCookiesToFile(cookiesStr, tempDir)
+
+	return cookiesStr, nil
+}
+
+// TrafficOpsRequest makes a request to Traffic Ops for the given method, url, and body.
+// If it gets an Unauthorized or Forbidden, it tries to log in again and makes the request again.
+func TrafficOpsRequest(cfg TCCfg, method string, url string, body []byte) (string, int, error) {
+	return trafficOpsRequestWithRetry(cfg, method, url, body, cfg.NumRetries)
+}
+
+func trafficOpsRequestWithRetry(
+	cfg TCCfg,
+	method string,
+	url string,
+	body []byte,
+	retryNum int,
+) (string, int, error) {
+	currentRetry := 0
+	for {
+		body, code, err := trafficOpsRequestWithLogin(cfg, method, url, body)
+		if err == nil || currentRetry == retryNum {
+			return body, code, err
+		}
+
+		sleepSeconds := RetryBackoffSeconds(currentRetry)
+		log.Errorf("getting '%v' '%v', sleeping for %v seconds: %v\n", method, url, sleepSeconds, err)
+		currentRetry++
+		time.Sleep(time.Second * time.Duration(sleepSeconds))
+	}
+}
+
+func trafficOpsRequestWithLogin(
+	cfg TCCfg,
+	method string,
+	url string,
+	body []byte,
+) (string, int, error) {
+	resp, toIP, err := rawTrafficOpsRequest(*cfg.TOClient, method, url, body)
+	if err != nil {
+		toIPStr := ""
+		if toIP != nil {
+			toIPStr = toIP.String()
+		}
+		return "", 1, errors.New("requesting from Traffic Ops '" + toIPStr + "': " + err.Error())
+	}
+
+	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
+		resp.Body.Close()
+
+		log.Infoln("TrafficOpsRequest got unauthorized/forbidden, logging in again")
+		log.Infof("TrafficOpsRequest url '%v' user '%v' pass '%v'\n", (*cfg.TOClient).URL, (*cfg.TOClient).UserName, (*cfg.TOClient).Password)
+
+		useCache := false
+		newTOClient, toIP, err := toclient.LoginWithAgent((*cfg.TOClient).URL, (*cfg.TOClient).UserName, (*cfg.TOClient).Password, cfg.TOInsecure, UserAgent, useCache, cfg.TOTimeout)
+		if err != nil {
+			toIPStr := ""
+			if toIP != nil {
+				toIPStr = toIP.String()
+			}
+			return "", 1, errors.New("logging in to Traffic Ops IP '" + toIPStr + "': " + err.Error())
+		}
+		*cfg.TOClient = newTOClient
+
+		resp, toIP, err = rawTrafficOpsRequest(*cfg.TOClient, method, url, body)
+		if err != nil {
+			toIPStr := ""
+			if toIP != nil {
+				toIPStr = toIP.String()
+			}
+			return "", 1, errors.New("requesting from Traffic Ops '" + toIPStr + "': " + err.Error())
+		}
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		bts, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			bts = []byte("(read failure)") // if it's a non-200 and the body read fails, don't error, just note the read fail in the error
+		}
+		return "", resp.StatusCode, errors.New("Traffic Ops returned non-200 code '" + strconv.Itoa(resp.StatusCode) + "' body '" + string(bts) + "'")
+	}
+
+	bts, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		toIPStr := ""
+		if toIP != nil {
+			toIPStr = toIP.String()
+		}
+		return "", resp.StatusCode, errors.New("reading body from Traffic Ops '" + toIPStr + "': " + err.Error())
+	}
+
+	if err := IntegrityCheck(resp.Header, bts, url); err != nil {
+		return "", resp.StatusCode, errors.New("integrity check failed for url '" + url + "': " + err.Error())
+	}
+
+	return string(bts), http.StatusOK, nil
+}
+
+func IntegrityCheck(hdr http.Header, body []byte, url string) error {
+	if hdrSHA := hdr.Get("Whole-Content-SHA512"); hdrSHA != "" {
+		realSHA := sha512.Sum512(body)
+		realSHAStr := base64.StdEncoding.EncodeToString(realSHA[:])
+		if realSHAStr != hdrSHA {
+			return errors.New("Body does not match header Whole-Content-SHA512")
+		}
+		log.Infoln("Integrity check for url '" + url + "' passed (sha match)")
+		return nil
+	}
+	if hdrLenStr := hdr.Get("Content-Length"); hdrLenStr != "" {
+		hdrLen, err := strconv.Atoi(hdrLenStr)
+		if err != nil {
+			return errors.New("No Whole-Content-SHA512, and Content-Length '" + hdrLenStr + "' is not an integer")
+		}
+		if hdrLen != len(body) {
+			return errors.New("No Whole-Content-SHA512, and Content-Length '" + hdrLenStr + "' does not match body length")
+		}
+		log.Infoln("Integrity check for url '" + url + "' passed (length match)\n")
+		return nil
+	}
+	return errors.New("No Whole-Content-SHA512, and no Content-Length, cannot verify content")
+}
+
+// rawTrafficOpsRequest makes a request to Traffic Ops for the given method, url, and body.
+// If it gets an Unauthorized or Forbidden, it tries to log in again and makes the request again.
+func rawTrafficOpsRequest(toClient *toclient.Session, method string, url string, body []byte) (*http.Response, net.Addr, error) {
+	bodyReader := io.Reader(nil)
+	if len(body) > 0 {
+		bodyReader = bytes.NewBuffer(body)
+	}
+
+	remoteAddr := net.Addr(nil)
+	req, err := http.NewRequest(method, url, bodyReader)
+	if err != nil {
+		return nil, remoteAddr, err
+	}
+
+	req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
+		GotConn: func(connInfo httptrace.GotConnInfo) {
+			remoteAddr = connInfo.Conn.RemoteAddr()
+		},
+	}))
+
+	req.Header.Set("User-Agent", toClient.UserAgentStr)
+
+	resp, err := toClient.Client.Do(req)
+	if err != nil {
+		return nil, remoteAddr, err
+	}
+
+	return resp, remoteAddr, nil
+}
+
+// MaybeIPStr returns the Traffic Ops IP string if it isn't nil, or the empty string if it is.
+func MaybeIPStr(reqInf toclient.ReqInf) string {
+	if reqInf.RemoteAddr != nil {
+		return reqInf.RemoteAddr.String()
+	}
+	return ""
+}
+
+// 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}
+
+		profiles := []string{}
+		if err := json.Unmarshal(tcParam.Profiles, &profiles); err != nil {
+			return nil, errors.New("unmarshalling JSON from parameter '" + strconv.Itoa(param.ID) + "': " + err.Error())
+		}
+		param.ProfileNames = profiles
+		param.Profiles = nil
+		params = append(params, param)
+	}
+	return params, nil
+}
+
+type ParameterWithProfiles struct {
+	tc.Parameter
+	ProfileNames []string
+}
diff --git a/traffic_ops/ort/atstccfg/unknownconfig.go b/traffic_ops/ort/atstccfg/unknownconfig.go
new file mode 100644
index 0000000..2fb7daf
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/unknownconfig.go
@@ -0,0 +1,83 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileUnknownConfig(cfg TCCfg, profileNameOrID string, fileName string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	scopeParams, err := GetParametersByName(cfg, "scope")
+	if err != nil {
+		return "", errors.New("getting scope parameters: " + err.Error())
+	}
+
+	inScope := false
+	for _, scopeParam := range scopeParams {
+		if scopeParam.ConfigFile != fileName {
+			continue
+		}
+		if scopeParam.Value != "profiles" {
+			continue
+		}
+		inScope = true
+		break
+	}
+
+	if !inScope {
+		return `{"alerts":[{"level":"error","text":"Error - incorrect file scope for route used.  Please use the servers route."}]}`, ErrBadRequest
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != fileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeUnknownConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/ort/atstccfg/urisigningconfig.go b/traffic_ops/ort/atstccfg/urisigningconfig.go
new file mode 100644
index 0000000..c9e4885
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/urisigningconfig.go
@@ -0,0 +1,54 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileURISigningConfig(cfg TCCfg, profileNameOrID string, fileName string) (string, error) {
+	dsName := GetDSFromURISigningConfigFileName(fileName)
+	if dsName == "" {
+		// extra safety, this should never happen, the routing shouldn't get here
+		return "", errors.New("getting ds name: malformed config file '" + fileName + "'")
+	}
+
+	uriSigningKeys, err := GetURISigningKeys(cfg, dsName)
+	if err != nil {
+		return "", errors.New("getting uri signing keys for ds '" + dsName + "': " + err.Error())
+	}
+
+	return atscfg.MakeURISigningConfig(uriSigningKeys), nil
+}
+
+// GetDSFromURISigningConfigFileName returns the DS of a URI Signing config file name.
+// For example, "uri_signing_foobar.config" returns "foobar".
+// If the given string is shorter than len("uri_signing_a.config"), the empty string is returned.
+func GetDSFromURISigningConfigFileName(fileName string) string {
+	if !strings.HasPrefix(fileName, "uri_signing_") || !strings.HasSuffix(fileName, ".config") || len(fileName) <= len("uri_signing_")+len(".config") {
+		return ""
+	}
+	fileName = fileName[len("uri_signing_"):]
+	fileName = fileName[:len(fileName)-len(".config")]
+	return fileName
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go b/traffic_ops/ort/atstccfg/urisigningconfig_test.go
similarity index 50%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
copy to traffic_ops/ort/atstccfg/urisigningconfig_test.go
index 4909766..84fd207 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
+++ b/traffic_ops/ort/atstccfg/urisigningconfig_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package main
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,29 +20,24 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"errors"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"testing"
 )
 
-func GetDropQString(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeDropQString)
-}
-
-func makeDropQString(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	dropQStringVal, hasDropQStringParam, err := ats.GetProfileParamValue(tx, profile.ID, "drop_qstring.config", "content")
-	if err != nil {
-		return "", errors.New("getting profile param val: " + err.Error())
+func TestGetDSFromURISigningConfigFileName(t *testing.T) {
+	expecteds := map[string]string{
+		"uri_signing_foo.config":                            "foo",
+		"uri_signing_.config":                               "",
+		"uri_signing.config":                                "",
+		"uri_signing_foo.conf":                              "",
+		"uri_signing_foo.confi":                             "",
+		"uri_signing_foo_bar_baz.config":                    "foo_bar_baz",
+		"uri_signing_uri_signing_foo_bar_baz.config.config": "uri_signing_foo_bar_baz.config",
 	}
 
-	text := ""
-	if hasDropQStringParam {
-		text += dropQStringVal + "\n"
-	} else {
-		text += `/([^?]+) $s://$t/$1` + "\n"
+	for fileName, expected := range expecteds {
+		actual := GetDSFromURISigningConfigFileName(fileName)
+		if expected != actual {
+			t.Errorf("GetDSFromURLSigConfigFileName('%v') expected '%v' actual '%v'\n", fileName, expected, actual)
+		}
 	}
-	return text, nil
 }
diff --git a/traffic_ops/ort/atstccfg/urlsigconfig.go b/traffic_ops/ort/atstccfg/urlsigconfig.go
new file mode 100644
index 0000000..8116c7f
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/urlsigconfig.go
@@ -0,0 +1,86 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+func GetConfigFileProfileURLSigConfig(cfg TCCfg, profileNameOrID string, fileName string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be storage.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != fileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	dsName := GetDSFromURLSigConfigFileName(fileName)
+	if dsName == "" {
+		// extra safety, this should never happen, the routing shouldn't get here
+		return "", errors.New("getting ds name: malformed config file '" + fileName + "'")
+	}
+
+	urlSigKeys, err := GetURLSigKeys(cfg, dsName)
+	if err != nil {
+		return "", errors.New("getting url sig keys for ds '" + dsName + "': " + err.Error())
+	}
+
+	return atscfg.MakeURLSigConfig(profileName, urlSigKeys, paramData, toToolName, toURL), nil
+}
+
+// GetDSFromURLSigConfigFileName returns the DS of a URLSig config file name.
+// For example, "url_sig_foobar.config" returns "foobar".
+// If the given string is shorter than len("url_sig_a.config"), the empty string is returned.
+func GetDSFromURLSigConfigFileName(fileName string) string {
+	if !strings.HasPrefix(fileName, "url_sig_") || !strings.HasSuffix(fileName, ".config") || len(fileName) <= len("url_sig_")+len(".config") {
+		return ""
+	}
+	fileName = fileName[len("url_sig_"):]
+	fileName = fileName[:len(fileName)-len(".config")]
+	return fileName
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go b/traffic_ops/ort/atstccfg/urlsigconfig_test.go
similarity index 52%
copy from traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
copy to traffic_ops/ort/atstccfg/urlsigconfig_test.go
index 16e5267..376970e 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
+++ b/traffic_ops/ort/atstccfg/urlsigconfig_test.go
@@ -1,4 +1,4 @@
-package atsprofile
+package main
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,27 +20,24 @@ package atsprofile
  */
 
 import (
-	"database/sql"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"testing"
 )
 
-const SysctlSeparator = " = "
-const SysctlFileName = "sysctl.conf"
-
-func GetSysctl(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeSysctl)
-}
-
-func makeSysctl(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, SysctlFileName, SysctlSeparator)
-	if err != nil {
-		return "", err
+func TestGetDSFromURLSigConfigFileName(t *testing.T) {
+	expecteds := map[string]string{
+		"url_sig_foo.config":                        "foo",
+		"url_sig_.config":                           "",
+		"url_sig.config":                            "",
+		"url_sig_foo.conf":                          "",
+		"url_sig_foo.confi":                         "",
+		"url_sig_foo_bar_baz.config":                "foo_bar_baz",
+		"url_sig_url_sig_foo_bar_baz.config.config": "url_sig_foo_bar_baz.config",
 	}
-	if txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+
+	for fileName, expected := range expecteds {
+		actual := GetDSFromURLSigConfigFileName(fileName)
+		if expected != actual {
+			t.Errorf("GetDSFromURLSigConfigFileName('%v') expected '%v' actual '%v'\n", fileName, expected, actual)
+		}
 	}
-	return txt, nil
 }
diff --git a/traffic_ops/ort/atstccfg/volumedotconfig.go b/traffic_ops/ort/atstccfg/volumedotconfig.go
new file mode 100644
index 0000000..60db522
--- /dev/null
+++ b/traffic_ops/ort/atstccfg/volumedotconfig.go
@@ -0,0 +1,64 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+)
+
+const VolumeFileName = StorageFileName
+
+func GetConfigFileProfileVolumeDotConfig(cfg TCCfg, profileNameOrID string) (string, error) {
+	profileName, err := GetProfileNameFromProfileNameOrID(cfg, profileNameOrID)
+	if err != nil {
+		return "", errors.New("getting profile name from '" + profileNameOrID + "': " + err.Error())
+	}
+
+	profileParameters, err := GetProfileParameters(cfg, profileName)
+	if err != nil {
+		return "", errors.New("getting profile '" + profileName + "' parameters: " + err.Error())
+	}
+	if len(profileParameters) == 0 {
+		// The TO endpoint behind toclient.GetParametersByProfileName returns an empty object with a 200, if the Profile doesn't exist.
+		// So we act as though we got a 404 if there are no params (and there should always be volume.config params), to make ORT behave correctly.
+		return "", ErrNotFound
+	}
+
+	toToolName, toURL, err := GetTOToolNameAndURLFromTO(cfg)
+	if err != nil {
+		return "", errors.New("getting global parameters: " + err.Error())
+	}
+
+	paramData := map[string]string{}
+	// TODO add configFile query param to profile/parameters endpoint, to only get needed data
+	for _, param := range profileParameters {
+		if param.ConfigFile != VolumeFileName {
+			continue
+		}
+		if param.Name == "location" {
+			continue
+		}
+		paramData[param.Name] = param.Value
+	}
+
+	return atscfg.MakeVolumeDotConfig(profileName, paramData, toToolName, toURL), nil
+}
diff --git a/traffic_ops/bin/supermicro_udev_mapper.pl b/traffic_ops/ort/supermicro_udev_mapper.pl
similarity index 100%
rename from traffic_ops/bin/supermicro_udev_mapper.pl
rename to traffic_ops/ort/supermicro_udev_mapper.pl
diff --git a/traffic_ops/bin/traffic_ops_ort.pl b/traffic_ops/ort/traffic_ops_ort.pl
similarity index 95%
rename from traffic_ops/bin/traffic_ops_ort.pl
rename to traffic_ops/ort/traffic_ops_ort.pl
index 4bcc5df..4b1bd30 100755
--- a/traffic_ops/bin/traffic_ops_ort.pl
+++ b/traffic_ops/ort/traffic_ops_ort.pl
@@ -41,12 +41,21 @@ my $login_dispersion = 0;
 my $reval_wait_time = 60;
 my $reval_in_use = 0;
 my $rev_proxy_disable = 0;
+my $skip_os_check = 0;
+my $override_hostname_short = '';
+my $override_hostname_full = '';
+my $override_domainname = '';
 
 GetOptions( "dispersion=i"       => \$dispersion, # dispersion (in seconds)
             "retries=i"          => \$retries,
             "wait_for_parents=i" => \$wait_for_parents,
             "login_dispersion=i" => \$login_dispersion,
-            "rev_proxy_disable=i" => \$rev_proxy_disable );
+            "rev_proxy_disable=i" => \$rev_proxy_disable,
+            "skip_os_check=i" => \$skip_os_check,
+            "override_hostname_short=s" => \$override_hostname_short,
+            "override_hostname_full=s" => \$override_hostname_full,
+            "override_domainname=s" => \$override_domainname,
+          );
 
 if ( $#ARGV < 1 ) {
 	&usage();
@@ -149,13 +158,25 @@ my $rev_proxy_in_use = 0;
 my $lwp_conn                   = &setup_lwp();
 my $unixtime       = time();
 my $hostname_short = `/bin/hostname -s`;
+if ($override_hostname_short ne '') {
+	$hostname_short = $override_hostname_short;
+}
 chomp($hostname_short);
 my $hostname_full = `/bin/hostname`;
+if ($override_hostname_full ne '') {
+	$hostname_full = $override_hostname_full;
+}
 chomp($hostname_full);
 my $server_ipv4;
 my $server_tcp_port;
 
 my $domainname = &set_domainname();
+if ($override_domainname ne '') {
+	$domainname = $override_domainname;
+}
+
+my $atstccfg_cmd = '/opt/ort/atstccfg';
+
 $lwp_conn->agent("$hostname_short-$unixtime");
 
 my $TMP_BASE  = "/tmp/ort";
@@ -307,8 +328,10 @@ sub os_version {
 	if (`uname -r` =~ m/.+(el\d)(?:\.\w+)*\.x86_64/)  {
 		$release = uc $1;
 	}
-	exists $supported_el_release{$release} ? return $release
-	    : die("unsupported el_version: $release");
+	if (!exists $supported_el_release{$release} && !$skip_os_check) {
+		die("skip_os_check: $skip_os_check dispersion: $dispersion unsupported el_version: $release");
+	}
+	return $release;
 }
 
 sub usage {
@@ -326,11 +349,15 @@ sub usage {
 	print "\n";
 	print "\t<Traffic_Ops_Login> => Example: 'username:password' \n";
 	print "\n\t[optional flags]:\n";
-	print "\t   dispersion=<time>        => wait a random number between 0 and <time> before starting. Default = 300.\n";
-	print "\t   login_dispersion=<time>  => wait a random number between 0 and <time> before login. Default = 0.\n";
-	print "\t   retries=<number>         => retry connection to Traffic Ops URL <number> times. Default = 3.\n";
-	print "\t   wait_for_parents=<0|1>   => do not update if parent_pending = 1 in the update json. Default = 1, wait for parents.\n";
-	print "\t   rev_proxy_disable=<0|1>  => bypass the reverse proxy even if one has been configured Default = 0.\n";
+	print "\t   dispersion=<time>              => wait a random number between 0 and <time> before starting. Default = 300.\n";
+	print "\t   login_dispersion=<time>        => wait a random number between 0 and <time> before login. Default = 0.\n";
+	print "\t   retries=<number>               => retry connection to Traffic Ops URL <number> times. Default = 3.\n";
+	print "\t   wait_for_parents=<0|1>         => do not update if parent_pending = 1 in the update json. Default = 1, wait for parents.\n";
+	print "\t   rev_proxy_disable=<0|1>        => bypass the reverse proxy even if one has been configured Default = 0.\n";
+	print "\t   skip_os_check=<0|1>            => bypass the check for a supported CentOS version. Default = 0.\n";
+	print "\t   override_hostname_short=<text> => override the short hostname of the OS for config generation. Default = ''.\n";
+	print "\t   override_hostname_full=<text>  => override the full hostname of the OS for config generation. Default = ''.\n";
+	print "\t   override_domainname=<text>     => override the domainname of the OS for config generation. Default = ''.\n";
 	print "====-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-====\n";
 	exit 1;
 }
@@ -1427,6 +1454,23 @@ sub check_this_plugin {
 	}
 }
 
+sub atstccfg_code_to_http_code {
+	# this is necessary, because Linux codes can only be 0-256, so we map e.g. 104 -> 404 to fake the Traffic Ops response code.
+	my $code = shift;
+
+	my $generic_http_err = 500;
+	my %atstccfg_to_http_codes = (
+		0,   200,
+		1,   500,
+		104, 404,
+	);
+	my $http_code = $atstccfg_to_http_codes{$code};
+	if (!defined($http_code)) {
+		$http_code = $generic_http_err;
+	}
+	return $http_code;
+}
+
 sub lwp_get {
 	my $uri           = shift;
 	my $retry_counter = $retries;
@@ -1437,8 +1481,8 @@ sub lwp_get {
 	my $response;
 	my $response_content;
 
-	while( $retry_counter > 0 ) {
-
+	# TODO add retry_counter arg to atstccfg
+	while(1) { # no retry counter, atstccfg handles retries
 		( $log_level >> $INFO ) && print "INFO Traffic Ops host: " . $traffic_ops_host . "\n";
 		( $log_level >> $DEBUG ) && print "DEBUG lwp_get called with $uri\n";
 		my $request = $traffic_ops_host . $uri;
@@ -1451,53 +1495,57 @@ sub lwp_get {
 			( $log_level >> $INFO ) && print "INFO Secure data request - bypassing reverse proxy and using $to_url.\n";
 		}
 
-		$response = $lwp_conn->get($request, %headers);
-		$response_content = $response->content;
+	my $atstccfg_log_path = "$TMP_BASE/atstccfg.log";
 
-		if ( &check_lwp_response_code($response, $ERROR) || &check_lwp_response_message_integrity($response, $ERROR) ) {
-			( $log_level >> $ERROR ) && print "ERROR result for $request is: ..." . $response->content . "...\n";
-			if ( $uri =~ m/configfiles\/ats/ && $response->code == 404) {
-					return $response->code;
-			}
-			if ($uri =~ m/update_status/ &&  $response->code == 404) {
-				return $response->code;
-			}
-			if ( $rev_proxy_in_use == 1 ) {
-				( $log_level >> $ERROR ) && print "ERROR There appears to be an issue with the Traffic Ops Reverse Proxy.  Reverting to primary Traffic Ops host.\n";
-				$traffic_ops_host = $to_url;
-				$rev_proxy_in_use = 0;
-			}
-			sleep 2**( $retries - $retry_counter );
-			$retry_counter--;
+	my ( $TO_USER, $TO_PASS ) = split( /:/, $TM_LOGIN );
+
+	$response_content = `$atstccfg_cmd --traffic-ops-user='$TO_USER' --traffic-ops-password='$TO_PASS' --traffic-ops-url='$request' --log-location-error=stderr --log-location-warning=stderr --log-location-info=null 2>$atstccfg_log_path`;
+
+	my $atstccfg_exit_code = $?;
+	$atstccfg_exit_code = atstccfg_code_to_http_code($atstccfg_exit_code);
+
+	if ($atstccfg_exit_code != 200) {
+		if ( $uri =~ m/configfiles\/ats/ && $atstccfg_exit_code == 404) {
+			return $atstccfg_exit_code;
 		}
-		# https://github.com/Comcast/traffic_control/issues/1168
-		elsif ( ( $uri =~ m/url\_sig\_(.*)\.config$/ || $uri =~ m/uri\_signing\_(.*)\.config$/ ) && $response->content =~ m/No RIAK servers are set to ONLINE/ ) {
-			( $log_level >> $FATAL ) && print "FATAL result for $uri is: ..." . $response->content . "...\n";
-			exit 1;
+		if ($uri =~ m/update_status/ && $atstccfg_exit_code == 404) {
+			return $$atstccfg_exit_code;
 		}
-		else {
-			( $log_level >> $DEBUG ) && print "DEBUG result for $uri is: ..." . $response->content . "...\n";
-			last;
+		if ( $atstccfg_exit_code != 200 && $rev_proxy_in_use == 1 ) {
+			( $log_level >> $ERROR ) && print "ERROR There appears to be an issue with the Traffic Ops Reverse Proxy.  Reverting to primary Traffic Ops host.\n";
+			$traffic_ops_host = $to_url;
+			$rev_proxy_in_use = 0;
+			next;
 		}
 
+		( $log_level >> $FATAL ) && print "FATAL atstccfg returned $atstccfg_exit_code - see $atstccfg_log_path\n";
+		exit 1;
+	}
+
+	# https://github.com/Comcast/traffic_control/issues/1168
+	if ( ( $uri =~ m/url\_sig\_(.*)\.config$/ || $uri =~ m/uri\_signing\_(.*)\.config$/ ) && $response_content =~ m/No RIAK servers are set to ONLINE/ ) {
+		( $log_level >> $FATAL ) && print "FATAL result for $uri is: ..." . $response_content . "...\n";
+		exit 1;
 	}
 
-	( &check_lwp_response_code($response, $FATAL) || &check_lwp_response_message_integrity($response, $FATAL) ) if ( $retry_counter == 0 );
+	( $log_level >> $DEBUG ) && print "DEBUG result for $uri is: ..." . $response_content . "...\n";
 
-	&eval_json($response) if ( $uri =~ m/\.json$/ );
+		&eval_json($request, $response_content) if ( $uri =~ m/\.json$/ );
+		last;
+	}
 
 	return $response_content;
-
 }
 
 sub eval_json {
-	my $lwp_response = shift;
+	my $uri = shift;
+	my $lwp_response_content = shift;
 	eval {
-		decode_json($lwp_response->content());
+		decode_json($lwp_response_content);
 		1;
 	} or do {
 		my $error = $@;
-		( $log_level >> $FATAL ) && print "FATAL " . $lwp_response->request->uri . " did not return valid JSON: " . $lwp_response->content() . " | Error: $error\n";
+		( $log_level >> $FATAL ) && print "FATAL " . $uri . " did not return valid JSON: " . $lwp_response_content . " | Error: $error\n";
 		exit 1;
 	}
 
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
index bbc302e..0a2bf96 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
@@ -21,23 +21,27 @@ package atsprofile
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const AstatsSeparator = "="
-const AstatsFileName = "astats.config"
-
 func GetAstats(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeAstats)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeAstats)
 }
 
 func makeAstats(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, AstatsFileName, AstatsSeparator)
-	if err == nil && txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.AstatsFileName)
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	return txt, err
+	return atscfg.MakeAStatsDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
index 45eb470..8c418ea 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
@@ -23,43 +23,26 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetATSDotRules(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeATSDotRules)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeATSDotRules)
 }
 
 func makeATSDotRules(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
 	// TODO add more efficient db func to only get drive params?
 	paramData, err := ats.GetProfileParamData(tx, profile.ID, "storage.config") // ats.rules is based on the storage.config params
 	if err != nil {
 		return "", errors.New("getting profile param data: " + err.Error())
 	}
-
-	drivePrefix := strings.TrimPrefix(paramData["Drive_Prefix"], `/dev/`)
-	drivePostfix := strings.Split(paramData["Drive_Letters"], ",")
-
-	text := ""
-	for _, l := range drivePostfix {
-		l = strings.TrimSpace(l)
-		if l == "" {
-			continue
-		}
-		text += `KERNEL=="` + drivePrefix + l + `", OWNER="ats"` + "\n"
-	}
-	if ramPrefix, ok := paramData["RAM_Drive_Prefix"]; ok {
-		ramPrefix = strings.TrimPrefix(ramPrefix, `/dev/`)
-		ramPostfix := strings.Split(paramData["RAM_Drive_Letters"], ",")
-		for _, l := range ramPostfix {
-			text += `KERNEL=="` + ramPrefix + l + `", OWNER="ats"` + "\n"
-		}
-	}
-	if text == "" {
-		text = "\n" // prevents it being flagged as "not found"
-	}
-	return text, nil
+	return atscfg.MakeATSDotRules(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
index affd050..23be2c9 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
@@ -23,67 +23,25 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetCache(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeCache)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeCache)
 }
 
 func makeCache(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	lines := map[string]struct{}{} // use a "set" for lines, to avoid duplicates, since we're looking up by profile
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
 	profileDSes, err := ats.GetProfileDS(tx, profile.ID)
 	if err != nil {
 		return "", errors.New("getting profile delivery services: " + err.Error())
 	}
-
-	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?
-			continue
-		}
-		originFQDN, originPort := getHostPortFromURI(*ds.OriginFQDN)
-		if originPort != "" {
-			l := "dest_domain=" + originFQDN + " port=" + originPort + " scheme=http action=never-cache\n"
-			lines[l] = struct{}{}
-		} else {
-			l := "dest_domain=" + originFQDN + " scheme=http action=never-cache\n"
-			lines[l] = struct{}{}
-		}
-	}
-
-	text := ""
-	for line, _ := range lines {
-		text += line
-	}
-	if text == "" {
-		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
-	}
-	return text, nil
-}
-
-func getHostPortFromURI(uriStr string) (string, string) {
-	originFQDN := uriStr
-	originFQDN = strings.TrimPrefix(originFQDN, "http://")
-	originFQDN = strings.TrimPrefix(originFQDN, "https://")
-
-	slashPos := strings.Index(originFQDN, "/")
-	if slashPos != -1 {
-		originFQDN = originFQDN[:slashPos]
-	}
-	portPos := strings.Index(originFQDN, ":")
-	portStr := ""
-	if portPos != -1 {
-		portStr = originFQDN[portPos+1:]
-		originFQDN = originFQDN[:portPos]
-	}
-	return originFQDN, portStr
+	return atscfg.MakeCacheDotConfig(profile.Name, profileDSes, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
index 4909766..4d4b331 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
@@ -24,25 +24,30 @@ import (
 	"errors"
 	"net/http"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetDropQString(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeDropQString)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeDropQString)
 }
 
 func makeDropQString(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	dropQStringVal, hasDropQStringParam, err := ats.GetProfileParamValue(tx, profile.ID, "drop_qstring.config", "content")
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", errors.New("getting profile param val: " + err.Error())
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
 
-	text := ""
+	dropQStringVal, hasDropQStringParam, err := ats.GetProfileParamValue(tx, profile.ID, atscfg.DropQStringDotConfigFileName, atscfg.DropQStringDotConfigParamName)
+	if err != nil {
+		return "", errors.New("getting profile param val: " + err.Error())
+	}
+	dropQStringValPtr := (*string)(nil)
 	if hasDropQStringParam {
-		text += dropQStringVal + "\n"
-	} else {
-		text += `/([^?]+) $s://$t/$1` + "\n"
+		dropQStringValPtr = &dropQStringVal
 	}
-	return text, nil
+
+	return atscfg.MakeDropQStringDotConfig(profile.Name, toolName, toURL, dropQStringValPtr), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
index 6b94a55..71b25fd 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
@@ -21,17 +21,24 @@ package atsprofile
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetFacts(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeFacts)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeFacts)
 }
 
 func makeFacts(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	text := "profile:" + profile.Name + "\n"
-	return text, nil
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
+
+	return atscfg.Make12MFacts(profile.Name, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
index 2fe0b38..14b1a87 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
@@ -23,114 +23,26 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strconv"
-	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const LoggingFileName = "logging.config"
-
 func GetLogging(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	WithProfileDataHdr(w, r, addHdr, tc.ContentTypeTextPlain, makeLogging) // TODO change to Content-Type text/x-lua? Perl uses text/plain.
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeLogging) // TODO change to Content-Type text/x-lua? Perl uses text/plain.
 }
 
 func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LoggingFileName)
-
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
-
-	hdrComment, err := ats.HeaderComment(tx, profile.Name)
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.LoggingFileName)
 	if err != nil {
-		return "", errors.New("getting header comment: " + err.Error())
-	}
-	// 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)
-	text := "-- " + hdrComment + " --\n"
-
-	for i := 0; i < MaxLogObjects; i++ {
-		logFormatField := "LogFormat"
-		if i > 0 {
-			logFormatField += strconv.Itoa(i)
-		}
-		if logFormatName := profileParamData[logFormatField+".Name"]; logFormatName != "" {
-			format := profileParamData[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", profile.Name, logFormatField)
-			}
-			format = strings.Replace(format, `"`, `\"`, -1)
-			text += logFormatName + " = format {\n"
-			text += "	Format = '" + format + " '\n"
-			text += "}\n"
-		}
-	}
-
-	for i := 0; i < MaxLogObjects; i++ {
-		logFilterField := "LogFilter"
-		if i > 0 {
-			logFilterField += strconv.Itoa(i)
-		}
-
-		if logFilterName := profileParamData[logFilterField+".Name"]; logFilterName != "" {
-			filter := profileParamData[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", profile.Name, logFilterField)
-			}
-
-			filter = strings.Replace(filter, `\`, `\\`, -1)
-			filter = strings.Replace(filter, `'`, `\'`, -1)
-
-			logFilterType := profileParamData[logFilterField+".Type"]
-			if logFilterType == "" {
-				logFilterType = "accept"
-			}
-			text += logFilterName + " = filter." + logFilterType + "('" + filter + "')\n"
-		}
-	}
-
-	for i := 0; i < MaxLogObjects; i++ {
-		logObjectField := "LogObject"
-		if i > 0 {
-			logObjectField += strconv.Itoa(i)
-		}
-
-		if logObjectFilename := profileParamData[logObjectField+".Filename"]; logObjectFilename != "" {
-			logObjectType := profileParamData[logObjectField+".Type"]
-			if logObjectType == "" {
-				logObjectType = "ascii"
-			}
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectFilters := profileParamData[logObjectField+".Filters"]
-
-			text += "\nlog." + logObjectType + " {\n"
-			text += "  Format = " + logObjectFormat + ",\n"
-			text += "  Filename = '" + logObjectFilename + "'"
-			if logObjectType != "pipe" {
-				text += ",\n"
-				text += "  RollingEnabled = " + logObjectRollingEnabled + ",\n"
-				text += "  RollingIntervalSec = " + logObjectRollingIntervalSec + ",\n"
-				text += "  RollingOffsetHr = " + logObjectRollingOffsetHr + ",\n"
-				text += "  RollingSizeMb = " + logObjectRollingSizeMb
-			}
-			if logObjectFilters != "" {
-				text += ",\n  Filters = { " + logObjectFilters + " }"
-			}
-			text += "\n}\n"
-		}
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
 
-	return text, nil
+	return atscfg.MakeLoggingDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
index fe302f0..538b0c3 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
@@ -23,111 +23,27 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strconv"
-	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const LoggingYAMLFileName = "logging.yaml"
-
 func GetLoggingYAML(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeLoggingYAML) // TODO change to Content-Type text/yaml? Perl uses text/plain.
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeLoggingYAML) // TODO change to Content-Type text/yaml? Perl uses text/plain.
 }
 
 func makeLoggingYAML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LoggingYAMLFileName)
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
-
-	// 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
-
-	text := "\nformats: \n"
-	for i := 0; i < maxLogObjects; i++ {
-		logFormatField := "LogFormat"
-		if i > 0 {
-			logFormatField += strconv.Itoa(i)
-		}
-		logFormatName := profileParamData[logFormatField+".Name"]
-		if logFormatName != "" {
-			format := profileParamData[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", profile.Name, logFormatField)
-			}
-			text += " - name: " + logFormatName + " \n"
-			text += "   format: '" + format + "'\n"
-		}
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
 
-	text += "filters:\n"
-	for i := 0; i < maxLogObjects; i++ {
-		logFilterField := "LogFilter"
-		if i > 0 {
-			logFilterField += strconv.Itoa(i)
-		}
-		if logFilterName := profileParamData[logFilterField+".Name"]; logFilterName != "" {
-			filter := profileParamData[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", profile.Name, logFilterField)
-			}
-			logFilterType := profileParamData[logFilterField+".Type"]
-			if logFilterType == "" {
-				logFilterType = "accept"
-			}
-			text += "- name: " + logFilterName + "\n"
-			text += "  action: " + logFilterType + "\n"
-			text += "  condition: " + filter + "\n"
-		}
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.LoggingYAMLFileName)
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
 
-	for i := 0; i < maxLogObjects; i++ {
-		logObjectField := "LogObject"
-		if i > 0 {
-			logObjectField += strconv.Itoa(i)
-		}
-
-		if logObjectFilename := profileParamData[logObjectField+".Filename"]; logObjectFilename != "" {
-			logObjectType := profileParamData[logObjectField+".Type"]
-			if logObjectType == "" {
-				logObjectType = "ascii"
-			}
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectFilters := profileParamData[logObjectField+".Filters"]
-
-			text += "\nlogs:\n"
-			text += "- mode: " + logObjectType + "\n"
-			text += "  filename: " + logObjectFilename + "\n"
-			text += "  format: " + logObjectFormat + "\n"
-
-			if logObjectType != "pipe" {
-				if logObjectRollingEnabled != "" {
-					text += "  rolling_enabled: " + logObjectRollingEnabled + "\n"
-				}
-				if logObjectRollingIntervalSec != "" {
-					text += "  rolling_interval_sec: " + logObjectRollingIntervalSec + "\n"
-				}
-				if logObjectRollingOffsetHr != "" {
-					text += "  rolling_offset_hr: " + logObjectRollingOffsetHr + "\n"
-				}
-				if logObjectRollingSizeMb != "" {
-					text += "  rolling_size_mb: " + logObjectRollingSizeMb + "\n"
-				}
-			}
-			if logObjectFilters != "" {
-				logObjectFilters = strings.Replace(logObjectFilters, "\v", "", -1)
-				text += "  filters: [" + logObjectFilters + "]"
-			}
-		}
-	}
-	return text, nil
+	return atscfg.MakeLoggingDotYAML(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
index 8d033ce..d2bfb04 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
@@ -23,86 +23,27 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strconv"
-	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const LogsXMLFileName = "logs_xml.config"
-
-const MaxLogObjects = 10
-
 func GetLogsXML(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	WithProfileDataHdr(w, r, addHdr, tc.ContentTypeTextPlain, makeLogsXML) // TODO change to Content-Type text/xml? Perl uses text/plain.
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeLogsXML) // TODO change to Content-Type text/xml? Perl uses text/plain.
 }
 
 func makeLogsXML(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, LogsXMLFileName)
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
 
-	hdrComment, err := ats.HeaderComment(tx, profile.Name)
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.LogsXMLFileName)
 	if err != nil {
-		return "", errors.New("getting header comment: " + err.Error())
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	hdrComment = strings.Replace(hdrComment, `# `, ``, -1)
-	hdrComment = strings.Replace(hdrComment, "\n", ``, -1)
-	text := "<!-- " + hdrComment + " -->\n"
-
-	for i := 0; i < MaxLogObjects; i++ {
-		logFormatField := "LogFormat"
-		logObjectField := "LogObject"
-		if i > 0 {
-			iStr := strconv.Itoa(i)
-			logFormatField += iStr
-			logObjectField += iStr
-		}
 
-		logFormatName := profileParamData[logFormatField+".Name"]
-		if logFormatName != "" {
-			format := profileParamData[logFormatField+".Format"]
-			format = strings.Replace(format, `"`, `\"`, -1)
-
-			text += `<LogFormat>
-  <Name = "` + logFormatName + `"/>
-  <Format = "` + format + `"/>
-</LogFormat>
-`
-		}
-
-		logObjectFileName := profileParamData[logObjectField+".Filename"]
-		if logObjectFileName != "" {
-			logObjectFormat := profileParamData[logObjectField+".Format"]
-			logObjectRollingEnabled := profileParamData[logObjectField+".RollingEnabled"]
-			logObjectRollingIntervalSec := profileParamData[logObjectField+".RollingIntervalSec"]
-			logObjectRollingOffsetHr := profileParamData[logObjectField+".RollingOffsetHr"]
-			logObjectRollingSizeMb := profileParamData[logObjectField+".RollingSizeMb"]
-			logObjectHeader := profileParamData[logObjectField+".Header"]
-
-			text += `<LogObject>
-  <Format = "` + logObjectFormat + `"/>
-  <Filename = "` + logObjectFileName + `"/>
-`
-			if logObjectRollingEnabled != "" {
-				text += `  <RollingEnabled = ` + logObjectRollingEnabled + `/>
-`
-			}
-			text += `  <RollingIntervalSec = ` + logObjectRollingIntervalSec + `/>
-  <RollingOffsetHr = ` + logObjectRollingOffsetHr + `/>
-  <RollingSizeMb = ` + logObjectRollingSizeMb + `/>
-`
-			if logObjectHeader != "" {
-				text += `  <Header = "` + logObjectHeader + `"/>
-`
-			}
-			text += `</LogObject>
-`
-		}
-	}
-	return text, nil
+	return atscfg.MakeLogsXMLDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
index 8731508..137b623 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
@@ -21,23 +21,27 @@ package atsprofile
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const PluginSeparator = " "
-const PluginFileName = "plugin.config"
-
 func GetPlugin(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makePlugin)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makePlugin)
 }
 
 func makePlugin(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, PluginFileName, PluginSeparator)
-	if err == nil && txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.PluginFileName)
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	return txt, err
+	return atscfg.MakePluginDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go
index d3d4ed7..47314f8 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go
@@ -32,56 +32,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-// 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(tx *sql.Tx, profile ats.ProfileData, fileName string, separator string) (string, error) {
-	profileParamData, err := ats.GetProfileParamData(tx, profile.ID, fileName)
-	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
-	}
-	text := ""
-	for name, val := range profileParamData {
-		name = trimParamUnderscoreNumSuffix(name)
-		text += name + separator + val + "\n"
-	}
-	return text, nil
-}
-
-// trimParamUnderscoreNumSuffix removes any trailing "__[0-9]+" and returns the trimmed string.
-func trimParamUnderscoreNumSuffix(paramName string) string {
-	underscorePos := strings.LastIndex(paramName, `__`)
-	if underscorePos == -1 {
-		return paramName
-	}
-	if _, err := strconv.ParseFloat(paramName[underscorePos+2:], 64); err != nil {
-		return paramName
-	}
-	return paramName[:underscorePos]
-}
-
-type MakeCfgFunc func(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, fileName string)
-
-// WithProfileData takes a makeCfg function which takes the ProfileData and returns the config text or any error.
-//
-// Most profile config files need the same data and write the same text file, so this can be used to reduce duplicate boilerplate code.
-//
-// This also adds HeaderComment with the profile name to the top of the config text.
-//
-// The route must include an "id" parameter.
-//
-// The route may include a "file" parameter, and if so, it will be passed to makeCfg as fileName. If not, fileName will be the empty string.
-//
-// If makeCfg returns a nil error and the empty string, a 404 Not Found will be returned to the client.
-//
-// If you need to avoid adding the standard header comment, or use a Content-Type other than text/plain, use WithProfileDataHdr.
-//
-func WithProfileData(w http.ResponseWriter, r *http.Request, makeCfg func(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, fileName string) (string, error)) {
-	addHdr := true
-	WithProfileDataHdr(w, r, addHdr, tc.ContentTypeTextPlain, makeCfg)
-}
-
-func WithProfileDataHdr(w http.ResponseWriter, r *http.Request, addHdr bool, contentType string, makeCfg func(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, fileName string) (string, error)) {
+func WithProfileData(w http.ResponseWriter, r *http.Request, contentType string, makeCfg func(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, fileName string) (string, error)) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"profile-name-or-id"}, nil)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
@@ -115,14 +66,6 @@ func WithProfileDataHdr(w http.ResponseWriter, r *http.Request, addHdr bool, con
 		return
 	}
 
-	hdr := ""
-	if addHdr {
-		if hdr, err = ats.HeaderComment(inf.Tx.Tx, profileData.Name); err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting file contents: "+err.Error()))
-			return
-		}
-	}
-
 	text, err := makeCfg(inf.Tx.Tx, inf.Config, profileData, fileName)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("making config: "+err.Error()))
@@ -138,5 +81,5 @@ func WithProfileDataHdr(w http.ResponseWriter, r *http.Request, addHdr bool, con
 	if contentType != "" {
 		w.Header().Set(tc.ContentType, contentType)
 	}
-	w.Write([]byte(hdr + text))
+	w.Write([]byte(text))
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
index 8e43600..dd1a0f0 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
@@ -21,41 +21,27 @@ package atsprofile
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
-	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
-const RecordsSeparator = " "
-const RecordsFileName = "records.config"
-
 func GetRecords(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeRecords)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeRecords)
 }
 
 func makeRecords(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, RecordsFileName, RecordsSeparator)
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", nil
-	}
-	if txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
-	txt = ReplaceLineSuffixes(txt, "STRING __HOSTNAME__", "STRING __FULL_HOSTNAME__")
-	return txt, nil
-}
-
-func ReplaceLineSuffixes(txt string, suffix string, newSuffix string) string {
-	lines := strings.Split(txt, "\n")
-	newLines := make([]string, 0, len(lines))
-	for _, line := range lines {
-		if strings.HasSuffix(line, suffix) {
-			line = line[:len(line)-len(suffix)]
-			line += newSuffix
-		}
-		newLines = append(newLines, line)
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.RecordsFileName)
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	return strings.Join(newLines, "\n")
+	return atscfg.MakeRecordsDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
index 1a3d687..39d6b68 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
@@ -23,10 +23,9 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strconv"
-	"strings"
 
-	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
@@ -34,55 +33,17 @@ import (
 const StorageFileName = "storage.config"
 
 func GetStorage(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeStorage)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeStorage)
 }
 
 func makeStorage(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	text := ""
-
-	paramData, err := ats.GetProfileParamData(tx, profile.ID, StorageFileName) // ats.rules is based on the storage.config params
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", errors.New("profile param data: " + err.Error())
-	}
-
-	nextVolume := 1
-	if drivePrefix := paramData["Drive_Prefix"]; drivePrefix != "" {
-		driveLetters := strings.TrimSpace(paramData["Drive_Letters"])
-		if driveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has Drive_Prefix parameter, but no Drive_Letters; creating anyway", profile.Name)
-		}
-		text += makeStorageVolumeText(drivePrefix, driveLetters, nextVolume)
-		nextVolume++
-	}
-
-	if ramDrivePrefix := paramData["RAM_Drive_Prefix"]; ramDrivePrefix != "" {
-		ramDriveLetters := strings.TrimSpace(paramData["RAM_Drive_Letters"])
-		if ramDriveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has RAM_Drive_Prefix parameter, but no RAM_Drive_Letters; creating anyway", profile.Name)
-		}
-		text += makeStorageVolumeText(ramDrivePrefix, ramDriveLetters, nextVolume)
-		nextVolume++
-	}
-
-	if ssdDrivePrefix := paramData["SSD_Drive_Prefix"]; ssdDrivePrefix != "" {
-		ssdDriveLetters := strings.TrimSpace(paramData["SSD_Drive_Letters"])
-		if ssdDriveLetters == "" {
-			log.Warnf("Creating storage.config: profile %+v has SSD_Drive_Prefix parameter, but no SSD_Drive_Letters; creating anyway", profile.Name)
-		}
-		text += makeStorageVolumeText(ssdDrivePrefix, ssdDriveLetters, nextVolume)
-		nextVolume++
-	}
-
-	if text == "" {
-		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
-	return text, nil
-}
-
-func makeStorageVolumeText(prefix string, letters string, volume int) string {
-	text := ""
-	for _, letter := range strings.Split(letters, ",") {
-		text += prefix + letter + " volume=" + strconv.Itoa(volume) + "\n"
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, StorageFileName) // ats.rules is based on the storage.config params
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	return text
+	return atscfg.MakeStorageDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
index 16e5267..1002f29 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
@@ -21,8 +21,11 @@ package atsprofile
 
 import (
 	"database/sql"
+	"errors"
 	"net/http"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
@@ -31,16 +34,17 @@ const SysctlSeparator = " = "
 const SysctlFileName = "sysctl.conf"
 
 func GetSysctl(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeSysctl)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeSysctl)
 }
 
 func makeSysctl(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
-	txt, err := GenericProfileConfig(tx, profile, SysctlFileName, SysctlSeparator)
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
 	if err != nil {
-		return "", err
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
-	if txt == "" {
-		txt = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, atscfg.SysctlFileName)
+	if err != nil {
+		return "", errors.New("getting profile param data: " + err.Error())
 	}
-	return txt, nil
+	return atscfg.MakeSysCtlDotConf(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go
index fc7d12d..67428c3 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go
@@ -23,46 +23,90 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
+	"strconv"
 	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetUnknown(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	WithProfileDataHdr(w, r, addHdr, "text/plain", makeUnknown)
-}
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"profile-name-or-id"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
 
-func makeUnknown(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, fileName string) (string, error) {
-	params, err := ats.GetProfileParamData(tx, profile.ID, fileName)
+	profileNameOrID := inf.Params["profile-name-or-id"]
+	profileID, err := strconv.Atoi(profileNameOrID)
 	if err != nil {
-		return "", errors.New("getting profile param data: " + err.Error())
+		profileName := profileNameOrID
+		ok := false
+		if profileID, ok, err = ats.GetProfileIDFromName(inf.Tx.Tx, profileName); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting profile id from name: "+err.Error()))
+			return
+		} else if !ok {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("Resource not found."), nil)
+			return
+		}
 	}
-	fileContents, err := takeAndBakeProfile(tx, profile.Name, params)
+
+	fileName := strings.TrimSuffix(inf.Params["file"], ".json")
+
+	profileData, ok, err := ats.GetProfileData(inf.Tx.Tx, profileID)
 	if err != nil {
-		return "", errors.New("GetProfileConfig: takeAndBakeProfile '" + fileName + "': " + err.Error())
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting profile data: "+err.Error()))
+		return
+	} else if !ok {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("not found"), nil)
+		return
 	}
-	return fileContents, nil
+
+	txt, userErr, sysErr, errCode := makeUnknown(inf.Tx.Tx, profileData, fileName)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	w.Header().Set(tc.ContentType, tc.ContentTypeTextPlain)
+	w.Write([]byte(txt))
 }
 
-func takeAndBakeProfile(tx *sql.Tx, profileName string, params map[string]string) (string, error) {
-	hdr, err := ats.HeaderComment(tx, profileName)
+// makeUnknown returns the text of the unknown config, any user error, any system error, and the HTTP code to return if there was an error.
+func makeUnknown(tx *sql.Tx, profile ats.ProfileData, fileName string) (string, error, error, int) {
+	scopeParams, err := ats.GetParamsByName(tx, "scope")
 	if err != nil {
-		return "", errors.New("getting header comment: " + err.Error())
+		return "", nil, errors.New("getting scope parameters: " + err.Error()), http.StatusInternalServerError
 	}
-	text := ""
-	for paramName, paramVal := range params {
-		if paramName == "header" {
-			if paramVal == "none" {
-				hdr = ""
-			} else {
-				hdr = paramVal + "\n"
-			}
-		} else {
-			text += paramVal + "\n"
+
+	inScope := false
+	for _, scopeParam := range scopeParams {
+		if scopeParam.ConfigFile != fileName {
+			continue
+		}
+		if scopeParam.Value != "profiles" {
+			continue
 		}
+		inScope = true
+		break
 	}
-	text = strings.Replace(text, "__RETURN__", "\n", -1)
-	return hdr + text, nil
+
+	if !inScope {
+		return "", errors.New("Error - incorrect file scope for route used.  Please use the servers route."), nil, http.StatusBadRequest
+	}
+
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", nil, errors.New("getting tool name and URL: " + err.Error()), http.StatusInternalServerError
+	}
+
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, fileName)
+	if err != nil {
+		return "", nil, errors.New("getting profile param data: " + err.Error()), http.StatusInternalServerError
+	}
+
+	return atscfg.MakeUnknownConfig(profile.Name, paramData, toolName, toURL), nil, nil, http.StatusOK
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go
index 18a628d..b6efc82 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go
@@ -25,6 +25,7 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
@@ -32,9 +33,7 @@ import (
 )
 
 func GetURISigning(w http.ResponseWriter, r *http.Request) {
-	addHdr := false
-	contentType := tc.ApplicationJson
-	WithProfileDataHdr(w, r, addHdr, contentType, uriSigningDotConfig)
+	WithProfileData(w, r, tc.ApplicationJson, uriSigningDotConfig)
 }
 
 func uriSigningDotConfig(tx *sql.Tx, cfg *config.Config, _ ats.ProfileData, fileName string) (string, error) {
@@ -44,7 +43,7 @@ func uriSigningDotConfig(tx *sql.Tx, cfg *config.Config, _ ats.ProfileData, file
 		return "", errors.New("getting uri signing keys from Riak: " + err.Error())
 	}
 	if !hasKeys {
-		return "", nil // TODO verify? Perl seems to return without returning its $text
+		keys = []byte{} // TODO verify? Perl seems to return without returning its $text
 	}
-	return string(keys), nil
+	return atscfg.MakeURISigningConfig(keys), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go
index 09d0278..cd26eaf 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go
@@ -23,41 +23,35 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
 )
 
 func GetURLSig(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, urlSigDotConfig)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, urlSigDotConfig)
 }
 
 func urlSigDotConfig(tx *sql.Tx, cfg *config.Config, profile ats.ProfileData, fileName string) (string, error) {
 	fileName = "url_sig_" + fileName + ".config" // the fileName from the http router is just the DS, missing "url_sig_" and ".config" - add them back now
 
-	sep := " = "
-
 	urlSigKeys, _, err := riaksvc.GetURLSigKeysFromConfigFileKey(tx, cfg.RiakAuthOptions, cfg.RiakPort, fileName)
 	if err != nil {
 		return "", errors.New("getting url sig keys from Riak: " + err.Error())
 	}
 
-	params, err := ats.GetProfileParamData(tx, profile.ID, fileName)
+	paramData, err := ats.GetProfileParamData(tx, profile.ID, fileName)
 	if err != nil {
 		return "", errors.New("getting profile param data: " + err.Error())
 	}
 
-	text := ""
-	for paramName, paramVal := range params {
-		if len(urlSigKeys) == 0 || !strings.HasPrefix(paramName, "key") {
-			text += paramName + sep + paramVal + "\n"
-		}
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
 	}
 
-	for key, val := range urlSigKeys {
-		text += key + sep + val + "\n"
-	}
-	return text, nil
+	return atscfg.MakeURLSigConfig(profile.Name, urlSigKeys, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go b/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
index bb177d5..9d686c4 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
@@ -23,56 +23,25 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"strconv"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 )
 
 func GetVolume(w http.ResponseWriter, r *http.Request) {
-	WithProfileData(w, r, makeVolume)
+	WithProfileData(w, r, tc.ContentTypeTextPlain, makeVolume)
 }
 
 func makeVolume(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ string) (string, error) {
+	toolName, toURL, err := ats.GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting tool name and URL: " + err.Error())
+	}
 	paramData, err := ats.GetProfileParamData(tx, profile.ID, StorageFileName) // volume.config is based on the storage.config params
 	if err != nil {
 		return "", errors.New("getting profile param data: " + err.Error())
 	}
-
-	numVolumes := getNumVolumes(paramData)
-
-	text := "# TRAFFIC OPS NOTE: This is running with forced volumes - the size is irrelevant\n"
-	nextVolume := 1
-	if drivePrefix := paramData["Drive_Prefix"]; drivePrefix != "" {
-		text += volumeText(strconv.Itoa(nextVolume), numVolumes)
-		nextVolume++
-	}
-	if ramDrivePrefix := paramData["RAM_Drive_Prefix"]; ramDrivePrefix != "" {
-		text += volumeText(strconv.Itoa(nextVolume), numVolumes)
-		nextVolume++
-	}
-	if ssdDrivePrefix := paramData["SSD_Drive_Prefix"]; ssdDrivePrefix != "" {
-		text += volumeText(strconv.Itoa(nextVolume), numVolumes)
-		nextVolume++
-	}
-
-	if text == "" {
-		text = "\n" // If no params exist, don't send "not found," but an empty file. We know the profile exists.
-	}
-	return text, nil
-}
-
-func volumeText(volume string, numVolumes int) string {
-	return "volume=" + volume + " scheme=http size=" + strconv.Itoa(100/numVolumes) + "%\n"
-}
-
-func getNumVolumes(paramData map[string]string) int {
-	num := 0
-	drivePrefixes := []string{"Drive_Prefix", "SSD_Drive_Prefix", "RAM_Drive_Prefix"}
-	for _, pre := range drivePrefixes {
-		if _, ok := paramData[pre]; ok {
-			num++
-		}
-	}
-	return num
+	return atscfg.MakeVolumeDotConfig(profile.Name, paramData, toolName, toURL), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/config.go b/traffic_ops/traffic_ops_golang/ats/config.go
index d722fe3..c0b999d 100644
--- a/traffic_ops/traffic_ops_golang/ats/config.go
+++ b/traffic_ops/traffic_ops_golang/ats/config.go
@@ -6,8 +6,8 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
-	"time"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 )
 
@@ -38,13 +38,19 @@ const CacheUrlPrefix = "cacheurl_"
 
 const RemapFile = "remap.config"
 
-const HeaderCommentDateFormat = "Mon Jan 2 15:04:05 MST 2006"
-
 func GetConfigFile(prefix string, xmlId string) string {
 	return prefix + xmlId + configSuffix
 }
 
 func GetNameVersionString(tx *sql.Tx) (string, error) {
+	toolName, url, err := GetToolNameAndURL(tx)
+	if err != nil {
+		return "", errors.New("getting toolname and url parameters: " + err.Error())
+	}
+	return atscfg.GetNameVersionStringFromToolNameAndURL(toolName, url), nil
+}
+
+func GetToolNameAndURL(tx *sql.Tx) (string, string, error) {
 	qry := `
 SELECT
   p.name,
@@ -56,16 +62,17 @@ WHERE
 `
 	rows, err := tx.Query(qry)
 	if err != nil {
-		return "", errors.New("querying: " + err.Error())
+		return "", "", errors.New("querying: " + err.Error())
 	}
 	defer rows.Close()
+
 	toolName := ""
 	url := ""
 	for rows.Next() {
 		name := ""
 		val := ""
 		if err := rows.Scan(&name, &val); err != nil {
-			return "", errors.New("scanning: " + err.Error())
+			return "", "", errors.New("scanning: " + err.Error())
 		}
 		if name == "tm.toolname" {
 			toolName = val
@@ -73,7 +80,7 @@ WHERE
 			url = val
 		}
 	}
-	return toolName + " (" + url + ")", nil
+	return toolName, url, nil
 }
 
 // getCDNNameFromNameOrID returns the CDN name from a parameter which may be the name or ID.
@@ -127,5 +134,5 @@ func HeaderComment(tx *sql.Tx, name string) (string, error) {
 	if err != nil {
 		return "", errors.New("getting name version string: " + err.Error())
 	}
-	return "# DO NOT EDIT - Generated for " + name + " by " + nameVersionStr + " on " + time.Now().Format(HeaderCommentDateFormat) + "\n", nil
+	return atscfg.HeaderCommentWithTOVersionStr(name, nameVersionStr), nil
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/db.go b/traffic_ops/traffic_ops_golang/ats/db.go
index 1dbbcf4..28be236 100644
--- a/traffic_ops/traffic_ops_golang/ats/db.go
+++ b/traffic_ops/traffic_ops_golang/ats/db.go
@@ -23,12 +23,12 @@ import (
 	"database/sql"
 	"errors"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func GetProfileParamData(tx *sql.Tx, profileID int, configFile string) (map[string]string, error) {
-	// TODO add another func to return a slice, for things that don't need a map, for performance? Does it make a difference?
 	qry := `
 SELECT
   p.name,
@@ -74,7 +74,6 @@ type ProfileData struct {
 
 // GetProfileData returns the necessary info about the profile, whether it exists, and any error.
 func GetProfileData(tx *sql.Tx, id int) (ProfileData, bool, error) {
-	// TODO implement, determine what fields are necessary
 	qry := `
 SELECT
   p.name
@@ -93,12 +92,7 @@ WHERE
 	return v, true, nil
 }
 
-type ProfileDS struct {
-	Type       tc.DSType
-	OriginFQDN *string
-}
-
-func GetProfileDS(tx *sql.Tx, profileID int) ([]ProfileDS, error) {
+func GetProfileDS(tx *sql.Tx, profileID int) ([]atscfg.ProfileDS, error) {
 	qry := `
 SELECT
   dstype.name AS ds_type,
@@ -114,7 +108,7 @@ WHERE
     SELECT DISTINCT deliveryservice
     FROM deliveryservice_server
     WHERE server IN (SELECT id FROM server WHERE profile = $1)
-  )
+  )p
 `
 	rows, err := tx.Query(qry, profileID)
 	if err != nil {
@@ -122,9 +116,9 @@ WHERE
 	}
 	defer rows.Close()
 
-	dses := []ProfileDS{}
+	dses := []atscfg.ProfileDS{}
 	for rows.Next() {
-		d := ProfileDS{}
+		d := atscfg.ProfileDS{}
 		if err := rows.Scan(&d.Type, &d.OriginFQDN); err != nil {
 			return nil, errors.New("scanning: " + err.Error())
 		}
@@ -170,3 +164,36 @@ func GetProfileIDFromName(tx *sql.Tx, profileName string) (int, bool, error) {
 	}
 	return id, true, nil
 }
+
+type Parameter struct {
+	Name       string
+	ConfigFile string
+	Value      string
+}
+
+func GetParamsByName(tx *sql.Tx, paramName string) ([]Parameter, error) {
+	qry := `
+SELECT
+  p.value,
+  p.config_file
+FROM
+  parameter p
+WHERE
+  p.name = $1
+`
+	rows, err := tx.Query(qry, paramName)
+	if err != nil {
+		return nil, errors.New("querying: " + err.Error())
+	}
+	defer rows.Close()
+
+	params := []Parameter{}
+	for rows.Next() {
+		pa := Parameter{Name: paramName}
+		if err := rows.Scan(&pa.Value, &pa.ConfigFile); err != nil {
+			return nil, errors.New("scanning: " + err.Error())
+		}
+		params = append(params, pa)
+	}
+	return params, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
index 0ab740b..5396e62 100644
--- a/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
+++ b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
@@ -23,24 +23,17 @@ import (
 	"database/sql"
 	"errors"
 	"net/http"
-	"net/url"
-	"regexp"
-	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 
 	"github.com/lib/pq"
 )
 
-const DefaultATSVersion = "5" // TODO Emulates Perl; change to 6? ATC no longer officially supports ATS 5.
-
-const InvalidID = -1
-
 func GetParentDotConfig(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id-or-host"}, nil)
 	if userErr != nil || sysErr != nil {
@@ -58,14 +51,14 @@ func GetParentDotConfig(w http.ResponseWriter, r *http.Request) {
 		hostName = idOrHost
 	}
 
-	serverInfo, ok, err := &ServerInfo{}, false, error(nil)
+	serverInfo, ok, err := &atscfg.ServerInfo{}, false, error(nil)
 	if isHost {
 		serverInfo, ok, err = getServerInfoByHost(inf.Tx.Tx, hostName)
 	} else {
 		serverInfo, ok, err = getServerInfoByID(inf.Tx.Tx, id)
 	}
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting server info: "+err.Error()))
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server info: "+err.Error()))
 		return
 	}
 	if !ok {
@@ -73,372 +66,65 @@ func GetParentDotConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	atsMajorVer, err := GetATSMajorVersion(inf.Tx.Tx, serverInfo.ProfileID)
+	atsMajorVer, err := GetATSMajorVersion(inf.Tx.Tx, int(serverInfo.ProfileID))
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting ATS major version: "+err.Error()))
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting ATS major version: "+err.Error()))
 		return
 	}
 
-	hdr, err := HeaderComment(inf.Tx.Tx, serverInfo.HostName)
+	toolName, toURL, err := GetToolNameAndURL(inf.Tx.Tx)
 	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting header comment: "+err.Error()))
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting toolname and TO url parameters: "+err.Error()))
 		return
 	}
 
-	textArr := []string{}
-	text := ""
-	// TODO put these in separate functions. No if-statement should be this long.
+	parentConfigDSes := []atscfg.ParentConfigDSTopLevel{}
 	if serverInfo.IsTopLevelCache() {
-		uniqueOrigins := map[string]struct{}{}
-
-		data, err := getParentConfigDSTopLevel(inf.Tx.Tx, serverInfo.CDN)
-		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting parent config DS data: "+err.Error()))
-			return
-		}
-
-		parentInfos := map[string][]ParentInfo{} // TODO better names (this was transliterated from Perl)
-
-		for _, ds := range data {
-			parentQStr := "ignore"
-			if ds.QStringHandling == "" && ds.MSOAlgorithm == AlgorithmConsistentHash && ds.QStringIgnore == tc.QStringIgnoreUseInCacheKeyAndPassUp {
-				parentQStr = "consider"
-			}
-
-			orgURIStr := ds.OriginFQDN
-			orgURI, err := url.Parse(orgURIStr) // TODO verify origin is always a host:port
-			if err != nil {
-				log.Errorln("Malformed ds '" + string(ds.Name) + "' origin  URI: '" + orgURIStr + "', skipping! : " + err.Error())
-				continue
-			}
-			// TODO put in function, to remove duplication
-			if orgURI.Port() == "" {
-				if orgURI.Scheme == "http" {
-					orgURI.Host += ":80"
-				} else if orgURI.Scheme == "https" {
-					orgURI.Host += ":443"
-				} else {
-					log.Errorln("parent.config generation: delivery service '" + string(ds.Name) + "' origin  URI: '" + orgURIStr + "' is unknown scheme '" + orgURI.Scheme + "', but has no port! Using as-is! ")
-				}
-			}
-
-			if _, ok := uniqueOrigins[ds.OriginFQDN]; ok {
-				continue // TODO warn?
-			}
-			uniqueOrigins[ds.OriginFQDN] = struct{}{}
-
-			textLine := ""
-
-			if ds.OriginShield != "" {
-				// TODO fix to only call once
-				serverParams, err := getParentConfigServerProfileParams(inf.Tx.Tx, serverInfo.ID)
-				if err != nil {
-					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting server params: "+err.Error()))
-					return
-				}
-
-				algorithm := ""
-				if parentSelectAlg := serverParams[ParentConfigParamAlgorithm]; strings.TrimSpace(parentSelectAlg) != "" {
-					algorithm = "round_robin=" + parentSelectAlg
-				}
-				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " parent=" + ds.OriginShield + " " + algorithm + " go_direct=true\n"
-			} else if ds.MultiSiteOrigin {
-				textLine += "dest_domain=" + orgURI.Hostname() + " port=" + orgURI.Port() + " "
-				if len(parentInfos) == 0 {
-					// If we have multi-site origin, get parent_data once
-					parentInfos, err = getParentInfo(inf.Tx.Tx, serverInfo)
-					if err != nil {
-						api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting server parent info: "+err.Error()))
-						return
-					}
-				}
-
-				if len(parentInfos[orgURI.Hostname()]) == 0 {
-					// TODO error? emulates Perl
-					log.Warnln("ParentInfo: delivery service " + ds.Name + " has no parent servers")
-				}
-
-				rankedParents := ParentInfoSortByRank(parentInfos[orgURI.Hostname()])
-				sort.Sort(rankedParents)
-
-				parentInfo := []string{}
-				secondaryParentInfo := []string{}
-				nullParentInfo := []string{}
-				for _, parent := range ([]ParentInfo)(rankedParents) {
-					if parent.PrimaryParent {
-						parentInfo = append(parentInfo, parent.Format())
-					} else if parent.SecondaryParent {
-						secondaryParentInfo = append(secondaryParentInfo, parent.Format())
-					} else {
-						nullParentInfo = append(nullParentInfo, parent.Format())
-					}
-				}
-
-				if len(parentInfo) == 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
-					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)
-				secondaryParentInfo, seen = util.RemoveStrDuplicates(secondaryParentInfo, seen)
-				nullParentInfo, seen = util.RemoveStrDuplicates(nullParentInfo, seen)
-
-				// If the ats version supports it and the algorithm is consistent hash, put secondary and non-primary parents into secondary parent group.
-				// This will ensure that secondary and tertiary parents will be unused unless all hosts in the primary group are unavailable.
-
-				parents := ""
-
-				if atsMajorVer >= 6 && ds.MSOAlgorithm == "consistent_hash" && (len(secondaryParentInfo) > 0 || len(nullParentInfo) > 0) {
-					parents = `parent="` + strings.Join(parentInfo, "") + `" secondary_parent="` + strings.Join(secondaryParentInfo, "") + strings.Join(nullParentInfo, "") + `"`
-				} else {
-					parents = `parent="` + strings.Join(parentInfo, "") + strings.Join(secondaryParentInfo, "") + strings.Join(nullParentInfo, "") + `"`
-				}
-				textLine += parents + ` round_robin=` + ds.MSOAlgorithm + ` qstring=` + parentQStr + ` go_direct=false parent_is_proxy=false`
-
-				parentRetry := ds.MSOParentRetry
-				if atsMajorVer >= 6 && parentRetry != "" {
-					if unavailableServerRetryResponsesValid(ds.MSOUnavailableServerRetryResponses) {
-						textLine += ` parent_retry=` + parentRetry + ` unavailable_server_retry_responses=` + ds.MSOUnavailableServerRetryResponses
-					} else {
-						if ds.MSOUnavailableServerRetryResponses != "" {
-							log.Errorln("Malformed unavailable_server_retry_responses parameter '" + ds.MSOUnavailableServerRetryResponses + "', not using!")
-						}
-						textLine += ` parent_retry=` + parentRetry
-					}
-					textLine += ` max_simple_retries=` + ds.MSOMaxSimpleRetries + ` max_unavailable_server_retries=` + ds.MSOMaxUnavailableServerRetries
-				}
-				textLine += "\n" // TODO remove, and join later on "\n" instead of ""?
-				textArr = append(textArr, textLine)
-			}
-		}
-		sort.Sort(sort.StringSlice(textArr))
-		text = hdr + strings.Join(textArr, "")
+		parentConfigDSes, err = getParentConfigDSTopLevel(inf.Tx.Tx, serverInfo.CDN)
 	} else {
-		// not a top level cache
-		parentConfigDSes, err := getParentConfigDS(inf.Tx.Tx, serverInfo.ID) // "data" in Perl
-		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting parent config DS data (non-top-level): "+err.Error()))
-			return
-		}
-
-		parentInfos, err := getParentInfo(inf.Tx.Tx, serverInfo)
-		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting server parent info (non-top-level: "+err.Error()))
-			return
-		}
-
-		processedOriginsToDSNames := map[string]tc.DeliveryServiceName{}
-
-		serverParams, err := getParentConfigServerProfileParams(inf.Tx.Tx, serverInfo.ID)
-		if err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting parent config server profile params: "+err.Error()))
-			return
-		}
-
-		queryStringHandling := serverParams[ParentConfigParamQStringHandling] // "qsh" in Perl
-		parentInfo := []string{}
-		secondaryParentInfo := []string{}
-
-		parentInfosAllParents := parentInfos[DeliveryServicesAllParentsKey]
-		sort.Sort(ParentInfoSortByRank(parentInfosAllParents))
-
-		for _, parent := range parentInfosAllParents { // TODO fix magic key
-			pTxt := parent.Format()
-			if parent.PrimaryParent {
-				parentInfo = append(parentInfo, pTxt)
-			} else if parent.SecondaryParent {
-				secondaryParentInfo = append(secondaryParentInfo, pTxt)
-			}
-		}
-
-		if len(parentInfo) == 0 {
-			parentInfo = secondaryParentInfo
-			secondaryParentInfo = []string{}
-		}
-
-		// TODO remove duplicate code with top level if block
-		seen := map[string]struct{}{} // TODO change to host+port? host isn't unique
-		parentInfo, seen = util.RemoveStrDuplicates(parentInfo, seen)
-		secondaryParentInfo, seen = util.RemoveStrDuplicates(secondaryParentInfo, seen)
-
-		parents := ""
-		secondaryParents := "" // "secparents" in Perl
-		sort.Sort(sort.StringSlice(parentInfo))
-		sort.Sort(sort.StringSlice(secondaryParentInfo))
-		if atsMajorVer >= 6 && len(secondaryParentInfo) > 0 {
-			parents = `parent="` + strings.Join(parentInfo, "") + `"`
-			secondaryParents = ` secondary_parent="` + strings.Join(secondaryParentInfo, "") + `"`
-		} else {
-			parents = `parent="` + strings.Join(parentInfo, "") + strings.Join(secondaryParentInfo, "") + `"`
-		}
-
-		roundRobin := `round_robin=consistent_hash`
-		goDirect := `go_direct=false`
-
-		sort.Sort(ParentConfigDSSortByName(parentConfigDSes))
-		for _, ds := range parentConfigDSes {
-			text := ""
-			originFQDN := ds.OriginFQDN
-			if originFQDN == "" {
-				continue // TODO warn? (Perl doesn't)
-			}
-
-			orgURI, err := url.Parse(originFQDN) // TODO verify
-			if err != nil {
-				log.Errorln("Malformed ds '" + string(ds.Name) + "' origin  URI: '" + originFQDN + "': skipping!" + err.Error())
-				continue
-			}
-
-			if existingDS, ok := processedOriginsToDSNames[originFQDN]; ok {
-				log.Errorln("parent.config generation: duplicate origin! services '" + string(ds.Name) + "' and '" + string(existingDS) + "' share origin '" + orgURI.Host + "': skipping '" + string(ds.Name) + "'!")
-				continue
-			}
-
-			// TODO put in function, to remove duplication
-			if orgURI.Port() == "" {
-				if orgURI.Scheme == "http" {
-					orgURI.Host += ":80"
-				} else if orgURI.Scheme == "https" {
-					orgURI.Host += ":443"
-				} else {
-					log.Errorln("parent.config generation non-top-level: ds '" + string(ds.Name) + "' origin  URI: '" + originFQDN + "' is unknown scheme '" + orgURI.Scheme + "', but has no port! Using as-is! ")
-				}
-			}
-
-			// TODO encode this in a DSType func, IsGoDirect() ?
-			if dsType := tc.DSType(ds.Type); dsType == tc.DSTypeHTTPNoCache || dsType == tc.DSTypeHTTPLive || dsType == tc.DSTypeDNSLive {
-				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` go_direct=true` + "\n"
-			} else {
-
-				// check for profile psel.qstring_handling.  If this parameter is assigned to the server profile,
-				// then edges will use the qstring handling value specified in the parameter for all profiles.
-
-				// If there is no defined parameter in the profile, then check the delivery service profile.
-				// If psel.qstring_handling exists in the DS profile, then we use that value for the specified DS only.
-				// This is used only if not overridden by a server profile qstring handling parameter.
-
-				// TODO refactor this logic, hard to understand (transliterated from Perl)
-				dsQSH := queryStringHandling
-				if dsQSH == "" {
-					dsQSH = ds.QStringHandling
-				}
-				parentQStr := dsQSH
-				if parentQStr == "" {
-					parentQStr = "ignore"
-				}
-				if ds.QStringIgnore == tc.QStringIgnoreUseInCacheKeyAndPassUp && dsQSH == "" {
-					parentQStr = "consider"
-				}
-
-				text += `dest_domain=` + orgURI.Hostname() + ` port=` + orgURI.Port() + ` ` + parents + ` ` + secondaryParents + ` ` + roundRobin + ` ` + goDirect + ` qstring=` + parentQStr + "\n"
-			}
-			textArr = append(textArr, text)
-			processedOriginsToDSNames[originFQDN] = ds.Name
-		}
-
-		defaultDestText := `dest_domain=. ` + parents
-		if serverParams[ParentConfigParamAlgorithm] == AlgorithmConsistentHash {
-			defaultDestText += secondaryParents
-		}
-		defaultDestText += ` round_robin=consistent_hash go_direct=false`
-
-		if qStr := serverParams[ParentConfigParamQString]; qStr != "" {
-			defaultDestText += ` qstring=` + qStr
-		}
-		defaultDestText += "\n"
-
-		sort.Sort(sort.StringSlice(textArr))
-		text = hdr + strings.Join(textArr, "") + defaultDestText
+		parentConfigDSes, err = getParentConfigDS(inf.Tx.Tx, serverInfo.ID)
 	}
-	w.Header().Set("Content-Type", "text/plain")
-	w.Write([]byte(text))
-}
 
-// unavailableServerRetryResponsesValid returns whether a unavailable_server_retry_responses parameter is valid for an ATS parent rule.
-func unavailableServerRetryResponsesValid(s string) bool {
-	// optimization if param is empty
-	if s == "" {
-		return false
+	serverParams, err := getParentConfigServerProfileParams(inf.Tx.Tx, serverInfo.ID)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server params: "+err.Error()))
+		return
 	}
-	re := regexp.MustCompile(`^"(:?\d{3},)+\d{3}"\s*$`) // TODO benchmark, cache if performance matters
-	return re.MatchString(s)
-}
-
-type OriginHost string
 
-type ParentInfos map[OriginHost]ParentInfo
-
-func (p ParentInfo) Format() string {
-	host := ""
-	if p.UseIP {
-		host = p.IP
-	} else {
-		host = p.Host + "." + p.Domain
+	parentInfos, err := getParentInfo(inf.Tx.Tx, serverInfo)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server parent info: "+err.Error()))
+		return
 	}
-	return host + ":" + strconv.Itoa(p.Port) + "|" + p.Weight + ";"
-}
 
-type ParentInfoSortByRank []ParentInfo
+	text := atscfg.MakeParentDotConfig(serverInfo, atsMajorVer, toolName, toURL, parentConfigDSes, serverParams, parentInfos)
 
-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 { return s[i].Rank < s[j].Rank }
+	w.Header().Set("Content-Type", "text/plain")
+	w.Write([]byte(text))
+}
 
-type ParentConfigDSSortByName []ParentConfigDS
+type ParentConfigDSSortByName []atscfg.ParentConfigDS
 
-func (s ParentConfigDSSortByName) Len() int      { return len(([]ParentConfigDS)(s)) }
+func (s ParentConfigDSSortByName) Len() int      { return len(([]atscfg.ParentConfigDS)(s)) }
 func (s ParentConfigDSSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
 func (s ParentConfigDSSortByName) Less(i, j int) bool {
 	// TODO make this match the Perl sort "foreach my $ds ( sort @{ $data->{dslist} } )" ?
 	return strings.Compare(string(s[i].Name), string(s[j].Name)) < 0
 }
 
-const AlgorithmConsistentHash = "consistent_hash"
-
-type ServerInfo struct {
-	CacheGroupID                  int
-	CDN                           tc.CDNName
-	CDNID                         int
-	DomainName                    string
-	HostName                      string
-	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)
-}
-
 // getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfoByID(tx *sql.Tx, id int) (*ServerInfo, bool, error) {
+func getServerInfoByID(tx *sql.Tx, id int) (*atscfg.ServerInfo, bool, error) {
 	return getServerInfo(tx, ServerInfoQuery()+`WHERE s.id = $1`, []interface{}{id})
 }
 
 // getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfoByHost(tx *sql.Tx, host string) (*ServerInfo, bool, error) {
+func getServerInfoByHost(tx *sql.Tx, host string) (*atscfg.ServerInfo, bool, error) {
 	return getServerInfo(tx, ServerInfoQuery()+` WHERE s.host_name = $1 `, []interface{}{host})
 }
 
 // getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfo(tx *sql.Tx, qry string, qryParams []interface{}) (*ServerInfo, bool, error) {
-	s := ServerInfo{}
+func getServerInfo(tx *sql.Tx, qry string, qryParams []interface{}) (*atscfg.ServerInfo, bool, error) {
+	s := atscfg.ServerInfo{}
 	if err := tx.QueryRow(qry, qryParams...).Scan(&s.CDN, &s.CDNID, &s.ID, &s.HostName, &s.DomainName, &s.IP, &s.ProfileID, &s.ProfileName, &s.Port, &s.Type, &s.CacheGroupID, &s.ParentCacheGroupID, &s.SecondaryParentCacheGroupID, &s.ParentCacheGroupType, &s.SecondaryParentCacheGroupType); err != nil {
 		if err == sql.ErrNoRows {
 			return nil, false, nil
@@ -462,8 +148,8 @@ SELECT
   s.tcp_port,
   t.name as type,
   s.cachegroup,
-  COALESCE(cg.parent_cachegroup_id, ` + strconv.Itoa(InvalidID) + `) as parent_cachegroup_id,
-  COALESCE(cg.secondary_parent_cachegroup_id, ` + strconv.Itoa(InvalidID) + `) as secondary_parent_cachegroup_id,
+  COALESCE(cg.parent_cachegroup_id, ` + strconv.Itoa(atscfg.InvalidID) + `) as parent_cachegroup_id,
+  COALESCE(cg.secondary_parent_cachegroup_id, ` + strconv.Itoa(atscfg.InvalidID) + `) as secondary_parent_cachegroup_id,
   COALESCE(parentt.name, '') as parent_cachegroup_type,
   COALESCE(sparentt.name, '') as secondary_parent_cachegroup_type
 FROM
@@ -479,20 +165,16 @@ FROM
 
 // GetATSMajorVersion returns the major version of the given profile's package trafficserver parameter.
 // If no parameter exists, this does not return an error, but rather logs a warning and uses DefaultATSVersion.
-func GetATSMajorVersion(tx *sql.Tx, serverProfileID ProfileID) (int, error) {
-	atsVersion, _, err := GetProfileParamValue(tx, int(serverProfileID), "package", "trafficserver")
+func GetATSMajorVersion(tx *sql.Tx, serverProfileID int) (int, error) {
+	atsVersion, _, err := GetProfileParamValue(tx, serverProfileID, "package", "trafficserver")
 	if err != nil {
 		return 0, errors.New("getting profile param value: " + err.Error())
 	}
 	if len(atsVersion) == 0 {
-		atsVersion = DefaultATSVersion
+		atsVersion = atscfg.DefaultATSVersion
 		log.Warnln("Parameter package.trafficserver missing for profile " + strconv.Itoa(int(serverProfileID)) + ". Assuming version " + atsVersion)
 	}
-	atsMajorVer, err := strconv.Atoi(atsVersion[:1])
-	if err != nil {
-		return 0, errors.New("ats version parameter '" + atsVersion + "' on this profile is not a number (config_file 'package', name 'trafficserver')")
-	}
-	return atsMajorVer, nil
+	return atscfg.GetATSMajorVersionFromATSVersion(atsVersion)
 }
 
 type ParentConfigDS struct {
@@ -569,14 +251,14 @@ func ParentConfigDSQueryTopLevel() string {
 		ParentConfigDSQueryOrder
 }
 
-func getParentConfigDSTopLevel(tx *sql.Tx, cdnName tc.CDNName) ([]ParentConfigDSTopLevel, error) {
+func getParentConfigDSTopLevel(tx *sql.Tx, cdnName tc.CDNName) ([]atscfg.ParentConfigDSTopLevel, error) {
 	dses, err := getParentConfigDSRaw(tx, ParentConfigDSQueryTopLevel(), []interface{}{cdnName})
 	if err != nil {
 		return nil, errors.New("getting top level raw parent config ds: " + err.Error())
 	}
-	topDSes := []ParentConfigDSTopLevel{}
+	topDSes := []atscfg.ParentConfigDSTopLevel{}
 	for _, ds := range dses {
-		topDSes = append(topDSes, ParentConfigDSTopLevel{ParentConfigDS: ds})
+		topDSes = append(topDSes, ds)
 	}
 
 	dsesWithParams, err := getParentConfigDSParamsTopLevel(tx, topDSes)
@@ -587,7 +269,7 @@ func getParentConfigDSTopLevel(tx *sql.Tx, cdnName tc.CDNName) ([]ParentConfigDS
 	return dsesWithParams, nil
 }
 
-func getParentConfigDS(tx *sql.Tx, serverID int) ([]ParentConfigDS, error) {
+func getParentConfigDS(tx *sql.Tx, serverID int) ([]atscfg.ParentConfigDSTopLevel, error) {
 
 	dses, err := getParentConfigDSRaw(tx, ParentConfigDSQuery(), []interface{}{serverID})
 
@@ -602,15 +284,6 @@ func getParentConfigDS(tx *sql.Tx, serverID int) ([]ParentConfigDS, error) {
 	return dsesWithParams, nil
 }
 
-const ParentConfigParamQStringHandling = "psel.qstring_handling"
-const ParentConfigParamMSOAlgorithm = "mso.algorithm"
-const ParentConfigParamMSOParentRetry = "mso.parent_retry"
-const ParentConfigParamUnavailableServerRetryResponses = "mso.unavailable_server_retry_responses"
-const ParentConfigParamMaxSimpleRetries = "mso.max_simple_retries"
-const ParentConfigParamMaxUnavailableServerRetries = "mso.max_unavailable_server_retries"
-const ParentConfigParamAlgorithm = "algorithm"
-const ParentConfigParamQString = "qstring"
-
 func getParentConfigServerProfileParams(tx *sql.Tx, serverID int) (map[string]string, error) {
 	qry := `
 SELECT
@@ -625,9 +298,9 @@ WHERE
   s.id = $1
   AND pa.config_file = 'parent.config'
   AND pa.name IN (
-    '` + ParentConfigParamQStringHandling + `',
-    '` + ParentConfigParamAlgorithm + `',
-    '` + ParentConfigParamQString + `'
+    '` + atscfg.ParentConfigParamQStringHandling + `',
+    '` + atscfg.ParentConfigParamAlgorithm + `',
+    '` + atscfg.ParentConfigParamQString + `'
   )
 `
 	rows, err := tx.Query(qry, serverID)
@@ -647,15 +320,16 @@ WHERE
 	return params, nil
 }
 
-func getParentConfigDSRaw(tx *sql.Tx, qry string, qryParams []interface{}) ([]ParentConfigDS, error) {
+// getParentConfigDSRaw returns a ParentConfigDSTopLevel, but all fields in addition to ParentConfigDS will be defaulted. This is because a ParentConfigDSTopLevel is returned to share the same interface, but it doesn't actually have top level data.
+func getParentConfigDSRaw(tx *sql.Tx, qry string, qryParams []interface{}) ([]atscfg.ParentConfigDSTopLevel, error) {
 	rows, err := tx.Query(qry, qryParams...)
 	if err != nil {
 		return nil, errors.New("querying: " + err.Error())
 	}
 	defer rows.Close()
-	dses := []ParentConfigDS{}
+	dses := []atscfg.ParentConfigDSTopLevel{}
 	for rows.Next() {
-		d := ParentConfigDS{}
+		d := atscfg.ParentConfigDS{}
 		if err := rows.Scan(&d.Name, &d.QStringIgnore, &d.OriginFQDN, &d.MultiSiteOrigin, &d.OriginShield, &d.Type); err != nil {
 			return nil, errors.New("scanning: " + err.Error())
 		}
@@ -665,12 +339,12 @@ func getParentConfigDSRaw(tx *sql.Tx, qry string, qryParams []interface{}) ([]Pa
 			continue
 		}
 		d.Type = tc.DSTypeFromString(string(d.Type))
-		dses = append(dses, d)
+		dses = append(dses, atscfg.ParentConfigDSTopLevel{ParentConfigDS: d})
 	}
 	return dses, nil
 }
 
-func parentConfigDSesToNames(dses []ParentConfigDS) []string {
+func parentConfigDSesToNames(dses []atscfg.ParentConfigDS) []string {
 	names := []string{}
 	for _, ds := range dses {
 		names = append(names, string(ds.Name))
@@ -678,7 +352,7 @@ func parentConfigDSesToNames(dses []ParentConfigDS) []string {
 	return names
 }
 
-func parentConfigDSesToNamesTopLevel(dses []ParentConfigDSTopLevel) []string {
+func parentConfigDSesToNamesTopLevel(dses []atscfg.ParentConfigDSTopLevel) []string {
 	names := []string{}
 	for _, ds := range dses {
 		names = append(names, string(ds.Name))
@@ -704,7 +378,7 @@ WHERE
   pa.config_file = 'parent.config'
   AND ds.xml_id = ANY($1)
   AND pa.name IN (
-    '` + ParentConfigParamQStringHandling + `'
+    '` + atscfg.ParentConfigParamQStringHandling + `'
   )
 `
 
@@ -713,12 +387,12 @@ WHERE
   pa.config_file = 'parent.config'
   AND ds.xml_id = ANY($1)
   AND pa.name IN (
-    '` + ParentConfigParamQStringHandling + `',
-    '` + ParentConfigParamMSOAlgorithm + `',
-    '` + ParentConfigParamMSOParentRetry + `',
-    '` + ParentConfigParamUnavailableServerRetryResponses + `',
-    '` + ParentConfigParamMaxSimpleRetries + `',
-    '` + ParentConfigParamMaxUnavailableServerRetries + `'
+    '` + atscfg.ParentConfigParamQStringHandling + `',
+    '` + atscfg.ParentConfigParamMSOAlgorithm + `',
+    '` + atscfg.ParentConfigParamMSOParentRetry + `',
+    '` + atscfg.ParentConfigParamUnavailableServerRetryResponses + `',
+    '` + atscfg.ParentConfigParamMaxSimpleRetries + `',
+    '` + atscfg.ParentConfigParamMaxUnavailableServerRetries + `'
   )
 `
 
@@ -726,8 +400,8 @@ const ParentConfigDSParamsQuery = ParentConfigDSParamsQuerySelect + ParentConfig
 
 var ParentConfigDSParamsQueryTopLevel = ParentConfigDSParamsQuerySelect + ParentConfigDSParamsQueryFrom + ParentConfigDSParamsQueryWhereTopLevel
 
-func getParentConfigDSParams(tx *sql.Tx, dses []ParentConfigDS) ([]ParentConfigDS, error) {
-	params, err := getParentConfigDSParamsRaw(tx, ParentConfigDSParamsQuery, parentConfigDSesToNames(dses))
+func getParentConfigDSParams(tx *sql.Tx, dses []atscfg.ParentConfigDSTopLevel) ([]atscfg.ParentConfigDSTopLevel, error) {
+	params, err := getParentConfigDSParamsRaw(tx, ParentConfigDSParamsQuery, parentConfigDSesToNamesTopLevel(dses))
 	if err != nil {
 		return nil, err
 	}
@@ -736,7 +410,7 @@ func getParentConfigDSParams(tx *sql.Tx, dses []ParentConfigDS) ([]ParentConfigD
 		if !ok {
 			continue
 		}
-		if v, ok := dsParams[ParentConfigParamQStringHandling]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamQStringHandling]; ok {
 			ds.QStringHandling = v
 			dses[i] = ds
 		}
@@ -744,46 +418,40 @@ func getParentConfigDSParams(tx *sql.Tx, dses []ParentConfigDS) ([]ParentConfigD
 	return dses, nil
 }
 
-const ParentConfigDSParamDefaultMSOAlgorithm = "consistent_hash"
-const ParentConfigDSParamDefaultMSOParentRetry = "both"
-const ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses = ""
-const ParentConfigDSParamDefaultMaxSimpleRetries = "1"
-const ParentConfigDSParamDefaultMaxUnavailableServerRetries = "1"
-
-func getParentConfigDSParamsTopLevel(tx *sql.Tx, dses []ParentConfigDSTopLevel) ([]ParentConfigDSTopLevel, error) {
+func getParentConfigDSParamsTopLevel(tx *sql.Tx, dses []atscfg.ParentConfigDSTopLevel) ([]atscfg.ParentConfigDSTopLevel, error) {
 	params, err := getParentConfigDSParamsRaw(tx, ParentConfigDSParamsQueryTopLevel, parentConfigDSesToNamesTopLevel(dses))
 	if err != nil {
 		return nil, err
 	}
 	for i, ds := range dses {
 		dsParams := params[ds.Name] // it's acceptable for this to not exist, if there are no params for the DS. If so, we still need to continue below, to set all the defaults.
-		if v, ok := dsParams[ParentConfigParamQStringHandling]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamQStringHandling]; ok {
 			ds.QStringHandling = v
 		}
-		if v, ok := dsParams[ParentConfigParamMSOAlgorithm]; ok && strings.TrimSpace(v) != "" {
+		if v, ok := dsParams[atscfg.ParentConfigParamMSOAlgorithm]; ok && strings.TrimSpace(v) != "" {
 			ds.MSOAlgorithm = v
 		} else {
-			ds.MSOAlgorithm = ParentConfigDSParamDefaultMSOAlgorithm
+			ds.MSOAlgorithm = atscfg.ParentConfigDSParamDefaultMSOAlgorithm
 		}
-		if v, ok := dsParams[ParentConfigParamMSOParentRetry]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamMSOParentRetry]; ok {
 			ds.MSOParentRetry = v
 		} else {
-			ds.MSOParentRetry = ParentConfigDSParamDefaultMSOParentRetry
+			ds.MSOParentRetry = atscfg.ParentConfigDSParamDefaultMSOParentRetry
 		}
-		if v, ok := dsParams[ParentConfigParamUnavailableServerRetryResponses]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamUnavailableServerRetryResponses]; ok {
 			ds.MSOUnavailableServerRetryResponses = v
 		} else {
-			ds.MSOUnavailableServerRetryResponses = ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses
+			ds.MSOUnavailableServerRetryResponses = atscfg.ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses
 		}
-		if v, ok := dsParams[ParentConfigParamMaxSimpleRetries]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamMaxSimpleRetries]; ok {
 			ds.MSOMaxSimpleRetries = v
 		} else {
-			ds.MSOMaxSimpleRetries = ParentConfigDSParamDefaultMaxSimpleRetries
+			ds.MSOMaxSimpleRetries = atscfg.ParentConfigDSParamDefaultMaxSimpleRetries
 		}
-		if v, ok := dsParams[ParentConfigParamMaxUnavailableServerRetries]; ok {
+		if v, ok := dsParams[atscfg.ParentConfigParamMaxUnavailableServerRetries]; ok {
 			ds.MSOMaxUnavailableServerRetries = v
 		} else {
-			ds.MSOMaxUnavailableServerRetries = ParentConfigDSParamDefaultMaxUnavailableServerRetries
+			ds.MSOMaxUnavailableServerRetries = atscfg.ParentConfigDSParamDefaultMaxUnavailableServerRetries
 		}
 		dses[i] = ds
 	}
@@ -813,20 +481,8 @@ func getParentConfigDSParamsRaw(tx *sql.Tx, qry string, dsNames []string) (map[t
 	return params, nil
 }
 
-type ParentInfo struct {
-	Host            string
-	Port            int
-	Domain          string
-	Weight          string
-	UseIP           bool
-	Rank            int
-	IP              string
-	PrimaryParent   bool
-	SecondaryParent bool
-}
-
-func getParentInfo(tx *sql.Tx, server *ServerInfo) (map[string][]ParentInfo, error) {
-	parentInfos := map[string][]ParentInfo{}
+func getParentInfo(tx *sql.Tx, server *atscfg.ServerInfo) (map[string][]atscfg.ParentInfo, error) {
+	parentInfos := map[string][]atscfg.ParentInfo{}
 
 	serverDomain, ok, err := getCDNDomainByProfileID(tx, server.ProfileID)
 	if err != nil {
@@ -840,78 +496,15 @@ func getParentInfo(tx *sql.Tx, server *ServerInfo) (map[string][]ParentInfo, err
 		return nil, errors.New("getting server parent cachegroup profiles: " + err.Error())
 	}
 
-	// note servers also contains an "all" key
-	// originFQDN is "prefix" in Perl; ds is not really a "ds", that's what it's named in Perl
-	for originFQDN, servers := range originServers {
-		for _, row := range servers {
-			profile := profileCaches[row.ProfileID]
-			if profile.NotAParent {
-				continue
-			}
-			// Perl has this check, but we only select servers ("deliveryServices" in Perl) with the right CDN in the first place
-			// if profile.Domain != serverDomain {
-			// 	continue
-			// }
-
-			parentInf := ParentInfo{
-				Host:            row.ServerHost,
-				Port:            profile.Port,
-				Domain:          row.Domain,
-				Weight:          profile.Weight,
-				UseIP:           profile.UseIP,
-				Rank:            profile.Rank,
-				IP:              row.ServerIP,
-				PrimaryParent:   server.ParentCacheGroupID == row.CacheGroupID,
-				SecondaryParent: server.SecondaryParentCacheGroupID == row.CacheGroupID,
-			}
-			if parentInf.Port < 1 {
-				parentInf.Port = row.ServerPort
-			}
-			parentInfos[originFQDN] = append(parentInfos[originFQDN], parentInf)
-		}
-	}
-	return parentInfos, nil
-}
-
-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
-	Status       int
-	Type         int
-	ProfileID    ProfileID
-	CDN          int
-	TypeName     string
-	Domain       string
+	return atscfg.MakeParentInfo(server, serverDomain, profileCaches, originServers), nil
 }
 
 // getServerParentCacheGroupProfiles gets the profile information for servers belonging to the parent cachegroup, and secondary parent cachegroup, of the cachegroup of each server.
-func getServerParentCacheGroupProfiles(tx *sql.Tx, server *ServerInfo) (map[ProfileID]ProfileCache, map[string][]CGServer, error) {
+func getServerParentCacheGroupProfiles(tx *sql.Tx, server *atscfg.ServerInfo) (map[atscfg.ProfileID]atscfg.ProfileCache, map[string][]atscfg.CGServer, error) {
 	// TODO make this more efficient - should be a single query - this was transliterated from Perl - it's extremely inefficient.
 
-	profileCaches := map[ProfileID]ProfileCache{}
-	originServers := map[string][]CGServer{} // "deliveryServices" in Perl
+	profileCaches := map[atscfg.ProfileID]atscfg.ProfileCache{}
+	originServers := map[string][]atscfg.CGServer{} // "deliveryServices" in Perl
 
 	qry := ""
 	if server.IsTopLevelCache() {
@@ -980,9 +573,9 @@ WHERE
 	defer rows.Close()
 
 	cgServerIDs := []int{}
-	cgServers := []CGServer{}
+	cgServers := []atscfg.CGServer{}
 	for rows.Next() {
-		s := CGServer{}
+		s := atscfg.CGServer{}
 		if err := rows.Scan(&s.ServerID, &s.ServerHost, &s.ServerIP, &s.ServerPort, &s.CacheGroupID, &s.Status, &s.Type, &s.ProfileID, &s.CDN, &s.TypeName, &s.Domain); err != nil {
 			return nil, nil, errors.New("scanning: " + err.Error())
 		}
@@ -1000,7 +593,7 @@ WHERE
 		return nil, nil, errors.New("getting cachegroup server profile params: " + err.Error())
 	}
 
-	allDSMap := map[DeliveryServiceID]struct{}{}
+	allDSMap := map[atscfg.DeliveryServiceID]struct{}{}
 	for _, dses := range cgServerDSes {
 		for _, ds := range dses {
 			allDSMap[ds] = struct{}{}
@@ -1024,11 +617,11 @@ WHERE
 				originServers[orgURI.Host] = append(originServers[orgURI.Host], cgServer)
 			}
 		} else {
-			originServers[DeliveryServicesAllParentsKey] = append(originServers[DeliveryServicesAllParentsKey], cgServer)
+			originServers[atscfg.DeliveryServicesAllParentsKey] = append(originServers[atscfg.DeliveryServicesAllParentsKey], cgServer)
 		}
 
 		if _, profileCachesHasProfile := profileCaches[cgServer.ProfileID]; !profileCachesHasProfile {
-			defaultProfileCache := DefaultProfileCache()
+			defaultProfileCache := atscfg.DefaultProfileCache()
 			if profileCache, profileParamsHasProfile := profileParams[cgServer.ProfileID]; !profileParamsHasProfile {
 				log.Warnf("cachegroup has server with profile %+v but that profile has no parameters", cgServer.ProfileID)
 				profileCaches[cgServer.ProfileID] = defaultProfileCache
@@ -1040,13 +633,8 @@ WHERE
 	return profileCaches, originServers, nil
 }
 
-// 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 ServerID int
-
-func getServerDSes(tx *sql.Tx, serverIDs []int) (map[ServerID][]DeliveryServiceID, error) {
-	sds := map[ServerID][]DeliveryServiceID{}
+func getServerDSes(tx *sql.Tx, serverIDs []int) (map[atscfg.ServerID][]atscfg.DeliveryServiceID, error) {
+	sds := map[atscfg.ServerID][]atscfg.DeliveryServiceID{}
 	if len(serverIDs) == 0 {
 		return sds, nil
 	}
@@ -1066,8 +654,8 @@ WHERE
 	defer rows.Close()
 
 	for rows.Next() {
-		sID := ServerID(0)
-		dsID := DeliveryServiceID(0)
+		sID := atscfg.ServerID(0)
+		dsID := atscfg.DeliveryServiceID(0)
 		if err := rows.Scan(&sID, &dsID); err != nil {
 			return nil, errors.New("scanning: " + err.Error())
 		}
@@ -1076,16 +664,8 @@ WHERE
 	return sds, nil
 }
 
-type DeliveryServiceID int
-
-type OriginURI struct {
-	Scheme string
-	Host   string
-	Port   string
-}
-
-func getDSOrigins(tx *sql.Tx, dsIDs []int) (map[DeliveryServiceID]*OriginURI, error) {
-	origins := map[DeliveryServiceID]*OriginURI{}
+func getDSOrigins(tx *sql.Tx, dsIDs []int) (map[atscfg.DeliveryServiceID]*atscfg.OriginURI, error) {
+	origins := map[atscfg.DeliveryServiceID]*atscfg.OriginURI{}
 	if len(dsIDs) == 0 {
 		return origins, nil
 	}
@@ -1109,8 +689,8 @@ WHERE
 	defer rows.Close()
 
 	for rows.Next() {
-		id := DeliveryServiceID(0)
-		uri := &OriginURI{}
+		id := atscfg.DeliveryServiceID(0)
+		uri := &atscfg.OriginURI{}
 		if err := rows.Scan(&id, &uri.Scheme, &uri.Host, &uri.Port); err != nil {
 			return nil, errors.New("scanning: " + err.Error())
 		}
@@ -1128,15 +708,7 @@ WHERE
 	return origins, nil
 }
 
-const ParentConfigCacheParamWeight = "weight"
-const ParentConfigCacheParamPort = "port"
-const ParentConfigCacheParamUseIP = "use_ip_address"
-const ParentConfigCacheParamRank = "rank"
-const ParentConfigCacheParamNotAParent = "not_a_parent"
-
-type ProfileID int
-
-func getParentConfigServerCacheProfileParams(tx *sql.Tx, serverIDs []int) (map[ProfileID]ProfileCache, error) {
+func getParentConfigServerCacheProfileParams(tx *sql.Tx, serverIDs []int) (map[atscfg.ProfileID]atscfg.ProfileCache, error) {
 	qry := `
 SELECT
   pr.id,
@@ -1151,11 +723,11 @@ WHERE
   s.id = ANY($1)
   AND pa.config_file = 'parent.config'
   AND pa.name IN (
-    '` + ParentConfigCacheParamWeight + `',
-    '` + ParentConfigCacheParamPort + `',
-    '` + ParentConfigCacheParamUseIP + `',
-    '` + ParentConfigCacheParamRank + `',
-    '` + ParentConfigCacheParamNotAParent + `'
+    '` + atscfg.ParentConfigCacheParamWeight + `',
+    '` + atscfg.ParentConfigCacheParamPort + `',
+    '` + atscfg.ParentConfigCacheParamUseIP + `',
+    '` + atscfg.ParentConfigCacheParamRank + `',
+    '` + atscfg.ParentConfigCacheParamNotAParent + `'
   )
 `
 	rows, err := tx.Query(qry, pq.Array(serverIDs))
@@ -1165,7 +737,7 @@ WHERE
 	defer rows.Close()
 
 	type Param struct {
-		ProfileID ProfileID
+		ProfileID atscfg.ProfileID
 		Name      string
 		Val       string
 	}
@@ -1179,14 +751,14 @@ WHERE
 		params = append(params, p)
 	}
 
-	sParams := map[ProfileID]ProfileCache{} // TODO change to map of pointers? Does efficiency matter?
+	sParams := map[atscfg.ProfileID]atscfg.ProfileCache{} // TODO change to map of pointers? Does efficiency matter?
 	for _, param := range params {
 		profileCache, ok := sParams[param.ProfileID]
 		if !ok {
-			profileCache = DefaultProfileCache()
+			profileCache = atscfg.DefaultProfileCache()
 		}
 		switch param.Name {
-		case ParentConfigCacheParamWeight:
+		case atscfg.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())
@@ -1195,16 +767,16 @@ WHERE
 			// }
 			// TODO validate float?
 			profileCache.Weight = param.Val
-		case ParentConfigCacheParamPort:
+		case atscfg.ParentConfigCacheParamPort:
 			i, err := strconv.ParseInt(param.Val, 10, 64)
 			if err != nil {
 				log.Errorln("parent.config generation: port param is not an integer, skipping! : " + err.Error())
 			} else {
 				profileCache.Port = int(i)
 			}
-		case ParentConfigCacheParamUseIP:
+		case atscfg.ParentConfigCacheParamUseIP:
 			profileCache.UseIP = param.Val == "1"
-		case ParentConfigCacheParamRank:
+		case atscfg.ParentConfigCacheParamRank:
 			i, err := strconv.ParseInt(param.Val, 10, 64)
 			if err != nil {
 				log.Errorln("parent.config generation: rank param is not an integer, skipping! : " + err.Error())
@@ -1212,7 +784,7 @@ WHERE
 				profileCache.Rank = int(i)
 			}
 
-		case ParentConfigCacheParamNotAParent:
+		case atscfg.ParentConfigCacheParamNotAParent:
 			profileCache.NotAParent = param.Val != "false"
 		default:
 			return nil, errors.New("query returned unexpected param: " + param.Name)
@@ -1236,9 +808,9 @@ WHERE
   s.id = $1
   AND pa.config_file = 'parent.config'
   AND pa.name IN (
-    '` + ParentConfigParamQStringHandling + `',
-    '` + ParentConfigParamAlgorithm + `',
-    '` + ParentConfigParamQString + `'
+    '` + atscfg.ParentConfigParamQStringHandling + `',
+    '` + atscfg.ParentConfigParamAlgorithm + `',
+    '` + atscfg.ParentConfigParamQString + `'
   )
 `
 	rows, err := tx.Query(qry, serverID)
@@ -1264,7 +836,7 @@ type ParentConfigServerParams struct {
 	QStringHandling bool
 }
 
-func getCDNDomainByProfileID(tx *sql.Tx, profileID ProfileID) (string, bool, error) {
+func getCDNDomainByProfileID(tx *sql.Tx, profileID atscfg.ProfileID) (string, bool, error) {
 	qry := `SELECT domain_name from cdn where id = (select cdn from profile where id = $1)`
 	val := ""
 	if err := tx.QueryRow(qry, profileID).Scan(&val); err != nil {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
index 2a5136f..d8b8c2f 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
@@ -47,7 +47,9 @@ import (
 type TODeliveryServiceServer struct {
 	api.APIInfoImpl `json:"-"`
 	tc.DeliveryServiceServer
-	TenantIDs pq.Int64Array `json:"-" db:"accessibleTenants"`
+	TenantIDs          pq.Int64Array `json:"-" db:"accessibleTenants"`
+	DeliveryServiceIDs pq.Int64Array `json:"-" db:"dsids"`
+	ServerIDs          pq.Int64Array `json:"-" db:"serverids"`
 }
 
 func (dss TODeliveryServiceServer) GetKeyFieldsInfo() []api.KeyFieldInfo {
@@ -112,7 +114,7 @@ func ReadDSSHandler(w http.ResponseWriter, r *http.Request) {
 
 	dss := TODeliveryServiceServer{}
 	dss.SetInfo(inf)
-	results, err := dss.readDSS(inf.Tx, inf.User, inf.Params, inf.IntParams)
+	results, err := dss.readDSS(inf.Tx, inf.User, inf.Params, inf.IntParams, nil, nil)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -120,7 +122,56 @@ func ReadDSSHandler(w http.ResponseWriter, r *http.Request) {
 	api.WriteRespRaw(w, r, results)
 }
 
-func (dss *TODeliveryServiceServer) readDSS(tx *sqlx.Tx, user *auth.CurrentUser, params map[string]string, intParams map[string]int) (*tc.DeliveryServiceServerResponse, error) {
+// ReadDSSHandler list all of the Deliveryservice Servers in response to requests to api/1.1/deliveryserviceserver$
+func ReadDSSHandlerV14(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"limit", "page"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	dsIDs := []int64{}
+	dsIDStrs := strings.Split(inf.Params["deliveryserviceids"], ",")
+	for _, dsIDStr := range dsIDStrs {
+		dsIDStr = strings.TrimSpace(dsIDStr)
+		if dsIDStr == "" {
+			continue
+		}
+		dsID, err := strconv.Atoi(dsIDStr)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, 400, errors.New("deliveryserviceids query parameter must be a comma-delimited list of integers, got '"+inf.Params["deliveryserviceids"]+"'"), nil)
+			return
+		}
+		dsIDs = append(dsIDs, int64(dsID))
+	}
+
+	serverIDs := []int64{}
+	serverIDStrs := strings.Split(inf.Params["serverids"], ",")
+	for _, serverIDStr := range serverIDStrs {
+		serverIDStr = strings.TrimSpace(serverIDStr)
+		if serverIDStr == "" {
+			continue
+		}
+		serverID, err := strconv.Atoi(serverIDStr)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, 400, errors.New("serverids query parameter must be a comma-delimited list of integers, got '"+inf.Params["serverids"]+"'"), nil)
+			return
+		}
+		serverIDs = append(serverIDs, int64(serverID))
+	}
+
+	dss := TODeliveryServiceServer{}
+	dss.SetInfo(inf)
+	results, err := dss.readDSS(inf.Tx, inf.User, inf.Params, inf.IntParams, dsIDs, serverIDs)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	api.WriteRespRaw(w, r, results)
+}
+
+func (dss *TODeliveryServiceServer) readDSS(tx *sqlx.Tx, user *auth.CurrentUser, params map[string]string, intParams map[string]int, dsIDs []int64, serverIDs []int64) (*tc.DeliveryServiceServerResponse, error) {
 	orderby := params["orderby"]
 	limit := 20
 	offset := 0
@@ -148,8 +199,10 @@ func (dss *TODeliveryServiceServer) readDSS(tx *sqlx.Tx, user *auth.CurrentUser,
 	for _, id := range tenantIDs {
 		dss.TenantIDs = append(dss.TenantIDs, int64(id))
 	}
+	dss.ServerIDs = serverIDs
+	dss.DeliveryServiceIDs = dsIDs
 
-	query, err := selectQuery(orderby, strconv.Itoa(limit), strconv.Itoa(offset))
+	query, err := selectQuery(orderby, strconv.Itoa(limit), strconv.Itoa(offset), dsIDs, serverIDs)
 	if err != nil {
 		return nil, errors.New("creating query for DeliveryserviceServers: " + err.Error())
 	}
@@ -171,7 +224,7 @@ func (dss *TODeliveryServiceServer) readDSS(tx *sqlx.Tx, user *auth.CurrentUser,
 	return &tc.DeliveryServiceServerResponse{orderby, servers, page, limit}, nil
 }
 
-func selectQuery(orderBy string, limit string, offset string) (string, error) {
+func selectQuery(orderBy string, limit string, offset string, dsIDs []int64, serverIDs []int64) (string, error) {
 	selectStmt := `SELECT
 	s.deliveryService,
 	s.server,
@@ -197,6 +250,16 @@ func selectQuery(orderBy string, limit string, offset string) (string, error) {
 JOIN deliveryservice d on s.deliveryservice = d.id
 WHERE d.tenant_id = ANY(CAST(:accessibleTenants AS bigint[]))
 `
+	if len(dsIDs) > 0 {
+		selectStmt += `
+AND s.deliveryservice = ANY(:dsids)
+`
+	}
+	if len(serverIDs) > 0 {
+		selectStmt += `
+AND s.server = ANY(:serverids)
+`
+	}
 
 	if orderBy != "" {
 		selectStmt += ` ORDER BY ` + orderBy
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 62685f7..0de74d2 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -232,6 +232,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 
 		// get all edge servers associated with a delivery service (from deliveryservice_server table)
 
+		{1.4, http.MethodGet, `deliveryserviceserver/?(\.json)?$`, dsserver.ReadDSSHandlerV14, auth.PrivLevelReadOnly, Authenticated, nil},
 		{1.1, http.MethodGet, `deliveryserviceserver/?(\.json)?$`, dsserver.ReadDSSHandler, auth.PrivLevelReadOnly, Authenticated, nil},
 		{1.1, http.MethodPost, `deliveryserviceserver$`, dsserver.GetReplaceHandler, auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodPost, `deliveryservices/{xml_id}/servers$`, dsserver.GetCreateHandler, auth.PrivLevelOperations, Authenticated, nil},
diff --git a/vendor/github.com/ogier/pflag/.travis.yml b/vendor/github.com/ogier/pflag/.travis.yml
new file mode 100644
index 0000000..4f2ee4d
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/.travis.yml
@@ -0,0 +1 @@
+language: go
diff --git a/vendor/github.com/ogier/pflag/LICENSE b/vendor/github.com/ogier/pflag/LICENSE
new file mode 100644
index 0000000..63ed1cf
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2012 Alex Ogier. All rights reserved.
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/ogier/pflag/README.md b/vendor/github.com/ogier/pflag/README.md
new file mode 100644
index 0000000..d9b189f
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/README.md
@@ -0,0 +1,157 @@
+[![Build Status](https://travis-ci.org/ogier/pflag.png?branch=master)](https://travis-ci.org/ogier/pflag)
+
+## Description
+
+pflag is a drop-in replacement for Go's flag package, implementing
+POSIX/GNU-style --flags.
+
+pflag is compatible with the [GNU extensions to the POSIX recommendations
+for command-line options][1]. For a more precise description, see the
+"Command-line flag syntax" section below.
+
+[1]: http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
+
+pflag is available under the same style of BSD license as the Go language,
+which can be found in the LICENSE file.
+
+## Installation
+
+pflag is available using the standard `go get` command.
+
+Install by running:
+
+    go get github.com/ogier/pflag
+
+Run tests by running:
+
+    go test github.com/ogier/pflag
+
+## Usage
+
+pflag is a drop-in replacement of Go's native flag package. If you import
+pflag under the name "flag" then all code should continue to function
+with no changes.
+
+``` go
+import flag "github.com/ogier/pflag"
+```
+
+There is one exception to this: if you directly instantiate the Flag struct
+there is one more field "Shorthand" that you will need to set.
+Most code never instantiates this struct directly, and instead uses
+functions such as String(), BoolVar(), and Var(), and is therefore
+unaffected.
+
+Define flags using flag.String(), Bool(), Int(), etc.
+
+This declares an integer flag, -flagname, stored in the pointer ip, with type *int.
+
+``` go
+var ip *int = flag.Int("flagname", 1234, "help message for flagname")
+```
+
+If you like, you can bind the flag to a variable using the Var() functions.
+
+``` go
+var flagvar int
+func init() {
+    flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
+}
+```
+
+Or you can create custom flags that satisfy the Value interface (with
+pointer receivers) and couple them to flag parsing by
+
+``` go
+flag.Var(&flagVal, "name", "help message for flagname")
+```
+
+For such flags, the default value is just the initial value of the variable.
+
+After all flags are defined, call
+
+``` go
+flag.Parse()
+```
+
+to parse the command line into the defined flags.
+
+Flags may then be used directly. If you're using the flags themselves,
+they are all pointers; if you bind to variables, they're values.
+
+``` go
+fmt.Println("ip has value ", *ip)
+fmt.Println("flagvar has value ", flagvar)
+```
+
+After parsing, the arguments after the flag are available as the
+slice flag.Args() or individually as flag.Arg(i).
+The arguments are indexed from 0 through flag.NArg()-1.
+
+The pflag package also defines some new functions that are not in flag,
+that give one-letter shorthands for flags. You can use these by appending
+'P' to the name of any function that defines a flag.
+
+``` go
+var ip = flag.IntP("flagname", "f", 1234, "help message")
+var flagvar bool
+func init() {
+    flag.BoolVarP("boolname", "b", true, "help message")
+}
+flag.VarP(&flagVar, "varname", "v", 1234, "help message")
+```
+
+Shorthand letters can be used with single dashes on the command line.
+Boolean shorthand flags can be combined with other shorthand flags.
+
+The default set of command-line flags is controlled by
+top-level functions.  The FlagSet type allows one to define
+independent sets of flags, such as to implement subcommands
+in a command-line interface. The methods of FlagSet are
+analogous to the top-level functions for the command-line
+flag set.
+
+## Command line flag syntax
+
+```
+--flag    // boolean flags only
+--flag=x
+```
+
+Unlike the flag package, a single dash before an option means something
+different than a double dash. Single dashes signify a series of shorthand
+letters for flags. All but the last shorthand letter must be boolean flags.
+
+```
+// boolean flags
+-f
+-abc
+
+// non-boolean flags
+-n 1234
+-Ifile
+
+// mixed
+-abcs "hello"
+-abcn1234
+```
+
+Flag parsing stops after the terminator "--". Unlike the flag package,
+flags can be interspersed with arguments anywhere on the command line
+before this terminator.
+
+Integer flags accept 1234, 0664, 0x1234 and may be negative.
+Boolean flags (in their long form) accept 1, 0, t, f, true, false,
+TRUE, FALSE, True, False.
+Duration flags accept any input valid for time.ParseDuration.
+
+## More info
+
+You can see the full reference documentation of the pflag package
+[at godoc.org][3], or through go's standard documentation system by
+running `godoc -http=:6060` and browsing to
+[http://localhost:6060/pkg/github.com/ogier/pflag][2] after
+installation.
+
+[2]: http://localhost:6060/pkg/github.com/ogier/pflag
+[3]: http://godoc.org/github.com/ogier/pflag
diff --git a/vendor/github.com/ogier/pflag/bool.go b/vendor/github.com/ogier/pflag/bool.go
new file mode 100644
index 0000000..617971a
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/bool.go
@@ -0,0 +1,79 @@
+package pflag
+
+import (
+	"fmt"
+	"strconv"
+)
+
+// optional interface to indicate boolean flags that can be
+// supplied without "=value" text
+type boolFlag interface {
+	Value
+	IsBoolFlag() bool
+}
+
+// -- bool Value
+type boolValue bool
+
+func newBoolValue(val bool, p *bool) *boolValue {
+	*p = val
+	return (*boolValue)(p)
+}
+
+func (b *boolValue) Set(s string) error {
+	v, err := strconv.ParseBool(s)
+	*b = boolValue(v)
+	return err
+}
+
+func (b *boolValue) String() string { return fmt.Sprintf("%v", *b) }
+
+func (b *boolValue) IsBoolFlag() bool { return true }
+
+// BoolVar defines a bool flag with specified name, default value, and usage string.
+// The argument p points to a bool variable in which to store the value of the flag.
+func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) {
+	f.VarP(newBoolValue(value, p), name, "", usage)
+}
+
+// Like BoolVar, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) BoolVarP(p *bool, name, shorthand string, value bool, usage string) {
+	f.VarP(newBoolValue(value, p), name, shorthand, usage)
+}
+
+// BoolVar defines a bool flag with specified name, default value, and usage string.
+// The argument p points to a bool variable in which to store the value of the flag.
+func BoolVar(p *bool, name string, value bool, usage string) {
+	CommandLine.VarP(newBoolValue(value, p), name, "", usage)
+}
+
+// Like BoolVar, but accepts a shorthand letter that can be used after a single dash.
+func BoolVarP(p *bool, name, shorthand string, value bool, usage string) {
+	CommandLine.VarP(newBoolValue(value, p), name, shorthand, usage)
+}
+
+// Bool defines a bool flag with specified name, default value, and usage string.
+// The return value is the address of a bool variable that stores the value of the flag.
+func (f *FlagSet) Bool(name string, value bool, usage string) *bool {
+	p := new(bool)
+	f.BoolVarP(p, name, "", value, usage)
+	return p
+}
+
+// Like Bool, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) BoolP(name, shorthand string, value bool, usage string) *bool {
+	p := new(bool)
+	f.BoolVarP(p, name, shorthand, value, usage)
+	return p
+}
+
+// Bool defines a bool flag with specified name, default value, and usage string.
+// The return value is the address of a bool variable that stores the value of the flag.
+func Bool(name string, value bool, usage string) *bool {
+	return CommandLine.BoolP(name, "", value, usage)
+}
+
+// Like Bool, but accepts a shorthand letter that can be used after a single dash.
+func BoolP(name, shorthand string, value bool, usage string) *bool {
+	return CommandLine.BoolP(name, shorthand, value, usage)
+}
diff --git a/vendor/github.com/ogier/pflag/bool_test.go b/vendor/github.com/ogier/pflag/bool_test.go
new file mode 100644
index 0000000..ecffae7
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/bool_test.go
@@ -0,0 +1,164 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pflag
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"testing"
+)
+
+// This value can be a boolean ("true", "false") or "maybe"
+type triStateValue int
+
+const (
+	triStateFalse triStateValue = 0
+	triStateTrue  triStateValue = 1
+	triStateMaybe triStateValue = 2
+)
+
+const strTriStateMaybe = "maybe"
+
+func (v *triStateValue) IsBoolFlag() bool {
+	return true
+}
+
+func (v *triStateValue) Get() interface{} {
+	return triStateValue(*v)
+}
+
+func (v *triStateValue) Set(s string) error {
+	if s == strTriStateMaybe {
+		*v = triStateMaybe
+		return nil
+	}
+	boolVal, err := strconv.ParseBool(s)
+	if boolVal {
+		*v = triStateTrue
+	} else {
+		*v = triStateFalse
+	}
+	return err
+}
+
+func (v *triStateValue) String() string {
+	if *v == triStateMaybe {
+		return strTriStateMaybe
+	}
+	return fmt.Sprintf("%v", bool(*v == triStateTrue))
+}
+
+// The type of the flag as required by the pflag.Value interface
+func (v *triStateValue) Type() string {
+	return "version"
+}
+
+func setUpFlagSet(tristate *triStateValue) *FlagSet {
+	f := NewFlagSet("test", ContinueOnError)
+	*tristate = triStateFalse
+	f.VarP(tristate, "tristate", "t", "tristate value (true, maybe or false)")
+	return f
+}
+
+func TestExplicitTrue(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{"--tristate=true"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateTrue {
+		t.Fatal("expected", triStateTrue, "(triStateTrue) but got", tristate, "instead")
+	}
+}
+
+func TestImplicitTrue(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{"--tristate"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateTrue {
+		t.Fatal("expected", triStateTrue, "(triStateTrue) but got", tristate, "instead")
+	}
+}
+
+func TestShortFlag(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{"-t"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateTrue {
+		t.Fatal("expected", triStateTrue, "(triStateTrue) but got", tristate, "instead")
+	}
+}
+
+func TestShortFlagExtraArgument(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	// The"maybe"turns into an arg, since short boolean options will only do true/false
+	err := f.Parse([]string{"-t", "maybe"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateTrue {
+		t.Fatal("expected", triStateTrue, "(triStateTrue) but got", tristate, "instead")
+	}
+	args := f.Args()
+	if len(args) != 1 || args[0] != "maybe" {
+		t.Fatal("expected an extra 'maybe' argument to stick around")
+	}
+}
+
+func TestExplicitMaybe(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{"--tristate=maybe"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateMaybe {
+		t.Fatal("expected", triStateMaybe, "(triStateMaybe) but got", tristate, "instead")
+	}
+}
+
+func TestExplicitFalse(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{"--tristate=false"})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateFalse {
+		t.Fatal("expected", triStateFalse, "(triStateFalse) but got", tristate, "instead")
+	}
+}
+
+func TestImplicitFalse(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	err := f.Parse([]string{})
+	if err != nil {
+		t.Fatal("expected no error; got", err)
+	}
+	if tristate != triStateFalse {
+		t.Fatal("expected", triStateFalse, "(triStateFalse) but got", tristate, "instead")
+	}
+}
+
+func TestInvalidValue(t *testing.T) {
+	var tristate triStateValue
+	f := setUpFlagSet(&tristate)
+	var buf bytes.Buffer
+	f.SetOutput(&buf)
+	err := f.Parse([]string{"--tristate=invalid"})
+	if err == nil {
+		t.Fatal("expected an error but did not get any, tristate has value", tristate)
+	}
+}
diff --git a/vendor/github.com/ogier/pflag/duration.go b/vendor/github.com/ogier/pflag/duration.go
new file mode 100644
index 0000000..db59463
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/duration.go
@@ -0,0 +1,74 @@
+package pflag
+
+import "time"
+
+// -- time.Duration Value
+type durationValue time.Duration
+
+func newDurationValue(val time.Duration, p *time.Duration) *durationValue {
+	*p = val
+	return (*durationValue)(p)
+}
+
+func (d *durationValue) Set(s string) error {
+	v, err := time.ParseDuration(s)
+	*d = durationValue(v)
+	return err
+}
+
+func (d *durationValue) String() string { return (*time.Duration)(d).String() }
+
+// Value is the interface to the dynamic value stored in a flag.
+// (The default value is represented as a string.)
+type Value interface {
+	String() string
+	Set(string) error
+}
+
+// DurationVar defines a time.Duration flag with specified name, default value, and usage string.
+// The argument p points to a time.Duration variable in which to store the value of the flag.
+func (f *FlagSet) DurationVar(p *time.Duration, name string, value time.Duration, usage string) {
+	f.VarP(newDurationValue(value, p), name, "", usage)
+}
+
+// Like DurationVar, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) DurationVarP(p *time.Duration, name, shorthand string, value time.Duration, usage string) {
+	f.VarP(newDurationValue(value, p), name, shorthand, usage)
+}
+
+// DurationVar defines a time.Duration flag with specified name, default value, and usage string.
+// The argument p points to a time.Duration variable in which to store the value of the flag.
+func DurationVar(p *time.Duration, name string, value time.Duration, usage string) {
+	CommandLine.VarP(newDurationValue(value, p), name, "", usage)
+}
+
+// Like DurationVar, but accepts a shorthand letter that can be used after a single dash.
+func DurationVarP(p *time.Duration, name, shorthand string, value time.Duration, usage string) {
+	CommandLine.VarP(newDurationValue(value, p), name, shorthand, usage)
+}
+
+// Duration defines a time.Duration flag with specified name, default value, and usage string.
+// The return value is the address of a time.Duration variable that stores the value of the flag.
+func (f *FlagSet) Duration(name string, value time.Duration, usage string) *time.Duration {
+	p := new(time.Duration)
+	f.DurationVarP(p, name, "", value, usage)
+	return p
+}
+
+// Like Duration, but accepts a shorthand letter that can be used after a single dash.
+func (f *FlagSet) DurationP(name, shorthand string, value time.Duration, usage string) *time.Duration {
+	p := new(time.Duration)
+	f.DurationVarP(p, name, shorthand, value, usage)
+	return p
+}
+
+// Duration defines a time.Duration flag with specified name, default value, and usage string.
+// The return value is the address of a time.Duration variable that stores the value of the flag.
+func Duration(name string, value time.Duration, usage string) *time.Duration {
+	return CommandLine.DurationP(name, "", value, usage)
+}
+
+// Like Duration, but accepts a shorthand letter that can be used after a single dash.
+func DurationP(name, shorthand string, value time.Duration, usage string) *time.Duration {
+	return CommandLine.DurationP(name, shorthand, value, usage)
+}
diff --git a/vendor/github.com/ogier/pflag/example_test.go b/vendor/github.com/ogier/pflag/example_test.go
new file mode 100644
index 0000000..03ebeaa
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/example_test.go
@@ -0,0 +1,73 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// These examples demonstrate more intricate uses of the flag package.
+package pflag_test
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	flag "github.com/ogier/pflag"
+)
+
+// Example 1: A single string flag called "species" with default value "gopher".
+var species = flag.String("species", "gopher", "the species we are studying")
+
+// Example 2: A flag with a shorthand letter.
+var gopherType = flag.StringP("gopher_type", "g", "pocket", "the variety of gopher")
+
+// Example 3: A user-defined flag type, a slice of durations.
+type interval []time.Duration
+
+// String is the method to format the flag's value, part of the flag.Value interface.
+// The String method's output will be used in diagnostics.
+func (i *interval) String() string {
+	return fmt.Sprint(*i)
+}
+
+// Set is the method to set the flag value, part of the flag.Value interface.
+// Set's argument is a string to be parsed to set the flag.
+// It's a comma-separated list, so we split it.
+func (i *interval) Set(value string) error {
+	// If we wanted to allow the flag to be set multiple times,
+	// accumulating values, we would delete this if statement.
+	// That would permit usages such as
+	//	-deltaT 10s -deltaT 15s
+	// and other combinations.
+	if len(*i) > 0 {
+		return errors.New("interval flag already set")
+	}
+	for _, dt := range strings.Split(value, ",") {
+		duration, err := time.ParseDuration(dt)
+		if err != nil {
+			return err
+		}
+		*i = append(*i, duration)
+	}
+	return nil
+}
+
+// Define a flag to accumulate durations. Because it has a special type,
+// we need to use the Var function and therefore create the flag during
+// init.
+
+var intervalFlag interval
+
+func init() {
+	// Tie the command-line flag to the intervalFlag variable and
+	// set a usage message.
+	flag.Var(&intervalFlag, "deltaT", "comma-separated list of intervals to use between events")
+}
+
+func Example() {
+	// All the interesting pieces are with the variables declared above, but
+	// to enable the flag package to see the flags defined there, one must
+	// execute, typically at the start of main (not init!):
+	//	flag.Parse()
+	// We don't run it here because this is not a main function and
+	// the testing suite has already parsed the flags.
+}
diff --git a/vendor/github.com/ogier/pflag/export_test.go b/vendor/github.com/ogier/pflag/export_test.go
new file mode 100644
index 0000000..9318fee
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/export_test.go
@@ -0,0 +1,29 @@
+// Copyright 2010 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pflag
+
+import (
+	"io/ioutil"
+	"os"
+)
+
+// Additional routines compiled into the package only during testing.
+
+// ResetForTesting clears all flag state and sets the usage function as directed.
+// After calling ResetForTesting, parse errors in flag handling will not
+// exit the program.
+func ResetForTesting(usage func()) {
+	CommandLine = &FlagSet{
+		name:          os.Args[0],
+		errorHandling: ContinueOnError,
+		output:        ioutil.Discard,
+	}
+	Usage = usage
+}
+
+// GetCommandLine returns the default FlagSet.
+func GetCommandLine() *FlagSet {
+	return CommandLine
+}
diff --git a/vendor/github.com/ogier/pflag/flag.go b/vendor/github.com/ogier/pflag/flag.go
new file mode 100644
index 0000000..9d1e0ca
--- /dev/null
+++ b/vendor/github.com/ogier/pflag/flag.go
@@ -0,0 +1,624 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+	pflag is a drop-in replacement for Go's flag package, implementing
+	POSIX/GNU-style --flags.
+
+	pflag is compatible with the GNU extensions to the POSIX recommendations
+	for command-line options. See
+	http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
+
+	Usage:
+
+	pflag is a drop-in replacement of Go's native flag package. If you import
+	pflag under the name "flag" then all code should continue to function
+	with no changes.
+
+		import flag "github.com/ogier/pflag"
+
+	There is one exception to this: if you directly instantiate the Flag struct
+	there is one more field "Shorthand" that you will need to set.
+	Most code never instantiates this struct directly, and instead uses
+	functions such as String(), BoolVar(), and Var(), and is therefore
+	unaffected.
+
+	Define flags using flag.String(), Bool(), Int(), etc.
+
+	This declares an integer flag, -flagname, stored in the pointer ip, with type *int.
+		var ip = flag.Int("flagname", 1234, "help message for flagname")
+	If you like, you can bind the flag to a variable using the Var() functions.
+		var flagvar int
+		func init() {
+			flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
+		}
+	Or you can create custom flags that satisfy the Value interface (with
+	pointer receivers) and couple them to flag parsing by
+		flag.Var(&flagVal, "name", "help message for flagname")
+	For such flags, the default value is just the initial value of the variable.
+
+	After all flags are defined, call
+		flag.Parse()
+	to parse the command line into the defined flags.
+
+	Flags may then be used directly. If you're using the flags themselves,
+	they are all pointers; if you bind to variables, they're values.
+		fmt.Println("ip has value ", *ip)
+		fmt.Println("flagvar has value ", flagvar)
+
+	After parsing, the arguments after the flag are available as the
+	slice flag.Args() or individually as flag.Arg(i).
+	The arguments are indexed from 0 through flag.NArg()-1.
+
+	The pflag package also defines some new functions that are not in flag,
+	that give one-letter shorthands for flags. You can use these by appending
+	'P' to the name of any function that defines a flag.
+		var ip = flag.IntP("flagname", "f", 1234, "help message")
+		var flagvar bool
+		func init() {
+			flag.BoolVarP("boolname", "b", true, "help message")
+		}
+		flag.VarP(&flagVar, "varname", "v", 1234, "help message")
+	Shorthand letters can be used with single dashes on the command line.
+	Boolean shorthand flags can be combined with other shorthand flags.
+
+	Command line flag syntax:
+		--flag    // boolean flags only
+		--flag=x
+
+	Unlike the flag package, a single dash before an option means something
+	different than a double dash. Single dashes signify a series of shorthand
+	letters for flags. All but the last shorthand letter must be boolean flags.
+		// boolean flags
+		-f
+		-abc
+		// non-boolean flags
+		-n 1234
+		-Ifile
+		// mixed
+		-abcs "hello"
+		-abcn1234
+
+	Flag parsing stops after the terminator "--". Unlike the flag package,
+	flags can be interspersed with arguments anywhere on the command line
+	before this terminator.
+
+	Integer flags accept 1234, 0664, 0x1234 and may be negative.
+	Boolean flags (in their long form) accept 1, 0, t, f, true, false,
+	TRUE, FALSE, True, False.
+	Duration flags accept any input valid for time.ParseDuration.
+
+	The default set of command-line flags is controlled by
+	top-level functions.  The FlagSet type allows one to define
+	independent sets of flags, such as to implement subcommands
+	in a command-line interface. The methods of FlagSet are
+	analogous to the top-level functions for the command-line
+	flag set.
+*/
+package pflag
+
+import (
+	"errors"
+	"fmt"
+	"io"
... 4104 lines suppressed ...


Mime
View raw message