You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by el...@apache.org on 2018/06/14 17:22:07 UTC

[trafficcontrol] branch master updated: Remove deliveryservice.org_server_fqdn column/compute it from Origin table

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 55d6eff  Remove deliveryservice.org_server_fqdn column/compute it from Origin table
55d6eff is described below

commit 55d6eff3eab42bdf7f51ca12ec5cd92b9e7d3498
Author: Rawlin Peters <ra...@comcast.com>
AuthorDate: Mon May 21 14:42:04 2018 -0600

    Remove deliveryservice.org_server_fqdn column/compute it from Origin table
    
    With the Origin table now, the deliveryservice.org_server_fqdn column
    can be removed. First, populate the Origin table from existing
    deliveryservice.org_server_fqdn columns. Second, delete the
    deliveryservice.org_server_fqdn column. Third, compute an
    org_server_fqdn column from a deliveryservice's primary Origin wherever
    needed.
    
    This doesn't affect the API, only the APIs backend implementation, so
    no new API version is needed. Whenever a DS is created, the DS's
    corresponding primary Origin is created from the orgServerFqdn request
    field. When a DS is updated, its primary Origin is also updated using
    the orgServerFqdn request field.
    
    As part of this, make the `isPrimary` field of the Origin API read-only,
    and only allow the DeliveryService API to write/update that column in
    the DB.
    
    Added missing orgServerFqdn validation in the golang DS API to match
    the perl API.
---
 docs/source/api/v13/origin.rst                     |   8 +-
 .../20180606000000_remove_org_server_fqdn.sql      |  46 +++
 .../app/lib/API/Configs/ApacheTrafficServer.pm     |   7 +-
 traffic_ops/app/lib/API/Deliveryservice.pm         |  37 ++-
 traffic_ops/app/lib/Fixtures/Deliveryservice.pm    |  15 -
 .../lib/Fixtures/Integration/Deliveryservice.pm    |   8 -
 traffic_ops/app/lib/Fixtures/Integration/Origin.pm | 186 +++++++++++++
 traffic_ops/app/lib/Fixtures/Origin.pm             | 307 +++++++++++++++++++++
 traffic_ops/app/lib/MojoPlugins/Job.pm             |   4 +-
 traffic_ops/app/lib/Schema/Result/Cachegroup.pm    |  35 ++-
 traffic_ops/app/lib/Schema/Result/Coordinate.pm    | 131 +++++++++
 .../Result/DeliveryServiceInfoForDomainList.pm     |   5 +-
 .../Result/DeliveryServiceInfoForServerList.pm     |   5 +-
 .../app/lib/Schema/Result/Deliveryservice.pm       |  38 ++-
 traffic_ops/app/lib/Schema/Result/Origin.pm        | 285 +++++++++++++++++++
 traffic_ops/app/lib/Schema/Result/Profile.pm       |  23 +-
 traffic_ops/app/lib/Schema/Result/Tenant.pm        |  23 +-
 traffic_ops/app/lib/Test/IntegrationTestHelper.pm  |   2 +
 traffic_ops/app/lib/Test/TestHelper.pm             |   3 +
 traffic_ops/app/lib/UI/Cdn.pm                      |   2 +-
 traffic_ops/app/lib/UI/ConfigFiles.pm              |   2 +-
 traffic_ops/app/lib/UI/DeliveryService.pm          |  80 +++++-
 traffic_ops/app/lib/UI/DeliveryServiceServer.pm    |   2 +-
 traffic_ops/app/lib/UI/Job.pm                      |   2 +-
 traffic_ops/app/t/deliveryservice.t                |   8 +-
 traffic_ops/app/t/purge.t                          |   5 +-
 .../app/templates/delivery_service/_form.html.ep   |  10 +-
 traffic_ops/testing/api/v13/tc-fixtures.json       |   2 -
 .../deliveryservice/deliveryservicesv12.go         |  11 +-
 .../deliveryservice/deliveryservicesv13.go         | 140 ++++++++--
 traffic_ops/traffic_ops_golang/origin/origins.go   |  27 +-
 .../traffic_ops_golang/origin/origins_test.go      |   4 -
 32 files changed, 1332 insertions(+), 131 deletions(-)

diff --git a/docs/source/api/v13/origin.rst b/docs/source/api/v13/origin.rst
index 4f96cbb..185f9ca 100644
--- a/docs/source/api/v13/origin.rst
+++ b/docs/source/api/v13/origin.rst
@@ -48,6 +48,8 @@ Origin
   +-------------------------+-----------------+---------------------------------------------------+
   | ``profileId``           | no              | Filter Origins by profile ID.                     |
   +-------------------------+-----------------+---------------------------------------------------+
+  | ``primary``             | no              | Filter Origins by isPrimary.                      |
+  +-------------------------+-----------------+---------------------------------------------------+
   | ``tenant``              | no              | Filter Origins by tenant ID.                      |
   +-------------------------+-----------------+---------------------------------------------------+
 
@@ -171,8 +173,6 @@ Origin
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``ipAddress``                     | no                | IPv4 address of the Origin                                               |
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
-  | ``isPrimary``                     | yes               | Whether or not this is the primary Origin for the delivery service       |
-  +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``name``                          | yes               | The name of the Origin                                                   |
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``port``                          | no                | The TCP port on which the Origin listens                                 |
@@ -193,7 +193,6 @@ Origin
         "fqdn": "foo.example.com",
         "ip6Address": "cafe:dead:d0d0::42",
         "ipAddress": "10.2.3.4",
-        "isPrimary": false,
         "name": "origin1",
         "port": 443,
         "profileId": 1,
@@ -320,8 +319,6 @@ Origin
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``ipAddress``                     | no                | IPv4 address of the Origin                                               |
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
-  | ``isPrimary``                     | yes               | Whether or not this is the primary Origin for the delivery service       |
-  +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``name``                          | yes               | The name of the Origin                                                   |
   +-----------------------------------+-------------------+--------------------------------------------------------------------------+
   | ``port``                          | no                | The TCP port on which the Origin listens                                 |
@@ -343,7 +340,6 @@ Origin
         "id": 1,
         "ip6Address": "cafe:dead:d0d0::42",
         "ipAddress": "10.2.3.4",
-        "isPrimary": false,
         "name": "origin1",
         "port": 443,
         "profileId": 1,
diff --git a/traffic_ops/app/db/migrations/20180606000000_remove_org_server_fqdn.sql b/traffic_ops/app/db/migrations/20180606000000_remove_org_server_fqdn.sql
new file mode 100644
index 0000000..967d1f6
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20180606000000_remove_org_server_fqdn.sql
@@ -0,0 +1,46 @@
+/*
+
+    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 origin (name, protocol, fqdn, port, deliveryservice, tenant, is_primary)
+SELECT
+d.xml_id,
+lower(split_part(d.org_server_fqdn, '://', 1))::origin_protocol,
+regexp_replace(d.org_server_fqdn, '(^https?://)|(:\d+$)', '', 'gi'),
+(SELECT (regexp_matches(d.org_server_fqdn, '(?<=:)\d+$'))[1]::bigint),
+d.id,
+d.tenant_id,
+TRUE
+FROM deliveryservice d
+WHERE d.org_server_fqdn IS NOT NULL;
+
+ALTER TABLE deliveryservice DROP COLUMN org_server_fqdn;
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+
+ALTER TABLE deliveryservice ADD COLUMN org_server_fqdn text;
+
+UPDATE deliveryservice d
+SET org_server_fqdn = (
+    SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':')
+    FROM origin o
+    WHERE o.deliveryservice = d.id AND o.is_primary
+);
+
+DELETE FROM origin o
+WHERE o.is_primary;
diff --git a/traffic_ops/app/lib/API/Configs/ApacheTrafficServer.pm b/traffic_ops/app/lib/API/Configs/ApacheTrafficServer.pm
index 4d6cd4d..d33cefe 100755
--- a/traffic_ops/app/lib/API/Configs/ApacheTrafficServer.pm
+++ b/traffic_ops/app/lib/API/Configs/ApacheTrafficServer.pm
@@ -526,7 +526,10 @@ sub delivery_service_data_by_profile {
 		deliveryservice.routing_name,
 		deliveryservice.signing_algorithm,
 		deliveryservice.qstring_ignore,
-		deliveryservice.org_server_fqdn,
+		(SELECT o.protocol::text || \'://\' || o.fqdn || rtrim(concat(\':\', o.port::text), \':\')
+			FROM origin o
+			WHERE o.deliveryservice = deliveryservice.id
+			AND o.is_primary) as org_server_fqdn,
 		deliveryservice.origin_shield,
 		regex.pattern AS pattern,
     	retype.name AS re_type,
@@ -2237,7 +2240,7 @@ sub cachegroup_profiles {
 		if ( $row->type->name eq 'ORG' ) {
 			my $rs_ds = $self->db->resultset('DeliveryserviceServer')->search( { server => $row->id }, { prefetch => ['deliveryservice'] } );
 			while ( my $ds_row = $rs_ds->next ) {
-				my $org_uri = URI->new( $ds_row->deliveryservice->org_server_fqdn );
+				my $org_uri = URI->new( UI::DeliveryService::compute_org_server_fqdn($self, $ds_row->deliveryservice->id) );
 				push( @{ $deliveryservices->{ $org_uri->host } }, $row );
 			}
 		}
diff --git a/traffic_ops/app/lib/API/Deliveryservice.pm b/traffic_ops/app/lib/API/Deliveryservice.pm
index 9793e25..d8bc37a 100644
--- a/traffic_ops/app/lib/API/Deliveryservice.pm
+++ b/traffic_ops/app/lib/API/Deliveryservice.pm
@@ -138,7 +138,7 @@ sub index {
 				"missLat"              => defined( $row->miss_lat ) ? 0.0 + $row->miss_lat : undef,
 				"missLong"             => defined( $row->miss_long ) ? 0.0 + $row->miss_long : undef,
 				"multiSiteOrigin"      => \$row->multi_site_origin,
-				"orgServerFqdn"        => $row->org_server_fqdn,
+				"orgServerFqdn"        => UI::DeliveryService::compute_org_server_fqdn($self, $row->id),
 				"originShield"         => $row->origin_shield,
 				"profileId"            => defined( $row->profile ) ? $row->profile->id : undef,
 				"profileName"          => defined( $row->profile ) ? $row->profile->name : undef,
@@ -262,7 +262,7 @@ sub show {
 				"missLat"              => defined( $row->miss_lat ) ? 0.0 + $row->miss_lat : undef,
 				"missLong"             => defined( $row->miss_long ) ? 0.0 + $row->miss_long : undef,
 				"multiSiteOrigin"      => \$row->multi_site_origin,
-				"orgServerFqdn"        => $row->org_server_fqdn,
+				"orgServerFqdn"        => UI::DeliveryService::compute_org_server_fqdn($self, $row->id),
 				"originShield"         => $row->origin_shield,
 				"profileId"            => defined( $row->profile ) ? $row->profile->id : undef,
 				"profileName"          => defined( $row->profile ) ? $row->profile->name : undef,
@@ -376,7 +376,6 @@ sub update {
 		miss_lat               => $params->{missLat},
 		miss_long              => $params->{missLong},
 		multi_site_origin      => $params->{multiSiteOrigin},
-		org_server_fqdn        => $params->{orgServerFqdn},
 		origin_shield          => $params->{originShield},
 		profile                => $params->{profileId},
 		protocol               => $params->{protocol},
@@ -413,6 +412,21 @@ sub update {
 	my $rs = $ds->update($values);
 	if ($rs) {
 
+		# find this DS's primary Origin and update it too
+		my $origin_rs = $self->db->resultset('Origin')->find( { deliveryservice => $id, is_primary => 1 } );
+		my $origin = UI::DeliveryService::get_primary_origin_from_deliveryservice($id, $values, $params->{orgServerFqdn});
+		if ( defined( $origin ) && defined( $origin_rs ) ) {
+			$origin_rs->update($origin);
+			&log( $self, "Updated primary origin [ '" . $origin_rs->name . "' ] with id: " . $origin_rs->id, "APICHANGE" );
+		} elsif ( defined( $origin ) && !defined( $origin_rs ) ) {
+			$origin_rs = $self->db->resultset('Origin')->create($origin)->insert();
+			&log( $self, "Created primary origin [ '" . $origin_rs->name . "' ] with id: " . $origin_rs->id, "APICHANGE" );
+		} elsif ( !defined( $origin ) && defined( $origin_rs ) ) {
+			my $name = $origin_rs->name;
+			$origin_rs->delete();
+			&log( $self, "Deleted primary origin [ '" . $name . "' ] ", "APICHANGE" );
+		}
+
 		# create location parameters for header_rewrite*, regex_remap* and cacheurl* config files if necessary
 		&UI::DeliveryService::header_rewrite( $self, $rs->id, $values->{profileId}, $values->{xmlId}, $values->{edgeHeaderRewrite}, "edge" );
 		&UI::DeliveryService::header_rewrite( $self, $rs->id, $values->{profileId}, $values->{xmlId}, $values->{midHeaderRewrite},  "mid" );
@@ -480,7 +494,7 @@ sub update {
 				"missLat"                  => defined($rs->miss_lat) ? 0.0 + $rs->miss_lat : undef,
 				"missLong"                 => defined($rs->miss_long) ? 0.0 + $rs->miss_long : undef,
 				"multiSiteOrigin"          => $rs->multi_site_origin,
-				"orgServerFqdn"            => $rs->org_server_fqdn,
+				"orgServerFqdn"            => UI::DeliveryService::compute_org_server_fqdn($self, $rs->id),
 				"originShield"             => $rs->origin_shield,
 				"profileId"                => defined($rs->profile) ? $rs->profile->id : undef,
 				"profileName"              => defined($rs->profile) ? $rs->profile->name : undef,
@@ -614,7 +628,7 @@ sub safe_update {
 				"missLat"                  => defined($rs->miss_lat) ? 0.0 + $rs->miss_lat : undef,
 				"missLong"                 => defined($rs->miss_long) ? 0.0 + $rs->miss_long : undef,
 				"multiSiteOrigin"          => $rs->multi_site_origin,
-				"orgServerFqdn"            => $rs->org_server_fqdn,
+				"orgServerFqdn"            => UI::DeliveryService::compute_org_server_fqdn($self, $rs->id),
 				"originShield"             => $rs->origin_shield,
 				"profileId"                => defined($rs->profile) ? $rs->profile->id : undef,
 				"profileName"              => defined($rs->profile) ? $rs->profile->name : undef,
@@ -719,7 +733,6 @@ sub create {
 		miss_lat               => $params->{missLat},
 		miss_long              => $params->{missLong},
 		multi_site_origin      => $params->{multiSiteOrigin},
-		org_server_fqdn        => $params->{orgServerFqdn},
 		origin_shield          => $params->{originShield},
 		profile                => $params->{profileId},
 		protocol               => $params->{protocol},
@@ -756,6 +769,12 @@ sub create {
 
 		&log( $self, "Created delivery service [ '" . $insert->xml_id . "' ] with id: " . $insert->id, "APICHANGE" );
 
+		my $origin = UI::DeliveryService::get_primary_origin_from_deliveryservice($insert->id, $values, $params->{orgServerFqdn});
+		if (defined( $origin )) {
+			my $origin_rs = $self->db->resultset('Origin')->create($origin)->insert();
+			&log( $self, "Created origin [ '" . $origin_rs->name . "' ] with id: " . $origin_rs->id, "APICHANGE" );
+		}
+
 		# create location parameters for header_rewrite*, regex_remap* and cacheurl* config files if necessary
 		&UI::DeliveryService::header_rewrite( $self, $insert->id, $values->{id}, $values->{xml_id}, $values->{edge_header_rewrite}, "edge" );
 		&UI::DeliveryService::header_rewrite( $self, $insert->id, $values->{profile_id}, $values->{xml_id}, $values->{mid_header_rewrite},  "mid" );
@@ -834,7 +853,7 @@ sub create {
 				"missLat"                  => defined($insert->miss_lat) ? 0.0 + $insert->miss_lat : undef,
 				"missLong"                 => defined($insert->miss_long) ? 0.0 + $insert->miss_long : undef,
 				"multiSiteOrigin"          => $insert->multi_site_origin,
-				"orgServerFqdn"            => $insert->org_server_fqdn,
+				"orgServerFqdn"            => UI::DeliveryService::compute_org_server_fqdn($self, $insert->id),
 				"originShield"             => $insert->origin_shield,
 				"profileId"                => defined($insert->profile) ? $insert->profile->id : undef,
 				"profileName"              => defined($insert->profile) ? $insert->profile->name : undef,
@@ -1027,7 +1046,7 @@ sub get_deliveryservices_by_serverId {
 					"missLat"              => defined( $row->miss_lat ) ? 0.0 + $row->miss_lat : undef,
 					"missLong"             => defined( $row->miss_long ) ? 0.0 + $row->miss_long : undef,
 					"multiSiteOrigin"      => \$row->multi_site_origin,
-					"orgServerFqdn"        => $row->org_server_fqdn,
+					"orgServerFqdn"        => UI::DeliveryService::compute_org_server_fqdn($self, $row->id),
 					"originShield"         => $row->origin_shield,
 					"profileId"            => defined( $row->profile ) ? $row->profile->id : undef,
 					"profileName"          => defined( $row->profile ) ? $row->profile->name : undef,
@@ -1128,7 +1147,7 @@ sub get_deliveryservices_by_userId {
 					"missLat"              => defined( $row->miss_lat ) ? 0.0 + $row->miss_lat : undef,
 					"missLong"             => defined( $row->miss_long ) ? 0.0 + $row->miss_long : undef,
 					"multiSiteOrigin"      => \$row->multi_site_origin,
-					"orgServerFqdn"        => $row->org_server_fqdn,
+					"orgServerFqdn"        => UI::DeliveryService::compute_org_server_fqdn($self, $row->id),
 					"originShield"         => $row->origin_shield,
 					"profileId"            => defined( $row->profile ) ? $row->profile->id : undef,
 					"profileName"          => defined( $row->profile ) ? $row->profile->name : undef,
diff --git a/traffic_ops/app/lib/Fixtures/Deliveryservice.pm b/traffic_ops/app/lib/Fixtures/Deliveryservice.pm
index 59f6cf4..e087be9 100644
--- a/traffic_ops/app/lib/Fixtures/Deliveryservice.pm
+++ b/traffic_ops/app/lib/Fixtures/Deliveryservice.pm
@@ -44,7 +44,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds1 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds1.edge',
 			info_url              => 'http://test-ds1.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -86,7 +85,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds2 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds2.edge',
 			info_url              => 'http://test-ds2.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -128,7 +126,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds3 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds3.edge',
 			info_url              => 'http://test-ds3.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -170,7 +167,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds4 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds4.edge',
 			info_url              => 'http://test-ds4.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -212,7 +208,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds5 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds5.edge',
 			info_url              => 'http://test-ds5.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -254,7 +249,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds6 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds6.edge',
 			info_url              => 'http://test-ds6.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -295,7 +289,6 @@ my %definition_for = (
 			long_desc_2           => 'steering-ds1 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://steering-ds1.edge',
 			info_url              => 'http://steering-ds1.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -335,7 +328,6 @@ my %definition_for = (
 			long_desc_2           => 'steering-ds2 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://steering-ds2.edge',
 			info_url              => 'http://steering-ds2.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -375,7 +367,6 @@ my %definition_for = (
 			long_desc_2           => 'new-steering-ds long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://new-steering-ds.edge',
 			info_url              => 'http://new-steering-ds.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -415,7 +406,6 @@ my %definition_for = (
 			long_desc_2           => 'target-ds1 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://target-ds1.edge',
 			info_url              => 'http://target-ds1.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -455,7 +445,6 @@ my %definition_for = (
 			long_desc_2           => 'target-ds2 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://target-ds2.edge',
 			info_url              => 'http://target-ds2.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -495,7 +484,6 @@ my %definition_for = (
 			long_desc_2           => 'target-ds3 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://target-ds3.edge',
 			info_url              => 'http://target-ds3.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -535,7 +523,6 @@ my %definition_for = (
 			long_desc_2           => 'target-ds4 long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://target-ds4.edge',
 			info_url              => 'http://target-ds4.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -576,7 +563,6 @@ my %definition_for = (
 			long_desc_2           => 'test-ds1-root long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://test-ds1-root.edge',
 			info_url              => 'http://test-ds1-root.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
@@ -618,7 +604,6 @@ my %definition_for = (
 			long_desc_2           => 'foo.bar long_desc_2',
 			max_dns_answers       => 0,
 			protocol              => 0,
-			org_server_fqdn       => 'http://foo.bar.edge',
 			info_url              => 'http://foo.bar.edge/info_url.html',
 			miss_lat              => '41.881944',
 			miss_long             => '-87.627778',
diff --git a/traffic_ops/app/lib/Fixtures/Integration/Deliveryservice.pm b/traffic_ops/app/lib/Fixtures/Integration/Deliveryservice.pm
index 678df65..5acc84d 100644
--- a/traffic_ops/app/lib/Fixtures/Integration/Deliveryservice.pm
+++ b/traffic_ops/app/lib/Fixtures/Integration/Deliveryservice.pm
@@ -70,7 +70,6 @@ my %definition_for = (
 			cdn_id                      => '2',
 			dns_bypass_ttl              => undef,
 			initial_dispersion          => '1',
-			org_server_fqdn             => 'http://cdl.origin.kabletown.net',
 			range_request_handling      => '0',
 			signing_algorithm           => 'url_sig',
 			dns_bypass_ip               => '',
@@ -117,7 +116,6 @@ my %definition_for = (
 			display_name                => 'games-c1',
 			http_bypass_fqdn            => '',
 			info_url                    => 'http://games.info.kabletown.net',
-			org_server_fqdn             => 'http://games.origin.kabletown.net',
 			ccr_dns_ttl                 => '3600',
 			dns_bypass_ip6              => undef,
 			last_updated                => '2015-12-10 15:44:37',
@@ -150,7 +148,6 @@ my %definition_for = (
 			deep_caching_type           => 'NEVER',
 			routing_name                => 'foo',
 			last_updated                => '2015-12-10 15:44:37',
-			org_server_fqdn             => 'http://images.origin.kabletown.net',
 			tr_response_headers         => undef,
 			cacheurl                    => undef,
 			check_path                  => '/crossdomain.xml',
@@ -224,7 +221,6 @@ my %definition_for = (
 			edge_header_rewrite         => 'cond %{REMAP_PSEUDO_HOOK} __RETURN__ set-config proxy.config.http.transaction_active_timeout_out 5 [L]',
 			global_max_mbps             => '0',
 			ssl_key_version             => '0',
-			org_server_fqdn             => 'http://movies.origin.kabletown.net',
 			range_request_handling      => '0',
 			regex_remap                 => undef,
 			miss_long                   => '-87.627778',
@@ -278,7 +274,6 @@ my %definition_for = (
 			check_path                  => '/crossdomain.xml',
 			dns_bypass_ip               => '',
 			tr_response_headers         => undef,
-			org_server_fqdn             => 'http://movies.origin.kabletown.net',
 			geo_limit                   => '0',
 			long_desc_2                 => 'test-ds1 long_desc_2',
 			edge_header_rewrite         => undef,
@@ -322,7 +317,6 @@ my %definition_for = (
 			fq_pacing_rate              => '0',			
 			http_bypass_fqdn            => '',
 			miss_long                   => '-87.627778',
-			org_server_fqdn             => 'https://games.origin.kabletown.net',
 			multi_site_origin           => undef,
 			cacheurl                    => undef,
 			dns_bypass_ip               => '',
@@ -374,7 +368,6 @@ my %definition_for = (
 			fq_pacing_rate              => '0',			
 			initial_dispersion          => '1',
 			multi_site_origin           => undef,
-			org_server_fqdn             => 'http://national-tv.origin.kabletown.net',
 			signing_algorithm           => undef,
 			display_name                => 'tv-nat-c2',
 			dns_bypass_ip6              => undef,
@@ -433,7 +426,6 @@ my %definition_for = (
 			regex_remap                 => undef,
 			remap_text                  => undef,
 			miss_long                   => '-87.627778',
-			org_server_fqdn             => 'http://cc.origin.kabletown.net',
 			display_name                => 'tv-nocache-c2',
 			qstring_ignore              => '0',
 			dns_bypass_ip6              => undef,
diff --git a/traffic_ops/app/lib/Fixtures/Integration/Origin.pm b/traffic_ops/app/lib/Fixtures/Integration/Origin.pm
new file mode 100644
index 0000000..5487506
--- /dev/null
+++ b/traffic_ops/app/lib/Fixtures/Integration/Origin.pm
@@ -0,0 +1,186 @@
+package Fixtures::Integration::Origin;
+
+# 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.
+
+
+use Moose;
+extends 'DBIx::Class::EasyFixture';
+use namespace::autoclean;
+
+my %definition_for = (
+	## id => 1
+	'0' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin1',
+            fqdn                  => 'cdl.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 1,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 2
+	'1' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin2',
+            fqdn                  => 'games.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 2,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 3
+	'2' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin3',
+            fqdn                  => 'images.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 3,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 4
+	'3' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin4',
+            fqdn                  => 'movies.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 4,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 5
+	'4' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin5',
+            fqdn                  => 'movies.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 5,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 6
+	'5' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin6',
+            fqdn                  => 'games.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 6,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 7
+	'6' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin7',
+            fqdn                  => 'national-tv.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 7,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+	## id => 8
+	'7' => {
+		new      => 'Origin',
+		using => {
+            name                  => 'test-origin8',
+            fqdn                  => 'cc.origin.kabletown.net',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 8,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+		},
+	},
+);
+
+sub name {
+	return "Origin";
+}
+
+sub get_definition {
+	my ( $self, $name ) = @_;
+	return $definition_for{$name};
+}
+
+sub all_fixture_names {
+	# sort by db name to guarantee insertion order
+	return (sort { $definition_for{$a}{using}{name} cmp $definition_for{$b}{using}{name} } keys %definition_for);
+}
+__PACKAGE__->meta->make_immutable;
+1;
diff --git a/traffic_ops/app/lib/Fixtures/Origin.pm b/traffic_ops/app/lib/Fixtures/Origin.pm
new file mode 100644
index 0000000..baf60ee
--- /dev/null
+++ b/traffic_ops/app/lib/Fixtures/Origin.pm
@@ -0,0 +1,307 @@
+package Fixtures::Origin;
+
+#
+#
+# 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.
+#
+use Moose;
+extends 'DBIx::Class::EasyFixture';
+use namespace::autoclean;
+
+my %definition_for = (
+    origin_cdn1 => {
+    new   => 'Origin',
+        using => {
+            id                    => 100,
+            name                  => 'test-origin1',
+            fqdn                  => 'test-ds1.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 100,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_cdn2 => {
+        new   => 'Origin',
+        using => {
+            id                    => 200,
+            name                  => 'test-origin2',
+            fqdn                  => 'test-ds2.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 200,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_cdn3 => {
+        new   => 'Origin',
+        using => {
+            id                    => 300,
+            name                  => 'test-origin3',
+            fqdn                  => 'test-ds3.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 300,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_cdn4 => {
+        new   => 'Origin',
+        using => {
+            id                    => 400,
+            name                  => 'test-origin4',
+            fqdn                  => 'test-ds4.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 400,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_dns => {
+        new   => 'Origin',
+        using => {
+            id                    => 500,
+            name                  => 'test-origin5',
+            fqdn                  => 'test-ds5.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 500,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_http_no_cache => {
+        new   => 'Origin',
+        using => {
+            id                    => 600,
+            name                  => 'test-origin6',
+            fqdn                  => 'test-ds6.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 600,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    steering_origin1 => {
+        new   => 'Origin',
+        using => {
+            id                    => 700,
+            name                  => 'test-origin7',
+            fqdn                  => 'steering-ds1.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 700,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    steering_origin2 => {
+        new   => 'Origin',
+        using => {
+            id                    => 800,
+            name                  => 'test-origin8',
+            fqdn                  => 'steering-ds2.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 800,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    new_origin_steering => {
+        new   => 'Origin',
+        using => {
+            id                    => 900,
+            name                  => 'test-origin9',
+            fqdn                  => 'new-steering-ds.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 900,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    target_origin1 => {
+        new   => 'Origin',
+        using => {
+            id                    => 1000,
+            name                  => 'test-origin10',
+            fqdn                  => 'target-ds1.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 1000,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    target_origin2 => {
+        new   => 'Origin',
+        using => {
+            id                    => 1100,
+            name                  => 'test-origin11',
+            fqdn                  => 'target-ds2.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 1100,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    target_origin3 => {
+        new   => 'Origin',
+        using => {
+            id                    => 1200,
+            name                  => 'test-origin12',
+            fqdn                  => 'target-ds3.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 1200,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    target_origin4 => {
+        new   => 'Origin',
+        using => {
+            id                    => 1300,
+            name                  => 'test-origin13',
+            fqdn                  => 'target-ds4.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 1300,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    origin_cdn1_root => {
+        new   => 'Origin',
+        using => {
+            id                    => 2100,
+            name                  => 'test-origin14',
+            fqdn                  => 'test-ds1-root.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 2100,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => 10**9,
+        },
+    },
+    origin_period1 => {
+        new   => 'Origin',
+        using => {
+            id                    => 2200,
+            name                  => 'test-origin15',
+            fqdn                  => 'foo.bar.edge',
+            protocol              => 'http',
+            is_primary            => 1,
+            port                  => undef,
+            ip_address            => undef,
+            ip6_address           => undef,
+            deliveryservice       => 2200,
+            coordinate            => undef,
+            profile               => undef,
+            cachegroup            => undef,
+            tenant                => undef,
+        },
+    },
+    
+);
+
+sub get_definition {
+    my ( $self, $name ) = @_;
+    return $definition_for{$name};
+}
+
+sub all_fixture_names {
+    # sort by db id to guarantee insertion order
+    return (sort { $definition_for{$a}{using}{id} cmp $definition_for{$b}{using}{id} } keys %definition_for);
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/traffic_ops/app/lib/MojoPlugins/Job.pm b/traffic_ops/app/lib/MojoPlugins/Job.pm
index 1ec0ed0..1f7634c 100755
--- a/traffic_ops/app/lib/MojoPlugins/Job.pm
+++ b/traffic_ops/app/lib/MojoPlugins/Job.pm
@@ -180,7 +180,7 @@ sub register {
 			}
 			my $start_time_gmt = strftime( "%Y-%m-%d %H:%M:%S", gmtime($start_time) );
 			my $entered_time   = strftime( "%Y-%m-%d %H:%M:%S", gmtime() );
-			my $org_server_fqdn = $self->db->resultset("Deliveryservice")->search( { id => $ds_id } )->get_column('org_server_fqdn')->single();
+			my $org_server_fqdn = UI::DeliveryService::compute_org_server_fqdn($self, $ds_id);
 
 			my $tm_user_id = $self->db->resultset('TmUser')->search( { username => $self->current_user()->{username} } )->get_column('id')->single();
 
@@ -227,7 +227,7 @@ sub register {
 				my ( $scheme, $asset_hostname, $path, $query, $fragment ) = $asset =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|;
 
 				while ( my $ds_row = $rs_ds->next ) {
-					my $org_server_fqdn = $ds_row->org_server_fqdn;
+					my $org_server_fqdn = UI::DeliveryService::compute_org_server_fqdn($self, $ds_row->id);
 					if ( defined($org_server_fqdn) && $asset =~ /$org_server_fqdn/ ) {
 						return 1;    # Success
 					}
diff --git a/traffic_ops/app/lib/Schema/Result/Cachegroup.pm b/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
index a93c039..453779f 100644
--- a/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
+++ b/traffic_ops/app/lib/Schema/Result/Cachegroup.pm
@@ -72,7 +72,7 @@ __PACKAGE__->table("cachegroup");
 
   data_type: 'timestamp with time zone'
   default_value: current_timestamp
-  is_nullable: 1
+  is_nullable: 0
   original: {default_value => \"now()"}
 
 =head2 fallback_to_closest
@@ -109,7 +109,7 @@ __PACKAGE__->add_columns(
   {
     data_type     => "timestamp with time zone",
     default_value => \"current_timestamp",
-    is_nullable   => 1,
+    is_nullable   => 0,
     original      => { default_value => \"now()" },
   },
   "fallback_to_closest",
@@ -132,7 +132,7 @@ __PACKAGE__->set_primary_key("id", "type");
 
 =head1 UNIQUE CONSTRAINTS
 
-=head2 C<idx_54252_cg_name_unique>
+=head2 C<idx_140208_cg_name_unique>
 
 =over 4
 
@@ -142,9 +142,9 @@ __PACKAGE__->set_primary_key("id", "type");
 
 =cut
 
-__PACKAGE__->add_unique_constraint("idx_54252_cg_name_unique", ["name"]);
+__PACKAGE__->add_unique_constraint("idx_140208_cg_name_unique", ["name"]);
 
-=head2 C<idx_54252_cg_short_unique>
+=head2 C<idx_140208_cg_short_unique>
 
 =over 4
 
@@ -154,9 +154,9 @@ __PACKAGE__->add_unique_constraint("idx_54252_cg_name_unique", ["name"]);
 
 =cut
 
-__PACKAGE__->add_unique_constraint("idx_54252_cg_short_unique", ["short_name"]);
+__PACKAGE__->add_unique_constraint("idx_140208_cg_short_unique", ["short_name"]);
 
-=head2 C<idx_54252_lo_id_unique>
+=head2 C<idx_140208_lo_id_unique>
 
 =over 4
 
@@ -166,7 +166,7 @@ __PACKAGE__->add_unique_constraint("idx_54252_cg_short_unique", ["short_name"]);
 
 =cut
 
-__PACKAGE__->add_unique_constraint("idx_54252_lo_id_unique", ["id"]);
+__PACKAGE__->add_unique_constraint("idx_140208_lo_id_unique", ["id"]);
 
 =head1 RELATIONS
 
@@ -260,6 +260,21 @@ __PACKAGE__->has_many(
   { cascade_copy => 0, cascade_delete => 0 },
 );
 
+=head2 origins
+
+Type: has_many
+
+Related object: L<Schema::Result::Origin>
+
+=cut
+
+__PACKAGE__->has_many(
+  "origins",
+  "Schema::Result::Origin",
+  { "foreign.cachegroup" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
 =head2 parent_cachegroup
 
 Type: belongs_to
@@ -346,8 +361,8 @@ __PACKAGE__->belongs_to(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07046 @ 2016-11-18 22:45:19
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lU7dUVFuoTyhpC7x7BGaDg
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:2EeelbrXDdiyrV9BXGuIeA
 
 # 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
new file mode 100644
index 0000000..7463079
--- /dev/null
+++ b/traffic_ops/app/lib/Schema/Result/Coordinate.pm
@@ -0,0 +1,131 @@
+use utf8;
+package Schema::Result::Coordinate;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+Schema::Result::Coordinate
+
+=cut
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 TABLE: C<coordinate>
+
+=cut
+
+__PACKAGE__->table("coordinate");
+
+=head1 ACCESSORS
+
+=head2 id
+
+  data_type: 'bigint'
+  is_auto_increment: 1
+  is_nullable: 0
+  sequence: 'coordinate_id_seq'
+
+=head2 name
+
+  data_type: 'text'
+  is_nullable: 0
+
+=head2 latitude
+
+  data_type: 'numeric'
+  default_value: 0.0
+  is_nullable: 0
+
+=head2 longitude
+
+  data_type: 'numeric'
+  default_value: 0.0
+  is_nullable: 0
+
+=head2 last_updated
+
+  data_type: 'timestamp with time zone'
+  default_value: current_timestamp
+  is_nullable: 0
+  original: {default_value => \"now()"}
+
+=cut
+
+__PACKAGE__->add_columns(
+  "id",
+  {
+    data_type         => "bigint",
+    is_auto_increment => 1,
+    is_nullable       => 0,
+    sequence          => "coordinate_id_seq",
+  },
+  "name",
+  { data_type => "text", is_nullable => 0 },
+  "latitude",
+  { data_type => "numeric", default_value => "0.0", is_nullable => 0 },
+  "longitude",
+  { data_type => "numeric", default_value => "0.0", is_nullable => 0 },
+  "last_updated",
+  {
+    data_type     => "timestamp with time zone",
+    default_value => \"current_timestamp",
+    is_nullable   => 0,
+    original      => { default_value => \"now()" },
+  },
+);
+
+=head1 PRIMARY KEY
+
+=over 4
+
+=item * L</id>
+
+=back
+
+=cut
+
+__PACKAGE__->set_primary_key("id");
+
+=head1 UNIQUE CONSTRAINTS
+
+=head2 C<coordinate_name_key>
+
+=over 4
+
+=item * L</name>
+
+=back
+
+=cut
+
+__PACKAGE__->add_unique_constraint("coordinate_name_key", ["name"]);
+
+=head1 RELATIONS
+
+=head2 origins
+
+Type: has_many
+
+Related object: L<Schema::Result::Origin>
+
+=cut
+
+__PACKAGE__->has_many(
+  "origins",
+  "Schema::Result::Origin",
+  { "foreign.coordinate" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
+
+# 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
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+1;
diff --git a/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForDomainList.pm b/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForDomainList.pm
index bb0f231..bc14e24 100644
--- a/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForDomainList.pm
+++ b/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForDomainList.pm
@@ -45,7 +45,10 @@ SELECT
     deliveryservice.routing_name,
     deliveryservice.signing_algorithm,
     deliveryservice.qstring_ignore,
-    deliveryservice.org_server_fqdn,
+    (SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':')
+        FROM origin o
+        WHERE o.deliveryservice = deliveryservice.id
+        AND o.is_primary) as org_server_fqdn,
     deliveryservice.multi_site_origin,
     deliveryservice.range_request_handling,
     deliveryservice.fq_pacing_rate,  
diff --git a/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForServerList.pm b/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForServerList.pm
index f190d33..ce32594 100644
--- a/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForServerList.pm
+++ b/traffic_ops/app/lib/Schema/Result/DeliveryServiceInfoForServerList.pm
@@ -45,7 +45,10 @@ SELECT
     deliveryservice.routing_name AS routing_name,
     deliveryservice.signing_algorithm AS signing_algorithm,
     deliveryservice.qstring_ignore AS qstring_ignore,
-    deliveryservice.org_server_fqdn as org_server_fqdn,
+    (SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':')
+        FROM origin o
+        WHERE o.deliveryservice = deliveryservice.id
+        AND o.is_primary) as org_server_fqdn,
     deliveryservice.multi_site_origin as multi_site_origin,
     deliveryservice.range_request_handling as range_request_handling,
     deliveryservice.fq_pacing_rate as fq_pacing_rate,
diff --git a/traffic_ops/app/lib/Schema/Result/Deliveryservice.pm b/traffic_ops/app/lib/Schema/Result/Deliveryservice.pm
index dab1cce..94d4b9c 100644
--- a/traffic_ops/app/lib/Schema/Result/Deliveryservice.pm
+++ b/traffic_ops/app/lib/Schema/Result/Deliveryservice.pm
@@ -87,11 +87,6 @@ __PACKAGE__->table("deliveryservice");
   data_type: 'bigint'
   is_nullable: 1
 
-=head2 org_server_fqdn
-
-  data_type: 'text'
-  is_nullable: 1
-
 =head2 type
 
   data_type: 'bigint'
@@ -170,7 +165,7 @@ __PACKAGE__->table("deliveryservice");
 
   data_type: 'timestamp with time zone'
   default_value: current_timestamp
-  is_nullable: 1
+  is_nullable: 0
   original: {default_value => \"now()"}
 
 =head2 protocol
@@ -349,8 +344,6 @@ __PACKAGE__->add_columns(
   { data_type => "text", is_nullable => 1 },
   "dns_bypass_ttl",
   { data_type => "bigint", is_nullable => 1 },
-  "org_server_fqdn",
-  { data_type => "text", is_nullable => 1 },
   "type",
   { data_type => "bigint", is_foreign_key => 1, is_nullable => 0 },
   "profile",
@@ -383,7 +376,7 @@ __PACKAGE__->add_columns(
   {
     data_type     => "timestamp with time zone",
     default_value => \"current_timestamp",
-    is_nullable   => 1,
+    is_nullable   => 0,
     original      => { default_value => \"now()" },
   },
   "protocol",
@@ -464,7 +457,7 @@ __PACKAGE__->set_primary_key("id", "type");
 
 =head1 UNIQUE CONSTRAINTS
 
-=head2 C<idx_89502_ds_id_unique>
+=head2 C<idx_140234_ds_id_unique>
 
 =over 4
 
@@ -474,9 +467,9 @@ __PACKAGE__->set_primary_key("id", "type");
 
 =cut
 
-__PACKAGE__->add_unique_constraint("idx_89502_ds_id_unique", ["id"]);
+__PACKAGE__->add_unique_constraint("idx_140234_ds_id_unique", ["id"]);
 
-=head2 C<idx_89502_ds_name_unique>
+=head2 C<idx_140234_ds_name_unique>
 
 =over 4
 
@@ -486,7 +479,7 @@ __PACKAGE__->add_unique_constraint("idx_89502_ds_id_unique", ["id"]);
 
 =cut
 
-__PACKAGE__->add_unique_constraint("idx_89502_ds_name_unique", ["xml_id"]);
+__PACKAGE__->add_unique_constraint("idx_140234_ds_name_unique", ["xml_id"]);
 
 =head1 RELATIONS
 
@@ -580,6 +573,21 @@ __PACKAGE__->has_many(
   { cascade_copy => 0, cascade_delete => 0 },
 );
 
+=head2 origins
+
+Type: has_many
+
+Related object: L<Schema::Result::Origin>
+
+=cut
+
+__PACKAGE__->has_many(
+  "origins",
+  "Schema::Result::Origin",
+  { "foreign.deliveryservice" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
 =head2 profile
 
 Type: belongs_to
@@ -681,8 +689,8 @@ __PACKAGE__->belongs_to(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07047 @ 2018-02-28 21:54:35
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3ETqht/3FTxKgD/YuRf3Bg
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-17 16:24:12
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Yjz2V+duaN88QPILxLqoHg
 
 # 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/Origin.pm b/traffic_ops/app/lib/Schema/Result/Origin.pm
new file mode 100644
index 0000000..6c0126e
--- /dev/null
+++ b/traffic_ops/app/lib/Schema/Result/Origin.pm
@@ -0,0 +1,285 @@
+use utf8;
+package Schema::Result::Origin;
+
+# Created by DBIx::Class::Schema::Loader
+# DO NOT MODIFY THE FIRST PART OF THIS FILE
+
+=head1 NAME
+
+Schema::Result::Origin
+
+=cut
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+=head1 TABLE: C<origin>
+
+=cut
+
+__PACKAGE__->table("origin");
+
+=head1 ACCESSORS
+
+=head2 id
+
+  data_type: 'bigint'
+  is_auto_increment: 1
+  is_nullable: 0
+  sequence: 'origin_id_seq'
+
+=head2 name
+
+  data_type: 'text'
+  is_nullable: 0
+
+=head2 fqdn
+
+  data_type: 'text'
+  is_nullable: 0
+
+=head2 protocol
+
+  data_type: 'enum'
+  default_value: 'http'
+  extra: {custom_type_name => "origin_protocol",list => ["http","https"]}
+  is_nullable: 0
+
+=head2 is_primary
+
+  data_type: 'boolean'
+  default_value: false
+  is_nullable: 0
+
+=head2 port
+
+  data_type: 'bigint'
+  is_nullable: 1
+
+=head2 ip_address
+
+  data_type: 'text'
+  is_nullable: 1
+
+=head2 ip6_address
+
+  data_type: 'text'
+  is_nullable: 1
+
+=head2 deliveryservice
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 0
+
+=head2 coordinate
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 1
+
+=head2 profile
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 1
+
+=head2 cachegroup
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 1
+
+=head2 tenant
+
+  data_type: 'bigint'
+  is_foreign_key: 1
+  is_nullable: 1
+
+=head2 last_updated
+
+  data_type: 'timestamp with time zone'
+  default_value: current_timestamp
+  is_nullable: 0
+  original: {default_value => \"now()"}
+
+=cut
+
+__PACKAGE__->add_columns(
+  "id",
+  {
+    data_type         => "bigint",
+    is_auto_increment => 1,
+    is_nullable       => 0,
+    sequence          => "origin_id_seq",
+  },
+  "name",
+  { data_type => "text", is_nullable => 0 },
+  "fqdn",
+  { data_type => "text", is_nullable => 0 },
+  "protocol",
+  {
+    data_type => "enum",
+    default_value => "http",
+    extra => { custom_type_name => "origin_protocol", list => ["http", "https"] },
+    is_nullable => 0,
+  },
+  "is_primary",
+  { data_type => "boolean", default_value => \"false", is_nullable => 0 },
+  "port",
+  { data_type => "bigint", is_nullable => 1 },
+  "ip_address",
+  { data_type => "text", is_nullable => 1 },
+  "ip6_address",
+  { data_type => "text", is_nullable => 1 },
+  "deliveryservice",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 0 },
+  "coordinate",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
+  "profile",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
+  "cachegroup",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
+  "tenant",
+  { data_type => "bigint", is_foreign_key => 1, is_nullable => 1 },
+  "last_updated",
+  {
+    data_type     => "timestamp with time zone",
+    default_value => \"current_timestamp",
+    is_nullable   => 0,
+    original      => { default_value => \"now()" },
+  },
+);
+
+=head1 PRIMARY KEY
+
+=over 4
+
+=item * L</id>
+
+=back
+
+=cut
+
+__PACKAGE__->set_primary_key("id");
+
+=head1 UNIQUE CONSTRAINTS
+
+=head2 C<origin_name_key>
+
+=over 4
+
+=item * L</name>
+
+=back
+
+=cut
+
+__PACKAGE__->add_unique_constraint("origin_name_key", ["name"]);
+
+=head1 RELATIONS
+
+=head2 cachegroup
+
+Type: belongs_to
+
+Related object: L<Schema::Result::Cachegroup>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "cachegroup",
+  "Schema::Result::Cachegroup",
+  { id => "cachegroup" },
+  {
+    is_deferrable => 0,
+    join_type     => "LEFT",
+    on_delete     => "RESTRICT",
+    on_update     => "NO ACTION",
+  },
+);
+
+=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     => "RESTRICT",
+    on_update     => "NO ACTION",
+  },
+);
+
+=head2 deliveryservice
+
+Type: belongs_to
+
+Related object: L<Schema::Result::Deliveryservice>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "deliveryservice",
+  "Schema::Result::Deliveryservice",
+  { id => "deliveryservice" },
+  { is_deferrable => 0, on_delete => "CASCADE", on_update => "NO ACTION" },
+);
+
+=head2 profile
+
+Type: belongs_to
+
+Related object: L<Schema::Result::Profile>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "profile",
+  "Schema::Result::Profile",
+  { id => "profile" },
+  {
+    is_deferrable => 0,
+    join_type     => "LEFT",
+    on_delete     => "RESTRICT",
+    on_update     => "NO ACTION",
+  },
+);
+
+=head2 tenant
+
+Type: belongs_to
+
+Related object: L<Schema::Result::Tenant>
+
+=cut
+
+__PACKAGE__->belongs_to(
+  "tenant",
+  "Schema::Result::Tenant",
+  { id => "tenant" },
+  {
+    is_deferrable => 0,
+    join_type     => "LEFT",
+    on_delete     => "RESTRICT",
+    on_update     => "NO ACTION",
+  },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:EFdWsJg/ANV/vUHBHfK0iA
+
+
+# You can replace this text with custom code or comments, and it will be preserved on regeneration
+1;
diff --git a/traffic_ops/app/lib/Schema/Result/Profile.pm b/traffic_ops/app/lib/Schema/Result/Profile.pm
index 998a8fa..c5deafe 100644
--- a/traffic_ops/app/lib/Schema/Result/Profile.pm
+++ b/traffic_ops/app/lib/Schema/Result/Profile.pm
@@ -44,7 +44,7 @@ __PACKAGE__->table("profile");
 
   data_type: 'timestamp with time zone'
   default_value: current_timestamp
-  is_nullable: 1
+  is_nullable: 0
   original: {default_value => \"now()"}
 
 =head2 type
@@ -83,7 +83,7 @@ __PACKAGE__->add_columns(
   {
     data_type     => "timestamp with time zone",
     default_value => \"current_timestamp",
-    is_nullable   => 1,
+    is_nullable   => 0,
     original      => { default_value => \"now()" },
   },
   "type",
@@ -179,6 +179,21 @@ __PACKAGE__->has_many(
   { cascade_copy => 0, cascade_delete => 0 },
 );
 
+=head2 origins
+
+Type: has_many
+
+Related object: L<Schema::Result::Origin>
+
+=cut
+
+__PACKAGE__->has_many(
+  "origins",
+  "Schema::Result::Origin",
+  { "foreign.profile" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
 =head2 profile_parameters
 
 Type: has_many
@@ -210,8 +225,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07047 @ 2017-09-05 09:54:32
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BzUCSsUKInomx0bcvL5eAw
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:eX4sXLElEMpaA0xfHTv5lw
 
 
 # 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/Tenant.pm b/traffic_ops/app/lib/Schema/Result/Tenant.pm
index 646e2c9..27b1279 100644
--- a/traffic_ops/app/lib/Schema/Result/Tenant.pm
+++ b/traffic_ops/app/lib/Schema/Result/Tenant.pm
@@ -52,7 +52,7 @@ __PACKAGE__->table("tenant");
 
   data_type: 'timestamp with time zone'
   default_value: current_timestamp
-  is_nullable: 1
+  is_nullable: 0
   original: {default_value => \"now()"}
 
 =cut
@@ -80,7 +80,7 @@ __PACKAGE__->add_columns(
   {
     data_type     => "timestamp with time zone",
     default_value => \"current_timestamp",
-    is_nullable   => 1,
+    is_nullable   => 0,
     original      => { default_value => \"now()" },
   },
 );
@@ -128,6 +128,21 @@ __PACKAGE__->has_many(
   { cascade_copy => 0, cascade_delete => 0 },
 );
 
+=head2 origins
+
+Type: has_many
+
+Related object: L<Schema::Result::Origin>
+
+=cut
+
+__PACKAGE__->has_many(
+  "origins",
+  "Schema::Result::Origin",
+  { "foreign.tenant" => "self.id" },
+  { cascade_copy => 0, cascade_delete => 0 },
+);
+
 =head2 parent
 
 Type: belongs_to
@@ -179,8 +194,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-04-04 20:51:35
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:7I7o08tTBHjshtXVypmOUw
+# Created by DBIx::Class::Schema::Loader v0.07042 @ 2018-05-15 16:06:00
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:fLrBvjW6JLyIRv59Qkrr+Q
 
 # You can replace this text with custom code or comments, and it will be preserved on regeneration
 #
diff --git a/traffic_ops/app/lib/Test/IntegrationTestHelper.pm b/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
index e86166f..9213d68 100644
--- a/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
+++ b/traffic_ops/app/lib/Test/IntegrationTestHelper.pm
@@ -45,6 +45,7 @@ use Fixtures::Integration::JobAgent;
 use Fixtures::Integration::Job;
 use Fixtures::Integration::JobStatus;
 use Fixtures::Integration::Log;
+use Fixtures::Integration::Origin;
 use Fixtures::Integration::Parameter;
 use Fixtures::Integration::PhysLocation;
 use Fixtures::Integration::ProfileParameter;
@@ -157,6 +158,7 @@ sub load_core_data {
 	$self->load_all_fixtures( Fixtures::Integration::Asn->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Server->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::Deliveryservice->new($schema_values) );
+	$self->load_all_fixtures( Fixtures::Integration::Origin->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::DeliveryserviceRegex->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::DeliveryserviceServer->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Integration::ToExtension->new($schema_values) );
diff --git a/traffic_ops/app/lib/Test/TestHelper.pm b/traffic_ops/app/lib/Test/TestHelper.pm
index 6c38f32..388f181 100644
--- a/traffic_ops/app/lib/Test/TestHelper.pm
+++ b/traffic_ops/app/lib/Test/TestHelper.pm
@@ -29,6 +29,7 @@ use Schema;
 use Utils::Tenant;
 use Fixtures::Cdn;
 use Fixtures::Deliveryservice;
+use Fixtures::Origin;
 use Fixtures::DeliveryserviceTmuser;
 use Fixtures::Asn;
 use Fixtures::Cachegroup;
@@ -136,6 +137,7 @@ sub load_core_data {
 	$self->load_all_fixtures( Fixtures::Server->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Asn->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Deliveryservice->new($schema_values) );
+	$self->load_all_fixtures( Fixtures::Origin->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::Regex->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::DeliveryserviceRegex->new($schema_values) );
 	$self->load_all_fixtures( Fixtures::DeliveryserviceTmuser->new($schema_values) );
@@ -162,6 +164,7 @@ sub unload_core_data {
 	$self->teardown($schema, 'DeliveryserviceRegex');
 	$self->teardown($schema, 'Regex');
 	$self->teardown($schema, 'DeliveryserviceServer');
+	$self->teardown($schema, 'Origin');
 	$self->teardown($schema, 'Deliveryservice');
 	$self->teardown($schema, 'Server');
 	$self->teardown($schema, 'PhysLocation');
diff --git a/traffic_ops/app/lib/UI/Cdn.pm b/traffic_ops/app/lib/UI/Cdn.pm
index dd1cb92..72f2aea 100644
--- a/traffic_ops/app/lib/UI/Cdn.pm
+++ b/traffic_ops/app/lib/UI/Cdn.pm
@@ -475,7 +475,7 @@ sub adeliveryservice {
         my $cdn_name = defined( $row->cdn_id ) ? $row->cdn->name : "";
 
         # This will be undefined for 'Steering' delivery services
-        my $org_server_fqdn = defined($row->org_server_fqdn) ? $row->org_server_fqdn : "";
+        my $org_server_fqdn = UI::DeliveryService::compute_org_server_fqdn($self, $row->id) // "";
 
         my $ptext = defined($row->profile) ? $row->profile->name : "-";
         my $line = [
diff --git a/traffic_ops/app/lib/UI/ConfigFiles.pm b/traffic_ops/app/lib/UI/ConfigFiles.pm
index ae1c937..90c4bd2 100644
--- a/traffic_ops/app/lib/UI/ConfigFiles.pm
+++ b/traffic_ops/app/lib/UI/ConfigFiles.pm
@@ -445,7 +445,7 @@ sub cachegroup_profiles {
 		if ( $row->type->name eq 'ORG' ) {
 			my $rs_ds = $self->db->resultset('DeliveryserviceServer')->search( { server => $row->id }, { prefetch => ['deliveryservice'] } );
 			while ( my $ds_row = $rs_ds->next ) {
-				my $ds_domain = $ds_row->deliveryservice->org_server_fqdn;
+				my $ds_domain = UI::DeliveryService::compute_org_server_fqdn($self, $ds_row->deliveryservice->id);
 				$ds_domain =~ s/https?:\/\/(.*)/$1/;
 				push( @{ $deliveryservices->{$ds_domain} }, $row );
 			}
diff --git a/traffic_ops/app/lib/UI/DeliveryService.pm b/traffic_ops/app/lib/UI/DeliveryService.pm
index a7e45a3..12417f5 100644
--- a/traffic_ops/app/lib/UI/DeliveryService.pm
+++ b/traffic_ops/app/lib/UI/DeliveryService.pm
@@ -58,11 +58,15 @@ sub edit {
 	my $server_count = $self->db->resultset('DeliveryserviceServer')->search( { deliveryservice => $id } )->count();
 	my $static_count = $self->db->resultset('Staticdnsentry')->search( { deliveryservice => $id } )->count();
 
+	my $origin = {};
+	$origin->{org_server_fqdn} = compute_org_server_fqdn($self, $id);
+
 	$self->stash_profile_selector('DS_PROFILE', defined($data->profile) ? $data->profile->id : undef);
 	$self->stash_cdn_selector($data->cdn->id);
 	&stash_role($self);
 	$self->stash(
 		ds           => $data,
+		origin       => $origin,
 		server_count => $server_count,
 		static_count => $static_count,
 		fbox_layout  => 1,
@@ -73,6 +77,24 @@ sub edit {
 	);
 }
 
+sub compute_org_server_fqdn {
+	my $self = shift;
+	my $ds_id = shift;
+
+	my $origin = $self->db->resultset('Origin')->search( { deliveryservice => $ds_id, is_primary => 1 } )->single();
+	if (!defined( $origin )) {
+		return undef;
+	}
+
+	my $protocol = $origin->protocol;
+	my $fqdn = $origin->fqdn;
+	my $port = $origin->port;
+
+	my $url = $protocol . "://" . $fqdn;
+
+	return defined($port) ? $url . ":" . $port : $url;
+}
+
 sub get_example_urls {
 	my $self       = shift;
 	my $id         = shift;
@@ -223,7 +245,7 @@ sub read {
 				"dns_bypass_ip6"              => $row->dns_bypass_ip6,
 				"dns_bypass_cname"            => $row->dns_bypass_cname,
 				"dns_bypass_ttl"              => $row->dns_bypass_ttl,
-				"org_server_fqdn"             => $row->org_server_fqdn,
+				"org_server_fqdn"             => compute_org_server_fqdn($self, $row->id),
 				"multi_site_origin"           => \$row->multi_site_origin,
 				"ccr_dns_ttl"                 => $row->ccr_dns_ttl,
 				"type"                        => $row->type->id,
@@ -440,13 +462,13 @@ sub check_deliveryservice_input {
 		$self->field('ds.routing_name')->is_equal("", $self->param('ds.routing_name') . " is not a valid hostname without periods.");
 	}
 
-	my $org_host_name = $self->param('ds.org_server_fqdn');
-	$self->field('ds.org_server_fqdn')->is_like( qr/^(https?:\/\/)/, "Origin Server Base URL must start with http(s)://" );
+	my $org_host_name = $self->param('origin.org_server_fqdn');
+	$self->field('origin.org_server_fqdn')->is_like( qr/^(https?:\/\/)/, "Origin Server Base URL must start with http(s)://" );
 	$org_host_name =~ s!^https?://?!!i;
 	$org_host_name =~ s/:(.*)$//;
 	my $port = defined($1) ? $1 : 80;
 	if ( !&is_hostname($org_host_name) || $port !~ /^[1-9][0-9]*$/ ) {
-		$self->field('ds.org_server_fqdn')
+		$self->field('origin.org_server_fqdn')
 			->is_equal( "", $org_host_name . " is not a valid org server name (rfc1123) or " . $port . " is not a valid port" );
 	}
 	if ( $self->param('ds.http_bypass_fqdn') ne ""
@@ -792,6 +814,31 @@ sub delete_cfg_file {
 	}
 }
 
+sub get_primary_origin_from_deliveryservice {
+	my $deliveryservice_id = shift;
+	my $deliveryservice = shift;
+	my $org_server_fqdn = shift;
+
+	if ( !defined( $org_server_fqdn ) || $org_server_fqdn eq "" ) {
+		return undef;
+	}
+
+	$org_server_fqdn =~ m{^(https?)://([^:]+)(:(\d+))?$}i;
+	my $protocol = lc($1);
+	my $fqdn = $2;
+	my $port = $4;
+
+	return {
+		name => $deliveryservice->{xml_id},
+		deliveryservice => $deliveryservice_id,
+		fqdn => $fqdn,
+		protocol => $protocol,
+		is_primary => 1,
+		port => $port,
+		tenant => $deliveryservice ->{tenant_id}
+	};
+}
+
 # Update
 sub update {
 	my $self = shift;
@@ -815,7 +862,6 @@ sub update {
 			geo_limit_countries         => sanitize_geo_limit_countries( $self->paramAsScalar('ds.geo_limit_countries') ),
 			geolimit_redirect_url       => $self->param('ds.geolimit_redirect_url'),
 			geo_provider                => $self->paramAsScalar('ds.geo_provider'),
-			org_server_fqdn             => $self->paramAsScalar('ds.org_server_fqdn'),
 			multi_site_origin           => $self->paramAsScalar('ds.multi_site_origin'),
 			ccr_dns_ttl                 => $self->paramAsScalar('ds.ccr_dns_ttl'),
 			type                        => $self->typeid(),
@@ -873,6 +919,15 @@ sub update {
 		$update->update();
 		&log( $self, "Update deliveryservice with xml_id:" . $self->param('ds.xml_id'), "UICHANGE" );
 
+		# find this DS's primary Origin and update it too
+		my $origin_rs = $self->db->resultset('Origin')->find( { deliveryservice => $id, is_primary => 1 } );
+		if ( defined( $origin_rs ) ) {
+			my $origin = get_primary_origin_from_deliveryservice($id, \%hash, $self->paramAsScalar('origin.org_server_fqdn'));
+			if ( defined( $origin ) ) {
+				$origin_rs->update($origin);
+			}
+		}
+
 		# get the existing regexp set in a hash
 		my $regexp_set;
 		my $i = 0;
@@ -979,12 +1034,15 @@ sub update {
 		my $regexp_set   = &get_regexp_set( $self, $id );
 		my @example_urls = &get_example_urls( $self, $id, $regexp_set, $data, $cdn_domain, $data->protocol );
 		my $action;
+		my $origin = {};
+		$origin->{org_server_fqdn} = compute_org_server_fqdn($self, $id);
 
 		$self->stash_profile_selector('DS_PROFILE', defined($data->profile) ? $data->profile->id : undef);
 		$self->stash_cdn_selector($data->cdn->id);
 
 		$self->stash(
 			ds           => $data,
+			origin       => $origin,
 			fbox_layout  => 1,
 			server_count => $server_count,
 			static_count => $static_count,
@@ -1056,7 +1114,6 @@ sub create {
 				dns_bypass_ip6              => $self->paramAsScalar('ds.dns_bypass_ip6'),
 				dns_bypass_cname            => $self->paramAsScalar('ds.dns_bypass_cname'),
 				dns_bypass_ttl              => $self->paramAsScalar('ds.dns_bypass_ttl'),
-				org_server_fqdn             => $self->paramAsScalar('ds.org_server_fqdn'),
 				multi_site_origin           => $self->paramAsScalar('ds.multi_site_origin'),
 				ccr_dns_ttl                 => $self->paramAsScalar('ds.ccr_dns_ttl'),
 				type                        => $self->paramAsScalar('ds.type'),
@@ -1098,6 +1155,14 @@ sub create {
 		$new_id = $insert->id;
 		&log( $self, "Create deliveryservice with xml_id:" . $self->param('ds.xml_id'), "UICHANGE" );
 
+		# create primary Origin for this DS
+		my $origin = get_primary_origin_from_deliveryservice($insert->id, $new_ds, $self->paramAsScalar('origin.org_server_fqdn'));
+		if (defined( $origin )) {
+			my $origin_rs = $self->db->resultset('Origin')->create($origin)->insert();
+			&log( $self, "Created origin [ '" . $origin_rs->name . "' ] with id: " . $origin_rs->id, "UICHANGE" );
+		}
+
+
 		if ( $new_id == -1 ) {    # there was an error the flash will already be set,
 			my $referer = $self->req->headers->header('referer');
 			my $qstring = "?";
@@ -1183,6 +1248,7 @@ sub create {
 				&stash_role($self);
 				$self->stash(
 					ds               => {},
+					origin           => {},
 					fbox_layout      => 1,
 					selected_type    => $selected_type,
 					selected_profile => $selected_profile,
@@ -1204,6 +1270,7 @@ sub create {
 		&stash_role($self);
 		$self->stash(
 			ds               => {},
+			origin           => {},
 			fbox_layout      => 1,
 			selected_type    => $selected_type,
 			selected_profile => $selected_profile,
@@ -1327,6 +1394,7 @@ sub add {
 	$self->stash(
 		fbox_layout      => 1,
 		ds               => {},
+		origin           => {},
 		selected_type    => "",
 		selected_profile => "",
 		selected_cdn     => "",
diff --git a/traffic_ops/app/lib/UI/DeliveryServiceServer.pm b/traffic_ops/app/lib/UI/DeliveryServiceServer.pm
index 1007c8e..b60c906 100644
--- a/traffic_ops/app/lib/UI/DeliveryServiceServer.pm
+++ b/traffic_ops/app/lib/UI/DeliveryServiceServer.pm
@@ -119,7 +119,7 @@ sub edit {
 
 	$self->stash( ds_id            => $id );
 	$self->stash( assigned_servers => $dss_data );
-	$self->stash( ds_name          => $ds->xml_id . ' (' . $ds->org_server_fqdn . ')' );
+	$self->stash( ds_name          => $ds->xml_id . ' (' . UI::DeliveryService::compute_org_server_fqdn($self, $ds->id) . ')' );
 	$self->stash( fbox_layout      => 1 );
 	$self->stash( dss_data         => $dss_data );
 	$self->stash( totals           => $totals );
diff --git a/traffic_ops/app/lib/UI/Job.pm b/traffic_ops/app/lib/UI/Job.pm
index 2be1810..e875107 100644
--- a/traffic_ops/app/lib/UI/Job.pm
+++ b/traffic_ops/app/lib/UI/Job.pm
@@ -74,7 +74,7 @@ sub newjob {
 			$org_server_fqdn =~ s/\/$//;
 		}
 		else {
-			$org_server_fqdn = $ds->org_server_fqdn;
+			$org_server_fqdn = UI::DeliveryService::compute_org_server_fqdn($self, $ds->id);
 		}
 		my $ds_id = $ds->id;
 
diff --git a/traffic_ops/app/t/deliveryservice.t b/traffic_ops/app/t/deliveryservice.t
index ce3fdfc..eca63e3 100644
--- a/traffic_ops/app/t/deliveryservice.t
+++ b/traffic_ops/app/t/deliveryservice.t
@@ -77,7 +77,7 @@ ok $t->post_ok(
 		'ds.max_dns_answers'             => '0',
 		'ds.miss_lat'                    => '41.881944',
 		'ds.miss_long'                   => '-87.627778',
-		'ds.org_server_fqdn'             => 'http://jvd.knutsel.com',
+		'origin.org_server_fqdn'         => 'http://jvd.knutsel.com',
 		'ds.multi_site_origin'           => '0',
 		'ds.multi_site_origin_algorithm' => '0',
 		'ds.profile'                     => '100',
@@ -130,7 +130,7 @@ ok $t->post_ok(
 		'ds.max_dns_answers'             => '0',
 		'ds.miss_lat'                    => '41.881944',
 		'ds.miss_long'                   => '-87.627778',
-		'ds.org_server_fqdn'             => 'http://jvd-1.knutsel.com',
+		'origin.org_server_fqdn'         => 'http://jvd-1.knutsel.com',
 		'ds.multi_site_origin'           => '0',
 		'ds.multi_site_origin_algorithm' => '0',
 		'ds.profile'                     => '100',
@@ -183,7 +183,7 @@ ok $t->post_ok(
 		'ds.max_dns_answers'             => '0',
 		'ds.miss_lat'                    => '41.881944',
 		'ds.miss_long'                   => '-87.627778',
-		'ds.org_server_fqdn'             => 'http://jvd.knutsel.com',
+		'origin.org_server_fqdn'         => 'http://jvd.knutsel.com',
 		'ds.multi_site_origin'           => '0',
 		'ds.multi_site_origin_algorithm' => '0',
 		'ds.profile'                     => '100',
@@ -252,7 +252,7 @@ ok $t->post_ok(
 		'ds.max_dns_answers'             => '1',
 		'ds.miss_lat'                    => '0',
 		'ds.miss_long'                   => '0',
-		'ds.org_server_fqdn'             => 'http://update.knutsel.com',
+		'origin.org_server_fqdn'         => 'http://update.knutsel.com',
 		'ds.multi_site_origin'           => '0',
 		'ds.multi_site_origin_algorithm' => '0',
 		'ds.profile'                     => '200',
diff --git a/traffic_ops/app/t/purge.t b/traffic_ops/app/t/purge.t
index 97286e6..dd3bdbe 100644
--- a/traffic_ops/app/t/purge.t
+++ b/traffic_ops/app/t/purge.t
@@ -48,7 +48,10 @@ $t->post_ok( '/login', => form => { u => 'admin', p => 'password' } )->status_is
 
 my $q = "SELECT deliveryservice.id, 
            deliveryservice.xml_id, 
-           deliveryservice.org_server_fqdn,
+           (SELECT o.protocol::text || '://' || o.fqdn || rtrim(concat(':', o.port::text), ':')
+               FROM origin o
+               WHERE o.deliveryservice = deliveryservice.id
+               AND o.is_primary) as org_server_fqdn,
            deliveryservice.type,
            profile.id AS profile_id, 
            cdn.name AS cdn_name 
diff --git a/traffic_ops/app/templates/delivery_service/_form.html.ep b/traffic_ops/app/templates/delivery_service/_form.html.ep
index 35b72a5..ed500d4 100644
--- a/traffic_ops/app/templates/delivery_service/_form.html.ep
+++ b/traffic_ops/app/templates/delivery_service/_form.html.ep
@@ -341,14 +341,14 @@
 		<% } %>
 	</div>
 	<div class="block form-row" id="org_server_fqdn_row">
-		<% unless (field('ds.org_server_fqdn')->valid) { %>
-			<span class="field-with-error"><%= field('ds.org_server_fqdn')->error %></span>
+		<% unless (field('origin.org_server_fqdn')->valid) { %>
+			<span class="field-with-error"><%= field('origin.org_server_fqdn')->error %></span>
 		<% } %>
-		%= label_for 'org_server_fqdn' => '* Origin Server Base URL', class => 'label'
+		%= label_for 'origin.org_server_fqdn' => '* Origin Server Base URL', class => 'label'
 		<% if ($priv_level >= 20) { %>
-		%= field('ds.org_server_fqdn')->text(class => 'field', id => 'org_server_fqdn', name => 'ds.org_server_fqdn');
+		%= field('origin.org_server_fqdn')->text(class => 'field', id => 'org_server_fqdn', name => 'origin.org_server_fqdn');
 		<% } else { %>
-		%= field('ds.org_server_fqdn')->text(class => 'field', readonly => 'readonly');
+		%= field('origin.org_server_fqdn')->text(class => 'field', readonly => 'readonly');
 		<% } %>
 	</div>
 	<div class="block form-row" id="multi_site_origin_row">
diff --git a/traffic_ops/testing/api/v13/tc-fixtures.json b/traffic_ops/testing/api/v13/tc-fixtures.json
index 2653051..6ce32b1 100644
--- a/traffic_ops/testing/api/v13/tc-fixtures.json
+++ b/traffic_ops/testing/api/v13/tc-fixtures.json
@@ -329,7 +329,6 @@
             "fqdn": "origin1.example.com",
             "ipAddress": "1.2.3.4",
             "ip6Address": "dead:beef:cafe::42",
-            "isPrimary": false,
             "port": 1234,
             "protocol": "http"
         },
@@ -338,7 +337,6 @@
             "fqdn": "origin2.example.com",
             "ipAddress": "5.6.7.8",
             "ip6Address": "cafe::42",
-            "isPrimary": false,
             "port": 5678,
             "protocol": "https"
         }
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
index 1b95ce1..cdfc319 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
@@ -260,7 +260,8 @@ func validateTypeFields(db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) []error
 		"multiSiteOrigin": validation.Validate(ds.MultiSiteOrigin,
 			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
 		"orgServerFqdn": validation.Validate(ds.OrgServerFQDN,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
+			validation.NewStringRule(validateOrgServerFQDN, "must start with http:// or https:// and be followed by a valid hostname with an optional port (no trailing slash)")),
 		"protocol": validation.Validate(ds.Protocol,
 			validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
 		"qstringIgnore": validation.Validate(ds.QStringIgnore,
@@ -275,6 +276,14 @@ func validateTypeFields(db *sqlx.DB, ds *tc.DeliveryServiceNullableV12) []error
 	return nil
 }
 
+func validateOrgServerFQDN(orgServerFQDN string) bool {
+	_, fqdn, port, err := parseOrgServerFQDN(orgServerFQDN)
+	if err != nil || !govalidator.IsHost(*fqdn) || (port != nil && !govalidator.IsPort(*port)) {
+		return false
+	}
+	return true
+}
+
 func requiredIfMatchesTypeName(patterns []string, typeName string) func(interface{}) error {
 	return func(value interface{}) error {
 		switch v := value.(type) {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
index b5d61ee..77045f0 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
@@ -25,6 +25,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -196,7 +197,7 @@ func create(db *sql.DB, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 	commitTx := false
 	defer dbhelpers.FinishTx(tx, &commitTx)
 
-	resultRows, err := tx.Query(insertQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.LongD [...]
+	resultRows, err := tx.Query(insertQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.LongD [...]
 
 	if err != nil {
 		if pqerr, ok := err.(*pq.Error); ok {
@@ -270,6 +271,11 @@ func create(db *sql.DB, cfg config.Config, user *auth.CurrentUser, ds tc.Deliver
 	if err := createDNSSecKeys(tx, cfg, *ds.ID, *ds.XMLID, cdnName, cdnDomain, dnssecEnabled, ds.ExampleURLs); err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
 	}
+
+	if err := createPrimaryOrigin(db, tx, user, ds); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating delivery service: " + err.Error())
+	}
+
 	ds.LastUpdated = &lastUpdated
 	commitTx = true
 	api.CreateChangeLogRaw(api.ApiChange, "Created ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), *user, db)
@@ -306,6 +312,88 @@ func createDefaultRegex(tx *sql.Tx, dsID int, xmlID string) error {
 	return nil
 }
 
+func parseOrgServerFQDN(orgServerFQDN string) (*string, *string, *string, error) {
+	originRegex := regexp.MustCompile(`^(https?)://([^:]+)(:(\d+))?$`)
+	matches := originRegex.FindStringSubmatch(orgServerFQDN)
+	if len(matches) == 0 {
+		return nil, nil, nil, fmt.Errorf("unable to parse invalid orgServerFqdn: '%s'", orgServerFQDN)
+	}
+
+	protocol := strings.ToLower(matches[1])
+	FQDN := matches[2]
+
+	if len(protocol) == 0 || len(FQDN) == 0 {
+		return nil, nil, nil, fmt.Errorf("empty Origin protocol or FQDN parsed from '%s'", orgServerFQDN)
+	}
+
+	var port *string
+	if len(matches[4]) != 0 {
+		port = &matches[4]
+	}
+	return &protocol, &FQDN, port, nil
+}
+
+func createPrimaryOrigin(db *sql.DB, tx *sql.Tx, user *auth.CurrentUser, ds tc.DeliveryServiceNullableV13) error {
+	if ds.OrgServerFQDN == nil {
+		return nil
+	}
+
+	protocol, fqdn, port, err := parseOrgServerFQDN(*ds.OrgServerFQDN)
+	if err != nil {
+		return fmt.Errorf("creating primary origin: %v", err)
+	}
+
+	originID := 0
+	q := `INSERT INTO origin (name, fqdn, protocol, is_primary, port, deliveryservice, tenant) VALUES ($1, $2, $3, TRUE, $4, $5, $6) RETURNING id`
+	if err := tx.QueryRow(q, ds.XMLID, fqdn, protocol, port, ds.ID, ds.TenantID).Scan(&originID); err != nil {
+		return fmt.Errorf("insert origin from '%s': %s", *ds.OrgServerFQDN, err.Error())
+	}
+
+	api.CreateChangeLogRaw(api.ApiChange, "Created primary origin id: "+strconv.Itoa(originID)+" for delivery service: "+*ds.XMLID, *user, db)
+
+	return nil
+}
+
+func updatePrimaryOrigin(db *sql.DB, tx *sql.Tx, user *auth.CurrentUser, ds tc.DeliveryServiceNullableV13) error {
+	count := 0
+	q := `SELECT count(*) FROM origin WHERE deliveryservice = $1 AND is_primary`
+	if err := tx.QueryRow(q, *ds.ID).Scan(&count); err != nil {
+		return fmt.Errorf("querying existing primary origin for ds %s: %s", *ds.XMLID, err.Error())
+	}
+
+	if ds.OrgServerFQDN == nil || *ds.OrgServerFQDN == "" {
+		if count == 1 {
+			// the update is removing the existing orgServerFQDN, so the existing row needs to be deleted
+			q = `DELETE FROM origin WHERE deliveryservice = $1 AND is_primary`
+			if _, err := tx.Exec(q, *ds.ID); err != nil {
+				return fmt.Errorf("deleting primary origin for ds %s: %s", *ds.XMLID, err.Error())
+			}
+			api.CreateChangeLogRaw(api.ApiChange, "Deleted primary origin for delivery service: "+*ds.XMLID, *user, db)
+		}
+		return nil
+	}
+
+	if count == 0 {
+		// orgServerFQDN is going from null to not null, so the primary origin needs to be created
+		return createPrimaryOrigin(db, tx, user, ds)
+	}
+
+	protocol, fqdn, port, err := parseOrgServerFQDN(*ds.OrgServerFQDN)
+	if err != nil {
+		return fmt.Errorf("updating primary origin: %v", err)
+	}
+
+	name := ""
+	q = `UPDATE origin SET protocol = $1, fqdn = $2, port = $3 WHERE is_primary AND deliveryservice = $4 RETURNING name`
+	if err := tx.QueryRow(q, protocol, fqdn, port, *ds.ID).Scan(&name); err != nil {
+		return fmt.Errorf("update primary origin for ds %s from '%s': %s", *ds.XMLID, *ds.OrgServerFQDN, err.Error())
+	}
+
+	api.CreateChangeLogRaw(api.ApiChange, "Updated primary origin: "+name+" for delivery service: "+*ds.XMLID, *user, db)
+
+	return nil
+}
+
 func getOldHostName(id int, tx *sql.Tx) (string, error) {
 	q := `
 SELECT ds.xml_id, ds.protocol, type.name, ds.routing_name, cdn.domain_name
@@ -423,7 +511,7 @@ func update(db *sql.DB, cfg config.Config, user auth.CurrentUser, ds *tc.Deliver
 		deepCachingType = ds.DeepCachingType.String() // necessary, because DeepCachingType's default needs to insert the string, not "", and Query doesn't call .String().
 	}
 
-	resultRows, err := tx.Query(updateDSQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.Lon [...]
+	resultRows, err := tx.Query(updateDSQuery(), &ds.Active, &ds.CacheURL, &ds.CCRDNSTTL, &ds.CDNID, &ds.CheckPath, &deepCachingType, &ds.DisplayName, &ds.DNSBypassCNAME, &ds.DNSBypassIP, &ds.DNSBypassIP6, &ds.DNSBypassTTL, &ds.DSCP, &ds.EdgeHeaderRewrite, &ds.GeoLimitRedirectURL, &ds.GeoLimit, &ds.GeoLimitCountries, &ds.GeoProvider, &ds.GlobalMaxMBPS, &ds.GlobalMaxTPS, &ds.FQPacingRate, &ds.HTTPBypassFQDN, &ds.InfoURL, &ds.InitialDispersion, &ds.IPV6RoutingEnabled, &ds.LogsEnabled, &ds.Lon [...]
 
 	if err != nil {
 		if err, ok := err.(*pq.Error); ok {
@@ -506,6 +594,11 @@ func update(db *sql.DB, cfg config.Config, user auth.CurrentUser, ds *tc.Deliver
 	if err := ensureCacheURLParams(tx, *ds.ID, *ds.XMLID, ds.CacheURL); err != nil {
 		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("creating mid cacheurl parameters: " + err.Error())
 	}
+
+	if err := updatePrimaryOrigin(db, tx, &user, *ds); err != nil {
+		return tc.DeliveryServiceNullableV13{}, http.StatusInternalServerError, nil, errors.New("updating delivery service: " + err.Error())
+	}
+
 	ds.LastUpdated = &lastUpdated
 	commitTx = true
 	api.CreateChangeLogRaw(api.ApiChange, "Updated ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), user, db)
@@ -1018,7 +1111,10 @@ ds.mid_header_rewrite,
 COALESCE(ds.miss_lat, 0.0),
 COALESCE(ds.miss_long, 0.0),
 ds.multi_site_origin,
-ds.org_server_fqdn,
+(SELECT o.protocol::::text || ':://' || o.fqdn || rtrim(concat('::', o.port::::text), '::')
+	FROM origin o
+	WHERE o.deliveryservice = ds.id
+	AND o.is_primary) as org_server_fqdn,
 ds.origin_shield,
 ds.profile as profileID,
 profile.name as profile_name,
@@ -1085,24 +1181,23 @@ mid_header_rewrite=$30,
 miss_lat=$31,
 miss_long=$32,
 multi_site_origin=$33,
-org_server_fqdn=$34,
-origin_shield=$35,
-profile=$36,
-protocol=$37,
-qstring_ignore=$38,
-range_request_handling=$39,
-regex_remap=$40,
-regional_geo_blocking=$41,
-remap_text=$42,
-routing_name=$43,
-signing_algorithm=$44,
-ssl_key_version=$45,
-tenant_id=$46,
-tr_request_headers=$47,
-tr_response_headers=$48,
-type=$49,
-xml_id=$50
-WHERE id=$51
+origin_shield=$34,
+profile=$35,
+protocol=$36,
+qstring_ignore=$37,
+range_request_handling=$38,
+regex_remap=$39,
+regional_geo_blocking=$40,
+remap_text=$41,
+routing_name=$42,
+signing_algorithm=$43,
+ssl_key_version=$44,
+tenant_id=$45,
+tr_request_headers=$46,
+tr_response_headers=$47,
+type=$48,
+xml_id=$49
+WHERE id=$50
 RETURNING last_updated
 `
 }
@@ -1143,7 +1238,6 @@ mid_header_rewrite,
 miss_lat,
 miss_long,
 multi_site_origin,
-org_server_fqdn,
 origin_shield,
 profile,
 protocol,
@@ -1161,7 +1255,7 @@ tr_response_headers,
 type,
 xml_id
 )
-VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$50)
+VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49)
 RETURNING id, last_updated
 `
 }
diff --git a/traffic_ops/traffic_ops_golang/origin/origins.go b/traffic_ops/traffic_ops_golang/origin/origins.go
index f514043..8db7541 100644
--- a/traffic_ops/traffic_ops_golang/origin/origins.go
+++ b/traffic_ops/traffic_ops_golang/origin/origins.go
@@ -98,7 +98,6 @@ func (origin *TOOrigin) Validate(db *sqlx.DB) []error {
 		"fqdn":              validation.Validate(origin.FQDN, validation.Required, is.DNSName),
 		"ip6Address":        validation.Validate(origin.IP6Address, validation.NilOrNotEmpty, is.IPv6),
 		"ipAddress":         validation.Validate(origin.IPAddress, validation.NilOrNotEmpty, is.IPv4),
-		"isPrimary":         validation.Validate(origin.Name, validation.NotNil),
 		"name":              validation.Validate(origin.Name, validation.Required, noSpaces),
 		"port":              validation.Validate(origin.Port, validation.NilOrNotEmpty.Error(portErr), validation.Min(1).Error(portErr), validation.Max(65535).Error(portErr)),
 		"profileId":         validation.Validate(origin.ProfileID, validation.Min(1)),
@@ -202,6 +201,7 @@ func getOrigins(params map[string]string, db *sqlx.DB, privLevel int) ([]v13.Ori
 		"deliveryservice": dbhelpers.WhereColumnInfo{"o.deliveryservice", api.IsInt},
 		"id":              dbhelpers.WhereColumnInfo{"o.id", api.IsInt},
 		"name":            dbhelpers.WhereColumnInfo{"o.name", nil},
+		"primary":         dbhelpers.WhereColumnInfo{"o.is_primary", api.IsBool},
 		"profileId":       dbhelpers.WhereColumnInfo{"o.profile", api.IsInt},
 		"tenant":          dbhelpers.WhereColumnInfo{"o.tenant", api.IsInt},
 	}
@@ -331,6 +331,17 @@ func (origin *TOOrigin) Update(db *sqlx.DB, user auth.CurrentUser) (error, tc.Ap
 		return tc.DBError, tc.SystemError
 	}
 
+	isPrimary := false
+	ds := 0
+	q := `SELECT is_primary, deliveryservice FROM origin WHERE id = $1`
+	if err := tx.QueryRow(q, *origin.ID).Scan(&isPrimary, &ds); err != nil {
+		log.Errorf("updating origin %d, received error in select: %v", *origin.ID, err)
+		return tc.DBError, tc.SystemError
+	}
+	if isPrimary && *origin.DeliveryServiceID != ds {
+		return errors.New("cannot update the delivery service of a primary origin"), tc.DataConflictError
+	}
+
 	log.Debugf("about to run exec query: %s with origin: %++v", updateQuery(), origin)
 	resultRows, err := tx.NamedQuery(updateQuery(), origin)
 	if err != nil {
@@ -385,7 +396,6 @@ deliveryservice=:deliveryservice_id,
 fqdn=:fqdn,
 ip6_address=:ip6_address,
 ip_address=:ip_address,
-is_primary=:is_primary,
 name=:name,
 port=:port,
 profile=:profile_id,
@@ -479,7 +489,6 @@ deliveryservice,
 fqdn,
 ip6_address,
 ip_address,
-is_primary,
 name,
 port,
 profile,
@@ -491,7 +500,6 @@ tenant) VALUES (
 :fqdn,
 :ip6_address,
 :ip_address,
-:is_primary,
 :name,
 :port,
 :profile_id,
@@ -519,6 +527,17 @@ func (origin *TOOrigin) Delete(db *sqlx.DB, user auth.CurrentUser) (error, tc.Ap
 		log.Error.Printf("could not begin transaction: %v", err)
 		return tc.DBError, tc.SystemError
 	}
+
+	isPrimary := false
+	q := `SELECT is_primary FROM origin WHERE id = $1`
+	if err := tx.QueryRow(q, *origin.ID).Scan(&isPrimary); err != nil {
+		log.Errorf("deleting origin %d, received error selecting is_primary: %v", *origin.ID, err)
+		return tc.DBError, tc.SystemError
+	}
+	if isPrimary {
+		return errors.New("cannot delete a primary origin"), tc.DataConflictError
+	}
+
 	log.Debugf("about to run exec query: %s with origin: %++v", deleteQuery(), origin)
 	result, err := tx.NamedExec(deleteQuery(), origin)
 	if err != nil {
diff --git a/traffic_ops/traffic_ops_golang/origin/origins_test.go b/traffic_ops/traffic_ops_golang/origin/origins_test.go
index c7d522a..a44c1e6 100644
--- a/traffic_ops/traffic_ops_golang/origin/origins_test.go
+++ b/traffic_ops/traffic_ops_golang/origin/origins_test.go
@@ -179,7 +179,6 @@ func TestValidate(t *testing.T) {
 		Name:              nil,
 		DeliveryServiceID: nil,
 		FQDN:              nil,
-		IsPrimary:         nil,
 		Protocol:          nil,
 	}
 	errs := test.SortErrors(c.Validate(nil))
@@ -187,7 +186,6 @@ func TestValidate(t *testing.T) {
 	expectedErrs := []error{
 		errors.New(`'deliveryServiceId' is required`),
 		errors.New(`'fqdn' cannot be blank`),
-		errors.New(`'isPrimary' is required`),
 		errors.New(`'name' cannot be blank`),
 		errors.New(`'protocol' cannot be blank`),
 	}
@@ -202,7 +200,6 @@ func TestValidate(t *testing.T) {
 	fqdn := "is.a.valid.hostname"
 	ip6 := "dead:beef::42"
 	ip := "1.2.3.4"
-	primary := false
 	port := 65535
 	pro := "http"
 	lu := tc.TimeNoMod{Time: time.Now()}
@@ -212,7 +209,6 @@ func TestValidate(t *testing.T) {
 		FQDN:              &fqdn,
 		IP6Address:        &ip6,
 		IPAddress:         &ip,
-		IsPrimary:         &primary,
 		Port:              &port,
 		Protocol:          &pro,
 		LastUpdated:       &lu,

-- 
To stop receiving notification emails like this one, please contact
elsloo@apache.org.