Add experimental Go TO proxying old Perl app
Project: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/commit/aa901953 Tree: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/tree/aa901953 Diff: http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/diff/aa901953 Branch: refs/heads/master Commit: aa901953ae5ba309ae97a793cf90818b0096ea08 Parents: 9c4eccc Author: Robert Butts <robert.o.bu...@gmail.com> Authored: Sat Jul 8 21:02:56 2017 -0600 Committer: Dewayne Richardson <dewr...@apache.org> Committed: Thu Aug 10 09:46:02 2017 -0600 ---------------------------------------------------------------------- .../go-monitoring/to-go-monitoring.go | 528 ------------------- traffic_ops/experimental/goto/config.go | 80 +++ traffic_ops/experimental/goto/config.json | 13 + traffic_ops/experimental/goto/goto.go | 56 ++ traffic_ops/experimental/goto/monitoring.go | 398 ++++++++++++++ traffic_ops/experimental/goto/routes.go | 95 ++++ traffic_ops/experimental/goto/wrappers.go | 55 ++ traffic_ops/experimental/tocookie/tocookie.go | 6 + 8 files changed, 703 insertions(+), 528 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/go-monitoring/to-go-monitoring.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/go-monitoring/to-go-monitoring.go b/traffic_ops/experimental/go-monitoring/to-go-monitoring.go deleted file mode 100644 index a508212..0000000 --- a/traffic_ops/experimental/go-monitoring/to-go-monitoring.go +++ /dev/null @@ -1,528 +0,0 @@ -// 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. - -package main - -import ( - "database/sql" - "encoding/json" - "flag" - "fmt" - "github.com/apache/incubator-trafficcontrol/traffic_ops/experimental/tocookie" - "github.com/lib/pq" - "net/http" - "strings" - "time" -) - -const CacheMonitorConfigFile = "rascal.properties" -const MonitorType = "RASCAL" -const RouterType = "CCR" -const MonitorProfilePrefix = "RASCAL" -const MonitorConfigFile = "rascal-config.txt" - -const KilobitsPerMegabit = 1000 -const DeliveryServiceStatus = "REPORTED" - -// Args encapsulates the command line arguments -type Args struct { - HTTPPort string - DBUser string - DBPass string - DBServer string - DBDB string - DBSSL bool - Auth bool - TOSecret string -} - -// getFlags parses and returns the command line arguments. The returned error -// will be non-nil if any expected arg is missing. -func getFlags() (Args, error) { - var args Args - flag.StringVar(&args.HTTPPort, "port", "", "the port to serve on") - flag.StringVar(&args.DBUser, "user", "", "the database user") - flag.StringVar(&args.DBPass, "pass", "", "the database password") - flag.StringVar(&args.DBServer, "server", "", "the database server IP or FQDN, without scheme") - flag.StringVar(&args.DBDB, "db", "", "the database name") - flag.BoolVar(&args.DBSSL, "ssl", true, "whether to require or disable SSL connecting to the database") - flag.StringVar(&args.TOSecret, "secret", "", "the Traffic Ops secret, used to authenticate mojolicious cookies") - flag.BoolVar(&args.Auth, "authenticate", true, "whether to authenticate requests, requiring valid Traffic Ops cookies") - flag.Parse() - if args.HTTPPort == "" { - return args, fmt.Errorf("missing port") - } - if args.DBUser == "" { - return args, fmt.Errorf("missing user") - } - if args.DBPass == "" { - return args, fmt.Errorf("missing password") - } - if args.DBServer == "" { - return args, fmt.Errorf("missing server") - } - if args.DBDB == "" { - return args, fmt.Errorf("missing database") - } - if args.Auth && args.TOSecret == "" { - return args, fmt.Errorf("missing secret") - } - return args, nil -} - -func printUsage() { - fmt.Println("Usage:") - flag.PrintDefaults() - fmt.Println("Example: to-go-monitoring -port 80 -user bill -pass thelizard -server db.to.example.net -db to") -} - -type BasicServer struct { - Profile string `json:"profile"` - Status string `json:"status"` - IP string `json:"ip"` - IP6 string `json:"ip6"` - Port int `json:"port"` - Cachegroup string `json:"cachegroup"` - HostName string `json:"hostName"` - FQDN string `json:"fqdn"` -} - -type Monitor struct { - BasicServer -} - -type Cache struct { - BasicServer - InterfaceName string `json:"interfaceName"` - Type string `json:"type"` - HashID string `json:"hashId"` -} - -type Cachegroup struct { - Name string `json:"name"` - Coordinates Coordinates `json:"coordinates"` -} - -type Coordinates struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} - -type Profile struct { - Name string `json:"name"` - Type string `json:"type"` - Parameters map[string]string `json:"parameters"` -} - -type Monitoring struct { - TrafficServers []Cache `json:"trafficServers"` - TrafficMonitors []Monitor `json:"trafficMonitors"` - Cachegroups []Cachegroup `json:"cacheGroups"` - Profiles []Profile `json:"profiles"` - DeliveryServices []DeliveryService `json:"deliveryServices"` - Config map[string]string `json:"config"` -} - -type MonitoringResponse struct { - Response Monitoring `json:"response"` -} - -type Router struct { - Type string - Profile string -} - -type DeliveryService struct { - XMLID string `json:"xmlId"` - TotalTPSThreshold float64 `json:"totalTpsThreshold"` - Status string `json:"status"` - TotalKBPSThreshold float64 `json:"totalKbpsThreshold"` -} - -func getServers(db *sql.DB, cdn string) ([]Monitor, []Cache, []Router, error) { - query := `SELECT -me.host_name as hostName, -CONCAT(me.host_name, '.', me.domain_name) as fqdn, -status.name as status, -cachegroup.name as cachegroup, -me.tcp_port as port, -me.ip_address as ip, -me.ip6_address as ip6, -profile.name as profile, -me.interface_name as interfaceName, -type.name as type, -me.xmpp_id as hashId -FROM server me -JOIN type type ON type.id = me.type -JOIN status status ON status.id = me.status -JOIN cachegroup cachegroup ON cachegroup.id = me.cachegroup -JOIN profile profile ON profile.id = me.profile -JOIN cdn cdn ON cdn.id = me.cdn_id -WHERE cdn.name = $1` - - rows, err := db.Query(query, cdn) - if err != nil { - return nil, nil, nil, err - } - defer rows.Close() - - monitors := []Monitor{} - caches := []Cache{} - routers := []Router{} - - for rows.Next() { - var hostName sql.NullString - var fqdn sql.NullString - var status sql.NullString - var cachegroup sql.NullString - var port sql.NullInt64 - var ip sql.NullString - var ip6 sql.NullString - var profile sql.NullString - var interfaceName sql.NullString - var ttype sql.NullString - var hashId sql.NullString - - if err := rows.Scan(&hostName, &fqdn, &status, &cachegroup, &port, &ip, &ip6, &profile, &interfaceName, &ttype, &hashId); err != nil { - return nil, nil, nil, err - } - - if ttype.String == MonitorType { - monitors = append(monitors, Monitor{ - BasicServer: BasicServer{ - Profile: profile.String, - Status: status.String, - IP: ip.String, - IP6: ip6.String, - Port: int(port.Int64), - Cachegroup: cachegroup.String, - HostName: hostName.String, - FQDN: fqdn.String, - }, - }) - } else if strings.HasPrefix(ttype.String, "EDGE") || strings.HasPrefix(ttype.String, "MID") { - caches = append(caches, Cache{ - BasicServer: BasicServer{ - Profile: profile.String, - Status: status.String, - IP: ip.String, - IP6: ip6.String, - Port: int(port.Int64), - Cachegroup: cachegroup.String, - HostName: hostName.String, - FQDN: fqdn.String, - }, - InterfaceName: interfaceName.String, - Type: ttype.String, - HashID: hashId.String, - }) - } else if ttype.String == RouterType { - routers = append(routers, Router{ - Type: ttype.String, - Profile: profile.String, - }) - } - } - return monitors, caches, routers, nil -} - -func getCachegroups(db *sql.DB, cdn string) ([]Cachegroup, error) { - query := ` -SELECT name, latitude, longitude -FROM cachegroup -WHERE id IN - (SELECT cachegroup FROM server WHERE server.cdn_id = - (SELECT id FROM cdn WHERE name = $1));` - - rows, err := db.Query(query, cdn) - if err != nil { - return nil, err - } - defer rows.Close() - - cachegroups := []Cachegroup{} - - for rows.Next() { - var name sql.NullString - var lat sql.NullFloat64 - var lon sql.NullFloat64 - if err := rows.Scan(&name, &lat, &lon); err != nil { - return nil, err - } - cachegroups = append(cachegroups, Cachegroup{ - Name: name.String, - Coordinates: Coordinates{ - Latitude: lat.Float64, - Longitude: lon.Float64, - }, - }) - } - return cachegroups, nil -} - -func getProfiles(db *sql.DB, caches []Cache, routers []Router) ([]Profile, error) { - cacheProfileTypes := map[string]string{} - profiles := map[string]Profile{} - profileNames := []string{} - for _, router := range routers { - profiles[router.Profile] = Profile{ - Name: router.Profile, - Type: router.Type, - } - } - - for _, cache := range caches { - if _, ok := cacheProfileTypes[cache.Profile]; !ok { - cacheProfileTypes[cache.Profile] = cache.Type - profiles[cache.Profile] = Profile{ - Name: cache.Profile, - Type: cache.Type, - } - profileNames = append(profileNames, cache.Profile) - } - } - - query := ` -SELECT p.name as profile, pr.name, pr.value -FROM parameter pr -JOIN profile p ON p.name = ANY($1) -JOIN profile_parameter pp ON pp.profile = p.id and pp.parameter = pr.id -WHERE pr.config_file = $2; -` - rows, err := db.Query(query, pq.Array(profileNames), CacheMonitorConfigFile) - if err != nil { - return nil, err - } - defer rows.Close() - - for rows.Next() { - var profileName sql.NullString - var name sql.NullString - var value sql.NullString - if err := rows.Scan(&profileName, &name, &value); err != nil { - return nil, err - } - if name.String == "" { - return nil, fmt.Errorf("null name") // TODO continue and warn? - } - profile := profiles[profileName.String] - if profile.Parameters == nil { - profile.Parameters = map[string]string{} - } - profile.Parameters[name.String] = value.String - profiles[profileName.String] = profile - - } - - profilesArr := []Profile{} // TODO make for efficiency? - for _, profile := range profiles { - profilesArr = append(profilesArr, profile) - } - return profilesArr, nil -} - -func getDeliveryServices(db *sql.DB, routers []Router) ([]DeliveryService, error) { - profileNames := []string{} - for _, router := range routers { - profileNames = append(profileNames, router.Profile) - } - - query := ` -SELECT ds.xml_id, ds.global_max_tps, ds.global_max_mbps -FROM deliveryservice ds -JOIN profile profile ON profile.id = ds.profile -WHERE profile.name = ANY($1) -AND ds.active = true -` - rows, err := db.Query(query, pq.Array(profileNames)) - if err != nil { - return nil, err - } - defer rows.Close() - - dses := []DeliveryService{} - - for rows.Next() { - var xmlid sql.NullString - var tps sql.NullFloat64 - var mbps sql.NullFloat64 - if err := rows.Scan(&xmlid, &tps, &mbps); err != nil { - return nil, err - } - dses = append(dses, DeliveryService{ - XMLID: xmlid.String, - TotalTPSThreshold: tps.Float64, - Status: DeliveryServiceStatus, - TotalKBPSThreshold: mbps.Float64 * KilobitsPerMegabit, - }) - } - return dses, nil -} - -func getConfig(db *sql.DB) (map[string]string, error) { - // TODO remove 'like' in query? Slow? - query := fmt.Sprintf(` -SELECT pr.name, pr.value -FROM parameter pr -JOIN profile p ON p.name LIKE '%s%%' -JOIN profile_parameter pp ON pp.profile = p.id and pp.parameter = pr.id -WHERE pr.config_file = '%s' -`, MonitorProfilePrefix, MonitorConfigFile) - - rows, err := db.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - cfg := map[string]string{} - - for rows.Next() { - var name sql.NullString - var val sql.NullString - if err := rows.Scan(&name, &val); err != nil { - return nil, err - } - cfg[name.String] = val.String - } - return cfg, nil -} - -func getMonitoringJson(cdnName string, db *sql.DB) (*MonitoringResponse, error) { - monitors, caches, routers, err := getServers(db, cdnName) - if err != nil { - return nil, fmt.Errorf("error getting servers: %v", err) - } - - cachegroups, err := getCachegroups(db, cdnName) - if err != nil { - return nil, fmt.Errorf("error getting cachegroups: %v", err) - } - - profiles, err := getProfiles(db, caches, routers) - if err != nil { - return nil, fmt.Errorf("error getting profiles: %v", err) - } - - deliveryServices, err := getDeliveryServices(db, routers) - if err != nil { - return nil, fmt.Errorf("error getting deliveryservices: %v", err) - } - - config, err := getConfig(db) - if err != nil { - return nil, fmt.Errorf("error getting config: %v", err) - } - - resp := MonitoringResponse{ - Response: Monitoring{ - TrafficServers: caches, - TrafficMonitors: monitors, - Cachegroups: cachegroups, - Profiles: profiles, - DeliveryServices: deliveryServices, - Config: config, - }, - } - return &resp, nil -} - -// authenticate attempts to authenticate the given request, looking for an auth cookie, using the auth settings in args. Returns nil if authentication succeeds, or a descriptive error if authentication fails (which should NOT be returned to clients, for security reasons). -func authenticate(r *http.Request, args Args) error { - if !args.Auth { - return nil - } - - cookie, err := r.Cookie(tocookie.Name) - if err != nil { - return fmt.Errorf("getting cookie: %v", err) - } - if cookie == nil { - return fmt.Errorf("no auth cookie") - } - - if _, err := tocookie.Parse(args.TOSecret, cookie.Value); err != nil { - return fmt.Errorf("parsing cookie: %v", err) - } - return nil -} - -func rootHandler(w http.ResponseWriter, r *http.Request, db *sql.DB, args Args) { - start := time.Now() - defer func() { - now := time.Now() - fmt.Printf("%v %v served %v in %v\n", now, r.RemoteAddr, r.URL.Path, now.Sub(start)) - }() - - handleErr := func(err error, status int) { - fmt.Printf("%v %v error %v\n", time.Now(), r.RemoteAddr, err) - w.WriteHeader(status) - fmt.Fprintf(w, http.StatusText(status)) - } - - if err := authenticate(r, args); err != nil { - handleErr(err, http.StatusUnauthorized) - return - } - - pathParts := strings.Split(r.URL.Path, "/") - if len(pathParts) < 5 { - handleErr(fmt.Errorf("nonexistent path requested: '%v'", r.URL.Path), http.StatusNotFound) - return - } - cdnName := pathParts[4] - - resp, err := getMonitoringJson(cdnName, db) - if err != nil { - handleErr(err, http.StatusInternalServerError) - return - } - - respBts, err := json.Marshal(resp) - if err != nil { - handleErr(err, http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - fmt.Fprintf(w, "%s", respBts) -} - -func main() { - args, err := getFlags() - if err != nil { - fmt.Println(err) - printUsage() - return - } - - sslStr := "require" - if !args.DBSSL { - sslStr = "disable" - } - - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", args.DBUser, args.DBPass, args.DBServer, args.DBDB, sslStr)) - if err != nil { - fmt.Printf("Error opening database: %v\n", err) - return - } - defer db.Close() - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - rootHandler(w, r, db, args) - }) - - if err := http.ListenAndServe(":"+args.HTTPPort, nil); err != nil { - fmt.Printf("Error stopping server: %v\n", err) - return - } -} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/config.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/config.go b/traffic_ops/experimental/goto/config.go new file mode 100644 index 0000000..a0f3401 --- /dev/null +++ b/traffic_ops/experimental/goto/config.go @@ -0,0 +1,80 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/url" +) + +type Config struct { + HTTPPort string `json:"port"` + DBUser string `json:"db_user"` + DBPass string `json:"db_pass"` + DBServer string `json:"db_server"` + DBDB string `json:"db_name"` + DBSSL bool `json:"db_ssl"` + TOSecret string `json:"to_secret"` + TOURLStr string `json:"to_url"` + TOURL *url.URL `json:"-"` + NoAuth bool `json:"no_auth"` + CertPath string `json:"cert_path"` + KeyPath string `json:"key_path"` +} + +func LoadConfig(fileName string) (Config, error) { + if fileName == "" { + return Config{}, fmt.Errorf("no filename") + } + + configBytes, err := ioutil.ReadFile(fileName) + if err != nil { + return Config{}, err + } + + cfg := Config{} + if err := json.Unmarshal(configBytes, &cfg); err != nil { + return Config{}, err + } + + if cfg, err = ParseConfig(cfg); err != nil { + return Config{}, err + } + + return cfg, nil +} + +// ParseConfig validates required fields, and parses non-JSON types +func ParseConfig(cfg Config) (Config, error) { + if cfg.HTTPPort == "" { + return Config{}, fmt.Errorf("missing port") + } + if cfg.DBUser == "" { + return Config{}, fmt.Errorf("missing database user") + } + if cfg.DBPass == "" { + return Config{}, fmt.Errorf("missing database password") + } + if cfg.DBServer == "" { + return Config{}, fmt.Errorf("missing database server") + } + if cfg.DBDB == "" { + return Config{}, fmt.Errorf("missing database name") + } + if cfg.TOSecret == "" { + return Config{}, fmt.Errorf("missing secret") + } + if cfg.CertPath == "" { + return Config{}, fmt.Errorf("missing certificate path") + } + if cfg.KeyPath == "" { + return Config{}, fmt.Errorf("missing certificate key path") + } + + var err error + if cfg.TOURL, err = url.Parse(cfg.TOURLStr); err != nil { + return Config{}, fmt.Errorf("Invalid Traffic Ops URL '%v': err", cfg.TOURL, err) + } + + return cfg, nil +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/config.json ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/config.json b/traffic_ops/experimental/goto/config.json new file mode 100644 index 0000000..19d2599 --- /dev/null +++ b/traffic_ops/experimental/goto/config.json @@ -0,0 +1,13 @@ +{ + "port": "443", + "db_user": "bill", + "db_pass": "thelizard", + "db_server": "db.trafficops.example.net", + "db_name": "trafficops", + "db_ssl": true, + "to_secret": "walrus", + "to_url": "https://trafficops.example.net:60443", + "no_auth": false, + "cert_path": "/opt/traffic_ops/cert", + "key_path": "/opt/traffic_ops/key" +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/goto.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/goto.go b/traffic_ops/experimental/goto/goto.go new file mode 100644 index 0000000..48d3c4e --- /dev/null +++ b/traffic_ops/experimental/goto/goto.go @@ -0,0 +1,56 @@ +// 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. + +package main + +import ( + "database/sql" + "flag" + "fmt" + _ "github.com/lib/pq" + "net/http" +) + +const DefaultConfigPath = "/etc/goto/config.json" + +func main() { + configFileName := flag.String("cfg", "", "The config file path") + flag.Parse() + if *configFileName == "" { + *configFileName = DefaultConfigPath + } + + cfg, err := LoadConfig(*configFileName) + if err != nil { + fmt.Println("Error loading config '" + *configFileName + "': " + err.Error()) + return + } + + sslStr := "require" + if !cfg.DBSSL { + sslStr = "disable" + } + + db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", cfg.DBUser, cfg.DBPass, cfg.DBServer, cfg.DBDB, sslStr)) + if err != nil { + fmt.Printf("Error opening database: %v\n", err) + return + } + defer db.Close() + + RegisterRoutes(ServerData{DB: db, Config: cfg}) + fmt.Println("Listening on " + cfg.HTTPPort) + if err := http.ListenAndServeTLS(":"+cfg.HTTPPort, cfg.CertPath, cfg.KeyPath, nil); err != nil { + fmt.Printf("Error stopping server: %v\n", err) + return + } +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/monitoring.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/monitoring.go b/traffic_ops/experimental/goto/monitoring.go new file mode 100644 index 0000000..b353fb5 --- /dev/null +++ b/traffic_ops/experimental/goto/monitoring.go @@ -0,0 +1,398 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "github.com/lib/pq" + "net/http" + "strings" + "time" +) + +const CacheMonitorConfigFile = "rascal.properties" +const MonitorType = "RASCAL" +const RouterType = "CCR" +const MonitorProfilePrefix = "RASCAL" +const MonitorConfigFile = "rascal-config.txt" +const KilobitsPerMegabit = 1000 +const DeliveryServiceStatus = "REPORTED" + +type BasicServer struct { + Profile string `json:"profile"` + Status string `json:"status"` + IP string `json:"ip"` + IP6 string `json:"ip6"` + Port int `json:"port"` + Cachegroup string `json:"cachegroup"` + HostName string `json:"hostName"` + FQDN string `json:"fqdn"` +} + +type Monitor struct { + BasicServer +} + +type Cache struct { + BasicServer + InterfaceName string `json:"interfaceName"` + Type string `json:"type"` + HashID string `json:"hashId"` +} + +type Cachegroup struct { + Name string `json:"name"` + Coordinates Coordinates `json:"coordinates"` +} + +type Coordinates struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type Profile struct { + Name string `json:"name"` + Type string `json:"type"` + Parameters map[string]string `json:"parameters"` +} + +type Monitoring struct { + TrafficServers []Cache `json:"trafficServers"` + TrafficMonitors []Monitor `json:"trafficMonitors"` + Cachegroups []Cachegroup `json:"cacheGroups"` + Profiles []Profile `json:"profiles"` + DeliveryServices []DeliveryService `json:"deliveryServices"` + Config map[string]string `json:"config"` +} + +type MonitoringResponse struct { + Response Monitoring `json:"response"` +} + +type Router struct { + Type string + Profile string +} + +type DeliveryService struct { + XMLID string `json:"xmlId"` + TotalTPSThreshold float64 `json:"totalTpsThreshold"` + Status string `json:"status"` + TotalKBPSThreshold float64 `json:"totalKbpsThreshold"` +} + +// TODO change to use the ParamMap, instead of parsing the URL +func monitoringHandler(db *sql.DB) RegexHandlerFunc { + return func(w http.ResponseWriter, r *http.Request, p ParamMap) { + handleErr := func(err error, status int) { + fmt.Printf("%v %v error %v\n", time.Now(), r.RemoteAddr, err) + w.WriteHeader(status) + fmt.Fprintf(w, http.StatusText(status)) + } + + cdnName := p["cdn"] + + resp, err := getMonitoringJson(cdnName, db) + if err != nil { + handleErr(err, http.StatusInternalServerError) + return + } + + respBts, err := json.Marshal(resp) + if err != nil { + handleErr(err, http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, "%s", respBts) + } +} + +func getServers(db *sql.DB, cdn string) ([]Monitor, []Cache, []Router, error) { + query := `SELECT +me.host_name as hostName, +CONCAT(me.host_name, '.', me.domain_name) as fqdn, +status.name as status, +cachegroup.name as cachegroup, +me.tcp_port as port, +me.ip_address as ip, +me.ip6_address as ip6, +profile.name as profile, +me.interface_name as interfaceName, +type.name as type, +me.xmpp_id as hashId +FROM server me +JOIN type type ON type.id = me.type +JOIN status status ON status.id = me.status +JOIN cachegroup cachegroup ON cachegroup.id = me.cachegroup +JOIN profile profile ON profile.id = me.profile +JOIN cdn cdn ON cdn.id = me.cdn_id +WHERE cdn.name = $1` + + rows, err := db.Query(query, cdn) + if err != nil { + return nil, nil, nil, err + } + defer rows.Close() + + monitors := []Monitor{} + caches := []Cache{} + routers := []Router{} + + for rows.Next() { + var hostName sql.NullString + var fqdn sql.NullString + var status sql.NullString + var cachegroup sql.NullString + var port sql.NullInt64 + var ip sql.NullString + var ip6 sql.NullString + var profile sql.NullString + var interfaceName sql.NullString + var ttype sql.NullString + var hashId sql.NullString + + if err := rows.Scan(&hostName, &fqdn, &status, &cachegroup, &port, &ip, &ip6, &profile, &interfaceName, &ttype, &hashId); err != nil { + return nil, nil, nil, err + } + + if ttype.String == MonitorType { + monitors = append(monitors, Monitor{ + BasicServer: BasicServer{ + Profile: profile.String, + Status: status.String, + IP: ip.String, + IP6: ip6.String, + Port: int(port.Int64), + Cachegroup: cachegroup.String, + HostName: hostName.String, + FQDN: fqdn.String, + }, + }) + } else if strings.HasPrefix(ttype.String, "EDGE") || strings.HasPrefix(ttype.String, "MID") { + caches = append(caches, Cache{ + BasicServer: BasicServer{ + Profile: profile.String, + Status: status.String, + IP: ip.String, + IP6: ip6.String, + Port: int(port.Int64), + Cachegroup: cachegroup.String, + HostName: hostName.String, + FQDN: fqdn.String, + }, + InterfaceName: interfaceName.String, + Type: ttype.String, + HashID: hashId.String, + }) + } else if ttype.String == RouterType { + routers = append(routers, Router{ + Type: ttype.String, + Profile: profile.String, + }) + } + } + return monitors, caches, routers, nil +} + +func getCachegroups(db *sql.DB, cdn string) ([]Cachegroup, error) { + query := ` +SELECT name, latitude, longitude +FROM cachegroup +WHERE id IN + (SELECT cachegroup FROM server WHERE server.cdn_id = + (SELECT id FROM cdn WHERE name = $1));` + + rows, err := db.Query(query, cdn) + if err != nil { + return nil, err + } + defer rows.Close() + + cachegroups := []Cachegroup{} + + for rows.Next() { + var name sql.NullString + var lat sql.NullFloat64 + var lon sql.NullFloat64 + if err := rows.Scan(&name, &lat, &lon); err != nil { + return nil, err + } + cachegroups = append(cachegroups, Cachegroup{ + Name: name.String, + Coordinates: Coordinates{ + Latitude: lat.Float64, + Longitude: lon.Float64, + }, + }) + } + return cachegroups, nil +} + +func getProfiles(db *sql.DB, caches []Cache, routers []Router) ([]Profile, error) { + cacheProfileTypes := map[string]string{} + profiles := map[string]Profile{} + profileNames := []string{} + for _, router := range routers { + profiles[router.Profile] = Profile{ + Name: router.Profile, + Type: router.Type, + } + } + + for _, cache := range caches { + if _, ok := cacheProfileTypes[cache.Profile]; !ok { + cacheProfileTypes[cache.Profile] = cache.Type + profiles[cache.Profile] = Profile{ + Name: cache.Profile, + Type: cache.Type, + } + profileNames = append(profileNames, cache.Profile) + } + } + + query := ` +SELECT p.name as profile, pr.name, pr.value +FROM parameter pr +JOIN profile p ON p.name = ANY($1) +JOIN profile_parameter pp ON pp.profile = p.id and pp.parameter = pr.id +WHERE pr.config_file = $2; +` + rows, err := db.Query(query, pq.Array(profileNames), CacheMonitorConfigFile) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var profileName sql.NullString + var name sql.NullString + var value sql.NullString + if err := rows.Scan(&profileName, &name, &value); err != nil { + return nil, err + } + if name.String == "" { + return nil, fmt.Errorf("null name") // TODO continue and warn? + } + profile := profiles[profileName.String] + if profile.Parameters == nil { + profile.Parameters = map[string]string{} + } + profile.Parameters[name.String] = value.String + profiles[profileName.String] = profile + + } + + profilesArr := []Profile{} // TODO make for efficiency? + for _, profile := range profiles { + profilesArr = append(profilesArr, profile) + } + return profilesArr, nil +} + +func getDeliveryServices(db *sql.DB, routers []Router) ([]DeliveryService, error) { + profileNames := []string{} + for _, router := range routers { + profileNames = append(profileNames, router.Profile) + } + + query := ` +SELECT ds.xml_id, ds.global_max_tps, ds.global_max_mbps +FROM deliveryservice ds +JOIN profile profile ON profile.id = ds.profile +WHERE profile.name = ANY($1) +AND ds.active = true +` + rows, err := db.Query(query, pq.Array(profileNames)) + if err != nil { + return nil, err + } + defer rows.Close() + + dses := []DeliveryService{} + + for rows.Next() { + var xmlid sql.NullString + var tps sql.NullFloat64 + var mbps sql.NullFloat64 + if err := rows.Scan(&xmlid, &tps, &mbps); err != nil { + return nil, err + } + dses = append(dses, DeliveryService{ + XMLID: xmlid.String, + TotalTPSThreshold: tps.Float64, + Status: DeliveryServiceStatus, + TotalKBPSThreshold: mbps.Float64 * KilobitsPerMegabit, + }) + } + return dses, nil +} + +func getConfig(db *sql.DB) (map[string]string, error) { + // TODO remove 'like' in query? Slow? + query := fmt.Sprintf(` +SELECT pr.name, pr.value +FROM parameter pr +JOIN profile p ON p.name LIKE '%s%%' +JOIN profile_parameter pp ON pp.profile = p.id and pp.parameter = pr.id +WHERE pr.config_file = '%s' +`, MonitorProfilePrefix, MonitorConfigFile) + + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + cfg := map[string]string{} + + for rows.Next() { + var name sql.NullString + var val sql.NullString + if err := rows.Scan(&name, &val); err != nil { + return nil, err + } + cfg[name.String] = val.String + } + return cfg, nil +} + +func getMonitoringJson(cdnName string, db *sql.DB) (*MonitoringResponse, error) { + monitors, caches, routers, err := getServers(db, cdnName) + if err != nil { + return nil, fmt.Errorf("error getting servers: %v", err) + } + + cachegroups, err := getCachegroups(db, cdnName) + if err != nil { + return nil, fmt.Errorf("error getting cachegroups: %v", err) + } + + profiles, err := getProfiles(db, caches, routers) + if err != nil { + return nil, fmt.Errorf("error getting profiles: %v", err) + } + + deliveryServices, err := getDeliveryServices(db, routers) + if err != nil { + return nil, fmt.Errorf("error getting deliveryservices: %v", err) + } + + config, err := getConfig(db) + if err != nil { + return nil, fmt.Errorf("error getting config: %v", err) + } + + resp := MonitoringResponse{ + Response: Monitoring{ + TrafficServers: caches, + TrafficMonitors: monitors, + Cachegroups: cachegroups, + Profiles: profiles, + DeliveryServices: deliveryServices, + Config: config, + }, + } + return &resp, nil +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/routes.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/routes.go b/traffic_ops/experimental/goto/routes.go new file mode 100644 index 0000000..cc14450 --- /dev/null +++ b/traffic_ops/experimental/goto/routes.go @@ -0,0 +1,95 @@ +package main + +import ( + "crypto/tls" + "database/sql" + "net/http" + "net/http/httputil" + "regexp" + "strings" +) + +type ServerData struct { + Config + DB *sql.DB +} + +type ParamMap map[string]string + +type RegexHandlerFunc func(w http.ResponseWriter, r *http.Request, params ParamMap) + +func getMonitoringRoute(d ServerData) RegexHandlerFunc { + return wrapLogTime(wrapAuth(monitoringHandler(d.DB), d.NoAuth, d.TOSecret)) +} + +// getRootHandler returns the / handler for the service, which reverse-proxies the old Perl Traffic Ops +func getRootHandler(d ServerData) http.Handler { + // debug + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + rp := httputil.NewSingleHostReverseProxy(d.TOURL) + rp.Transport = tr + return rp +} + +// GetRoutes returns the map of regex routes, and a catchall route if no regex matches. +func GetRoutes(d ServerData) (map[string]RegexHandlerFunc, http.Handler) { + return map[string]RegexHandlerFunc{ + "api/1.2/cdns/{cdn}/configs/monitoring.json": getMonitoringRoute(d), + }, getRootHandler(d) +} + +type CompiledRoute struct { + Handler RegexHandlerFunc + Regex *regexp.Regexp + Params []string +} + +func CompileRoutes(routes *map[string]RegexHandlerFunc) map[string]CompiledRoute { + compiledRoutes := map[string]CompiledRoute{} + for route, handler := range *routes { + originalRoute := route + var params []string + for open := strings.Index(route, "{"); open > 0; open = strings.Index(route, "{") { + close := strings.Index(route, "}") + if close < 0 { + panic("malformed route") + } + param := route[open+1 : close] + + params = append(params, param) + route = route[:open] + `(.+)` + route[close+1:] + } + regex := regexp.MustCompile(route) + compiledRoutes[originalRoute] = CompiledRoute{Handler: handler, Regex: regex, Params: params} + } + return compiledRoutes +} + +func Handler(routes map[string]CompiledRoute, catchall http.Handler, w http.ResponseWriter, r *http.Request) { + requested := r.URL.Path[1:] + + for _, compiledRoute := range routes { + match := compiledRoute.Regex.FindStringSubmatch(requested) + if len(match) == 0 { + continue + } + + params := map[string]string{} + for i, v := range compiledRoute.Params { + params[v] = match[i+1] + } + compiledRoute.Handler(w, r, params) + return + } + catchall.ServeHTTP(w, r) +} + +func RegisterRoutes(d ServerData) { + routes, catchall := GetRoutes(d) + compiledRoutes := CompileRoutes(&routes) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + Handler(compiledRoutes, catchall, w, r) + }) +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/goto/wrappers.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/goto/wrappers.go b/traffic_ops/experimental/goto/wrappers.go new file mode 100644 index 0000000..daf5e34 --- /dev/null +++ b/traffic_ops/experimental/goto/wrappers.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "github.com/apache/incubator-trafficcontrol/traffic_ops/experimental/tocookie" + "log" // TODO change to traffic_monitor_golang/common/log + "net/http" + "time" +) + +func wrapAuth(h RegexHandlerFunc, noAuth bool, secret string) RegexHandlerFunc { + if noAuth { + return h + } + return func(w http.ResponseWriter, r *http.Request, p ParamMap) { + handleUnauthorized := func(reason string) { + log.Printf("%v %v %v sent 401 - %v\n", time.Now(), r.RemoteAddr, r.URL.Path, reason) + status := http.StatusUnauthorized + w.WriteHeader(status) + fmt.Fprintf(w, http.StatusText(status)) + } + + cookie, err := r.Cookie(tocookie.Name) + if err != nil { + handleUnauthorized("error getting cookie: " + err.Error()) + return + } + + if cookie == nil { + handleUnauthorized("no auth cookie") + return + } + + oldCookie, err := tocookie.Parse(secret, cookie.Value) + if err != nil { + handleUnauthorized("cookie error: " + err.Error()) + return + } + + newCookieVal := tocookie.Refresh(oldCookie, secret) + http.SetCookie(w, &http.Cookie{Name: tocookie.Name, Value: newCookieVal}) + h(w, r, p) + } +} + +func wrapLogTime(h RegexHandlerFunc) RegexHandlerFunc { + return func(w http.ResponseWriter, r *http.Request, p ParamMap) { + start := time.Now() + defer func() { + now := time.Now() + log.Printf("%v %v served %v in %v\n", now, r.RemoteAddr, r.URL.Path, now.Sub(start)) + }() + h(w, r, p) + } +} http://git-wip-us.apache.org/repos/asf/incubator-trafficcontrol/blob/aa901953/traffic_ops/experimental/tocookie/tocookie.go ---------------------------------------------------------------------- diff --git a/traffic_ops/experimental/tocookie/tocookie.go b/traffic_ops/experimental/tocookie/tocookie.go index 4dd319b..658224d 100644 --- a/traffic_ops/experimental/tocookie/tocookie.go +++ b/traffic_ops/experimental/tocookie/tocookie.go @@ -24,6 +24,7 @@ import ( ) const Name = "mojolicious" +const DefaultDuration = time.Hour type Cookie struct { AuthData string `json:"auth_data"` @@ -95,3 +96,8 @@ func New(user string, expiration time.Time, key string) string { msg, _ := json.Marshal(cookieMsg) return NewRawMsg(msg, []byte(key)) } + +// Update takes an existing cookie and returns a new serialized cookie with an updated expiration +func Refresh(c *Cookie, key string) string { + return New(c.AuthData, time.Now().Add(DefaultDuration), key) +}