You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2018/03/12 20:24:58 UTC

[GitHub] KevinMackenzie closed pull request #1639: [Issue-1617] - Traffic Ops Keeps track of configuration differences between database and Traffic Servers

KevinMackenzie closed pull request #1639: [Issue-1617] - Traffic Ops Keeps track of configuration differences between database and Traffic Servers
URL: https://github.com/apache/incubator-trafficcontrol/pull/1639
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/docs/source/development/traffic_ops.rst b/docs/source/development/traffic_ops.rst
index 03e2f91d5..ff45eabf7 100644
--- a/docs/source/development/traffic_ops.rst
+++ b/docs/source/development/traffic_ops.rst
@@ -593,6 +593,7 @@ API 1.2 Reference
   traffic_ops_api/v12/capability
   traffic_ops_api/v12/cdn
   traffic_ops_api/v12/changelog
+  traffic_ops_api/v12/configdiffs
   traffic_ops_api/v12/configfiles_ats
   traffic_ops_api/v12/deliveryservice
   traffic_ops_api/v12/deliveryservice_regex
diff --git a/docs/source/development/traffic_ops_api/v12/configdiffs.rst b/docs/source/development/traffic_ops_api/v12/configdiffs.rst
new file mode 100644
index 000000000..3d1ec8888
--- /dev/null
+++ b/docs/source/development/traffic_ops_api/v12/configdiffs.rst
@@ -0,0 +1,138 @@
+.. 
+.. 
+.. 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.
+.. 
+
+
+.. _to-api-v12-config-diffs:
+
+Configuration Differences
+=========================
+
+.. _to-api-v12-config-diffs-route:
+
+/api/1.2/servers/:host-name/config_diffs.json
++++++++++++++++++++++++++++++++++++++++++++++
+
+**GET servers/:host-name/config_diffs.json**
+
+  Retrieves a all of the configuration file differences between a server and Traffic Ops
+
+  Authentication Required: Yes
+
+  Role(s) Required: None
+
+  **Request Route Parameters**
+
+  +-----------------+----------+----------------------------------------------------------+
+  | Name            | Required | Description                                              |
+  +=================+==========+==========================================================+
+  | ``host-name``   | yes      | The host name of the server to get the differences for   |
+  +-----------------+----------+----------------------------------------------------------+
+
+  **Response Properties**
+
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | Parameter             | Type     | Description                                                              |
+  +=======================+==========+==========================================================================+
+  | ``fileName``          | string   | The name of the configuration file                                       |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``dbLinesMissing``    | string[] | The lines not in the Traffic Ops database, but on the server             |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``diskLinesMissing``  | string[] | The lines in the Traffic Ops database, but not on the server             |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``timestamp``         | string   | The last time the server updated this entry                              |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+
+  **Response Example** ::
+
+    {
+     "response": [
+        {
+          "fileName": "configName1",
+          "dbLinesMissing": [ 
+            "LocalOnlyLine One",
+            "Local Only Line Two"
+          ],
+          "diskLinesMissing": [
+            "DBOnlyLine One",
+            "DB Only Line Two"
+          ],
+          "timestamp": "2015-02-03 17:04:20"
+        },
+        {
+          "fileName": "otherConfigName",
+          "dbLinesMissing": [ 
+            "Config Line Local",
+            "Another Config Line Local"
+          ],
+          "diskLinesMissing": [
+            "DB Only Line",
+            "Another Config DB Line"
+          ],
+          "timestamp": "2015-02-03 17:04:20"
+        },
+     ],
+    }
+
+|
+
+**PUT /api/1.2/servers/:host-name/:cfg-file-name**
+
+  Updates the configuration file differences between the server and Traffic Ops.
+
+  Authentication Required: Yes
+
+  Role(s) Required: None
+
+  **Request Route Parameters**
+
+  +-------------------+----------+---------------------------------------------------------------+
+  | Name              | Required | Description                                                   |
+  +===================+==========+===============================================================+
+  | ``host-name``     | yes      | The host name of the server to set the differences for        |
+  +-------------------+----------+---------------------------------------------------------------+
+  | ``cfg-file-name`` | yes      | The name of the configuration file to update differences for  |
+  +-------------------+----------+---------------------------------------------------------------+
+
+  **Request Properties**
+
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | Parameter             | Type     | Description                                                              |
+  +=======================+==========+==========================================================================+
+  | ``fileName``          | string   | The name of the configuration file (optional)                            |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``dbLinesMissing``    | string[] | The lines not in the Traffic Ops database, but on the server             |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``diskLinesMissing``  | string[] | The lines in the Traffic Ops database, but not on the server             |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+  | ``timestamp``         | string   | The last time the server updated this entry                              |
+  +-----------------------+----------+--------------------------------------------------------------------------+
+
+  **Request Example** ::
+
+    {
+      "dbLinesMissing": [ 
+        "LocalOnlyLine One",
+        "Local Only Line Two"
+      ],
+      "diskLinesMissing": [
+        "DBOnlyLine One",
+        "DB Only Line Two"
+      ],
+      "timestamp": "2015-02-03 17:04:20"
+    }
+
+  **Response Properties**
+  
+  No Response body
diff --git a/traffic_ops/app/db/migrations/20171205000000_create_config_diffs.sql b/traffic_ops/app/db/migrations/20171205000000_create_config_diffs.sql
new file mode 100644
index 000000000..619e228c6
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20171205000000_create_config_diffs.sql
@@ -0,0 +1,40 @@
+/*
+
+    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.
+*/
+
+--
+-- Name: config_diffs; Type: TABLE; Schema: public; Owner: traffic_ops
+--
+
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+CREATE TABLE config_diffs (
+    config_id bigserial NOT NULL PRIMARY KEY,
+    server bigint NOT NULL REFERENCES server (id) ON UPDATE CASCADE ON DELETE CASCADE,
+    config_name text NOT NULL,
+    db_lines_missing text[],
+    disk_lines_missing text[],
+    last_checked timestamp without time zone NOT NULL
+);
+
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+ALTER TABLE config_diffs OWNER to traffic_ops;
+
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+DROP TABLE config_diffs;
\ No newline at end of file
diff --git a/traffic_ops/bin/traffic_ops_ort.pl b/traffic_ops/bin/traffic_ops_ort.pl
index 32d25baa4..5d6861ca3 100755
--- a/traffic_ops/bin/traffic_ops_ort.pl
+++ b/traffic_ops/bin/traffic_ops_ort.pl
@@ -239,7 +239,6 @@
 }
 
 
-
 #### First time
 &process_config_files();
 
@@ -402,6 +401,16 @@ sub process_cfg_file {
 		$return_code = $CFG_FILE_UNCHANGED;
 	}
 
+	# submit the line_missing to the traffic ops Config Diff API
+	if ( $script_mode != $REVALIDATE ) {
+		my $db_lines_missing_json = encode_json(\@db_lines_missing);
+		my $disk_lines_missing_json = encode_json(\@disk_lines_missing);
+		my $datetime = gmtime();
+		my $json_text = "{ \"dbLinesMissing\": $db_lines_missing_json, \"diskLinesMissing\": $disk_lines_missing_json, \"timestamp\": \"$datetime\" }";
+		$result = &lwp_put("/api/1.2/servers/$hostname_short/$cfg_file", $json_text);
+		# If this fails in any significant way, it will be caught by the lwp_put method
+	}
+
 	if ( $cfg_file eq "50-ats.rules" ) {
 		&adv_processing_udev( \@db_file_lines );
 	}
@@ -1480,6 +1489,73 @@ sub lwp_get {
 
 }
 
+sub lwp_put {
+	my $uri           = shift;
+	my $body 		  = shift;
+	my $retry_counter = $retries;
+
+	( $log_level >> $DEBUG ) && print "DEBUG Total connections in LWP cache: " . $lwp_conn->conn_cache->get_connections("https") . "\n";
+	my %headers = ( 'Cookie' => $cookie );
+
+	my $response;
+	my $response_content;
+
+	while( $retry_counter > 0 ) {
+
+		( $log_level >> $INFO ) && print "INFO Traffic Ops host: " . $traffic_ops_host . "\n";
+		( $log_level >> $DEBUG ) && print "DEBUG lwp_get called with $uri\n";
+		my $request = $traffic_ops_host . $uri;
+		if ( $uri =~ m/^http/ ) {
+			$request = $uri;
+			( $log_level >> $DEBUG ) && print "DEBUG Complete URL found. Downloading from external source $request.\n";
+		}
+		if ( ($uri =~ m/sslkeys/ || $uri =~ m/url\_sig/) && $rev_proxy_in_use == 1 ) {
+			$request = $to_url . $uri;
+			( $log_level >> $INFO ) && print "INFO Secure data request - bypassing reverse proxy and using $to_url.\n";
+		}
+		
+	    # TODO: is there a way to generalize this for most verbs?
+		my $httpRequest = HTTP::Request->new( 'PUT', $request );
+		$httpRequest->header( 'Cookie' => $cookie );
+		$httpRequest->header( 'Content-Type' => 'application/json' );
+		$httpRequest->content( $body );
+
+		$response = $lwp_conn->request($httpRequest);
+		$response_content = $response->content;
+
+		if ( &check_lwp_response_code($response, $ERROR) || &check_lwp_response_message_integrity($response, $ERROR) ) {
+			( $log_level >> $ERROR ) && print "ERROR result for $request is: ..." . $response->content . "...\n";
+			if ( $uri =~ m/configfiles\/ats/ && $response->code == 404) {
+					return $response->code;
+			}
+			if ( $rev_proxy_in_use == 1 ) {
+				( $log_level >> $ERROR ) && print "ERROR There appears to be an issue with the Traffic Ops Reverse Proxy.  Reverting to primary Traffic Ops host.\n";
+				$traffic_ops_host = $to_url;
+				$rev_proxy_in_use = 0;
+			}
+			sleep 2**( $retries - $retry_counter );
+			$retry_counter--;
+		}
+		# https://github.com/Comcast/traffic_control/issues/1168
+		elsif ( $uri =~ m/url\_sig\_(.*)\.config$/ && $response->content =~ m/No RIAK servers are set to ONLINE/ ) {
+			( $log_level >> $FATAL ) && print "FATAL result for $uri is: ..." . $response->content . "...\n";
+			exit 1;
+		}
+		else {
+			( $log_level >> $DEBUG ) && print "DEBUG result for $uri is: ..." . $response->content . "...\n";
+			last;
+		}
+
+	}
+
+	( &check_lwp_response_code($response, $FATAL) || &check_lwp_response_message_integrity($response, $FATAL) ) if ( $retry_counter == 0 );
+
+	&eval_json($response) if ( $uri =~ m/\.json$/ );
+
+	return $response_content;
+
+}
+
 sub eval_json {
 	my $lwp_response = shift;
 	eval {
diff --git a/traffic_ops/traffic_ops_golang/cfgdiffs.go b/traffic_ops/traffic_ops_golang/cfgdiffs.go
new file mode 100644
index 000000000..7f8b9ea3e
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cfgdiffs.go
@@ -0,0 +1,303 @@
+package main
+
+import (
+	"database/sql"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+
+	"github.com/jmoiron/sqlx"
+)
+
+const CfgDiffsPrivLevel = auth.PrivLevelReadOnly
+const CfgDiffsWritePrivLevel = auth.PrivLevelOperations
+
+type CfgFileDiffs struct {
+	FileName         string   `json:"fileName"`
+	DBLinesMissing   []string `json:"dbLinesMissing"`
+	DiskLinesMissing []string `json:"diskLinesMissing"`
+	ReportTimestamp  string   `json:"timestamp"`
+}
+
+type CfgFileDiffsResponse struct {
+	Response []CfgFileDiffs `json:"response"`
+}
+
+type ServerExistsMethod func(db *sqlx.DB, hostname string) (bool, error)
+type UpdateCfgDiffsMethod func(db *sqlx.DB, hostname string, diffs CfgFileDiffs) (bool, error)
+type InsertCfgDiffsMethod func(db *sqlx.DB, hostname string, diffs CfgFileDiffs) error
+type GetCfgDiffsMethod func(db *sqlx.DB, hostName string) ([]CfgFileDiffs, error)
+
+func getCfgDiffsHandler(db *sqlx.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		handleErr := func(err error, status int) {
+			log.Errorf("%v %v\n", r.RemoteAddr, err)
+			w.WriteHeader(status)
+			fmt.Fprintf(w, http.StatusText(status))
+		}
+		ctx := r.Context()
+		pathParams, err := getPathParams(ctx)
+		if err != nil {
+			handleErr(err, http.StatusInternalServerError)
+			return
+		}
+
+		hostName := pathParams["host-name"]
+
+		resp, err := getCfgDiffsJson(hostName, db, getCfgDiffs)
+		if err != nil {
+			handleErr(err, http.StatusInternalServerError)
+			return
+		}
+
+		// if the response has a length of zero, no results were found for that server
+		if len(resp.Response) == 0 {
+			w.WriteHeader(http.StatusNotFound)
+			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 putCfgDiffsHandler(db *sqlx.DB) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		handleErr := func(err error, status int) {
+			log.Errorf("%v %v\n", r.RemoteAddr, err)
+			w.WriteHeader(status)
+			fmt.Fprintf(w, http.StatusText(status))
+		}
+		ctx := r.Context()
+		pathParams, err := getPathParams(ctx)
+		if err != nil {
+			handleErr(err, http.StatusInternalServerError)
+			return
+		}
+
+		hostName := pathParams["host-name"]
+		configName := pathParams["cfg-file-name"]
+
+		decoder := json.NewDecoder(r.Body)
+		var diffs CfgFileDiffs
+		err = decoder.Decode(&diffs)
+		if err != nil {
+			handleErr(err, http.StatusBadRequest)
+			return
+		}
+
+		defer r.Body.Close()
+
+		diffs.FileName = configName
+
+		result, err := putCfgDiffs(db, hostName, diffs, serverExists, updateCfgDiffs, insertCfgDiffs)
+		if err != nil {
+			handleErr(err, http.StatusInternalServerError)
+			return
+		}
+
+		// Not found (invalid hostname)
+		if result == 0 { // This keeps happening
+			w.WriteHeader(404)
+			return
+		}
+		// Created (newly added)
+		if result == 1 {
+			w.WriteHeader(201)
+			return
+		}
+		// Updated (already existed)
+		if result == 2 {
+			w.WriteHeader(202)
+			return
+		}
+	}
+}
+
+func serverExists(db *sqlx.DB, hostName string) (bool, error) {
+	query := `SELECT EXISTS(SELECT 1 FROM server me WHERE me.host_name=$1)`
+	rows, err := db.Query(query, hostName)
+	if err != nil {
+		return false, err
+	}
+
+	defer rows.Close()
+
+	for rows.Next() {
+		var exists sql.NullString
+
+		err = rows.Scan(&exists)
+		if err != nil {
+			return false, err
+		}
+
+		log.Infof(exists.String)
+
+		if exists.String == "true" {
+			return true, nil
+		} else {
+			return false, nil
+		}
+	}
+	return false, errors.New("Failed to load row!") // What does this mean?
+}
+
+func getCfgDiffs(db *sqlx.DB, hostName string) ([]CfgFileDiffs, error) {
+	query := `SELECT
+me.config_name as config_name,
+array_to_json(me.db_lines_missing) as db_lines_missing,
+array_to_json(me.disk_lines_missing) as disk_lines_missing,
+me.last_checked as timestamp
+FROM config_diffs me
+WHERE me.server=(SELECT server.id FROM server WHERE host_name=$1)`
+
+	rows, err := db.Query(query, hostName)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	configs := []CfgFileDiffs{}
+
+	// TODO: what if there are zero rows?
+	for rows.Next() {
+		var config_name sql.NullString
+		var db_lines_missing sql.NullString
+		var disk_lines_missing sql.NullString
+		var timestamp sql.NullString
+
+		var db_lines_missing_arr []string
+		var disk_lines_missing_arr []string
+
+		if err := rows.Scan(&config_name, &db_lines_missing, &disk_lines_missing, &timestamp); err != nil {
+			return nil, err
+		}
+
+		err = json.Unmarshal([]byte(db_lines_missing.String), &db_lines_missing_arr)
+		if err != nil {
+			return nil, err
+		}
+
+		err := json.Unmarshal([]byte(disk_lines_missing.String), &disk_lines_missing_arr)
+		if err != nil {
+			return nil, err
+		}
+
+		configs = append(configs, CfgFileDiffs{
+			FileName:         config_name.String,
+			DBLinesMissing:   db_lines_missing_arr,
+			DiskLinesMissing: disk_lines_missing_arr,
+			ReportTimestamp:  timestamp.String,
+		})
+	}
+	return configs, nil
+}
+
+func getCfgDiffsJson(hostName string, db *sqlx.DB, getCfgDiffsMethod GetCfgDiffsMethod) (*CfgFileDiffsResponse, error) {
+	cfgDiffs, err := getCfgDiffsMethod(db, hostName)
+	if err != nil {
+		return nil, fmt.Errorf("error getting my data: %v", err)
+	}
+
+	response := CfgFileDiffsResponse{
+		Response: cfgDiffs,
+	}
+
+	return &response, nil
+}
+
+func insertCfgDiffs(db *sqlx.DB, hostName string, diffs CfgFileDiffs) error {
+	query := `INSERT INTO 
+config_diffs(server, config_name, db_lines_missing, disk_lines_missing, last_checked)
+VALUES((SELECT server.id FROM server WHERE host_name=$1), $2, (SELECT ARRAY(SELECT * FROM json_array_elements_text($3))), (SELECT ARRAY(SELECT * FROM json_array_elements_text($4))), $5)`
+
+	dbLinesMissingJson, err := json.Marshal(diffs.DBLinesMissing)
+	if err != nil {
+		return err
+	}
+	diskLinesMissingJson, err := json.Marshal(diffs.DiskLinesMissing)
+	if err != nil {
+		return err
+	}
+
+	_, err = db.Exec(query,
+		hostName,
+		diffs.FileName,
+		dbLinesMissingJson,
+		diskLinesMissingJson,
+		diffs.ReportTimestamp)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func updateCfgDiffs(db *sqlx.DB, hostName string, diffs CfgFileDiffs) (bool, error) {
+	query := `UPDATE config_diffs SET db_lines_missing=(SELECT ARRAY(SELECT * FROM json_array_elements_text($1))), 
+disk_lines_missing=(SELECT ARRAY(SELECT * FROM json_array_elements_text($2))), last_checked=$3 WHERE server=(SELECT server.id FROM server WHERE host_name=$4) AND config_name=$5`
+
+	dbLinesMissingJson, err := json.Marshal(diffs.DBLinesMissing)
+	if err != nil {
+		return false, err
+	}
+	diskLinesMissingJson, err := json.Marshal(diffs.DiskLinesMissing)
+	if err != nil {
+		return false, err
+	}
+
+	rows, err := db.Exec(query,
+		dbLinesMissingJson,
+		diskLinesMissingJson,
+		diffs.ReportTimestamp,
+		hostName,
+		diffs.FileName)
+
+	if err != nil {
+		return false, err
+	}
+
+	count, err := rows.RowsAffected()
+	if err != nil {
+		return false, err
+	}
+
+	if count > 0 {
+		return true, nil
+	}
+
+	return false, nil
+
+}
+
+func putCfgDiffs(db *sqlx.DB, hostName string, diffs CfgFileDiffs, serverExistsMethod ServerExistsMethod, updateCfgDiffsMethod UpdateCfgDiffsMethod, insertCfgDiffsMethod InsertCfgDiffsMethod) (int, error) {
+
+	sExists, err := serverExistsMethod(db, hostName)
+	if err != nil {
+		return -1, err
+	}
+	if sExists == false {
+		return 0, nil
+	}
+
+	// Try updating the information first
+	updated, err := updateCfgDiffsMethod(db, hostName, diffs)
+	if err != nil {
+		return -1, err
+	}
+	if updated {
+		return 2, nil
+	}
+	return 1, insertCfgDiffsMethod(db, hostName, diffs)
+}
diff --git a/traffic_ops/traffic_ops_golang/cfgdiffs_test.go b/traffic_ops/traffic_ops_golang/cfgdiffs_test.go
new file mode 100644
index 000000000..de7e16b56
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cfgdiffs_test.go
@@ -0,0 +1,391 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/jmoiron/sqlx"
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestGetCfgDiffs(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	defer mockDB.Close()
+	db := sqlx.NewDb(mockDB, "sqlmock")
+
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer db.Close()
+
+	hostName := "myedge"
+
+	timestamp := time.Now().UTC().String()
+	cfgFileDiffs1 := CfgFileDiffs{
+		FileName:         "TestFile.cfg",
+		DBLinesMissing:   []string{"db_line_missing1", "db_line_missing2"},
+		DiskLinesMissing: []string{"disk_line_missing1", "disk_line_missing2"},
+		ReportTimestamp:  timestamp,
+	}
+
+	rows := sqlmock.NewRows([]string{"config_name", "db_lines_missing", "disk_lines_missing", "last_checked"})
+
+	dbLinesMissingJson, err := json.Marshal(cfgFileDiffs1.DBLinesMissing)
+	diskLinesMissingJson, err := json.Marshal(cfgFileDiffs1.DiskLinesMissing)
+	rows = rows.AddRow(cfgFileDiffs1.FileName, dbLinesMissingJson, diskLinesMissingJson, cfgFileDiffs1.ReportTimestamp)
+
+	mock.ExpectQuery("SELECT").WithArgs(hostName).WillReturnRows(rows)
+
+	cfgFileDiffs, err := getCfgDiffs(db, hostName)
+	if err != nil {
+		t.Errorf("getCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	if len(cfgFileDiffs) != 1 {
+		t.Errorf("getCfgDiffs expected: len(cfgFileDiffs) == 1, actual: %v", len(cfgFileDiffs))
+	}
+	sqlCfgFileDiffs := cfgFileDiffs[0]
+	if !reflect.DeepEqual(sqlCfgFileDiffs, cfgFileDiffs1) {
+		t.Errorf("getCfgDiffs expected: cfgFileDiffs == %+v, actual: %+v", cfgFileDiffs1, sqlCfgFileDiffs)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+}
+
+func TestGetCfgDiffsJson(t *testing.T) {
+
+	var db *sqlx.DB = nil
+	hostName := "myedge"
+
+	timestamp := time.Now().UTC().String()
+	cfgFileDiffsResponse := CfgFileDiffsResponse{
+		Response: []CfgFileDiffs{{
+			FileName:         "TestFile.cfg",
+			DBLinesMissing:   []string{"db_line_missing1", "db_line_missing2"},
+			DiskLinesMissing: []string{"disk_line_missing1", "disk_line_missing2"},
+			ReportTimestamp:  timestamp,
+		}},
+	}
+
+	// Test successful request
+	cfgFileDiffsResponseT, err := getCfgDiffsJson(hostName, db,
+		func(db *sqlx.DB, hostName string) ([]CfgFileDiffs, error) {
+			return cfgFileDiffsResponse.Response, nil
+		})
+
+	if err != nil {
+		t.Errorf("getCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	if len(cfgFileDiffsResponseT.Response) != 1 {
+		t.Errorf("getCfgDiffsJson expected: len(cfgFileDiffsResponseT.Response) == 1, actual: %v", len(cfgFileDiffsResponseT.Response))
+	}
+
+	if !reflect.DeepEqual(*cfgFileDiffsResponseT, cfgFileDiffsResponse) {
+		t.Errorf("getCfgDiffsJson expected: cfgFileDiffsResponseT == %+v, actual: %+v", cfgFileDiffsResponseT, cfgFileDiffsResponse)
+	}
+
+	// Test error case
+	cfgFileDiffsResponseT, err = getCfgDiffsJson(hostName, db,
+		func(db *sqlx.DB, hostName string) ([]CfgFileDiffs, error) {
+			return nil, fmt.Errorf("Intentional Error for testing")
+		})
+
+	if err == nil {
+		t.Errorf("getCfgDiffsJson expected: non-nil error, actual: nil")
+	}
+
+	if cfgFileDiffsResponseT != nil {
+		t.Errorf("getCfgFileDiffsJson expected: nil response, actual: %v", cfgFileDiffsResponseT)
+	}
+}
+
+func TestServerExists(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	defer mockDB.Close()
+	db := sqlx.NewDb(mockDB, "sqlmock")
+
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer db.Close()
+
+	hostName := "myedge"
+
+	// Test Expecting True Response
+	rows := sqlmock.NewRows([]string{"host_name"}).AddRow("true")
+
+	mock.ExpectQuery("SELECT EXISTS").WithArgs(hostName).WillReturnRows(rows)
+
+	result, err := serverExists(db, hostName)
+	if err != nil {
+		t.Errorf("serverExists expected: nil error, actual: %v", err)
+	}
+
+	if result != true {
+		t.Errorf("serverExists expected: result == true, actual: %v", result)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+
+	// Test Expecting False Response
+	rows = sqlmock.NewRows([]string{"host_name"}).AddRow("false")
+
+	mock.ExpectQuery("SELECT EXISTS").WithArgs(hostName).WillReturnRows(rows)
+
+	result, err = serverExists(db, hostName)
+	if err != nil {
+		t.Errorf("serverExists expected: nil error, actual: %v", err)
+	}
+
+	if result != false {
+		t.Errorf("serverExists expected: result == false, actual: %v", result)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+}
+
+func TestInsertCfgDiffs(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	defer mockDB.Close()
+	db := sqlx.NewDb(mockDB, "sqlmock")
+
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer db.Close()
+
+	hostName := "myedge"
+	timestamp := time.Now().UTC().String()
+
+	cfgFileDiffs := CfgFileDiffs{
+		FileName:         "TestFile.cfg",
+		DBLinesMissing:   []string{"db_line_missing1", "db_line_missing2"},
+		DiskLinesMissing: []string{"disk_line_missing1", "disk_line_missing2"},
+		ReportTimestamp:  timestamp,
+	}
+
+	// Since "insertCfgDiffs" Marshals the json, we must store the unmarshalled json here.
+	//		This will need to be updated if the above text gets changed
+	dbLinesMissingJson := []uint8{91, 34, 100, 98, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 49, 34, 44, 34, 100, 98, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 50, 34, 93}
+	diskLinesMissingJson := []uint8{91, 34, 100, 105, 115, 107, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 49, 34, 44, 34, 100, 105, 115, 107, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 50, 34, 93}
+
+	mock.ExpectExec("INSERT INTO").WithArgs(
+		hostName,
+		cfgFileDiffs.FileName,
+		dbLinesMissingJson,
+		diskLinesMissingJson,
+		cfgFileDiffs.ReportTimestamp).WillReturnResult(sqlmock.NewResult(1, 1))
+
+	err = insertCfgDiffs(db, hostName, cfgFileDiffs)
+	if err != nil {
+		t.Errorf("insertCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+}
+
+func TestUpdateCfgDiiffs(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	defer mockDB.Close()
+	db := sqlx.NewDb(mockDB, "sqlmock")
+
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer db.Close()
+
+	hostName := "myedge"
+	timestamp := time.Now().UTC().String()
+
+	cfgFileDiffs := CfgFileDiffs{
+		FileName:         "TestFile.cfg",
+		DBLinesMissing:   []string{"db_line_missing1", "db_line_missing2"},
+		DiskLinesMissing: []string{"disk_line_missing1", "disk_line_missing2"},
+		ReportTimestamp:  timestamp,
+	}
+
+	// Since "updateCfgDiffs" Marshals the json, we must store the unmarshalled json here.
+	//		This will need to be updated if the above text gets changed
+	dbLinesMissingJson := []uint8{91, 34, 100, 98, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 49, 34, 44, 34, 100, 98, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 50, 34, 93}
+	diskLinesMissingJson := []uint8{91, 34, 100, 105, 115, 107, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 49, 34, 44, 34, 100, 105, 115, 107, 95, 108, 105, 110, 101, 95, 109, 105, 115, 115, 105, 110, 103, 50, 34, 93}
+
+	// Test Update Successful
+	mock.ExpectExec("UPDATE").WithArgs(
+		dbLinesMissingJson,
+		diskLinesMissingJson,
+		cfgFileDiffs.ReportTimestamp,
+		hostName,
+		cfgFileDiffs.FileName).WillReturnResult(sqlmock.NewResult(0, 1))
+
+	result, err := updateCfgDiffs(db, hostName, cfgFileDiffs)
+	if err != nil {
+		t.Errorf("updateCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	if result != true {
+		t.Errorf("updateCfgDiffs expected: result == true, actual: %v", result)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+
+	// Test Update Unsuccessful
+	mock.ExpectExec("UPDATE").WithArgs(
+		dbLinesMissingJson,
+		diskLinesMissingJson,
+		cfgFileDiffs.ReportTimestamp,
+		hostName,
+		cfgFileDiffs.FileName).WillReturnResult(sqlmock.NewResult(0, 0))
+
+	result, err = updateCfgDiffs(db, hostName, cfgFileDiffs)
+	if err != nil {
+		t.Errorf("updateCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	if result != false {
+		t.Errorf("updateCfgDiffs expected: result == false, actual: %v", result)
+	}
+
+	if err := mock.ExpectationsWereMet(); err != nil {
+		t.Errorf("there were unfulfilled expections: %s", err)
+	}
+}
+
+func serverExistsError(db *sqlx.DB, hostName string) (bool, error) {
+	return false, fmt.Errorf("Intentional Error")
+}
+func serverExistsFalse(db *sqlx.DB, hostName string) (bool, error) {
+	return false, nil
+}
+func serverExistsTrue(db *sqlx.DB, hostName string) (bool, error) {
+	return true, nil
+}
+func updateCfgDiffsError(db *sqlx.DB, hostname string, diffs CfgFileDiffs) (bool, error) {
+	return false, fmt.Errorf("Intentional Error")
+}
+func updateCfgDiffsTrue(db *sqlx.DB, hostname string, diffs CfgFileDiffs) (bool, error) {
+	return true, nil
+}
+func updateCfgDiffsFalse(db *sqlx.DB, hostname string, diffs CfgFileDiffs) (bool, error) {
+	return false, nil
+}
+func insertCfgDiffsError(db *sqlx.DB, hostname string, diffs CfgFileDiffs) error {
+	return fmt.Errorf("Intentional Error")
+}
+func insertCfgDiffsSuccess(db *sqlx.DB, hostname string, diffs CfgFileDiffs) error {
+	return nil
+}
+
+func TestPutCfgDiffs(t *testing.T) {
+	var db *sqlx.DB = nil
+	hostName := "myedge"
+	timestamp := time.Now().UTC().String()
+
+	cfgFileDiffs := CfgFileDiffs{
+		FileName:         "TestFile.cfg",
+		DBLinesMissing:   []string{"db_line_missing1", "db_line_missing2"},
+		DiskLinesMissing: []string{"disk_line_missing1", "disk_line_missing2"},
+		ReportTimestamp:  timestamp,
+	}
+
+	// Test when server request has error
+	code, err := putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsError, updateCfgDiffsError, insertCfgDiffsError)
+
+	if code != -1 {
+		t.Errorf("putCfgDiffs expected: -1 code, actual: %v", code)
+	}
+	if err == nil {
+		t.Errorf("putCfgDiffs expected: non-nil error, actual: nil")
+	}
+
+	// Test when the server doesn't exist
+	code, err = putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsFalse, updateCfgDiffsError, insertCfgDiffsError)
+
+	if code != 0 {
+		t.Errorf("putCfgDiffs expected: 0 code, actual: %v", code)
+	}
+	if err != nil {
+		t.Errorf("putCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+	// Test when the server exists and the update query fails
+	code, err = putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsTrue, updateCfgDiffsError, insertCfgDiffsError)
+
+	if code != -1 {
+		t.Errorf("putCfgDiffs expected: -1 code, actual: %v", code)
+	}
+	if err == nil {
+		t.Errorf("putCfgDiffs expected: non-nil error, actual: nil")
+	}
+
+	// Test when the server exists and the update is successful
+	code, err = putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsTrue, updateCfgDiffsTrue, insertCfgDiffsError)
+
+	if code != 2 {
+		t.Errorf("putCfgDiffs expected: 2 code, actual: %v", code)
+	}
+	if err != nil {
+		t.Errorf("putCfgDiffs expected: non-nil error, actual: %v", err)
+	}
+
+	// Test when the server exists and the update was unsuccessful and the insert had an error
+	code, err = putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsTrue, updateCfgDiffsFalse, insertCfgDiffsError)
+
+	if code != 1 {
+		t.Errorf("putCfgDiffs expected: 1 code, actual: %v", code)
+	}
+	if err == nil {
+		t.Errorf("putCfgDiffs expected: non-nil error, actual: nil")
+	}
+
+	// Test when the server exists and the update was unsuccessful and the insert was successful
+	code, err = putCfgDiffs(db, hostName, cfgFileDiffs, serverExistsTrue, updateCfgDiffsFalse, insertCfgDiffsSuccess)
+
+	if code != 1 {
+		t.Errorf("putCfgDiffs expected: 1 code, actual: %v", code)
+	}
+	if err != nil {
+		t.Errorf("putCfgDiffs expected: nil error, actual: %v", err)
+	}
+
+}
+
+func TestGetCfgDiffsHandler(t *testing.T) {
+
+}
+
+func TestPutCfgDiffsHandler(t *testing.T) {
+
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go b/traffic_ops/traffic_ops_golang/routes.go
index 80e557f20..8e99fd0cc 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -170,6 +170,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 
 		{1.2, http.MethodPost, `servers/{id}/deliveryservices$`, server.AssignDeliveryServicesToServerHandler(d.DB), auth.PrivLevelOperations, Authenticated, nil},
 		{1.2, http.MethodGet, `servers/{host_name}/update_status$`, server.GetServerUpdateStatusHandler(d.DB), auth.PrivLevelReadOnly, Authenticated, nil},
+    
+		//Config Differences
+		{1.2, http.MethodGet, "servers/{host-name}/config_diffs.json$", getCfgDiffsHandler(d.DB), CfgDiffsPrivLevel, Authenticated, nil},
+		{1.2, http.MethodPut, "servers/{host-name}/{cfg-file-name}$", putCfgDiffsHandler(d.DB), CfgDiffsWritePrivLevel, Authenticated, nil},
 
 		//Profiles
 		{1.3, http.MethodGet, `profiles/?(\.json)?$`, api.ReadHandler(profile.GetRefType(), d.DB), auth.PrivLevelReadOnly, Authenticated, nil},


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services