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

rawlin 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 695cd3f  Traffic Ops Golang parent.config (#3075)
695cd3f is described below

commit 695cd3f06ef21962441386d23a5ed58fde147741
Author: Robert Butts <[email protected]>
AuthorDate: Fri May 31 15:25:54 2019 -0600

    Traffic Ops Golang parent.config (#3075)
    
    * Add TO client ATS config funcs
    
    * Fix missing Apache header
    
    * WIP Add TOGo parent.config
    
    * WIP TO Go parent.config fix mid details
    
    * Fix TO Go parent.config spacing to match Perl
    
    * Add TO parent.config API test
    
    * Fix TO parent.config bad round_robin default
    
    parent.config was defaulting to 'round_robin=urlhash', which is not
    a valid ATS config. This fixes it to default to consistent_hash,
    the usual default.
    
    Fixes #3071
    
    * Fix TO parent.config to sort edge parents by rank
    
    Fixes #2725
    
    * Add doc comment to to.session request, rawRequest
    
    * Change TO API Test to use With helper
    
    * Remove debug prints, fix parent.config typo
    
    * Change TOGo parent.config to API 1.1+
    
    * Remove TO unnecessary comment
    
    * Remove TO TODO to determine whether to warn
    
    Remove comment asking if a warning should be logged when a DS has
    no parameters, per PR Review.
    
    I am unsure, but I believe this to be normal behavior and not require
    a warning.
    
    * Remove TO unnecessary comments
    
    * Renamed TO CGPDeliveryService to CGServer
    
    More accurately reflects what it is and the data it contains. It's
    not a great name, but I'm not sure what would be better.
    
    As requested in PR Review.
    
    * Change TO parentdotconfig to use type enums
    
    * Renamed TO variable
    
    As requested in PR Review.
    
    * Rename TO parentdotconfig symbols
    
    As requested in PR Review.
    
    * Fix TO parent.config missing defaults
    
    * Move TO RemoveStrDuplicates to lib/go-util
    
    Also renames local variables, per PR Review.
    
    * Rename TO parent.config variable
    
    Per PR Review.
    
    * Change TO comment to TODO
    
    * Fix TO error log typo
    
    * Add TO TODO for improving parent.config logic
    
    * Change TO parent.config var to longer name
    
    Per PR Review
---
 lib/go-tc/ats.go                                   |   84 ++
 lib/go-tc/enum.go                                  |   29 +
 lib/go-util/str.go                                 |   35 +
 traffic_ops/client/atsconfig.go                    |   49 +
 traffic_ops/client/session.go                      |    4 +
 traffic_ops/testing/api/v14/atsconfig_test.go      |   82 ++
 .../testing/api/v14/parentdotconfig_test.go        |  155 +++
 .../traffic_ops_golang/ats/parentdotconfig.go      | 1302 ++++++++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |    3 +
 9 files changed, 1743 insertions(+)

diff --git a/lib/go-tc/ats.go b/lib/go-tc/ats.go
new file mode 100644
index 0000000..3df463b
--- /dev/null
+++ b/lib/go-tc/ats.go
@@ -0,0 +1,84 @@
+package tc
+
+/*
+ * 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"
+)
+
+type ATSConfigMetaData struct {
+       Info        ATSConfigMetaDataInfo         `json:"info"`
+       ConfigFiles []ATSConfigMetaDataConfigFile `json:"configFiles"`
+}
+
+type ATSConfigMetaDataInfo struct {
+       CDNID             int    `json:"cdnId"`
+       CDNName           string `json:"cdnName"`
+       ServerID          int    `json:"serverId"`
+       ServerIPv4        string `json:"serverIpv4"`
+       ServerName        string `json:"serverName"`
+       ServerPort        int    `json:"serverTcpPort"`
+       ProfileID         int    `json:"profileId"`
+       ProfileName       string `json:"profileName"`
+       TOReverseProxyURL string `json:"toRevProxyUrl"`
+       TOURL             string `json:"toUrl"`
+}
+
+type ATSConfigMetaDataConfigFileScope string
+
+const ATSConfigMetaDataConfigFileScopeProfiles = 
ATSConfigMetaDataConfigFileScope("profiles")
+const ATSConfigMetaDataConfigFileScopeServers = 
ATSConfigMetaDataConfigFileScope("servers")
+const ATSConfigMetaDataConfigFileScopeCDNs = 
ATSConfigMetaDataConfigFileScope("cdns")
+const ATSConfigMetaDataConfigFileScopeInvalid = 
ATSConfigMetaDataConfigFileScope("")
+
+type ATSConfigMetaDataConfigFile struct {
+       FileNameOnDisk string `json:"fnameOnDisk"`
+       Location       string `json:"location"`
+       APIURI         string `json:"apiUri, omitempty"`
+       URL            string `json:"url, omitempty"`
+       Scope          string `json:"scope"`
+}
+
+func (t ATSConfigMetaDataConfigFileScope) String() string {
+       switch t {
+       case ATSConfigMetaDataConfigFileScopeProfiles:
+               fallthrough
+       case ATSConfigMetaDataConfigFileScopeServers:
+               fallthrough
+       case ATSConfigMetaDataConfigFileScopeCDNs:
+               return string(t)
+       default:
+               return "invalid"
+       }
+}
+
+func ATSConfigMetaDataConfigFileScopeFromString(s string) 
ATSConfigMetaDataConfigFileScope {
+       s = strings.ToLower(s)
+       switch s {
+       case "profiles":
+               return ATSConfigMetaDataConfigFileScopeProfiles
+       case "servers":
+               return ATSConfigMetaDataConfigFileScopeServers
+       case "cdns":
+               return ATSConfigMetaDataConfigFileScopeCDNs
+       default:
+               return ATSConfigMetaDataConfigFileScopeInvalid
+       }
+}
diff --git a/lib/go-tc/enum.go b/lib/go-tc/enum.go
index fbf7c6d..7a95400 100644
--- a/lib/go-tc/enum.go
+++ b/lib/go-tc/enum.go
@@ -70,6 +70,10 @@ const RouterTypeName = "CCR"
 const EdgeTypePrefix = "EDGE"
 const MidTypePrefix = "MID"
 
+const OriginTypeName = "ORG"
+
+const CacheGroupOriginTypeName = "ORG_LOC"
+
 func (c CacheName) String() string {
        return string(c)
 }
@@ -560,6 +564,31 @@ func (t DSType) UsesMidCache() bool {
        return true
 }
 
+// QStringIgnore is an entry in the delivery_service table qstring_ignore 
column, and represents how to treat the URL query string for requests to that 
delivery service.
+// This enum's String function returns the numeric representation, because it 
is a legacy database value, and the number should be kept for both database and 
API JSON uses. For the same reason, this enum has no FromString function.
+type QStringIgnore int
+
+const (
+       QStringIgnoreUseInCacheKeyAndPassUp    QStringIgnore = 0
+       QStringIgnoreIgnoreInCacheKeyAndPassUp QStringIgnore = 1
+       QStringIgnoreDrop                      QStringIgnore = 2
+)
+
+// String returns the string number of the QStringIgnore value.
+// Note this returns the number, not a human-readable value, because 
QStringIgnore is a legacy database sigil, and both database and API JSON uses 
should use the number. This also returns 'INVALID' for unknown values, to fail 
fast in the event of bad data.
+func (e QStringIgnore) String() string {
+       switch e {
+       case QStringIgnoreUseInCacheKeyAndPassUp:
+               fallthrough
+       case QStringIgnoreIgnoreInCacheKeyAndPassUp:
+               fallthrough
+       case QStringIgnoreDrop:
+               return strconv.Itoa(int(e))
+       default:
+               return "INVALID"
+       }
+}
+
 type DSMatchType string
 
 const (
diff --git a/lib/go-util/str.go b/lib/go-util/str.go
new file mode 100644
index 0000000..3bfa7b7
--- /dev/null
+++ b/lib/go-util/str.go
@@ -0,0 +1,35 @@
+package util
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// RemoveStrDuplicates removes duplicates from strings, considering a map of 
already-seen duplicates.
+// Returns the strings which are unique, and not already present in seen; and 
a map of the unique strings in inputStrings and seenStrings.
+//
+// This can be used, for example, to remove duplicates from multiple lists of 
strings, in order, using a shared map of seen strings.
+func RemoveStrDuplicates(inputStrings []string, seenStrings 
map[string]struct{}) ([]string, map[string]struct{}) {
+       uniqueStrings := []string{}
+       for _, str := range inputStrings {
+               if _, ok := seenStrings[str]; !ok {
+                       uniqueStrings = append(uniqueStrings, str)
+                       seenStrings[str] = struct{}{}
+               }
+       }
+       return uniqueStrings, seenStrings
+}
diff --git a/traffic_ops/client/atsconfig.go b/traffic_ops/client/atsconfig.go
index c2ade03..6432c23 100644
--- a/traffic_ops/client/atsconfig.go
+++ b/traffic_ops/client/atsconfig.go
@@ -13,11 +13,60 @@
 package client
 
 import (
+       "encoding/json"
        "io/ioutil"
        "net/http"
        "strconv"
+
+       "github.com/apache/trafficcontrol/lib/go-tc"
 )
 
+func (to *Session) GetATSServerConfigList(serverID int) (tc.ATSConfigMetaData, 
ReqInf, error) {
+       resp, remoteAddr, err := to.request(http.MethodGet, 
apiBase+"/servers/"+strconv.Itoa(serverID)+"/configfiles/ats", nil)
+       reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: 
remoteAddr}
+       if err != nil {
+               return tc.ATSConfigMetaData{}, reqInf, err
+       }
+       defer resp.Body.Close()
+
+       data := tc.ATSConfigMetaData{}
+       if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+               return tc.ATSConfigMetaData{}, reqInf, err
+       }
+       return data, reqInf, nil
+}
+
+func (to *Session) GetATSServerConfigListByName(serverHostName string) 
(tc.ATSConfigMetaData, ReqInf, error) {
+       resp, remoteAddr, err := to.request(http.MethodGet, 
apiBase+"/servers/"+serverHostName+"/configfiles/ats", nil)
+       reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: 
remoteAddr}
+       if err != nil {
+               return tc.ATSConfigMetaData{}, reqInf, err
+       }
+       defer resp.Body.Close()
+
+       data := tc.ATSConfigMetaData{}
+       if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+               return tc.ATSConfigMetaData{}, reqInf, err
+       }
+       return data, reqInf, nil
+}
+
+func (to *Session) GetATSServerConfig(serverID int, fileName string) (string, 
ReqInf, error) {
+       return to.getConfigFile(apiBase + "/servers/" + strconv.Itoa(serverID) 
+ "/configfiles/ats/" + fileName)
+}
+
+func (to *Session) GetATSServerConfigByName(serverHostName string, fileName 
string) (string, ReqInf, error) {
+       return to.getConfigFile(apiBase + "/servers/" + serverHostName + 
"/configfiles/ats/" + fileName)
+}
+
+func (to *Session) GetATSProfileConfig(profileID int, fileName string) 
(string, ReqInf, error) {
+       return to.getConfigFile(apiBase + "/profiles/" + 
strconv.Itoa(profileID) + "/configfiles/ats/" + fileName)
+}
+
+func (to *Session) GetATSProfileConfigByName(profileName string, fileName 
string) (string, ReqInf, error) {
+       return to.getConfigFile(apiBase + "/profiles/" + profileName + 
"/configfiles/ats/" + fileName)
+}
+
 func (to *Session) GetATSCDNConfig(cdnID int, fileName string) (string, 
ReqInf, error) {
        return to.getConfigFile(apiBase + "/cdns/" + strconv.Itoa(cdnID) + 
"/configfiles/ats/" + fileName)
 }
diff --git a/traffic_ops/client/session.go b/traffic_ops/client/session.go
index 4faf0b2..116a48c 100644
--- a/traffic_ops/client/session.go
+++ b/traffic_ops/client/session.go
@@ -269,6 +269,8 @@ func (to *Session) ErrUnlessOK(resp *http.Response, 
remoteAddr net.Addr, err err
 func (to *Session) getURL(path string) string { return to.URL + path }
 
 // request performs the HTTP request to Traffic Ops, trying to refresh the 
cookie if an Unauthorized or Forbidden code is received. It only tries once. If 
the login fails, the original Unauthorized/Forbidden response is returned. If 
the login succeeds and the subsequent re-request fails, the re-request's 
response is returned even if it's another Unauthorized/Forbidden.
+// Returns the response, the remote address of the Traffic Ops instance used, 
and any error.
+// The returned net.Addr is guaranteed to be either nil or valid, even if the 
returned error is not nil. Callers are encouraged to check and use the net.Addr 
if an error is returned, and use the remote address in their own error 
messages. This violates the Go idiom that a non-nil error implies all other 
values are undefined, but it's more straightforward than alternatives like 
typecasting.
 func (to *Session) request(method, path string, body []byte) (*http.Response, 
net.Addr, error) {
        r, remoteAddr, err := to.rawRequest(method, path, body)
        if err != nil {
@@ -287,6 +289,8 @@ func (to *Session) request(method, path string, body 
[]byte) (*http.Response, ne
 }
 
 // rawRequest performs the actual HTTP request to Traffic Ops, simply, without 
trying to refresh the cookie if an Unauthorized code is returned.
+// Returns the response, the remote address of the Traffic Ops instance used, 
and any error.
+// The returned net.Addr is guaranteed to be either nil or valid, even if the 
returned error is not nil. Callers are encouraged to check and use the net.Addr 
if an error is returned, and use the remote address in their own error 
messages. This violates the Go idiom that a non-nil error implies all other 
values are undefined, but it's more straightforward than alternatives like 
typecasting.
 func (to *Session) rawRequest(method, path string, body []byte) 
(*http.Response, net.Addr, error) {
        url := to.getURL(path)
 
diff --git a/traffic_ops/testing/api/v14/atsconfig_test.go 
b/traffic_ops/testing/api/v14/atsconfig_test.go
new file mode 100644
index 0000000..3289075
--- /dev/null
+++ b/traffic_ops/testing/api/v14/atsconfig_test.go
@@ -0,0 +1,82 @@
+package v14
+
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+import (
+       "testing"
+)
+
+func TestATSConfigs(t *testing.T) {
+       WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, 
Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, 
DeliveryServices}, func() {
+               GetTestATSConfigs(t)
+       })
+}
+
+func GetTestATSConfigs(t *testing.T) {
+       if len(testData.Servers) < 1 {
+               t.Fatalf("cannot GET Server: no test data\n")
+       }
+       testServer := testData.Servers[0]
+
+       serverList, _, err := TOSession.GetServerByHostName(testServer.HostName)
+       if err != nil {
+               t.Fatalf("cannot GET Server: %v\n", err)
+       }
+       if len(serverList) < 1 {
+               t.Fatalf("cannot GET Server '" + testServer.HostName + "', 
returned no servers\n")
+       }
+       server := serverList[0]
+
+       _, _, err = TOSession.GetATSServerConfigList(server.ID)
+       if err != nil {
+               t.Fatalf("Getting server '" + server.HostName + "' config list: 
" + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSServerConfigListByName(server.HostName)
+       if err != nil {
+               t.Fatalf("Getting server by name '" + server.HostName + "' 
config list: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSServerConfig(server.ID, "remap.config")
+       if err != nil {
+               t.Fatalf("Getting server '" + server.HostName + "' config 
remap.config: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSServerConfigByName(server.HostName, 
"remap.config")
+       if err != nil {
+               t.Fatalf("Getting server by name '" + server.HostName + "' 
config remap.config: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSProfileConfig(server.ProfileID, 
"storage.config")
+       if err != nil {
+               t.Fatalf("Getting profile '" + server.Profile + "' config 
storage.config: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSProfileConfigByName(server.Profile, 
"storage.config")
+       if err != nil {
+               t.Fatalf("Getting profile by name '" + server.Profile + "' 
config storage.config: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSCDNConfig(server.CDNID, "bg_fetch.config")
+       if err != nil {
+               t.Fatalf("Getting cdn '" + server.CDNName + "' config 
bg_fetch.config: " + err.Error() + "\n")
+       }
+
+       _, _, err = TOSession.GetATSCDNConfigByName(server.CDNName, 
"bg_fetch.config")
+       if err != nil {
+               t.Fatalf("Getting cdn by name '" + server.CDNName + "' config 
bg_fetch.config: " + err.Error() + "\n")
+       }
+}
diff --git a/traffic_ops/testing/api/v14/parentdotconfig_test.go 
b/traffic_ops/testing/api/v14/parentdotconfig_test.go
new file mode 100644
index 0000000..909e01b
--- /dev/null
+++ b/traffic_ops/testing/api/v14/parentdotconfig_test.go
@@ -0,0 +1,155 @@
+package v14
+
+/*
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+import (
+       "net/url"
+       "strconv"
+       "strings"
+       "testing"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+)
+
+func TestParentDotConfig(t *testing.T) {
+       WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, 
Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, 
DeliveryServices}, func() {
+               defer DeleteTestDeliveryServiceServersCreated(t)
+               CreateTestDeliveryServiceServers(t)
+               GetTestParentDotConfig(t)
+       })
+}
+
+func GetTestParentDotConfig(t *testing.T) {
+       dsServers, _, err := TOSession.GetDeliveryServiceServers()
+       if err != nil {
+               t.Fatalf("GET delivery service servers: %v\n", err)
+       } else if len(dsServers.Response) == 0 {
+               t.Fatalf("GET delivery service servers: no servers found\n")
+       } else if dsServers.Response[0].Server == nil {
+               t.Fatalf("GET delivery service servers: returned nil server\n")
+       } else if dsServers.Response[0].DeliveryService == nil {
+               t.Fatalf("GET delivery service servers: returned nil ds\n")
+       }
+       serverID := *dsServers.Response[0].Server
+
+       ds, _, err := 
TOSession.GetDeliveryService(strconv.Itoa(*dsServers.Response[0].DeliveryService))
+       if err != nil {
+               t.Fatalf("Getting ds %+v: "+err.Error()+"\n", 
*dsServers.Response[0].DeliveryService)
+       } else if ds == nil {
+               t.Fatalf("Getting ds %+v: "+"got nil response"+"\n", 
*dsServers.Response[0].DeliveryService)
+       } else if ds.OrgServerFQDN == "" {
+               t.Fatalf("Getting ds %+v: "+"got empty ds.OrgServerFQDN"+"\n", 
*dsServers.Response[0].DeliveryService)
+       }
+
+       originURI, err := url.Parse(ds.OrgServerFQDN)
+       if err != nil {
+               t.Fatalf("Getting ds %+v: "+" ds.OrgServerFQDN '%+v' failed to 
parse as a URL: %+v\n", *dsServers.Response[0].DeliveryService, 
ds.OrgServerFQDN, err)
+       }
+       originHost := originURI.Hostname()
+
+       parentDotConfig, _, err := TOSession.GetATSServerConfig(serverID, 
"parent.config")
+       if err != nil {
+               t.Fatalf("Getting server %+v config parent.config: 
"+err.Error()+"\n", serverID)
+       }
+
+       if !strings.Contains(parentDotConfig, originHost) {
+               t.Errorf("expected: parent.config to contain delivery service 
origin FQDN '%+v' host '%+v', actual: '''%+v'''", ds.OrgServerFQDN, originHost, 
parentDotConfig)
+       }
+}
+
+func CreateTestDeliveryServiceServers(t *testing.T) {
+       log.Debugln("DeleteTestDeliveryServiceServers")
+
+       dses, _, err := TOSession.GetDeliveryServices()
+       if err != nil {
+               t.Errorf("cannot GET DeliveryServices: %v\n", err)
+       }
+       if len(dses) < 1 {
+               t.Errorf("GET DeliveryServices returned no dses, must have at 
least 1 to test ds-servers")
+       }
+       ds := dses[0]
+
+       servers, _, err := TOSession.GetServers()
+       if err != nil {
+               t.Errorf("cannot GET Servers: %v\n", err)
+       }
+       if len(servers) < 1 {
+               t.Errorf("GET Servers returned no dses, must have at least 1 to 
test ds-servers")
+       }
+       server := servers[0]
+
+       _, err = TOSession.CreateDeliveryServiceServers(ds.ID, 
[]int{server.ID}, true)
+       if err != nil {
+               t.Errorf("POST delivery service servers: %v\n", err)
+       }
+}
+
+// DeleteTestDeliveryServiceServersCreated deletes the dss assignments created 
by CreateTestDeliveryServiceServers.
+func DeleteTestDeliveryServiceServersCreated(t *testing.T) {
+       dses, _, err := TOSession.GetDeliveryServices()
+       if err != nil {
+               t.Errorf("cannot GET DeliveryServices: %v\n", err)
+       }
+       if len(dses) < 1 {
+               t.Errorf("GET DeliveryServices returned no dses, must have at 
least 1 to test ds-servers")
+       }
+       ds := dses[0]
+
+       servers, _, err := TOSession.GetServers()
+       if err != nil {
+               t.Errorf("cannot GET Servers: %v\n", err)
+       }
+       if len(servers) < 1 {
+               t.Errorf("GET Servers returned no dses, must have at least 1 to 
test ds-servers")
+       }
+       server := servers[0]
+
+       dsServers, _, err := TOSession.GetDeliveryServiceServers()
+       if err != nil {
+               t.Errorf("GET delivery service servers: %v\n", err)
+       }
+
+       found := false
+       for _, dss := range dsServers.Response {
+               if *dss.DeliveryService == ds.ID && *dss.Server == server.ID {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               t.Errorf("POST delivery service servers returned success, but 
ds-server not in GET")
+       }
+
+       if _, _, err := TOSession.DeleteDeliveryServiceServer(ds.ID, 
server.ID); err != nil {
+               t.Errorf("DELETE delivery service server: %v\n", err)
+       }
+
+       dsServers, _, err = TOSession.GetDeliveryServiceServers()
+       if err != nil {
+               t.Errorf("GET delivery service servers: %v\n", err)
+       }
+
+       found = false
+       for _, dss := range dsServers.Response {
+               if *dss.DeliveryService == ds.ID && *dss.Server == server.ID {
+                       found = true
+                       break
+               }
+       }
+       if found {
+               t.Errorf("DELETE delivery service servers returned success, but 
still in GET")
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go 
b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
new file mode 100644
index 0000000..d4bee85
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
@@ -0,0 +1,1302 @@
+package ats
+
+/*
+ * 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 (
+       "database/sql"
+       "errors"
+       "net/http"
+       "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"
+       "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 {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       idOrHost := strings.TrimSuffix(inf.Params["id-or-host"], ".json")
+       hostName := ""
+       isHost := false
+       id, err := strconv.Atoi(idOrHost)
+       if err != nil {
+               isHost = true
+               hostName = idOrHost
+       }
+
+       serverInfo, ok, err := &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()))
+               return
+       }
+       if !ok {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, 
errors.New("server not found"), nil)
+               return
+       }
+
+       atsMajorVer, err := GetATSMajorVersion(inf.Tx.Tx, serverInfo.ProfileID)
+       if err != nil {
+               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)
+       if err != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
nil, errors.New("Getting header comment: "+err.Error()))
+               return
+       }
+
+       textArr := []string{}
+       text := ""
+       // TODO put these in separate functions. No if-statement should be this 
long.
+       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, "")
+       } 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
+       }
+       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
+       }
+       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
+       }
+       return host + ":" + strconv.Itoa(p.Port) + "|" + p.Weight + ";"
+}
+
+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 ParentConfigDSSortByName []ParentConfigDS
+
+func (s ParentConfigDSSortByName) Len() int      { return 
len(([]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) {
+       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) {
+       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) {
+       log.Errorf("getServerInfo qq "+qry+" p %++v\n", qryParams)
+       s := 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
+               }
+               return nil, false, errors.New("querying server info: " + 
err.Error())
+       }
+       return &s, true, nil
+}
+
+func ServerInfoQuery() string {
+       return `
+SELECT
+  c.name as cdn,
+  s.cdn_id,
+  s.id,
+  s.host_name,
+  c.domain_name,
+  s.ip_address,
+  s.profile AS profile_id,
+  p.name AS profile_name,
+  s.tcp_port,
+  t.name as type,
+  s.cachegroup,
+  COALESCE(cg.parent_cachegroup_id, ` + strconv.Itoa(InvalidID) + `),
+  COALESCE(cg.secondary_parent_cachegroup_id, ` + strconv.Itoa(InvalidID) + `),
+  COALESCE(parentt.name, '') as parent_cachegroup_type,
+  COALESCE(sparentt.name, '') as secondary_parent_cachegroup_type
+FROM
+  server s
+  JOIN cdn c ON s.cdn_id = c.id
+  JOIN type t ON s.type = t.id
+  JOIN profile p ON p.id = s.profile
+  JOIN cachegroup cg on s.cachegroup = cg.id
+  LEFT JOIN type parentt on parentt.id = (select type from cachegroup where id 
= cg.parent_cachegroup_id)
+  LEFT JOIN type sparentt on sparentt.id = (select type from cachegroup where 
id = cg.secondary_parent_cachegroup_id)
+`
+}
+
+// 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, serverProfileID, 
"package", "trafficserver")
+       if err != nil {
+               return 0, errors.New("getting profile param value: " + 
err.Error())
+       }
+       if len(atsVersion) == 0 {
+               atsVersion = 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
+}
+
+// GetProfileParamValue gets the value of a parameter assigned to a profile, 
by name and config file.
+// Returns the parameter, whether it existed, and any error.
+func GetProfileParamValue(tx *sql.Tx, profileID ProfileID, configFile string, 
name string) (string, bool, error) {
+       qry := `
+SELECT
+  p.value
+FROM
+  parameter p
+  JOIN profile_parameter pp ON p.id = pp.parameter
+WHERE
+  pp.profile = $1
+  AND p.config_file = $2
+  AND p.name = $3
+`
+       val := ""
+       if err := tx.QueryRow(qry, profileID, configFile, name).Scan(&val); err 
!= nil {
+               if err == sql.ErrNoRows {
+                       return "", false, nil
+               }
+               return "", false, errors.New("querying: " + err.Error())
+       }
+       return val, true, nil
+}
+
+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
+}
+
+func ParentConfigDSQuerySelect() string {
+       return `
+SELECT
+  ds.xml_id,
+  COALESCE(ds.qstring_ignore, ` + 
tc.QStringIgnoreUseInCacheKeyAndPassUp.String() + `),
+  COALESCE((SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', 
o.port::text), ':')
+    FROM origin o
+    WHERE o.deliveryservice = ds.id
+    AND o.is_primary), '') as org_server_fqdn,
+  COALESCE(ds.multi_site_origin, false),
+  COALESCE(ds.origin_shield, ''),
+  dt.name AS ds_type
+`
+}
+
+const ParentConfigDSQueryFromTopLevel = `
+FROM
+  deliveryservice ds
+  JOIN type as dt ON ds.type = dt.id
+  JOIN cdn ON cdn.id = ds.cdn_id
+` // TODO Perl does 'JOIN deliveryservice_regex dsr ON dsr.deliveryservice = 
ds.id   JOIN regex r ON dsr.regex = r.id   JOIN type as rt ON r.type = rt.id' 
and orders by, but doesn't use; ensure it isn't necessary
+
+const ParentConfigDSQueryFrom = ParentConfigDSQueryFromTopLevel + `
+`
+
+const ParentConfigDSQueryOrder = `
+ORDER BY ds.id
+` // TODO: perl does 'ORDER BY ds.id, rt.name, dsr.set_number' - but doesn't 
actually use regexes - ensure it isn't necessary
+
+const ParentConfigDSQueryWhere = `
+WHERE ds.id in (SELECT DISTINCT(dss.deliveryservice) FROM 
deliveryservice_server dss where dss.server = $1)
+`
+
+const ParentConfigDSQueryWhereTopLevel = `
+WHERE
+  cdn.name = $1
+  AND ds.id in (SELECT deliveryservice_server.deliveryservice FROM 
deliveryservice_server)
+  AND ds.active = true
+`
+
+func ParentConfigDSQuery() string {
+       return ParentConfigDSQuerySelect() +
+               ParentConfigDSQueryFrom +
+               ParentConfigDSQueryWhere +
+               ParentConfigDSQueryOrder
+}
+
+func ParentConfigDSQueryTopLevel() string {
+       return ParentConfigDSQuerySelect() +
+               ParentConfigDSQueryFromTopLevel +
+               ParentConfigDSQueryWhereTopLevel +
+               ParentConfigDSQueryOrder
+}
+
+func getParentConfigDSTopLevel(tx *sql.Tx, cdnName tc.CDNName) 
([]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{}
+       for _, ds := range dses {
+               topDSes = append(topDSes, 
ParentConfigDSTopLevel{ParentConfigDS: ds})
+       }
+
+       dsesWithParams, err := getParentConfigDSParamsTopLevel(tx, topDSes)
+       if err != nil {
+               return nil, errors.New("getting top level ds params: " + 
err.Error())
+       }
+
+       return dsesWithParams, nil
+}
+
+func getParentConfigDS(tx *sql.Tx, serverID int) ([]ParentConfigDS, error) {
+
+       dses, err := getParentConfigDSRaw(tx, ParentConfigDSQuery(), 
[]interface{}{serverID})
+
+       if err != nil {
+               return nil, errors.New("getting raw parent config ds: " + 
err.Error())
+       }
+
+       dsesWithParams, err := getParentConfigDSParams(tx, dses)
+       if err != nil {
+               return nil, errors.New("getting ds params: " + err.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
+  pa.name,
+  pa.value
+FROM
+  parameter pa
+  JOIN profile_parameter pp ON pp.parameter = pa.id
+  JOIN profile pr ON pr.id = pp.profile
+  JOIN server s on s.profile = pr.id
+WHERE
+  s.id = $1
+  AND pa.config_file = 'parent.config'
+  AND pa.name IN (
+    '` + ParentConfigParamQStringHandling + `',
+    '` + ParentConfigParamAlgorithm + `',
+    '` + ParentConfigParamQString + `'
+  )
+`
+       rows, err := tx.Query(qry, serverID)
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+       params := map[string]string{}
+       for rows.Next() {
+               name := ""
+               val := ""
+               if err := rows.Scan(&name, &val); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               params[name] = val
+       }
+       return params, nil
+}
+
+func getParentConfigDSRaw(tx *sql.Tx, qry string, qryParams []interface{}) 
([]ParentConfigDS, error) {
+       rows, err := tx.Query(qry, qryParams...)
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+       dses := []ParentConfigDS{}
+       for rows.Next() {
+               d := 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())
+               }
+               if d.OriginFQDN == "" {
+                       // TODO skip ANY_MAP DSes? Why? Did Perl, I didn't see 
it?
+                       log.Errorf("parent.config generation: getting parent 
config ds: server %+v has no origin, skipping!\n", d.Name)
+                       continue
+               }
+               d.Type = tc.DSTypeFromString(string(d.Type))
+               dses = append(dses, d)
+       }
+       return dses, nil
+}
+
+func parentConfigDSesToNames(dses []ParentConfigDS) []string {
+       names := []string{}
+       for _, ds := range dses {
+               names = append(names, string(ds.Name))
+       }
+       return names
+}
+
+func parentConfigDSesToNamesTopLevel(dses []ParentConfigDSTopLevel) []string {
+       names := []string{}
+       for _, ds := range dses {
+               names = append(names, string(ds.Name))
+       }
+       return names
+}
+
+const ParentConfigDSParamsQuerySelect = `
+SELECT
+  ds.xml_id,
+  pa.name,
+  pa.value
+`
+const ParentConfigDSParamsQueryFrom = `
+FROM
+  parameter pa
+  JOIN profile_parameter pp ON pp.parameter = pa.id
+  JOIN profile pr ON pr.id = pp.profile
+  JOIN deliveryservice ds on ds.profile = pr.id
+`
+const ParentConfigDSParamsQueryWhere = `
+WHERE
+  pa.config_file = 'parent.config'
+  AND ds.xml_id = ANY($1)
+  AND pa.name IN (
+    '` + ParentConfigParamQStringHandling + `'
+  )
+`
+
+var ParentConfigDSParamsQueryWhereTopLevel = `
+WHERE
+  pa.config_file = 'parent.config'
+  AND ds.xml_id = ANY($1)
+  AND pa.name IN (
+    '` + ParentConfigParamQStringHandling + `',
+    '` + ParentConfigParamMSOAlgorithm + `',
+    '` + ParentConfigParamMSOParentRetry + `',
+    '` + ParentConfigParamUnavailableServerRetryResponses + `',
+    '` + ParentConfigParamMaxSimpleRetries + `',
+    '` + ParentConfigParamMaxUnavailableServerRetries + `'
+  )
+`
+
+const ParentConfigDSParamsQuery = ParentConfigDSParamsQuerySelect + 
ParentConfigDSParamsQueryFrom + ParentConfigDSParamsQueryWhere
+
+var ParentConfigDSParamsQueryTopLevel = ParentConfigDSParamsQuerySelect + 
ParentConfigDSParamsQueryFrom + ParentConfigDSParamsQueryWhereTopLevel
+
+func getParentConfigDSParams(tx *sql.Tx, dses []ParentConfigDS) 
([]ParentConfigDS, error) {
+       params, err := getParentConfigDSParamsRaw(tx, 
ParentConfigDSParamsQuery, parentConfigDSesToNames(dses))
+       if err != nil {
+               return nil, err
+       }
+       for i, ds := range dses {
+               dsParams, ok := params[ds.Name]
+               if !ok {
+                       continue
+               }
+               if v, ok := dsParams[ParentConfigParamQStringHandling]; ok {
+                       ds.QStringHandling = v
+                       dses[i] = ds
+               }
+       }
+       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) {
+       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 {
+                       ds.QStringHandling = v
+               }
+               if v, ok := dsParams[ParentConfigParamMSOAlgorithm]; ok && 
strings.TrimSpace(v) != "" {
+                       ds.MSOAlgorithm = v
+               } else {
+                       ds.MSOAlgorithm = ParentConfigDSParamDefaultMSOAlgorithm
+               }
+               if v, ok := dsParams[ParentConfigParamMSOParentRetry]; ok {
+                       ds.MSOParentRetry = v
+               } else {
+                       ds.MSOParentRetry = 
ParentConfigDSParamDefaultMSOParentRetry
+               }
+               if v, ok := 
dsParams[ParentConfigParamUnavailableServerRetryResponses]; ok {
+                       ds.MSOUnavailableServerRetryResponses = v
+               } else {
+                       ds.MSOUnavailableServerRetryResponses = 
ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses
+               }
+               if v, ok := dsParams[ParentConfigParamMaxSimpleRetries]; ok {
+                       ds.MSOMaxSimpleRetries = v
+               } else {
+                       ds.MSOMaxSimpleRetries = 
ParentConfigDSParamDefaultMaxSimpleRetries
+               }
+               if v, ok := 
dsParams[ParentConfigParamMaxUnavailableServerRetries]; ok {
+                       ds.MSOMaxUnavailableServerRetries = v
+               } else {
+                       ds.MSOMaxUnavailableServerRetries = 
ParentConfigDSParamDefaultMaxUnavailableServerRetries
+               }
+               dses[i] = ds
+       }
+       return dses, nil
+}
+
+func getParentConfigDSParamsRaw(tx *sql.Tx, qry string, dsNames []string) 
(map[tc.DeliveryServiceName]map[string]string, error) {
+       rows, err := tx.Query(qry, pq.Array(dsNames))
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       params := map[tc.DeliveryServiceName]map[string]string{}
+       for rows.Next() {
+               dsName := tc.DeliveryServiceName("")
+               pName := ""
+               pVal := ""
+               if err := rows.Scan(&dsName, &pName, &pVal); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               if _, ok := params[dsName]; !ok {
+                       params[dsName] = map[string]string{}
+               }
+               params[dsName][pName] = pVal
+       }
+       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{}
+
+       serverDomain, ok, err := getCDNDomainByProfileID(tx, server.ProfileID)
+       if err != nil {
+               return nil, errors.New("getting CDN domain from profile ID: " + 
err.Error())
+       } else if !ok || serverDomain == "" {
+               return parentInfos, nil // TODO warn? Perl doesn't.
+       }
+
+       profileCaches, originServers, err := 
getServerParentCacheGroupProfiles(tx, server)
+       if err != nil {
+               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
+}
+
+// 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) {
+       // 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
+
+       qry := ""
+       if server.IsTopLevelCache() {
+               // multisite origins take all the org groups in to account
+               qry = `
+WITH parent_cachegroup_ids AS (
+  SELECT cg.id as v
+  FROM cachegroup cg
+  JOIN type on type.id = cg.type
+  WHERE type.name = '` + tc.CacheGroupOriginTypeName + `'
+)
+`
+       } else {
+               qry = `
+WITH server_cachegroup_ids AS (
+  SELECT cachegroup as v FROM server WHERE id = $2
+),
+parent_cachegroup_ids AS (
+  SELECT parent_cachegroup_id as v
+  FROM cachegroup WHERE id IN (SELECT v from server_cachegroup_ids)
+  UNION ALL
+  SELECT secondary_parent_cachegroup_id as v
+  FROM cachegroup WHERE id IN (SELECT v from server_cachegroup_ids)
+)
+`
+       }
+
+       qry += `
+SELECT
+  s.id,
+  s.host_name,
+  s.ip_address,
+  s.tcp_port,
+  s.cachegroup,
+  s.status,
+  s.type,
+  s.profile,
+  s.cdn_id,
+  stype.name as type_name,
+  s.domain_name
+FROM
+  server s
+  JOIN type stype ON s.type = stype.id
+  JOIN cachegroup cg ON cg.id = s.cachegroup
+  JOIN cdn on s.cdn_id = cdn.id
+  JOIN status st ON st.id = s.status
+WHERE
+  cg.id IN (SELECT v FROM parent_cachegroup_ids)
+  AND (stype.name = '` + tc.OriginTypeName + `' OR stype.name LIKE '` + 
tc.EdgeTypePrefix + `%' OR stype.name LIKE '` + tc.MidTypePrefix + `%')
+  AND (st.name = '` + string(tc.CacheStatusReported) + `' OR st.name = '` + 
string(tc.CacheStatusOnline) + `')
+  AND cdn.name = $1
+`
+
+       // TODO move qry, qryParams to separate funcs/consts
+       qryParams := []interface{}{}
+       if server.IsTopLevelCache() {
+               qryParams = []interface{}{server.CDN}
+       } else {
+               qryParams = []interface{}{server.CDN, server.ID}
+       }
+
+       rows, err := tx.Query(qry, qryParams...)
+       if err != nil {
+               return nil, nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       cgServerIDs := []int{}
+       cgServers := []CGServer{}
+       for rows.Next() {
+               s := 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())
+               }
+               cgServers = append(cgServers, s)
+               cgServerIDs = append(cgServerIDs, int(s.ServerID))
+       }
+
+       cgServerDSes, err := getServerDSes(tx, cgServerIDs)
+       if err != nil {
+               return nil, nil, errors.New("getting cachegroup server 
deliveryservices: " + err.Error())
+       }
+
+       profileParams, err := getParentConfigServerCacheProfileParams(tx, 
cgServerIDs) // TODO change to take cg IDs directly?
+       if err != nil {
+               return nil, nil, errors.New("getting cachegroup server profile 
params: " + err.Error())
+       }
+
+       allDSMap := map[DeliveryServiceID]struct{}{}
+       for _, dses := range cgServerDSes {
+               for _, ds := range dses {
+                       allDSMap[ds] = struct{}{}
+               }
+       }
+       allDSes := []int{}
+       for ds, _ := range allDSMap {
+               allDSes = append(allDSes, int(ds))
+       }
+
+       dsOrigins, err := getDSOrigins(tx, allDSes)
+       if err != nil {
+               return nil, nil, errors.New("getting deliveryservice origins: " 
+ err.Error())
+       }
+
+       for _, cgServer := range cgServers {
+               if cgServer.TypeName == tc.OriginTypeName {
+                       dses := cgServerDSes[cgServer.ServerID]
+                       for _, ds := range dses {
+                               orgURI := dsOrigins[ds]
+                               originServers[orgURI.Host] = 
append(originServers[orgURI.Host], cgServer)
+                       }
+               } else {
+                       originServers[DeliveryServicesAllParentsKey] = 
append(originServers[DeliveryServicesAllParentsKey], cgServer)
+               }
+
+               if _, profileCachesHasProfile := 
profileCaches[cgServer.ProfileID]; !profileCachesHasProfile {
+                       defaultProfileCache := 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
+                       } else {
+                               profileCaches[cgServer.ProfileID] = profileCache
+                       }
+               }
+       }
+       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{}
+       if len(serverIDs) == 0 {
+               return sds, nil
+       }
+       qry := `
+SELECT
+  dss.server,
+  dss.deliveryservice
+FROM
+  deliveryservice_server dss
+WHERE
+  dss.server = ANY($1)
+`
+       rows, err := tx.Query(qry, pq.Array(serverIDs))
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       for rows.Next() {
+               sID := ServerID(0)
+               dsID := DeliveryServiceID(0)
+               if err := rows.Scan(&sID, &dsID); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               sds[sID] = append(sds[sID], dsID)
+       }
+       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{}
+       if len(dsIDs) == 0 {
+               return origins, nil
+       }
+       qry := `
+SELECT
+  ds.id,
+  o.protocol::text,
+  o.fqdn,
+  COALESCE(o.port::text, '')
+FROM
+  deliveryservice ds
+  JOIN origin o ON o.deliveryservice = ds.id
+WHERE
+  ds.id = ANY($1)
+  AND o.is_primary
+`
+       rows, err := tx.Query(qry, pq.Array(dsIDs))
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       for rows.Next() {
+               id := DeliveryServiceID(0)
+               uri := &OriginURI{}
+               if err := rows.Scan(&id, &uri.Scheme, &uri.Host, &uri.Port); 
err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               if uri.Port == "" {
+                       if uri.Scheme == "http" {
+                               uri.Port = "80"
+                       } else if uri.Scheme == "https" {
+                               uri.Port = "443"
+                       } else {
+                               log.Warnf("parent.config generation: origin had 
unknown scheme '" + uri.Scheme + "' and no port; leaving port empty")
+                       }
+               }
+               origins[id] = uri
+       }
+       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) {
+       qry := `
+SELECT
+  pr.id,
+  pa.name,
+  pa.value
+FROM
+  parameter pa
+  JOIN profile_parameter pp ON pp.parameter = pa.id
+  JOIN profile pr ON pr.id = pp.profile
+  JOIN server s on s.profile = pr.id
+WHERE
+  s.id = ANY($1)
+  AND pa.config_file = 'parent.config'
+  AND pa.name IN (
+    '` + ParentConfigCacheParamWeight + `',
+    '` + ParentConfigCacheParamPort + `',
+    '` + ParentConfigCacheParamUseIP + `',
+    '` + ParentConfigCacheParamRank + `',
+    '` + ParentConfigCacheParamNotAParent + `'
+  )
+`
+       rows, err := tx.Query(qry, pq.Array(serverIDs))
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       type Param struct {
+               ProfileID ProfileID
+               Name      string
+               Val       string
+       }
+
+       params := []Param{}
+       for rows.Next() {
+               p := Param{}
+               if err := rows.Scan(&p.ProfileID, &p.Name, &p.Val); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               params = append(params, p)
+       }
+
+       sParams := map[ProfileID]ProfileCache{} // TODO change to map of 
pointers? Does efficiency matter?
+       for _, param := range params {
+               profileCache, ok := sParams[param.ProfileID]
+               if !ok {
+                       profileCache = DefaultProfileCache()
+               }
+               switch param.Name {
+               case ParentConfigCacheParamWeight:
+                       // f, err := strconv.ParseFloat(param.Val, 64)
+                       // if err != nil {
+                       //      log.Errorln("parent.config generation: weight 
param is not a float, skipping! : " + err.Error())
+                       // } else {
+                       //      profileCache.Weight = f
+                       // }
+                       // TODO validate float?
+                       profileCache.Weight = param.Val
+               case 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:
+                       profileCache.UseIP = param.Val == "1"
+               case 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())
+                       } else {
+                               profileCache.Rank = int(i)
+                       }
+
+               case ParentConfigCacheParamNotAParent:
+                       profileCache.NotAParent = param.Val != "false"
+               default:
+                       return nil, errors.New("query returned unexpected 
param: " + param.Name)
+               }
+               sParams[param.ProfileID] = profileCache
+       }
+       return sParams, nil
+}
+
+func getServerParams(tx *sql.Tx, serverID int) (map[string]string, error) {
+       qry := `
+SELECT
+  pa.name
+  pa.value
+FROM
+  parameter pa
+  JOIN profile_parameter pp ON pp.parameter = pa.id
+  JOIN profile pr ON pr.id = pp.profile
+  JOIN server s on s.profile = pr.id
+WHERE
+  s.id = $1
+  AND pa.config_file = 'parent.config'
+  AND pa.name IN (
+    '` + ParentConfigParamQStringHandling + `',
+    '` + ParentConfigParamAlgorithm + `',
+    '` + ParentConfigParamQString + `'
+  )
+`
+       rows, err := tx.Query(qry, serverID)
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+       params := map[string]string{}
+       for rows.Next() {
+               name := ""
+               val := ""
+               if err := rows.Scan(&name, &val); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               params[name] = val
+       }
+       return params, nil
+}
+
+type ParentConfigServerParams struct {
+       QString         string
+       Algorithm       string
+       QStringHandling bool
+}
+
+func getCDNDomainByProfileID(tx *sql.Tx, profileID 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 {
+               if err == sql.ErrNoRows {
+                       return "", false, nil
+               }
+               return "", false, errors.New("querying: " + err.Error())
+       }
+       return val, true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index fe59806..0c88232 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -381,6 +381,9 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                {1.1, http.MethodGet, 
`cdns/{cdn-name-or-id}/configfiles/ats/hdr_rw_mid_{xml-id}.config/?(\.json)?$`, 
ats.GetMidHeaderRewriteDotConfig, auth.PrivLevelOperations, Authenticated, nil},
                {1.1, http.MethodGet, 
`cdns/{cdn-name-or-id}/configfiles/ats/hdr_rw_{xml-id}.config/?(\.json)?$`, 
ats.GetEdgeHeaderRewriteDotConfig, auth.PrivLevelOperations, Authenticated, 
nil},
 
+               // Cache Configs
+               {1.1, http.MethodGet, 
`servers/{id-or-host}/configfiles/ats/parent.config/?(\.json)?$`, 
ats.GetParentDotConfig, auth.PrivLevelOperations, Authenticated, nil},
+
                // Federations
                {1.4, http.MethodGet, `federations/all/?(\.json)?$`, 
federations.GetAll, auth.PrivLevelAdmin, Authenticated, nil},
                {1.1, http.MethodGet, `federations/?(\.json)?$`, 
federations.Get, auth.PrivLevelFederation, Authenticated, nil},

Reply via email to