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 171fe3b  Add TO Go profile ATS configs (#3593)
171fe3b is described below

commit 171fe3b60831188404e89549c8cb63be95d486de
Author: Robert Butts <[email protected]>
AuthorDate: Mon Aug 5 16:55:31 2019 -0600

    Add TO Go profile ATS configs (#3593)
---
 CHANGELOG.md                                       |   1 +
 traffic_ops/testing/api/v14/profileconfig_test.go  |  85 ++++++++++
 .../traffic_ops_golang/ats/atsprofile/astats.go    |  43 ++++++
 .../ats/atsprofile/atsdotrules.go                  |  65 ++++++++
 .../traffic_ops_golang/ats/atsprofile/cache.go     |  89 +++++++++++
 .../ats/atsprofile/dropqstring.go                  |  48 ++++++
 .../traffic_ops_golang/ats/atsprofile/facts.go     |  37 +++++
 .../traffic_ops_golang/ats/atsprofile/logging.go   | 136 ++++++++++++++++
 .../ats/atsprofile/loggingyaml.go                  | 133 ++++++++++++++++
 .../traffic_ops_golang/ats/atsprofile/logsxml.go   | 108 +++++++++++++
 .../traffic_ops_golang/ats/atsprofile/plugin.go    |  43 ++++++
 .../traffic_ops_golang/ats/atsprofile/profile.go   | 142 +++++++++++++++++
 .../ats/atsprofile/profile_test.go                 |  52 +++++++
 .../traffic_ops_golang/ats/atsprofile/records.go   |  61 ++++++++
 .../ats/atsprofile/records_test.go                 | 107 +++++++++++++
 .../traffic_ops_golang/ats/atsprofile/storage.go   |  88 +++++++++++
 .../traffic_ops_golang/ats/atsprofile/sysctl.go    |  46 ++++++
 .../traffic_ops_golang/ats/atsprofile/unknown.go   |  68 ++++++++
 .../ats/atsprofile/urisigning.go                   |  50 ++++++
 .../traffic_ops_golang/ats/atsprofile/urlsig.go    |  63 ++++++++
 .../traffic_ops_golang/ats/atsprofile/volume.go    |  78 ++++++++++
 traffic_ops/traffic_ops_golang/ats/config.go       |   2 +-
 traffic_ops/traffic_ops_golang/ats/db.go           | 172 +++++++++++++++++++++
 .../traffic_ops_golang/ats/headerrewrite.go        |   4 +-
 .../traffic_ops_golang/ats/parentdotconfig.go      |  28 +---
 .../traffic_ops_golang/ats/regexrevalidate.go      |   2 +-
 traffic_ops/traffic_ops_golang/riaksvc/dsutil.go   |  47 ++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |  18 +++
 28 files changed, 1786 insertions(+), 30 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index da3ad42..2a04a43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
   - /api/1.1/cdns/name/:name/dnsseckeys `GET`
   - /api/1.4/cdns/name/:name/dnsseckeys `GET`
   - /api/1.4/user/login/oauth `POST`
+  - /api/1.1/profiles/:name/configfiles/ats/* `GET`
 - To support reusing a single riak cluster connection, an optional parameter 
is added to riak.conf: "HealthCheckInterval". This options takes a 'Duration' 
value (ie: 10s, 5m) which affects how often the riak cluster is health checked. 
 Default is currently set to: "HealthCheckInterval": "5s".
 - Added a new Go db/admin binary to replace the Perl db/admin.pl script which 
is now deprecated and will be removed in a future release. The new db/admin 
binary is essentially a drop-in replacement for db/admin.pl since it supports 
all of the same commands and options; therefore, it should be used in place of 
db/admin.pl for all the same tasks.
 - Added an API 1.4 endpoint, /api/1.4/cdns/dnsseckeys/refresh, to perform 
necessary behavior previously served outside the API under `/internal`.
diff --git a/traffic_ops/testing/api/v14/profileconfig_test.go 
b/traffic_ops/testing/api/v14/profileconfig_test.go
new file mode 100644
index 0000000..8126c74
--- /dev/null
+++ b/traffic_ops/testing/api/v14/profileconfig_test.go
@@ -0,0 +1,85 @@
+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-tc"
+)
+
+func TestProfileDotConfig(t *testing.T) {
+       WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, 
Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, 
DeliveryServices}, func() {
+               defer DeleteTestDeliveryServiceServersCreated(t)
+               CreateTestDeliveryServiceServers(t)
+               GetTestProfileDotConfig(t)
+       })
+}
+
+func GetTestProfileDotConfig(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)
+       }
+
+       if _, err := url.Parse(ds.OrgServerFQDN); 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)
+       }
+
+       servers, _, err := TOSession.GetServers()
+       if err != nil {
+               t.Errorf("cannot GET Servers: %v\n", err)
+       }
+
+       server := tc.Server{ID: -1}
+       for _, potentialServer := range servers {
+               if potentialServer.Type != string(tc.CacheTypeEdge) {
+                       continue
+               }
+               server = potentialServer
+       }
+       if server.ID == -1 {
+               t.Errorf("GET Servers returned no edge servers, must have at 
least 1 to test")
+       }
+
+       profileDotConfig, _, err := 
TOSession.GetATSProfileConfig(server.ProfileID, "cache.config")
+       if err != nil {
+               t.Fatalf("Getting server %+v config parent.config: 
"+err.Error()+"\n", serverID)
+       }
+
+       if !strings.Contains(profileDotConfig, server.Profile) {
+               t.Errorf("expected: profile cache.config to contain profile 
name '%+v', actual: '''%+v'''", server.Profile, profileDotConfig)
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
new file mode 100644
index 0000000..bbc302e
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/astats.go
@@ -0,0 +1,43 @@
+package atsprofile
+
+/*
+ * 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"
+       "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 == "" {
+               txt = "\n" // If no params exist, don't send "not found," but 
an empty file. We know the profile exists.
+       }
+       return txt, err
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
new file mode 100644
index 0000000..45eb470
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/atsdotrules.go
@@ -0,0 +1,65 @@
+package atsprofile
+
+/*
+ * 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"
+       "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())
+       }
+
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
new file mode 100644
index 0000000..affd050
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/cache.go
@@ -0,0 +1,89 @@
+package atsprofile
+
+/*
+ * 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"
+       "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)
+}
+
+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
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
new file mode 100644
index 0000000..4909766
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/dropqstring.go
@@ -0,0 +1,48 @@
+package atsprofile
+
+/*
+ * 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"
+
+       "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)
+}
+
+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())
+       }
+
+       text := ""
+       if hasDropQStringParam {
+               text += dropQStringVal + "\n"
+       } else {
+               text += `/([^?]+) $s://$t/$1` + "\n"
+       }
+       return text, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
new file mode 100644
index 0000000..6b94a55
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/facts.go
@@ -0,0 +1,37 @@
+package atsprofile
+
+/*
+ * 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"
+       "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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
new file mode 100644
index 0000000..2fe0b38
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/logging.go
@@ -0,0 +1,136 @@
+package atsprofile
+
+/*
+ * 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"
+       "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.
+}
+
+func makeLogging(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, _ 
string) (string, error) {
+       profileParamData, err := ats.GetProfileParamData(tx, profile.ID, 
LoggingFileName)
+
+       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())
+       }
+       // 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 text, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
new file mode 100644
index 0000000..fe302f0
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/loggingyaml.go
@@ -0,0 +1,133 @@
+package atsprofile
+
+/*
+ * 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"
+       "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())
+       }
+
+       // 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"
+               }
+       }
+
+       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"
+               }
+       }
+
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
new file mode 100644
index 0000000..8d033ce
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/logsxml.go
@@ -0,0 +1,108 @@
+package atsprofile
+
+/*
+ * 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"
+       "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())
+       }
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
new file mode 100644
index 0000000..8731508
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/plugin.go
@@ -0,0 +1,43 @@
+package atsprofile
+
+/*
+ * 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"
+       "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 == "" {
+               txt = "\n" // If no params exist, don't send "not found," but 
an empty file. We know the profile exists.
+       }
+       return txt, err
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go
new file mode 100644
index 0000000..d3d4ed7
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile.go
@@ -0,0 +1,142 @@
+package atsprofile
+
+/*
+ * 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"
+       "strconv"
+       "strings"
+
+       "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"
+)
+
+// 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)) {
+       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()
+
+       profileNameOrID := inf.Params["profile-name-or-id"]
+       profileID, err := strconv.Atoi(profileNameOrID)
+       if err != nil {
+               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
+               }
+       }
+
+       fileName := strings.TrimSuffix(inf.Params["file"], ".json")
+
+       profileData, ok, err := ats.GetProfileData(inf.Tx.Tx, profileID)
+       if err != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
nil, errors.New("getting profile data: "+err.Error()))
+               return
+       }
+       if !ok {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, 
errors.New("not found"), nil)
+               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()))
+               return
+       }
+
+       if text == "" {
+               // TODO replicates old Perl; verify required.
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, 
errors.New("not found"), nil)
+               return
+       }
+
+       if contentType != "" {
+               w.Header().Set(tc.ContentType, contentType)
+       }
+       w.Write([]byte(hdr + text))
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/profile_test.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile_test.go
new file mode 100644
index 0000000..5d0a50b
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/profile_test.go
@@ -0,0 +1,52 @@
+package atsprofile
+
+/*
+ * 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 "testing"
+
+func TestTrimParamUnderscoreNumSuffix(t *testing.T) {
+       inputExpected := map[string]string{
+               ``:                         ``,
+               `a`:                        `a`,
+               `_`:                        `_`,
+               `foo__`:                    `foo__`,
+               `foo__1`:                   `foo`,
+               `foo__1234567890`:          `foo`,
+               `foo_1234567890`:           `foo_1234567890`,
+               `foo__1234__1234567890`:    `foo__1234`,
+               `foo__1234__1234567890_`:   `foo__1234__1234567890_`,
+               `foo__1234__1234567890a`:   `foo__1234__1234567890a`,
+               `foo__1234__1234567890__`:  `foo__1234__1234567890__`,
+               `foo__1234__1234567890__a`: `foo__1234__1234567890__a`,
+               `__`:                       `__`,
+               `__9`:                      ``,
+               `_9`:                       `_9`,
+               `__35971234789124`:         ``,
+               `a__35971234789124`:        `a`,
+               `1234`:                     `1234`,
+               `foo__asdf_1234`:           `foo__asdf_1234`,
+       }
+
+       for input, expected := range inputExpected {
+               if actual := trimParamUnderscoreNumSuffix(input); expected != 
actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
new file mode 100644
index 0000000..8e43600
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/records.go
@@ -0,0 +1,61 @@
+package atsprofile
+
+/*
+ * 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"
+       "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
+       }
+       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
+}
+
+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)
+       }
+       return strings.Join(newLines, "\n")
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/records_test.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/records_test.go
new file mode 100644
index 0000000..3e50b85
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/records_test.go
@@ -0,0 +1,107 @@
+package atsprofile
+
+/*
+ * 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 "testing"
+
+func TestReplaceLineSuffixes(t *testing.T) {
+       {
+               input := `
+foo STRING __HOSTNAME__
+bar
+baz
+`
+               expected := `
+foo STRING __FULL_HOSTNAME__
+bar
+baz
+`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := `STRING __HOSTNAME__`
+               expected := `STRING __FULL_HOSTNAME__`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := `
+STRING __HOSTNAME__
+`
+               expected := `
+STRING __FULL_HOSTNAME__
+`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := `
+  
+STRING __HOSTNAME__
+`
+               expected := `
+  
+STRING __FULL_HOSTNAME__
+`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := `
+STRING __HOSTNAME__
+  STRING __HOSTNAME__
+`
+               expected := `
+STRING __FULL_HOSTNAME__
+  STRING __FULL_HOSTNAME__
+`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := `
+`
+               expected := `
+`
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+       {
+               input := ``
+               expected := ``
+               actual := ReplaceLineSuffixes(input, "STRING __HOSTNAME__", 
"STRING __FULL_HOSTNAME__")
+               if expected != actual {
+                       t.Errorf("Expected '%v' Actual '%v'", expected, actual)
+               }
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
new file mode 100644
index 0000000..1a3d687
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/storage.go
@@ -0,0 +1,88 @@
+package atsprofile
+
+/*
+ * 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"
+       "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) {
+       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)
+               }
+               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 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"
+       }
+       return text
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
new file mode 100644
index 0000000..16e5267
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/sysctl.go
@@ -0,0 +1,46 @@
+package atsprofile
+
+/*
+ * 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"
+       "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
+       }
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go
new file mode 100644
index 0000000..fc7d12d
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/unknown.go
@@ -0,0 +1,68 @@
+package atsprofile
+
+/*
+ * 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"
+       "strings"
+
+       "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)
+}
+
+func makeUnknown(tx *sql.Tx, _ *config.Config, profile ats.ProfileData, 
fileName string) (string, error) {
+       params, err := ats.GetProfileParamData(tx, profile.ID, fileName)
+       if err != nil {
+               return "", errors.New("getting profile param data: " + 
err.Error())
+       }
+       fileContents, err := takeAndBakeProfile(tx, profile.Name, params)
+       if err != nil {
+               return "", errors.New("GetProfileConfig: takeAndBakeProfile '" 
+ fileName + "': " + err.Error())
+       }
+       return fileContents, nil
+}
+
+func takeAndBakeProfile(tx *sql.Tx, profileName string, params 
map[string]string) (string, error) {
+       hdr, err := ats.HeaderComment(tx, profileName)
+       if err != nil {
+               return "", errors.New("getting header comment: " + err.Error())
+       }
+       text := ""
+       for paramName, paramVal := range params {
+               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, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go
new file mode 100644
index 0000000..18a628d
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/urisigning.go
@@ -0,0 +1,50 @@
+package atsprofile
+
+/*
+ * 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"
+       "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"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/riaksvc"
+)
+
+func GetURISigning(w http.ResponseWriter, r *http.Request) {
+       addHdr := false
+       contentType := tc.ApplicationJson
+       WithProfileDataHdr(w, r, addHdr, contentType, uriSigningDotConfig)
+}
+
+func uriSigningDotConfig(tx *sql.Tx, cfg *config.Config, _ ats.ProfileData, 
fileName string) (string, error) {
+       riakKey := strings.TrimSuffix(strings.TrimPrefix(fileName, 
"uri_signing_"), ".config")
+       keys, hasKeys, err := riaksvc.GetURISigningKeysRaw(tx, 
cfg.RiakAuthOptions, cfg.RiakPort, riakKey)
+       if err != nil {
+               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
+       }
+       return string(keys), nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go
new file mode 100644
index 0000000..09d0278
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/urlsig.go
@@ -0,0 +1,63 @@
+package atsprofile
+
+/*
+ * 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"
+       "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/traffic_ops/traffic_ops_golang/riaksvc"
+)
+
+func GetURLSig(w http.ResponseWriter, r *http.Request) {
+       WithProfileData(w, r, 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)
+       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"
+               }
+       }
+
+       for key, val := range urlSigKeys {
+               text += key + sep + val + "\n"
+       }
+       return text, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go 
b/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
new file mode 100644
index 0000000..bb177d5
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/atsprofile/volume.go
@@ -0,0 +1,78 @@
+package atsprofile
+
+/*
+ * 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"
+       "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())
+       }
+
+       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
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/config.go 
b/traffic_ops/traffic_ops_golang/ats/config.go
index 8e9c1d8..d722fe3 100644
--- a/traffic_ops/traffic_ops_golang/ats/config.go
+++ b/traffic_ops/traffic_ops_golang/ats/config.go
@@ -122,7 +122,7 @@ func getServerNameFromNameOrID(tx *sql.Tx, serverNameOrID 
string) (string, error
        return serverName, nil, nil, http.StatusOK
 }
 
-func headerComment(tx *sql.Tx, name string) (string, error) {
+func HeaderComment(tx *sql.Tx, name string) (string, error) {
        nameVersionStr, err := GetNameVersionString(tx)
        if err != nil {
                return "", errors.New("getting name version string: " + 
err.Error())
diff --git a/traffic_ops/traffic_ops_golang/ats/db.go 
b/traffic_ops/traffic_ops_golang/ats/db.go
new file mode 100644
index 0000000..1dbbcf4
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/ats/db.go
@@ -0,0 +1,172 @@
+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"
+
+       "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,
+  p.value
+FROM
+  parameter p
+  join profile_parameter pp on p.id = pp.parameter
+  JOIN profile pr on pr.id = pp.profile
+WHERE
+  pr.id = $1
+  AND p.config_file = $2
+`
+       rows, err := tx.Query(qry, profileID, configFile)
+       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())
+               }
+               if name == "location" {
+                       continue
+               }
+
+               if _, ok := params[name]; ok {
+                       log.Warnf("Profile %v has multiple parameters '%v' 
assigned! ATS config generation ignoring value '%v'!", profileID, name, 
params[name])
+               }
+
+               params[name] = val
+       }
+       return params, nil
+}
+
+type ProfileData struct {
+       ID   int
+       Name string
+}
+
+// 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
+FROM
+  profile p
+WHERE
+  p.id = $1
+`
+       v := ProfileData{ID: id}
+       if err := tx.QueryRow(qry, id).Scan(&v.Name); err != nil {
+               if err == sql.ErrNoRows {
+                       return ProfileData{}, false, nil
+               }
+               return ProfileData{}, false, errors.New("querying: " + 
err.Error())
+       }
+       return v, true, nil
+}
+
+type ProfileDS struct {
+       Type       tc.DSType
+       OriginFQDN *string
+}
+
+func GetProfileDS(tx *sql.Tx, profileID int) ([]ProfileDS, error) {
+       qry := `
+SELECT
+  dstype.name AS ds_type,
+  (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
+FROM
+  deliveryservice ds
+  JOIN type as dstype ON ds.type = dstype.id
+WHERE
+  ds.id IN (
+    SELECT DISTINCT deliveryservice
+    FROM deliveryservice_server
+    WHERE server IN (SELECT id FROM server WHERE profile = $1)
+  )
+`
+       rows, err := tx.Query(qry, profileID)
+       if err != nil {
+               return nil, errors.New("querying: " + err.Error())
+       }
+       defer rows.Close()
+
+       dses := []ProfileDS{}
+       for rows.Next() {
+               d := ProfileDS{}
+               if err := rows.Scan(&d.Type, &d.OriginFQDN); err != nil {
+                       return nil, errors.New("scanning: " + err.Error())
+               }
+               d.Type = tc.DSTypeFromString(string(d.Type))
+               dses = append(dses, d)
+       }
+       return dses, 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 int, 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
+}
+
+// GetProfileIDFromName returns the profile's ID, whether it exists, and any 
error.
+func GetProfileIDFromName(tx *sql.Tx, profileName string) (int, bool, error) {
+       qry := `SELECT id from profile where name = $1`
+       id := 0
+       if err := tx.QueryRow(qry, profileName).Scan(&id); err != nil {
+               if err == sql.ErrNoRows {
+                       return 0, false, nil
+               }
+               return 0, false, errors.New("querying: " + err.Error())
+       }
+       return id, true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/ats/headerrewrite.go 
b/traffic_ops/traffic_ops_golang/ats/headerrewrite.go
index 221c9ef..fb4b492 100644
--- a/traffic_ops/traffic_ops_golang/ats/headerrewrite.go
+++ b/traffic_ops/traffic_ops_golang/ats/headerrewrite.go
@@ -46,7 +46,7 @@ func GetEdgeHeaderRewriteDotConfig(w http.ResponseWriter, r 
*http.Request) {
                return
        }
 
-       text, err := headerComment(inf.Tx.Tx, "CDN "+cdnName)
+       text, err := HeaderComment(inf.Tx.Tx, "CDN "+cdnName)
        if err != nil {
                api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
nil, errors.New("getting hdr_rw_xml-id.config text: "+err.Error()))
                return
@@ -111,7 +111,7 @@ func GetMidHeaderRewriteDotConfig(w http.ResponseWriter, r 
*http.Request) {
                return
        }
 
-       text, err := headerComment(inf.Tx.Tx, "CDN "+cdnName)
+       text, err := HeaderComment(inf.Tx.Tx, "CDN "+cdnName)
        if err != nil {
                api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, 
nil, errors.New("getting hdr_rw_mid_xml-id.config text: "+err.Error()))
                return
diff --git a/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go 
b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
index 982081d..0ab740b 100644
--- a/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
+++ b/traffic_ops/traffic_ops_golang/ats/parentdotconfig.go
@@ -79,7 +79,7 @@ func GetParentDotConfig(w http.ResponseWriter, r 
*http.Request) {
                return
        }
 
-       hdr, err := headerComment(inf.Tx.Tx, serverInfo.HostName)
+       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
@@ -480,7 +480,7 @@ 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, serverProfileID, 
"package", "trafficserver")
+       atsVersion, _, err := GetProfileParamValue(tx, int(serverProfileID), 
"package", "trafficserver")
        if err != nil {
                return 0, errors.New("getting profile param value: " + 
err.Error())
        }
@@ -495,30 +495,6 @@ func GetATSMajorVersion(tx *sql.Tx, serverProfileID 
ProfileID) (int, error) {
        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
diff --git a/traffic_ops/traffic_ops_golang/ats/regexrevalidate.go 
b/traffic_ops/traffic_ops_golang/ats/regexrevalidate.go
index 1dc424d..2865f63 100644
--- a/traffic_ops/traffic_ops_golang/ats/regexrevalidate.go
+++ b/traffic_ops/traffic_ops_golang/ats/regexrevalidate.go
@@ -75,7 +75,7 @@ func getRegexRevalidate(tx *sql.Tx, cdnName string) (string, 
error) {
                return "", errors.New("getting jobs: " + err.Error())
        }
 
-       text, err := headerComment(tx, "CDN "+cdnName)
+       text, err := HeaderComment(tx, "CDN "+cdnName)
        if err != nil {
                return "", errors.New("getting header comment: " + err.Error())
        }
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go 
b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
index b99f0c8..0d4c8bb 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
@@ -35,6 +35,7 @@ const DNSSECKeysBucket = "dnssec"
 const DSSSLKeyVersionLatest = "latest"
 const DefaultDSSSLKeyVersion = DSSSLKeyVersionLatest
 const URLSigKeysBucket = "url_sig_keys"
+const URISigningKeysBucket = "cdn_uri_sig_keys"
 
 func MakeDSSSLKeyKey(dsName, version string) string {
        if version == "" {
@@ -349,3 +350,49 @@ func GetCDNSSLKeysDSNames(tx *sql.Tx, authOpts 
*riak.AuthOptions, riakPort *uint
        }
        return dsVersions, nil
 }
+
+// GetURISigningKeysRaw gets the URL Sig keys for the given delivery service, 
as the raw bytes stored in Riak.
+func GetURISigningKeysRaw(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort 
*uint, key string) ([]byte, bool, error) {
+       val := []byte(nil)
+       found := false
+       err := WithCluster(tx, authOpts, riakPort, func(cluster StorageCluster) 
error {
+               ro, err := FetchObjectValues(key, URISigningKeysBucket, cluster)
+               if err != nil {
+                       return err
+               }
+               if len(ro) == 0 {
+                       return nil // not found
+               }
+               val = ro[0].Value
+               found = true
+               return nil
+       })
+       if err != nil {
+               return nil, false, err
+       }
+       return val, found, nil
+}
+
+// GetURLSigKeysFromKey gets the URL Sig keys from the raw Riak key, which is 
the ATS config file name.
+func GetURLSigKeysFromConfigFileKey(tx *sql.Tx, authOpts *riak.AuthOptions, 
riakPort *uint, configFileKey string) (tc.URLSigKeys, bool, error) {
+       val := tc.URLSigKeys{}
+       found := false
+       err := WithCluster(tx, authOpts, riakPort, func(cluster StorageCluster) 
error {
+               ro, err := FetchObjectValues(configFileKey, URLSigKeysBucket, 
cluster)
+               if err != nil {
+                       return err
+               }
+               if len(ro) == 0 {
+                       return nil // not found
+               }
+               if err := json.Unmarshal(ro[0].Value, &val); err != nil {
+                       return errors.New("unmarshalling Riak response: " + 
err.Error())
+               }
+               found = true
+               return nil
+       })
+       if err != nil {
+               return val, false, err
+       }
+       return val, found, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 5feac7b..50c19c8 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -37,6 +37,7 @@ import (
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/apitenant"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/asn"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats/atsprofile"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachegroup"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachesstats"
@@ -379,6 +380,23 @@ 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},
 
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/12M_facts/?$`, 
atsprofile.GetFacts, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/50-ats.rules/?$`, 
atsprofile.GetATSDotRules, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/astats.config/?$`, 
atsprofile.GetAstats, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/cache.config/?$`, 
atsprofile.GetCache, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/drop_qstring.config/?$`, 
atsprofile.GetDropQString, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/logging.config/?$`, 
atsprofile.GetLogging, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/logging.yaml/?$`, 
atsprofile.GetLoggingYAML, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/logs_xml.config/?$`, 
atsprofile.GetLogsXML, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/plugin.config/?$`, 
atsprofile.GetPlugin, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/records.config/?$`, 
atsprofile.GetRecords, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/storage.config/?$`, 
atsprofile.GetStorage, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/sysctl.conf/?$`, 
atsprofile.GetSysctl, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/url_sig_{file}.config/?$`, 
atsprofile.GetURLSig, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/uri_signing_{file}.config/?$`, 
atsprofile.GetURISigning, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/volume.config/?$`, 
atsprofile.GetVolume, auth.PrivLevelOperations, Authenticated, nil},
+               {1.1, http.MethodGet, 
`profiles/{profile-name-or-id}/configfiles/ats/{file}/?$`, 
atsprofile.GetUnknown, 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},
 

Reply via email to