You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by de...@apache.org on 2018/07/09 20:28:46 UTC

[trafficcontrol] 01/02: Refactor cachegroups to use coordinates table

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

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

commit 054b451cc82e37c077b6e2d60035912071814989
Author: Rawlin Peters <ra...@comcast.com>
AuthorDate: Mon Jun 25 11:35:44 2018 -0600

    Refactor cachegroups to use coordinates table
    
    Replace cachegroup lat/long columns with a coordinate FK. On cg create,
    create a coordinate and set the coordinate ID in the cg. On cg update,
    update the referenced coordinate too. On delete, delete the cg and its
    reference coordinate.
    
    Keep the API unchanged; only refactor the backend.
    
    Keep the TO UI functional for cachegroup CRUD.
    
    Update the Perl API backend enough to keep the TO UI and API tests
    working (for Perl APIs yet to be rewritten).
---
 lib/go-tc/constants.go                             |   2 +
 .../20180627000000_cachegroup_coordinate.sql       |  67 ++++++++
 traffic_ops/app/lib/API/Cachegroup.pm              |  57 +++++--
 traffic_ops/app/lib/Fixtures/Cachegroup.pm         |  12 +-
 .../lib/Fixtures/{Cachegroup.pm => Coordinate.pm}  |  31 ++--
 traffic_ops/app/lib/Fixtures/EdgeCachegroup.pm     |   3 +-
 .../app/lib/Fixtures/Integration/Cachegroup.pm     |  36 ++--
 .../Integration/{Cachegroup.pm => Coordinate.pm}   |  76 ++-------
 traffic_ops/app/lib/Schema/Result/Cachegroup.pm    |  46 ++++--
 traffic_ops/app/lib/Schema/Result/Coordinate.pm    |  19 ++-
 traffic_ops/app/lib/Test/IntegrationTestHelper.pm  |   2 +
 traffic_ops/app/lib/Test/TestHelper.pm             |   3 +
 traffic_ops/app/lib/UI/Cachegroup.pm               |  85 ++++++----
 traffic_ops/app/lib/UI/Cdn.pm                      |   6 +-
 traffic_ops/app/lib/UI/Topology.pm                 |   6 +-
 traffic_ops/app/lib/UI/UploadHandlerCsv.pm         |   4 -
 traffic_ops/app/t/api/1.2/cachegroup.t             |   4 +
 traffic_ops/app/templates/cachegroup/_form.html.ep |  12 +-
 traffic_ops/app/templates/cachegroup/view.html.ep  |   4 +-
 .../traffic_ops_golang/cachegroup/cachegroups.go   | 184 ++++++++++++++++++---
 .../traffic_ops_golang/crconfig/edgelocations.go   |   3 +-
 traffic_ops/traffic_ops_golang/monitoring.go       |   7 +-
 22 files changed, 443 insertions(+), 226 deletions(-)

diff --git a/lib/go-tc/constants.go b/lib/go-tc/constants.go
index 4f91944..a72aefb 100644
--- a/lib/go-tc/constants.go
+++ b/lib/go-tc/constants.go
@@ -63,3 +63,5 @@ var apiErrorTypes = []string{"noError", "systemError", "dataConflictError", "dat
 func (a ApiErrorType) String() string {
 	return apiErrorTypes[a]
 }
+
+const CachegroupCoordinateNamePrefix = "from_cachegroup_"
diff --git a/traffic_ops/app/db/migrations/20180627000000_cachegroup_coordinate.sql b/traffic_ops/app/db/migrations/20180627000000_cachegroup_coordinate.sql
new file mode 100644
index 0000000..0cf3df1
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20180627000000_cachegroup_coordinate.sql
@@ -0,0 +1,67 @@
+/*
+
+    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.
+*/
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+
+INSERT INTO coordinate (name, latitude, longitude)
+SELECT
+'from_cachegroup_' || cg.name,
+cg.latitude,
+cg.longitude
+FROM cachegroup cg
+WHERE cg.latitude IS NOT NULL AND cg.longitude IS NOT NULL;
+
+ALTER TABLE cachegroup ADD COLUMN coordinate bigint REFERENCES coordinate(id);
+
+UPDATE cachegroup
+SET coordinate = (
+    SELECT co.id
+    FROM coordinate co
+    WHERE co.name = 'from_cachegroup_' || cachegroup.name
+);
+
+CREATE INDEX cachegroup_coordinate_fkey ON cachegroup USING btree (coordinate);
+
+ALTER TABLE cachegroup DROP COLUMN latitude;
+ALTER TABLE cachegroup DROP COLUMN longitude;
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+ALTER TABLE cachegroup ADD COLUMN latitude numeric;
+ALTER TABLE cachegroup ADD COLUMN longitude numeric;
+
+UPDATE cachegroup
+SET latitude = (
+    SELECT co.latitude
+    FROM coordinate co
+    WHERE cachegroup.coordinate = co.id
+),
+longitude = (
+    SELECT co.longitude
+    FROM coordinate co
+    WHERE cachegroup.coordinate = co.id
+)
+WHERE cachegroup.coordinate IS NOT NULL;
+
+ALTER TABLE cachegroup DROP CONSTRAINT cachegroup_coordinate_fkey;
+
+DELETE FROM coordinate
+WHERE id IN (
+    SELECT cg.coordinate FROM cachegroup cg
+);
+
+ALTER TABLE cachegroup DROP COLUMN coordinate;
diff --git a/traffic_ops/app/lib/API/Cachegroup.pm b/traffic_ops/app/lib/API/Cachegroup.pm
index db430b2..bd55b6a 100644
--- a/traffic_ops/app/lib/API/Cachegroup.pm
+++ b/traffic_ops/app/lib/API/Cachegroup.pm
@@ -45,15 +45,15 @@ sub index {
 		$criteria{'type'} = $type_id;
 	}
 
-	my $rs_data = $self->db->resultset("Cachegroup")->search( \%criteria, { prefetch => [ 'type' ], order_by => 'me.' . $orderby } );
+	my $rs_data = $self->db->resultset("Cachegroup")->search( \%criteria, { prefetch => [ 'type', 'coordinate' ], order_by => 'me.' . $orderby } );
 	while ( my $row = $rs_data->next ) {
 		push(
 			@data, {
 				"id"                            => $row->id,
 				"name"                          => $row->name,
 				"shortName"                     => $row->short_name,
-				"latitude"                      => defined($row->latitude) ? 0.0 + $row->latitude : undef,
-				"longitude"                     => defined($row->longitude) ? 0.0 + $row->longitude : undef,
+				"latitude"                      => defined($row->coordinate) ? 0.0 + $row->coordinate->latitude : undef,
+				"longitude"                     => defined($row->coordinate) ? 0.0 + $row->coordinate->longitude : undef,
 				"lastUpdated"                   => $row->last_updated,
 				"parentCachegroupId"            => $row->parent_cachegroup_id,
 				"parentCachegroupName"          => ( defined $row->parent_cachegroup_id ) ? $idnames{ $row->parent_cachegroup_id } : undef,
@@ -90,7 +90,7 @@ sub show {
 	my $self = shift;
 	my $id   = $self->param('id');
 
-	my $rs_data = $self->db->resultset("Cachegroup")->search( { 'me.id' => $id }, { prefetch => [ 'type' ] } );
+	my $rs_data = $self->db->resultset("Cachegroup")->search( { 'me.id' => $id }, { prefetch => [ 'type', 'coordinate' ] } );
 
 	my @data = ();
 	my %idnames;
@@ -106,8 +106,8 @@ sub show {
 				"id"                            => $row->id,
 				"name"                          => $row->name,
 				"shortName"                     => $row->short_name,
-				"latitude"                      => defined($row->latitude) ? 0.0 + $row->latitude : undef,
-				"longitude"                     => defined($row->longitude) ? 0.0 + $row->longitude : undef,
+				"latitude"                      => defined($row->coordinate) ? 0.0 + $row->coordinate->latitude : undef,
+				"longitude"                     => defined($row->coordinate) ? 0.0 + $row->coordinate->longitude : undef,
 				"lastUpdated"                   => $row->last_updated,
 				"parentCachegroupId"            => $row->parent_cachegroup_id,
 				"parentCachegroupName"          => ( defined $row->parent_cachegroup_id ) ? $idnames{ $row->parent_cachegroup_id } : undef,
@@ -139,11 +139,13 @@ sub update {
 		return $self->alert($result);
 	}
 
-	my $cachegroup = $self->db->resultset('Cachegroup')->find( { id => $id } );
+	my $cachegroup = $self->db->resultset('Cachegroup')->find( { id => $id }, { prefetch => 'coordinate' } );
 	if ( !defined($cachegroup) ) {
 		return $self->not_found();
 	}
 
+	my $coordinate = $cachegroup->coordinate;
+
 	my $name = $params->{name};
 	if ( $cachegroup->name ne $name ) {
 		my $existing = $self->db->resultset('Cachegroup')->find( { name => $name } );
@@ -165,11 +167,20 @@ sub update {
 		$fallback_to_closest = $cachegroup->fallback_to_closest;
 	}
 
+	# note: this Perl API has been updated just to keep the Perl API tests happy until they can be removed.
+	# Hence, this assumes lat/long are always passed (even though they're technically optional)
+	my $lat = $params->{latitude};
+	my $long = $params->{longitude};
+	my $coordinate_values = {
+		latitude  => $lat,
+		longitude => $long
+	};
+
+	my $coordinate_rs = $coordinate->update($coordinate_values);
+
 	my $values = {
 		name                           => $params->{name},
 		short_name                     => $params->{shortName},
-		latitude                       => $params->{latitude},
-		longitude                      => $params->{longitude},
 		parent_cachegroup_id           => $params->{parentCachegroupId},
 		fallback_to_closest            => $fallback_to_closest,
 		secondary_parent_cachegroup_id => $params->{secondaryParentCachegroupId},
@@ -177,7 +188,7 @@ sub update {
 	};
 
 	my $rs = $cachegroup->update($values);
-	if ($rs) {
+	if ($rs && $coordinate_rs) {
 		my %idnames;
 		my $response;
 
@@ -189,8 +200,8 @@ sub update {
 		$response->{id}                 = $rs->id;
 		$response->{name}               = $rs->name;
 		$response->{shortName}          = $rs->short_name;
-		$response->{latitude}           = 0.0 + $rs->latitude;
-		$response->{longitude}          = 0.0 + $rs->longitude;
+		$response->{latitude}           = 0.0 + $coordinate_rs->latitude;
+		$response->{longitude}          = 0.0 + $coordinate_rs->longitude;
 		$response->{lastUpdated}        = $rs->last_updated;
 		$response->{parentCachegroupId} = $rs->parent_cachegroup_id;
 		$response->{parentCachegroupName} =
@@ -242,11 +253,19 @@ sub create {
 		return $self->alert( "A cachegroup with short_name " . $short_name . " already exists." );
 	}
 
+	my $coordinate_values = {
+		name      => "from_cachegroup_" . $params->{name},
+		latitude  => $params->{latitude},
+		longitude => $params->{longitude},
+	};
+
+	my $coordinate = $self->db->resultset('Coordinate')->create($coordinate_values);
+	my $coordinate_rs = $coordinate->insert();
+
 	my $values = {
 		name                           => $params->{name},
 		short_name                     => $params->{shortName},
-		latitude                       => $params->{latitude},
-		longitude                      => $params->{longitude},
+		coordinate                     => $coordinate_rs->id,
 		parent_cachegroup_id           => $params->{parentCachegroupId},
 		fallback_to_closest            => exists ($params->{fallbackToClosest}) ? $params->{fallbackToClosest} : 1,# defaults to true
 		secondary_parent_cachegroup_id => $params->{secondaryParentCachegroupId},
@@ -255,7 +274,7 @@ sub create {
 
 	my $insert = $self->db->resultset('Cachegroup')->create($values);
 	my $rs = $insert->insert();
-	if ($rs) {
+	if ($rs && $coordinate_rs) {
 		my %idnames;
 		my $response;
 
@@ -267,8 +286,8 @@ sub create {
 		$response->{id}                 = $rs->id;
 		$response->{name}               = $rs->name;
 		$response->{shortName}          = $rs->short_name;
-		$response->{latitude}           = 0.0 + $rs->latitude;
-		$response->{longitude}          = 0.0 + $rs->longitude;
+		$response->{latitude}           = 0.0 + $coordinate_rs->latitude;
+		$response->{longitude}          = 0.0 + $coordinate_rs->longitude;
 		$response->{lastUpdated}        = $rs->last_updated;
 		$response->{parentCachegroupId} = $rs->parent_cachegroup_id;
 		$response->{parentCachegroupName} =
@@ -327,8 +346,10 @@ sub delete {
 		return $self->alert("This cachegroup is currently used by one or more ASNs.");
 	}
 
+	my $coordinate = $cg->coordinate;
 	my $rs = $cg->delete();
-	if ($rs) {
+	my $rs_coordinate = $coordinate->delete();
+	if ($rs && $rs_coordinate) {
 		&log( $self, "Deleted Cachegroup named '" . $cg->name . "' with id: " . $cg->id, "APICHANGE" );
 		return $self->success_message("Cachegroup deleted.");
 	} else {
diff --git a/traffic_ops/app/lib/Fixtures/Cachegroup.pm b/traffic_ops/app/lib/Fixtures/Cachegroup.pm
index 8592c56..6064259 100644
--- a/traffic_ops/app/lib/Fixtures/Cachegroup.pm
+++ b/traffic_ops/app/lib/Fixtures/Cachegroup.pm
@@ -24,9 +24,8 @@ my %definition_for = (
 			id                   => 100,
 			name                 => 'mid-northeast-group',
 			short_name           => 'ne',
+			coordinate           => 100,
 			type                 => 2,
-			latitude             => 120,
-			longitude            => 120,
 			parent_cachegroup_id => undef,
 		},
 	},
@@ -36,9 +35,8 @@ my %definition_for = (
 			id                   => 200,
 			name                 => 'mid-northwest-group',
 			short_name           => 'nw',
+			coordinate           => 200,
 			type                 => 2,
-			latitude             => 100,
-			longitude            => 100,
 			parent_cachegroup_id => 100,
 		},
 	},
@@ -48,9 +46,8 @@ my %definition_for = (
 			id                   => 800,
 			name                 => 'mid_cg3',
 			short_name           => 'mid_cg3',
+			coordinate           => 800,
 			type                 => 6,
-			latitude             => 100,
-			longitude            => 100,
 			parent_cachegroup_id => undef,
 		},
 	},
@@ -60,9 +57,8 @@ my %definition_for = (
 			id                   => 900,
 			name                 => 'edge_cg4',
 			short_name           => 'edge_cg4',
+			coordinate           => 900,
 			type                 => 5,
-			latitude             => 100,
-			longitude            => 100,
 			parent_cachegroup_id => 800,
 		},
 	},
diff --git a/traffic_ops/app/lib/Fixtures/Cachegroup.pm b/traffic_ops/app/lib/Fixtures/Coordinate.pm
similarity index 76%
copy from traffic_ops/app/lib/Fixtures/Cachegroup.pm
copy to traffic_ops/app/lib/Fixtures/Coordinate.pm
index 8592c56..c4f39e8 100644
--- a/traffic_ops/app/lib/Fixtures/Cachegroup.pm
+++ b/traffic_ops/app/lib/Fixtures/Coordinate.pm
@@ -1,4 +1,4 @@
-package Fixtures::Cachegroup;
+package Fixtures::Coordinate;
 #
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,51 +19,48 @@ use namespace::autoclean;
 
 my %definition_for = (
 	mid_northeast => {
-		new   => 'Cachegroup',
+		new   => 'Coordinate',
 		using => {
 			id                   => 100,
 			name                 => 'mid-northeast-group',
-			short_name           => 'ne',
-			type                 => 2,
 			latitude             => 120,
 			longitude            => 120,
-			parent_cachegroup_id => undef,
 		},
 	},
 	mid_northwest => {
-		new   => 'Cachegroup',
+		new   => 'Coordinate',
 		using => {
 			id                   => 200,
 			name                 => 'mid-northwest-group',
-			short_name           => 'nw',
-			type                 => 2,
 			latitude             => 100,
 			longitude            => 100,
-			parent_cachegroup_id => 100,
 		},
 	},
 	mid_cg3 => {
-		new   => 'Cachegroup',
+		new   => 'Coordinate',
 		using => {
 			id                   => 800,
 			name                 => 'mid_cg3',
-			short_name           => 'mid_cg3',
-			type                 => 6,
 			latitude             => 100,
 			longitude            => 100,
-			parent_cachegroup_id => undef,
 		},
 	},
 	edge_cg4 => {
-		new   => 'Cachegroup',
+		new   => 'Coordinate',
 		using => {
 			id                   => 900,
 			name                 => 'edge_cg4',
-			short_name           => 'edge_cg4',
-			type                 => 5,
 			latitude             => 100,
 			longitude            => 100,
-			parent_cachegroup_id => 800,
+		},
+	},
+	edge_atl_group => {
+		new   => 'Coordinate',
+		using => {
+			id                   => 1000,
+			name                 => 'edge_atl_group',
+			latitude             => 120,
+			longitude            => 120,
 		},
 	},
 );
diff --git a/traffic_ops/app/lib/Fixtures/EdgeCachegroup.pm b/traffic_ops/app/lib/Fixtures/EdgeCachegroup.pm
index dc7ef00..c9b5244 100644
--- a/traffic_ops/app/lib/Fixtures/EdgeCachegroup.pm
+++ b/traffic_ops/app/lib/Fixtures/EdgeCachegroup.pm
@@ -25,8 +25,7 @@ my %definition_for = (
 			name                           => 'edge_atl_group',
 			short_name                     => 'atl',
 			type                           => 5,
-			latitude                       => 120,
-			longitude                      => 120,
+			coordinate                     => 1000,
 			parent_cachegroup_id           => 100,
 			secondary_parent_cachegroup_id => 200,
 		},
diff --git a/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm b/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm
index 6f0b094..fd30838 100644
--- a/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm
+++ b/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm
@@ -31,12 +31,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'dc-cloudeast',
-			longitude => '0',
+			coordinate => '1',
 			parent_cachegroup_id => undef,
 			short_name => '0-cle',
 			type => '4',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
 		},
 	},
 	## id => 2
@@ -44,11 +43,10 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'dc-cloudwest',
+			coordinate => '2',
 			short_name => '1-clw',
 			type => '4',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
-			longitude => '0',
 			parent_cachegroup_id => undef,
 		},
 	},
@@ -57,11 +55,10 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'origin-east',
+			coordinate => '3',
 			short_name => '2-org-east',
 			type => '25',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
-			longitude => '0',
 			parent_cachegroup_id => undef,
 		},
 	},
@@ -70,12 +67,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'mid-east',
+			coordinate => '4',
 			parent_cachegroup_id => '5',
 			short_name => '3-east',
 			type => '23',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
-			longitude => '0',
 		},
 	},
 	## id => 5
@@ -83,12 +79,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'origin-west',
+			coordinate => '5',
 			parent_cachegroup_id => undef,
 			short_name => '4-org-west',
 			type => '25',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
-			longitude => '0',
 		},
 	},
 	## id => 6
@@ -96,10 +91,9 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'mid-west',
+			coordinate => '6',
 			type => '23',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '0',
-			longitude => '0',
 			parent_cachegroup_id => '6',
 			short_name => '5-west',
 		},
@@ -109,12 +103,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-ca-losangeles',
+			coordinate => '7',
 			parent_cachegroup_id => '4',
 			short_name => '6-lax',
 			type => '14',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '34.05',
-			longitude => '-118.25',
 		},
 	},
 	## id => 8
@@ -122,10 +115,9 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-co-denver',
+			coordinate => '8',
 			type => '14',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '39.739167',
-			longitude => '-104.984722',
 			parent_cachegroup_id => '4',
 			short_name => '7-den',
 		},
@@ -135,12 +127,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-il-chicago',
+			coordinate => '9',
 			parent_cachegroup_id => '4',
 			short_name => '8-chi',
 			type => '14',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '41.881944',
-			longitude => '-87.627778',
 		},
 	},
 	## id => 10
@@ -148,9 +139,8 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-ny-newyork',
+			coordinate => '10',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '40.71435',
-			longitude => '-74.00597',
 			parent_cachegroup_id => '3',
 			short_name => '9-nyc',
 			type => '14',
@@ -161,12 +151,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-pa-philadelphia',
+			coordinate => '11',
 			parent_cachegroup_id => '3',
 			short_name => '10-phl',
 			type => '14',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '39.664722',
-			longitude => '-75.565278',
 		},
 	},
 	## id => 12
@@ -174,12 +163,11 @@ my %definition_for = (
 		new => 'Cachegroup',
 		using => {
 			name => 'us-tx-houston',
+			coordinate => '12',
 			parent_cachegroup_id => '3',
 			short_name => '11-hou',
 			type => '14',
 			last_updated => '2015-12-10 15:44:36',
-			latitude => '29.762778',
-			longitude => '-95.383056',
 		},
 	},
 );
diff --git a/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm b/traffic_ops/app/lib/Fixtures/Integration/Coordinate.pm
similarity index 61%
copy from traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm
copy to traffic_ops/app/lib/Fixtures/Integration/Coordinate.pm
index 6f0b094..a51e428 100644
--- a/traffic_ops/app/lib/Fixtures/Integration/Cachegroup.pm
+++ b/traffic_ops/app/lib/Fixtures/Integration/Coordinate.pm
@@ -1,4 +1,4 @@
-package Fixtures::Integration::Cachegroup;
+package Fixtures::Integration::Coordinate;
 
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -28,156 +28,108 @@ use namespace::autoclean;
 my %definition_for = (
 	## id => 1
 	'0' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'dc-cloudeast',
 			longitude => '0',
-			parent_cachegroup_id => undef,
-			short_name => '0-cle',
-			type => '4',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 		},
 	},
 	## id => 2
 	'1' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'dc-cloudwest',
-			short_name => '1-clw',
-			type => '4',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 			longitude => '0',
-			parent_cachegroup_id => undef,
 		},
 	},
 	## id => 3
 	'2' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'origin-east',
-			short_name => '2-org-east',
-			type => '25',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 			longitude => '0',
-			parent_cachegroup_id => undef,
 		},
 	},
 	## id => 4
 	'3' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'mid-east',
-			parent_cachegroup_id => '5',
-			short_name => '3-east',
-			type => '23',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 			longitude => '0',
 		},
 	},
 	## id => 5
 	'4' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'origin-west',
-			parent_cachegroup_id => undef,
-			short_name => '4-org-west',
-			type => '25',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 			longitude => '0',
 		},
 	},
 	## id => 6
 	'5' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'mid-west',
-			type => '23',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '0',
 			longitude => '0',
-			parent_cachegroup_id => '6',
-			short_name => '5-west',
 		},
 	},
 	## id => 7
 	'6' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-ca-losangeles',
-			parent_cachegroup_id => '4',
-			short_name => '6-lax',
-			type => '14',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '34.05',
 			longitude => '-118.25',
 		},
 	},
 	## id => 8
 	'7' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-co-denver',
-			type => '14',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '39.739167',
 			longitude => '-104.984722',
-			parent_cachegroup_id => '4',
-			short_name => '7-den',
 		},
 	},
 	## id => 9
 	'8' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-il-chicago',
-			parent_cachegroup_id => '4',
-			short_name => '8-chi',
-			type => '14',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '41.881944',
 			longitude => '-87.627778',
 		},
 	},
 	## id => 10
 	'9' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-ny-newyork',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '40.71435',
 			longitude => '-74.00597',
-			parent_cachegroup_id => '3',
-			short_name => '9-nyc',
-			type => '14',
 		},
 	},
 	## id => 11
 	'10' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-pa-philadelphia',
-			parent_cachegroup_id => '3',
-			short_name => '10-phl',
-			type => '14',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '39.664722',
 			longitude => '-75.565278',
 		},
 	},
 	## id => 12
 	'11' => {
-		new => 'Cachegroup',
+		new => 'Coordinate',
 		using => {
 			name => 'us-tx-houston',
-			parent_cachegroup_id => '3',
-			short_name => '11-hou',
-			type => '14',
-			last_updated => '2015-12-10 15:44:36',
 			latitude => '29.762778',
 			longitude => '-95.383056',
 		},
@@ -185,7 +137,7 @@ my %definition_for = (
 );
 
 sub name {
-		return "Cachegroup";
+		return "Coordinate";
 }
 
 sub get_definition {
diff --git a/traffic_ops/app/lib/Schema/Result/Cachegroup.pm b/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
index 453779f..aee4532 100644
--- a/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
+++ b/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
@@ -40,16 +40,6 @@ __PACKAGE__->table("cachegroup");
   data_type: 'text'
   is_nullable: 0
 
-=head2 latitude
-
-  data_type: 'numeric'
-  is_nullable: 1
-
-=head2 longitude
-
-  data_type: 'numeric'
-  is_nullable: 1
-
 =head2 parent_cachegroup_id
 
   data_type: 'bigint'
@@ -81,6 +71,12 @@ __PACKAGE__->table("cachegroup");
   default_value: true
   is_nullable: 1
 
+=head2 coordinate
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 1
+
 =cut
 
 __PACKAGE__->add_columns(
@@ -95,10 +91,6 @@ __PACKAGE__->add_columns(
   { data_type => "text", is_nullable => 0 },
   "short_name",
   { data_type => "text", is_nullable => 0 },
-  "latitude",
-  { data_type => "numeric", is_nullable => 1 },
-  "longitude",
-  { data_type => "numeric", is_nullable => 1 },
   "parent_cachegroup_id",
   { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
   "secondary_parent_cachegroup_id",
@@ -114,6 +106,8 @@ __PACKAGE__->add_columns(
   },
   "fallback_to_closest",
   { data_type => "boolean", default_value => \"true", is_nullable => 1 },
+  "coordinate",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
 );
 
 =head1 PRIMARY KEY
@@ -260,6 +254,26 @@ __PACKAGE__->has_many(
   { cascade_copy => 0, cascade_delete => 0 },
 );
 
+=head2 coordinate
+
+Type: belongs_to
+
+Related object: L<Schema::Result::Coordinate>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "coordinate",
+  "Schema::Result::Coordinate",
+  { id => "coordinate" },
+  {
+    is_deferrable => 0,
+    join_type     => "LEFT",
+    on_delete     => "NO ACTION",
+    on_update     => "NO ACTION",
+  },
+);
+
 =head2 origins
 
 Type: has_many
@@ -361,8 +375,8 @@ __PACKAGE__->belongs_to(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:2EeelbrXDdiyrV9BXGuIeA
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-06-27 16:34:28
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Xv7TVScj3TvTuzk/Gd9Mug
 
 # You can replace this text with custom code or comments, and it will be preserved on regeneration
 #
diff --git a/traffic_ops/app/lib/Schema/Result/Coordinate.pm b/traffic_ops/app/lib/Schema/Result/Coordinate.pm
index 03b8beb..6f94678 100644
--- a/traffic_ops/app/lib/Schema/Result/Coordinate.pm
+++ b/traffic_ops/app/lib/Schema/Result/Coordinate.pm
@@ -107,6 +107,21 @@ __PACKAGE__->add_unique_constraint("coordinate_name_key", ["name"]);
 
 =head1 RELATIONS
 
+=head2 cachegroups
+
+Type: has_many
+
+Related object: L<Schema::Result::Cachegroup>
+
+=cut
+
+__PACKAGE__->has_many(
+  "cachegroups",
+  "Schema::Result::Cachegroup",
+  { "foreign.coordinate" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
 =head2 origins
 
 Type: has_many
@@ -123,8 +138,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FZ64Zkbh+B6CECd1k/h66w
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-06-27 16:34:28
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:TdhJL1P7uk/07Oz2Y73Plw
 
 #
 #
diff --git a/traffic_ops/app/lib/Test/IntegrationTestHelper.pm b/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
index 9213d68..3dfe93d 100644
--- a/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
+++ b/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
@@ -30,6 +30,7 @@ use Fixtures::Integration::Asn;
 use Fixtures::Integration::CachegroupParameter;
 use Fixtures::Integration::Cachegroup;
 use Fixtures::Integration::Cdn;
+use Fixtures::Integration::Coordinate;
 use Fixtures::Integration::Deliveryservice;
 use Fixtures::Integration::DeliveryserviceRegex;
 use Fixtures::Integration::DeliveryserviceServer;
@@ -150,6 +151,7 @@ sub load_core_data {
 	$self->load_all_fixtures( Fixtures::Integration::Region->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::PhysLocation->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Status->new($schema_values) );
+	$self->load_all_fixtures( Fixtures::Integration::Coordinate->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Cachegroup->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Regex->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Parameter->new($schema_values) );
diff --git a/traffic_ops/app/lib/Test/TestHelper.pm b/traffic_ops/app/lib/Test/TestHelper.pm
index 388f181..5deca43 100644
--- a/traffic_ops/app/lib/Test/TestHelper.pm
+++ b/traffic_ops/app/lib/Test/TestHelper.pm
@@ -33,6 +33,7 @@ use Fixtures::Origin;
 use Fixtures::DeliveryserviceTmuser;
 use Fixtures::Asn;
 use Fixtures::Cachegroup;
+use Fixtures::Coordinate;
 use Fixtures::EdgeCachegroup;
 use Fixtures::Log;
 use Fixtures::Job;
@@ -129,6 +130,7 @@ sub load_core_data {
 	$self->load_all_fixtures( Fixtures::Profile->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::ProfileParameter->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Type->new($schema_values) );
+	$self->load_all_fixtures( Fixtures::Coordinate->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Cachegroup->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::EdgeCachegroup->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Division->new($schema_values) );
@@ -171,6 +173,7 @@ sub unload_core_data {
 	$self->teardown($schema, 'Region');
 	$self->teardown($schema, 'Division');
 	$self->teardown_cachegroup();
+	$self->teardown($schema, 'Coordinate');
 	$self->teardown($schema, 'Profile');
 	$self->teardown($schema, 'Parameter');
 	$self->teardown($schema, 'ProfileParameter');
diff --git a/traffic_ops/app/lib/UI/Cachegroup.pm b/traffic_ops/app/lib/UI/Cachegroup.pm
index 7662212..6819d0b 100644
--- a/traffic_ops/app/lib/UI/Cachegroup.pm
+++ b/traffic_ops/app/lib/UI/Cachegroup.pm
@@ -36,7 +36,7 @@ sub index {
 
 sub add {
 	my $self = shift;
-	$self->stash( fbox_layout => 1, cg_data => {} );
+	$self->stash( fbox_layout => 1, cg_data => {}, coordinate => {} );
 	&stash_role($self);
 	if ( $self->stash('priv_level') < 30 ) {
 		$self->stash( alertmsg => "Insufficient privileges!" );
@@ -50,7 +50,7 @@ sub view {
 	my $id   = $self->param('id');
 	$self->stash( cg_data => {} );
 
-	my $rs_param = $self->db->resultset('Cachegroup')->search( { id => $id } );
+	my $rs_param = $self->db->resultset('Cachegroup')->search( { 'me.id' => $id }, { prefetch => 'coordinate' } );
 	my $data = $rs_param->single;
 
 	my $type_id = $self->db->resultset('Cachegroup')->search( { id => $id } )->get_column('type')->single();
@@ -99,7 +99,8 @@ sub view {
 	);
 
 	&stash_role($self);
-	$self->stash( fbox_layout => 1, cg_data => $data, selected_type => $selected_type );
+	my $coordinate_data = defined( $data->coordinate ) ? $data->coordinate : {};
+	$self->stash( fbox_layout => 1, cg_data => $data, coordinate => $coordinate_data, selected_type => $selected_type );
 
 	if ( $mode eq "edit" and $self->stash('priv_level') > 20 ) {
 		$self->render( template => 'cachegroup/edit' );
@@ -125,7 +126,7 @@ sub read {
 		$idnames{ $row->id } = $row->name;
 	}
 
-	my $rs_data = $self->db->resultset("Cachegroup")->search( undef, { prefetch => [ { 'type' => undef, } ], order_by => 'me.' . $orderby } );
+	my $rs_data = $self->db->resultset("Cachegroup")->search( undef, { prefetch => [ { 'type' => undef, }, 'coordinate' ], order_by => 'me.' . $orderby } );
 	while ( my $row = $rs_data->next ) {
 		if ( defined $row->parent_cachegroup_id ) {
 			push(
@@ -133,8 +134,8 @@ sub read {
 					"id"                   => $row->id,
 					"name"                 => $row->name,
 					"short_name"           => $row->short_name,
-					"latitude"             => $row->latitude,
-					"longitude"            => $row->longitude,
+					"latitude"             => $row->coordinate->latitude,
+					"longitude"            => $row->coordinate->longitude,
 					"last_updated"         => $row->last_updated,
 					"parent_location_id"   => $row->parent_cachegroup_id,
 					"parent_location_name" => $idnames{ $row->parent_cachegroup_id },
@@ -149,8 +150,8 @@ sub read {
 					"id"                   => $row->id,
 					"name"                 => $row->name,
 					"short_name"           => $row->short_name,
-					"latitude"             => $row->latitude,
-					"longitude"            => $row->longitude,
+					"latitude"             => $row->coordinate->latitude,
+					"longitude"            => $row->coordinate->longitude,
 					"last_updated"         => $row->last_updated,
 					"parent_location_id"   => $row->parent_cachegroup_id,
 					"parent_location_name" => undef,
@@ -193,8 +194,12 @@ sub delete {
 	}
 	else {
 		my $p_name = $self->db->resultset('Cachegroup')->search( { id => $id } )->get_column('name')->single();
-		my $delete = $self->db->resultset('Cachegroup')->search( { id => $id } );
+		my $delete = $self->db->resultset('Cachegroup')->search( { 'me.id' => $id }, { prefetch => 'coordinate' } );
+		my $coordinate = $delete->single()->coordinate;
 		$delete->delete();
+		if ( defined( $coordinate ) ) {
+			$coordinate->delete();
+		}
 		&log( $self, "Delete cachegroup " . $p_name, "UICHANGE" );
 	}
 	return $self->redirect_to('/close_fancybox.html');
@@ -235,11 +240,13 @@ sub update {
 			id                             => $id,
 			name                           => $self->param('cg_data.name'),
 			short_name                     => $self->param('cg_data.short_name'),
-			latitude                       => $self->param('cg_data.latitude'),
-			longitude                      => $self->param('cg_data.longitude'),
 			parent_cachegroup_id           => $parent_cachegroup_id,
 			secondary_parent_cachegroup_id => $secondary_parent_cachegroup_id,
 			type                           => $self->param('cg_data.type')
+		},
+		coordinate => {
+			latitude => $self->param('coordinate.latitude'),
+			longitude => $self->param('coordinate.longitude')
 		}
 	);
 
@@ -254,11 +261,12 @@ sub update {
 		$self->flash( alertmsg => $err );
 	}
 	else {
-		my $update = $self->db->resultset('Cachegroup')->find( { id => $self->param('id') } );
+		my $update = $self->db->resultset('Cachegroup')->find( { id => $self->param('id') }, { prefetch => 'coordinate' } );
 		$update->name( $self->param('cg_data.name') );
 		$update->short_name( $self->param('cg_data.short_name') );
-		$update->latitude( $self->param('cg_data.latitude') );
-		$update->longitude( $self->param('cg_data.longitude') );
+		$update->coordinate->name( 'from_cachegroup_' . $self->param('cg_data.name') );
+		$update->coordinate->latitude( $self->param('coordinate.latitude') );
+		$update->coordinate->longitude( $self->param('coordinate.longitude') );
 		if ( $parent_cachegroup_id != -1 ) {
 			$update->parent_cachegroup_id( $self->param('cg_data.parent_cachegroup_id') );
 		}
@@ -273,6 +281,7 @@ sub update {
                 }
 		$update->type( $self->param('cg_data.type') );
 		$update->update();
+		$update->coordinate->update();
 
 		foreach my $param ( $self->param ) {
 			next unless $param =~ /^param:/;
@@ -296,8 +305,8 @@ sub create {
 	my $self        = shift;
 	my $name        = $self->param('cg_data.name');
 	my $short_name  = $self->param('cg_data.short_name');
-	my $latitude    = $self->param('cg_data.latitude');
-	my $longitude   = $self->param('cg_data.longitude');
+	my $latitude    = $self->param('coordinate.latitude');
+	my $longitude   = $self->param('coordinate.longitude');
 	my $cachegroup  = $self->param('cg_data.parent_cachegroup_id');
 	my $type        = $self->param('cg_data.type');
 	my $data        = $self->get_cachegroups();
@@ -310,8 +319,10 @@ sub create {
 			cg_data     => {
 				name       => $name,
 				short_name => $short_name,
-				latitude   => $latitude,
-				longitude  => $longitude
+			},
+			coordinate => {
+				latitude => $latitude,
+				longitude => $longitude
 			}
 		);
 		return $self->render('cachegroup/add');
@@ -323,8 +334,10 @@ sub create {
 			cg_data     => {
 				name       => $name,
 				short_name => $short_name,
-				latitude   => $latitude,
-				longitude  => $longitude
+			},
+			coordinate  => {
+				latitude  => $latitude,
+				longitude => $longitude
 			}
 		);
 		return $self->render('cachegroup/add');
@@ -336,8 +349,10 @@ sub create {
 			cg_data     => {
 				name       => $name,
 				short_name => $short_name,
-				latitude   => $latitude,
-				longitude  => $longitude
+			},
+			coordinate  => {
+				latitude  => $latitude,
+				longitude => $longitude
 			}
 		);
 		return $self->render('cachegroup/add');
@@ -352,12 +367,22 @@ sub create {
 
 		my $parent_cachegroup_id = $cachegroup;    # sharing the code in JS for create and edit.
 		$parent_cachegroup_id = undef if ( $parent_cachegroup_id == -1 );
+
+		my $coordinate = $self->db->resultset('Coordinate')->create(
+			{
+				name => 'from_cachegroup_' . $name,
+				latitude => $latitude,
+				longitude => $longitude
+			}
+		);
+		$coordinate->insert();
+		my $coordinate_id = $coordinate->id;
+
 		my $insert = $self->db->resultset('Cachegroup')->create(
 			{
 				name                 => $name,
 				short_name           => $short_name,
-				latitude             => $latitude,
-				longitude            => $longitude,
+				coordinate           => $coordinate_id,
 				parent_cachegroup_id => $parent_cachegroup_id,
 				type                 => $type,
 			}
@@ -380,16 +405,16 @@ sub isValidCachegroup {
 	my $self = shift;
 	$self->field('cg_data.name')->is_required->is_like( qr/^[0-9a-zA-Z_\.\-]+$/, "Use alphanumeric . or _ ." );
 	$self->field('cg_data.short_name')->is_required->is_like( qr/^[0-9a-zA-Z_\.\-]+$/, "Use alphanumeric . or _" );
-	$self->field('cg_data.latitude')->is_required->is_like( qr/^[-]*[0-9]+[.]*[0-9]*/, "Invalid latitude entered." );
-	$self->field('cg_data.longitude')->is_required->is_like( qr/^[-]*[0-9]+[.]*[0-9]*/, "Invalid latitude entered." );
-	my $latitude  = $self->param('cg_data.latitude');
-	my $longitude = $self->param('cg_data.longitude');
+	$self->field('coordinate.latitude')->is_required->is_like( qr/^[-]*[0-9]+[.]*[0-9]*/, "Invalid latitude entered." );
+	$self->field('coordinate.longitude')->is_required->is_like( qr/^[-]*[0-9]+[.]*[0-9]*/, "Invalid latitude entered." );
+	my $latitude  = $self->param('coordinate.latitude');
+	my $longitude = $self->param('coordinate.longitude');
 
 	if ( abs $latitude > 90 ) {
-		$self->field('cg_data.latitude')->is_required->is_like( qr/^\./, "May not exceed +- 90.0." );
+		$self->field('coordinate.latitude')->is_required->is_like( qr/^\./, "May not exceed +- 90.0." );
 	}
 	if ( abs $longitude > 180 ) {
-		$self->field('cg_data.longitude')->is_required->is_like( qr/^\./, "May not exceed +- 180.0." );
+		$self->field('coordinate.longitude')->is_required->is_like( qr/^\./, "May not exceed +- 180.0." );
 	}
 
 	return $self->valid;
diff --git a/traffic_ops/app/lib/UI/Cdn.pm b/traffic_ops/app/lib/UI/Cdn.pm
index 72f2aea..30bcaf4 100644
--- a/traffic_ops/app/lib/UI/Cdn.pm
+++ b/traffic_ops/app/lib/UI/Cdn.pm
@@ -635,11 +635,13 @@ sub acachegroup {
         $id_to_name{ $row->id } = $row->name;
     }
 
-    $rs = $self->db->resultset('Cachegroup')->search( undef, { prefetch => [ { 'type' => undef } ] } );
+    $rs = $self->db->resultset('Cachegroup')->search( undef, { prefetch => [ { 'type' => undef }, 'coordinate' ] } );
 
     while ( my $row = $rs->next ) {
         my @line = [
-            $row->id, $row->name, $row->short_name, $row->type->name, 0.0 + $row->latitude, 0.0 + $row->longitude,
+            $row->id, $row->name, $row->short_name, $row->type->name,
+            defined( $row->coordinate ) ? 0.0 + $row->coordinate->latitude : undef,
+            defined( $row->coordinate ) ? 0.0 + $row->coordinate->longitude: undef,
             defined( $row->parent_cachegroup_id )
             ? $id_to_name{ $row->parent_cachegroup_id }
             : undef,
diff --git a/traffic_ops/app/lib/UI/Topology.pm b/traffic_ops/app/lib/UI/Topology.pm
index c87f8f0..17cbb74 100644
--- a/traffic_ops/app/lib/UI/Topology.pm
+++ b/traffic_ops/app/lib/UI/Topology.pm
@@ -201,7 +201,7 @@ sub gen_crconfig_json {
             'type.name' => [ { -like => 'EDGE%' }, { -like => 'MID%' }, { -like => 'CCR' }, { -like => 'RASCAL' }, { -like => 'TR' }, { -like => 'TM' } ],
             'me.cdn_id' => $cdn_id
         }, {
-            prefetch => [ 'type',      'status',      'cachegroup', 'profile' ],
+            prefetch => [ 'type',      'status',      { 'cachegroup' => 'coordinate' }, 'profile' ],
             columns  => [ 'host_name', 'domain_name', 'tcp_port', 'https_port',   'interface_name', 'ip_address', 'ip6_address', 'id', 'xmpp_id', 'profile.routing_disabled' ]
         }
     );
@@ -248,8 +248,8 @@ sub gen_crconfig_json {
         elsif ( $row->type->name =~ m/^EDGE/ || $row->type->name =~ m/^MID/ ) {
 
             if ( $row->type->name =~ m/^EDGE/ ) {
-                $data_obj->{'edgeLocations'}->{ $row->cachegroup->name }->{'latitude'}  = $row->cachegroup->latitude + 0;
-                $data_obj->{'edgeLocations'}->{ $row->cachegroup->name }->{'longitude'} = $row->cachegroup->longitude + 0;
+                $data_obj->{'edgeLocations'}->{ $row->cachegroup->name }->{'latitude'}  = $row->cachegroup->coordinate->latitude + 0;
+                $data_obj->{'edgeLocations'}->{ $row->cachegroup->name }->{'longitude'} = $row->cachegroup->coordinate->longitude + 0;
                 $data_obj->{'edgeLocations'}->{ $row->cachegroup->name }->{'backupLocations'}->{'fallbackToClosest'} = $row->cachegroup->fallback_to_closest ? "true" : "false";
 
                 my $rs_backups = $self->db->resultset('CachegroupFallback')->search({ primary_cg => $row->cachegroup->id}, {order_by => 'set_order'});
diff --git a/traffic_ops/app/lib/UI/UploadHandlerCsv.pm b/traffic_ops/app/lib/UI/UploadHandlerCsv.pm
index 139b24a..d1e5914 100644
--- a/traffic_ops/app/lib/UI/UploadHandlerCsv.pm
+++ b/traffic_ops/app/lib/UI/UploadHandlerCsv.pm
@@ -75,8 +75,6 @@ sub getCachegroupCheckData {    # renamed to 'CacheGroup'
 					"id"                     => $row->id,
 					"name"                   => $row->name,
 					"short_name"             => $row->short_name,
-					"latitude"               => $row->latitude,
-					"longitude"              => $row->longitude,
 					"last_updated"           => $row->last_updated,
 					"parent_cachegroup_id"   => $row->parent_cachegroup_id,
 					"parent_cachegroup_name" => $idnames{ $row->parent_cachegroup_id },
@@ -91,8 +89,6 @@ sub getCachegroupCheckData {    # renamed to 'CacheGroup'
 					"id"                     => $row->id,
 					"name"                   => $row->name,
 					"short_name"             => $row->short_name,
-					"latitude"               => $row->latitude,
-					"longitude"              => $row->longitude,
 					"last_updated"           => $row->last_updated,
 					"parent_cachegroup_id"   => $row->parent_cachegroup_id,
 					"parent_cachegroup_name" => undef,
diff --git a/traffic_ops/app/t/api/1.2/cachegroup.t b/traffic_ops/app/t/api/1.2/cachegroup.t
index ea16284..1384298 100644
--- a/traffic_ops/app/t/api/1.2/cachegroup.t
+++ b/traffic_ops/app/t/api/1.2/cachegroup.t
@@ -214,6 +214,8 @@ ok $t->put_ok('/api/1.2/cachegroups/' . $cg_id => {Accept => 'application/json'}
 ok $t->put_ok('/api/1.2/cachegroups/' . $cg_id => {Accept => 'application/json'} => json => {
         "name" => "cache_group_edge_2",
         "shortName" => "cg_edge_2",
+        "latitude" => 0,
+        "longitude" => 0,
         "parentCachegroupId" => undef,
         "secondaryParentCachegroupId" => undef,
         "typeId" => 5 })->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } )
@@ -230,6 +232,8 @@ ok $t->put_ok('/api/1.2/cachegroups/' . $cg_id => {Accept => 'application/json'}
 ok $t->put_ok('/api/1.2/cachegroups/' . $cg_id => {Accept => 'application/json'} => json => {
         "name" => "cache_group_edge_2",
         "shortName" => "cg_edge_2",
+        "latitude" => 0,
+        "longitude" => 0,
         "parentCachegroupId" => $cache_group_mid_id,
         "typeId" => 5})->status_is(200)->or( sub { diag $t->tx->res->content->asset->{content}; } )
     ->json_is( "/response/parentCachegroupId" => $cache_group_mid_id)
diff --git a/traffic_ops/app/templates/cachegroup/_form.html.ep b/traffic_ops/app/templates/cachegroup/_form.html.ep
index 055a8b0..8104ab4 100644
--- a/traffic_ops/app/templates/cachegroup/_form.html.ep
+++ b/traffic_ops/app/templates/cachegroup/_form.html.ep
@@ -30,19 +30,19 @@
 	</div>
 	<br>
 	<div class="block">
-		<% unless (field('cg_data.latitude')->valid) { %>
-			<span class="field-with-error"><%= field('cg_data.latitude')->error %></span>
+		<% unless (field('coordinate.latitude')->valid) { %>
+			<span class="field-with-error"><%= field('coordinate.latitude')->error %></span>
 		<% } %>
 		%= label_for 'name' => '* Geo Magnetic Latitude', class => 'label'
-		%= field('cg_data.latitude')->text(class => 'field', required => 'required', size => 10, name => 'cg_data.latitude', id => 'cg_data.latitude')
+		%= field('coordinate.latitude')->text(class => 'field', required => 'required', size => 10, name => 'coordinate.latitude', id => 'coordinate.latitude')
 	</div>
 	<br>
 	<div class="block">
-		<% unless (field('cg_data.longitude')->valid) { %>
-			<span class="field-with-error"><%= field('cg_data.longitude')->error %></span>
+		<% unless (field('coordinate.longitude')->valid) { %>
+			<span class="field-with-error"><%= field('coordinate.longitude')->error %></span>
 		<% } %>
 		%= label_for 'name' => '* Geo Magnetic Longitude', class => 'label'
-		%= field('cg_data.longitude')->text(class => 'field', required => 'required', size => 10, name => 'cg_data.longitude', id => 'cg_data.longitude')
+		%= field('coordinate.longitude')->text(class => 'field', required => 'required', size => 10, name => 'coordinate.longitude', id => 'coordinate.longitude')
 	</div>
 	<br>
 	<div class="block">
diff --git a/traffic_ops/app/templates/cachegroup/view.html.ep b/traffic_ops/app/templates/cachegroup/view.html.ep
index 891bb18..2f93acf 100644
--- a/traffic_ops/app/templates/cachegroup/view.html.ep
+++ b/traffic_ops/app/templates/cachegroup/view.html.ep
@@ -40,11 +40,11 @@ $(function () {
 							</tr>
 							<tr>
 								<td>Latitude</td>
-								<td class="editable" id="latitude"><%= $cg_data->latitude %></td>
+								<td class="editable" id="latitude"><%= $coordinate->latitude %></td>
 							</tr>
 							<tr>
 								<td>Longitude</td>
-								<td class="editable" id="longitude"><%= $cg_data->longitude %></td>
+								<td class="editable" id="longitude"><%= $coordinate->longitude %></td>
 							</tr>
 							<tr>
 								<td>Parent Location</td>
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
index 4c53856..6a008fc 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
@@ -20,6 +20,7 @@ package cachegroup
  */
 
 import (
+	"database/sql"
 	"errors"
 	"fmt"
 	"strconv"
@@ -33,7 +34,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
 
-	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/go-ozzo/ozzo-validation"
 	"github.com/jmoiron/sqlx"
 	"github.com/lib/pq"
 )
@@ -215,7 +216,21 @@ func (cg *TOCacheGroup) Create() (error, tc.ApiErrorType) {
 		return tc.DBError, tc.SystemError
 	}
 
-	resultRows, err := cg.ReqInfo.Tx.NamedQuery(insertQuery(), cg)
+	coordinateID, err := cg.createCoordinate()
+	if err != nil {
+		log.Errorf("creating cachegroup: %v", err)
+		return tc.DBError, tc.SystemError
+	}
+
+	resultRows, err := cg.ReqInfo.Tx.Query(
+		insertQuery(),
+		cg.Name,
+		cg.ShortName,
+		coordinateID,
+		cg.TypeID,
+		cg.ParentCachegroupID,
+		cg.SecondaryParentCachegroupID,
+	)
 	if err != nil {
 		if pqErr, ok := err.(*pq.Error); ok {
 			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
@@ -254,6 +269,64 @@ func (cg *TOCacheGroup) Create() (error, tc.ApiErrorType) {
 	return nil, tc.NoError
 }
 
+func (cg *TOCacheGroup) createCoordinate() (*int, error) {
+	var coordinateID *int
+	if cg.Latitude != nil && cg.Longitude != nil {
+		q := `INSERT INTO coordinate (name, latitude, longitude) VALUES ($1, $2, $3) RETURNING id`
+		if err := cg.ReqInfo.Tx.QueryRow(q, tc.CachegroupCoordinateNamePrefix+*cg.Name, *cg.Latitude, *cg.Longitude).Scan(&coordinateID); err != nil {
+			return nil, fmt.Errorf("insert coordinate for cg '%s': %s", *cg.Name, err.Error())
+		}
+	}
+	return coordinateID, nil
+}
+
+func (cg *TOCacheGroup) updateCoordinate() error {
+	if cg.Latitude != nil && cg.Longitude != nil {
+		q := `UPDATE coordinate SET name = $1, latitude = $2, longitude = $3 WHERE id = (SELECT coordinate FROM cachegroup WHERE id = $4)`
+		result, err := cg.ReqInfo.Tx.Exec(q, tc.CachegroupCoordinateNamePrefix+*cg.Name, *cg.Latitude, *cg.Longitude, *cg.ID)
+		if err != nil {
+			return fmt.Errorf("update coordinate for cg '%s': %s", *cg.Name, err.Error())
+		}
+		rowsAffected, err := result.RowsAffected()
+		if err != nil {
+			return fmt.Errorf("update coordinate for cg '%s', getting rows affected: %s", *cg.Name, err.Error())
+		}
+		if rowsAffected == 0 {
+			return fmt.Errorf("update coordinate for cg '%s', zero rows affected", *cg.Name)
+		}
+	}
+	return nil
+}
+
+func (cg *TOCacheGroup) deleteCoordinate(coordinateID int) error {
+	q := `UPDATE cachegroup SET coordinate = NULL WHERE id = $1`
+	result, err := cg.ReqInfo.Tx.Exec(q, *cg.ID)
+	if err != nil {
+		return fmt.Errorf("updating cg %d coordinate to null: %s", *cg.ID, err.Error())
+	}
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return fmt.Errorf("updating cg %d coordinate to null, getting rows affected: %s", *cg.ID, err.Error())
+	}
+	if rowsAffected == 0 {
+		return fmt.Errorf("updating cg %d coordinate to null, zero rows affected", *cg.ID)
+	}
+
+	q = `DELETE FROM coordinate WHERE id = $1`
+	result, err = cg.ReqInfo.Tx.Exec(q, coordinateID)
+	if err != nil {
+		return fmt.Errorf("delete coordinate %d for cg %d: %s", coordinateID, *cg.ID, err.Error())
+	}
+	rowsAffected, err = result.RowsAffected()
+	if err != nil {
+		return fmt.Errorf("delete coordinate %d for cg %d, getting rows affected: %s", coordinateID, *cg.ID, err.Error())
+	}
+	if rowsAffected == 0 {
+		return fmt.Errorf("delete coordinate %d for cg %d, zero rows affected", coordinateID, *cg.ID)
+	}
+	return nil
+}
+
 func (cg *TOCacheGroup) Read(parameters map[string]string) ([]interface{}, []error, tc.ApiErrorType) {
 	// Query Parameters to Database Query column mappings
 	// see the fields mapped in the SQL query
@@ -304,8 +377,22 @@ func (cg *TOCacheGroup) Update() (error, tc.ApiErrorType) {
 		return tc.DBError, tc.SystemError
 	}
 
+	coordinateID, err, errType := cg.handleCoordinateUpdate()
+	if err != nil {
+		return err, errType
+	}
+
 	log.Debugf("about to run exec query: %s with cg: %++v", updateQuery(), cg)
-	resultRows, err := cg.ReqInfo.Tx.NamedQuery(updateQuery(), cg)
+	resultRows, err := cg.ReqInfo.Tx.Query(
+		updateQuery(),
+		cg.Name,
+		cg.ShortName,
+		coordinateID,
+		cg.ParentCachegroupID,
+		cg.SecondaryParentCachegroupID,
+		cg.TypeID,
+		cg.ID,
+	)
 	if err != nil {
 		if pqErr, ok := err.(*pq.Error); ok {
 			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
@@ -341,6 +428,46 @@ func (cg *TOCacheGroup) Update() (error, tc.ApiErrorType) {
 	return nil, tc.NoError
 }
 
+func (cg *TOCacheGroup) handleCoordinateUpdate() (*int, error, tc.ApiErrorType) {
+	coordinateID, err := cg.getCoordinateID()
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, fmt.Errorf("no cg with id %d found", *cg.ID), tc.DataMissingError
+		}
+		log.Errorf("updating cg %d got error when querying coordinate: %s\n", *cg.ID, err)
+		return nil, tc.DBError, tc.SystemError
+	}
+	if coordinateID == nil && cg.Latitude != nil && cg.Longitude != nil {
+		newCoordinateID, err := cg.createCoordinate()
+		if err != nil {
+			log.Errorf("updating cg %d: %s\n", *cg.ID, err)
+			return nil, tc.DBError, tc.SystemError
+		}
+		coordinateID = newCoordinateID
+	} else if coordinateID != nil && (cg.Latitude == nil || cg.Longitude == nil) {
+		if err = cg.deleteCoordinate(*coordinateID); err != nil {
+			log.Errorf("updating cg %d: %s\n", *cg.ID, err)
+			return nil, tc.DBError, tc.SystemError
+		}
+		coordinateID = nil
+	} else {
+		if err = cg.updateCoordinate(); err != nil {
+			log.Errorf("updating cg %d: %s\n", *cg.ID, err)
+			return nil, tc.DBError, tc.SystemError
+		}
+	}
+	return coordinateID, nil, tc.NoError
+}
+
+func (cg *TOCacheGroup) getCoordinateID() (*int, error) {
+	q := `SELECT coordinate FROM cachegroup WHERE id = $1`
+	var coordinateID *int
+	if err := cg.ReqInfo.Tx.QueryRow(q, *cg.ID).Scan(&coordinateID); err != nil {
+		return nil, err
+	}
+	return coordinateID, nil
+}
+
 //The CacheGroup implementation of the Deleter interface
 //all implementations of Deleter should use transactions and return the proper errorType
 func (cg *TOCacheGroup) Delete() (error, tc.ApiErrorType) {
@@ -353,6 +480,22 @@ func (cg *TOCacheGroup) Delete() (error, tc.ApiErrorType) {
 		return err, tc.DataConflictError
 	}
 
+	coordinateID, err := cg.getCoordinateID()
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return fmt.Errorf("no cachegroup with id %d found", *cg.ID), tc.DataMissingError
+		}
+		log.Errorf("deleting cachegroup %d, got error when querying coordinate: %s\n", *cg.ID, err.Error())
+		return tc.DBError, tc.SystemError
+	}
+
+	if coordinateID != nil {
+		if err = cg.deleteCoordinate(*coordinateID); err != nil {
+			log.Errorf("deleting cachegroup %d: %s\n", *cg.ID, err.Error())
+			return tc.DBError, tc.SystemError
+		}
+	}
+
 	log.Debugf("about to run exec query: %s with cg: %++v", deleteQuery(), cg)
 	result, err := cg.ReqInfo.Tx.NamedExec(deleteQuery(), cg)
 	if err != nil {
@@ -376,26 +519,15 @@ func (cg *TOCacheGroup) Delete() (error, tc.ApiErrorType) {
 
 // insert query
 func insertQuery() string {
-	// to disambiguate struct scans, the named
-	// parameter 'type_id' is an alias to cachegroup.type
-	//see also the v13.CacheGroupNullable struct 'db' metadata
 	query := `INSERT INTO cachegroup (
 name,
 short_name,
-latitude,
-longitude,
+coordinate,
 type,
 parent_cachegroup_id,
 secondary_parent_cachegroup_id
-) VALUES(
-:name,
-:short_name,
-:latitude,
-:longitude,
-:type_id,
-:parent_cachegroup_id,
-:secondary_parent_cachegroup_id
-) RETURNING id,last_updated`
+) VALUES($1,$2,$3,$4,$5,$6)
+RETURNING id,last_updated`
 	return query
 }
 
@@ -409,8 +541,8 @@ func selectQuery() string {
 cachegroup.id,
 cachegroup.name,
 cachegroup.short_name,
-cachegroup.latitude,
-cachegroup.longitude,
+coordinate.latitude,
+coordinate.longitude,
 cachegroup.parent_cachegroup_id,
 cgp.name AS parent_cachegroup_name,
 cachegroup.secondary_parent_cachegroup_id,
@@ -419,6 +551,7 @@ type.name AS type_name,
 cachegroup.type AS type_id,
 cachegroup.last_updated
 FROM cachegroup
+LEFT JOIN coordinate ON coordinate.id = cachegroup.coordinate
 INNER JOIN type ON cachegroup.type = type.id
 LEFT JOIN cachegroup AS cgp ON cachegroup.parent_cachegroup_id = cgp.id
 LEFT JOIN cachegroup AS cgs ON cachegroup.secondary_parent_cachegroup_id = cgs.id`
@@ -432,13 +565,12 @@ func updateQuery() string {
 	//see also the v13.CacheGroupNullable struct 'db' metadata
 	query := `UPDATE
 cachegroup SET
-name=:name,
-short_name=:short_name,
-latitude=:latitude,
-longitude=:longitude,
-parent_cachegroup_id=:parent_cachegroup_id,
-secondary_parent_cachegroup_id=:secondary_parent_cachegroup_id,
-type=:type_id WHERE id=:id RETURNING last_updated`
+name=$1,
+short_name=$2,
+coordinate=$3,
+parent_cachegroup_id=$4,
+secondary_parent_cachegroup_id=$5,
+type=$6 WHERE id=$7 RETURNING last_updated`
 	return query
 }
 
diff --git a/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go b/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
index 924afb6..364d336 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/edgelocations.go
@@ -32,7 +32,8 @@ func makeLocations(cdn string, db *sql.DB) (map[string]tc.CRConfigLatitudeLongit
 
 	// TODO test whether it's faster to do a single query, joining lat/lon into servers
 	q := `
-select cg.name, cg.id, t.name as type, cg.latitude, cg.longitude, cg.fallback_to_closest from cachegroup as cg
+select cg.name, cg.id, t.name as type, co.latitude, co.longitude, cg.fallback_to_closest from cachegroup as cg
+left join coordinate as co on co.id = cg.coordinate
 inner join server as s on s.cachegroup = cg.id
 inner join type as t on t.id = s.type
 inner join status as st ON st.id = s.status
diff --git a/traffic_ops/traffic_ops_golang/monitoring.go b/traffic_ops/traffic_ops_golang/monitoring.go
index d439ed4..77fa26e 100644
--- a/traffic_ops/traffic_ops_golang/monitoring.go
+++ b/traffic_ops/traffic_ops_golang/monitoring.go
@@ -248,9 +248,10 @@ WHERE cdn.name = $1`
 
 func getCachegroups(db *sqlx.DB, cdn string) ([]Cachegroup, error) {
 	query := `
-SELECT name, latitude, longitude
-FROM cachegroup
-WHERE id IN
+SELECT cg.name, co.latitude, co.longitude
+FROM cachegroup cg
+LEFT JOIN coordinate co ON co.id = cg.coordinate
+WHERE cg.id IN
   (SELECT cachegroup FROM server WHERE server.cdn_id =
     (SELECT id FROM cdn WHERE name = $1));`