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

[trafficcontrol] branch master updated: Servers interfaces (#4700)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0480f8f  Servers interfaces (#4700)
0480f8f is described below

commit 0480f8fcd82527c6d7d26ad65e348db9ea8e1ac5
Author: ocket8888 <oc...@apache.org>
AuthorDate: Tue Jun 9 11:58:13 2020 -0600

    Servers interfaces (#4700)
    
    * Reworked GET /servers for new interfaces table
    
    * Updated query
    
    * Simplified V2 server conversion
    
    * Finally got GET working
    
    * Implemented POST
    
    * Implemented DELETE
    
    * Added netmask parsing and changelogs
    
    * Added method for converting legacy data to interface arrays
    
    * Implemented PUT
    
    * Fixed a couple unit tests
    
    * Finished fixing unit tests
    
    * Cleaned up old approaches left in comments
    
    * Fixed server teardown for v1 tests
    
    * Fixed broken POST handler for API versions < 3
    
    Also removed some CRUDer boilerplate that is no longer used
    
    * Fixed read query not returning IDs
    
    * fixed unmarshaling problems, some API tests
    
    * Fixed ip_allow.config for server interfaces
    
    * Fixed PUT not returning proper names
    
    * Fixed using unitialized data in PUT 1.1/servers handler
    
    * Fixed/improved error messages
    
    * Fixed select query
    
    * Fixed downgraded servers not setting ipIsService and ip6IsService
    
    * Fixed GET v1 servers returning v2 servers and vice-versa
    
    * Changed responses to reference new API version, updated old client methods to use old structures
    
    * Updated client to send/return new server structure
    
    * Changed tc-fixtures to use new server objects
    
    * made TS use v2 client
    
    * Fixed teardowns
    
    * Fixed renaming a struct which breaks a vendored version of the client for ORT.....
    
    * Fixed bad error message in v3 validation
    
    * Fixed possible segfault in test failure state
    
    * Fixed null response for no servers
    
    * Fixed v3 tests for new server model
    
    * Fixed possible nil panics from unitialized maps
    
    * Added unit tests for server handler
    
    * Added test for ToInterfaces method
    
    * Go fmt
    
    * Rolled back cdn.conf changes
    
    * Rolled back database.conf changes
    
    * Rolled back a change to ATSTCCFG that snuck in
    
    * Removed unnecessary log
    
    * Removed extra DELETE statement
    
    * Removed unnecessary log
    
    * Removed unnecessary log
    
    * Removed unnecessary log
    
    * Removed unused import
    
    * Updated docs for /servers in v3
    
    * updated servers/{{ID}} docs for v3
    
    * Fixed broken footnote reference
    
    * Fixed a ton of context-insensitive match warnings in Sphinx v3+
    
    * Added response structure for DELETE /servers/{{ID}}
    
    * Added summary field descriptions
    
    * Updated CHANGELOG
    
    * Proper capitalization of IP in ServerIPAddress
    
    * s/ServersResponsev3/ServersV3Response/g
    
    * Unset netmask when not present
    
    * Short-circuit eval for interface-to-legacy conversion
    
    * Removed ip_allow.config, remap.config, and parent.config v1 generation routes
    
    * Removed tests for now-non-existent routes
    
    * Added deprecation notices to docs
    
    * Updated changelog
    
    * ACTUALLY removed tests for removed endpoints
    
    * Abstracted conditionals
    
    * Removed unused queries
    
    * Removed unused validation function
    
    * lowercase errors
    
    * Fixed servers tests
    
    * go fmt
    
    * Fixed bad details query
    
    * Fixed details integration test
    
    * Fixed potential segfault
    
    * Switched to returning response object
    
    * Fixed max bandwidth case issue
    
    * Pulled interfaces array into shared constant
    
    * Fixed possible segfault in MTU validation
    
    * Moved back to grave accents for query addition
    
    * Fix server count query
    
    It wasn't properly considering non-paginiation query parameters. Also changed 'unfiltered' names to be more appropriate.
    
    * Changed querys to exec's wherever results don't matter
    
    * Fixed unit tests
    
    * go fmt
    
    * Stop ignoring no rows error
    
    * Fixed api tests
    
    * Switched to checking rows affected in DELETE
    
    * Reworked query to build out data structures after db fetch
    
    * Improved performance by only fetching interfaces for requested servers
    
    * Fixed XMPPIDs not being properly set on server creation
    
    * Fixed unit tests
---
 CHANGELOG.md                                       |    5 +
 .../admin/quick_howto/content_invalidation.rst     |    6 +-
 docs/source/admin/quick_howto/oauth_login.rst      |    2 +-
 .../admin/quick_howto/server_capabililty.rst       |   12 +-
 .../admin/traffic_portal/usingtrafficportal.rst    |   36 +-
 docs/source/admin/traffic_router.rst               |    2 +-
 docs/source/api/index.rst                          |   11 +
 docs/source/api/v1/cdns_name_snapshot.rst          |    6 +-
 docs/source/api/v1/cdns_name_snapshot_new.rst      |    6 +-
 .../v1/deliveryservices_id_servers_eligible.rst    |    2 +-
 docs/source/api/v1/origins.rst                     |  160 +--
 docs/source/api/v1/regions_name_phys_locations.rst |    2 +-
 docs/source/api/v1/server_server_capabilities.rst  |    4 +-
 .../api/v1/servers_server_configfiles_ats.rst      |    3 +
 .../v1/servers_server_configfiles_ats_filename.rst |    3 +
 docs/source/api/v2/cdns_name_snapshot.rst          |    6 +-
 docs/source/api/v2/cdns_name_snapshot_new.rst      |    6 +-
 .../v2/deliveryservices_id_servers_eligible.rst    |    2 +-
 docs/source/api/v2/origins.rst                     |  160 +--
 docs/source/api/v2/server_server_capabilities.rst  |    4 +-
 docs/source/api/v3/cdns_name_snapshot.rst          |    6 +-
 docs/source/api/v3/cdns_name_snapshot_new.rst      |    6 +-
 .../v3/deliveryservices_id_servers_eligible.rst    |    2 +-
 docs/source/api/v3/origins.rst                     |  160 +--
 docs/source/api/v3/server_server_capabilities.rst  |    4 +-
 docs/source/api/v3/servers.rst                     |  400 +++---
 docs/source/api/v3/servers_id.rst                  |  364 +++--
 docs/source/basics/content_delivery_networks.rst   |    2 +-
 docs/source/glossary.rst                           |   36 +-
 docs/source/overview/cache_groups.rst              |    4 +-
 docs/source/overview/delivery_services.rst         |   24 +-
 docs/source/overview/profiles_and_parameters.rst   |    4 +-
 docs/source/overview/traffic_portal.rst            |    2 +-
 docs/source/overview/traffic_router.rst            |    4 +-
 lib/go-tc/cachegroups.go                           |    1 -
 lib/go-tc/servers.go                               |  164 ++-
 lib/go-tc/servers_test.go                          |   58 +
 traffic_ops/client/capability.go                   |    2 +-
 traffic_ops/client/server.go                       |  222 ++-
 traffic_ops/ort/atstccfg/toreqnew/toreqnew.go      |    2 +-
 traffic_ops/testing/api/v1/atsconfig_test.go       |   81 +-
 .../testing/api/v1/ip_allow_dot_config_test.go     |  125 --
 traffic_ops/testing/api/v1/parentdotconfig_test.go |  154 ---
 traffic_ops/testing/api/v1/remapdotconfig_test.go  |  103 --
 .../testing/api/v1/serverupdatestatus_test.go      |    8 +-
 traffic_ops/testing/api/v1/todb_test.go            |    3 +-
 traffic_ops/testing/api/v2/todb_test.go            |    3 +-
 traffic_ops/testing/api/v3/cachegroups_test.go     |    1 +
 .../api/v3/cachegroupsdeliveryservices_test.go     |   12 +-
 traffic_ops/testing/api/v3/crconfig_test.go        |    9 +-
 .../deliveryservices_required_capabilities_test.go |   13 +-
 .../testing/api/v3/deliveryserviceservers_test.go  |   60 +-
 traffic_ops/testing/api/v3/servers_test.go         |  160 ++-
 .../servers_to_deliveryservice_assignment_test.go  |   74 +-
 .../testing/api/v3/serverservercapability_test.go  |   26 +-
 .../testing/api/v3/serverupdatestatus_test.go      |  188 ++-
 traffic_ops/testing/api/v3/tc-fixtures.json        |  470 +++++--
 traffic_ops/testing/api/v3/todb_test.go            |    3 +-
 traffic_ops/testing/api/v3/traffic_control_test.go |    2 +-
 traffic_ops/traffic_ops_golang/api/api.go          |   27 +-
 .../traffic_ops_golang/ats/atsserver/chkconfig.go  |    4 +-
 .../ats/atsserver/hostingdotconfig.go              |    2 +-
 .../ats/atsserver/ipallowdotconfig.go              |  148 --
 .../traffic_ops_golang/ats/atsserver/meta.go       |    4 +-
 .../traffic_ops_golang/ats/atsserver/packages.go   |    3 +-
 .../ats/atsserver/parentdotconfig.go               |  832 -----------
 .../ats/atsserver/remapdotconfig.go                |   93 --
 .../traffic_ops_golang/ats/atsserver/unknown.go    |    2 +-
 traffic_ops/traffic_ops_golang/ats/db.go           |   69 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |   46 +
 traffic_ops/traffic_ops_golang/routing/routes.go   |   30 +-
 traffic_ops/traffic_ops_golang/server/detail.go    |  139 +-
 .../traffic_ops_golang/server/detail_test.go       |   10 +-
 traffic_ops/traffic_ops_golang/server/servers.go   | 1441 ++++++++++++++------
 .../traffic_ops_golang/server/servers_test.go      |  381 +++++-
 traffic_ops/v1-client/capability.go                |    2 +-
 traffic_ops/v2-client/capability.go                |    2 +-
 traffic_stats/traffic_stats.go                     |    2 +-
 78 files changed, 3582 insertions(+), 3055 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e35fec..b2ea9ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,12 +38,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Changed ORT Config Generation to be deterministic, which will prevent spurious diffs when nothing actually changed.
 - Changed the access logs in Traffic Ops to now show the route ID with every API endpoint call. The Route ID is appended to the end of the access log line.
 - With the addition of multiple server interfaces, interface data is constructed from IP Address/Gateway/Netmask (and their IPv6 counterparts) and Interface Name and Interface MTU fields on services. These **MUST** have proper, valid data before attempting to upgrade or the upgrade **WILL** fail. In particular IP fields need to be valid IP addresses/netmasks, and MTU must only be positive integers of at least 1280.
+- The `/servers` and `/servers/{{ID}}}` API endpoints have been updated to use and reflect multi-interface servers.
 
 ### Deprecated
 - Deprecated the non-nullable `DeliveryService` Go struct and other structs that use it. `DeliveryServiceNullable` structs should be used instead.
 
 ### Removed
 - Removed deprecated Traffic Ops Go Client methods.
+- Configuration generation logic in the TO API (v1) for:
+  - `ip_allow.config`
+  - `parent.config`
+  - `remap.config`
 
 ## [4.1.0] - 2020-04-23
 ### Added
diff --git a/docs/source/admin/quick_howto/content_invalidation.rst b/docs/source/admin/quick_howto/content_invalidation.rst
index 98248eb..32d2aa2 100644
--- a/docs/source/admin/quick_howto/content_invalidation.rst
+++ b/docs/source/admin/quick_howto/content_invalidation.rst
@@ -18,11 +18,11 @@
 ****************************
 Forcing Content Invalidation
 ****************************
-Invalidating content on the CDN is sometimes necessary when the :term:`origin` was mis-configured and something is cached in the CDN  that needs to be removed.
+Invalidating content on the CDN is sometimes necessary when the :term:`Origin` was mis-configured and something is cached in the CDN  that needs to be removed.
 
 .. impl-detail:: Given the size of a typical Traffic Control CDN and the amount of content that can be cached in it, removing the content from all the caches may take a long time. To speed up content invalidation, Traffic Control does not try to remove the content from the caches, but it makes the content inaccessible using the `regex_revalidate plugin for Apache Traffic Server <https://docs.trafficserver.apache.org/en/8.0.x/admin-guide/plugins/regex_revalidate.en.html>`_. This forces a  [...]
 
-.. Note:: This method forces :term:`cache servers` to "re-validate" content, so in order to work properly the :term:`origin` needs to support revalidation according to section 13 of :rfc:`2616`.
+.. Note:: This method forces :term:`cache servers` to "re-validate" content, so in order to work properly the :term:`Origin` needs to support revalidation according to section 13 of :rfc:`2616`.
 
 To invalidate content for a specific :term:`Delivery Service`, follow these steps:
 
@@ -42,7 +42,7 @@ To invalidate content for a specific :term:`Delivery Service`, follow these step
 
 		Select 'Manage Invalidation Requests'
 
-#. Click/tap on the :guilabel:`+` button to open the submission form for a new content invalidation. Fill out this form. The "Path Regex" field should be a `PCRE <http://www.pcre.org/>`_-compatible regular expression that matches all content that must be invalidated - and should **not** match any content that must *not* be invalidated. "TTL (hours)" specifies the number of hours for which the invalidation should remain active. Best practice is to set this to the same as the content's cac [...]
+#. Click/tap on the :guilabel:`+` button to open the submission form for a new content invalidation. Fill out this form. The "Path Regex" field should be a `PCRE <http://www.pcre.org/>`_-compatible regular expression that matches all content that must be invalidated - and should **not** match any content that must *not* be invalidated. "TTL (hours)" specifies the number of hours for which the invalidation should remain active. Best practice is to set this to the same as the content's cac [...]
 
 	.. figure:: content_invalidation/03.png
 		:align: center
diff --git a/docs/source/admin/quick_howto/oauth_login.rst b/docs/source/admin/quick_howto/oauth_login.rst
index 4e76ce9..98a7d89 100644
--- a/docs/source/admin/quick_howto/oauth_login.rst
+++ b/docs/source/admin/quick_howto/oauth_login.rst
@@ -26,7 +26,7 @@ An opt-in configuration for SSO using OAuth is supported and can be configured t
 
 .. Note:: The ``POST`` from the API to the OAuth provider to exchange the code for a token expects the response to have the token in JSON format with `access_token` as the desired field (and can include other fields).  It also supports a response with just the token itself as the body.  Further development work will need to be done to allow other resposne forms or other response fields.
 
-.. Note:: Users must exist in both Traffic Ops as well as in the OAuth provider's system.  The user's rights are defined by the :term:`role` assigned to the user.
+.. Note:: Users must exist in both Traffic Ops as well as in the OAuth provider's system.  The user's rights are defined by the :term:`Role` assigned to the user.
 
 To configure OAuth login:
 
diff --git a/docs/source/admin/quick_howto/server_capabililty.rst b/docs/source/admin/quick_howto/server_capabililty.rst
index d124699..76ed2e0 100644
--- a/docs/source/admin/quick_howto/server_capabililty.rst
+++ b/docs/source/admin/quick_howto/server_capabililty.rst
@@ -18,9 +18,9 @@
 **************************
 Manage Server Capabilities
 **************************
-Server capabilities are designed to enable users with the Operations or Admin :term:`Role` to control the flow of :term:`delivery service` traffic through only the :term:`cache servers` (:term:`Edge` or :term:`Mid`) that have the required capabilities. For example, :term:`delivery services` designed to serve large binary files should only have requests routed to :term:`cache servers` with sufficient disk cache. Currently, this can be controlled at the :term:`Edge-tier` where system opera [...]
+Server capabilities are designed to enable users with the Operations or Admin :term:`Role` to control the flow of :term:`Delivery Service` traffic through only the :term:`cache servers` (:term:`Edge` or :term:`Mid`) that have the required capabilities. For example, :term:`Delivery Services` designed to serve large binary files should only have requests routed to :term:`cache servers` with sufficient disk cache. Currently, this can be controlled at the :term:`Edge-tier` where system opera [...]
 
-A list of the server capabilities can be found under :menuselection:`Configure --> Server Capabilities`. Users with a higher-level :term:`role` ("operations" or "admin") can create or delete server capabilities. Server capabilities can only be deleted if they are not currently being used by a :term:`cache server` or required by a :term:`delivery service`.
+A list of the server capabilities can be found under :menuselection:`Configure --> Server Capabilities`. Users with a higher-level :term:`Role` ("operations" or "admin") can create or delete server capabilities. Server capabilities can only be deleted if they are not currently being used by a :term:`cache server` or required by a :term:`Delivery Service`.
 
 .. figure:: server_capability/server_caps_table.png
 	:align: center
@@ -44,12 +44,12 @@ Users with the Operations or Admin :term:`Role` can associate one or more server
 
 Manage delivery service required server capabilities
 ====================================================
-Users with the Operations or Admin :term:`Role` can associate one or more required server capabilities with a :term:`delivery service` by navigating to a :term:`delivery service` via :menuselection:`Services --> Delivery Services` and using the context menu for the :term:`delivery services` table and selecting :menuselection:`Manage Required Server Capabilities` or by navigating to :menuselection:`Services --> Delivery Services --> Delivery Service --> More --> Manage Required Server Cap [...]
+Users with the Operations or Admin :term:`Role` can associate one or more required server capabilities with a :term:`Delivery Service` by navigating to a :term:`Delivery Service` via :menuselection:`Services --> Delivery Services` and using the context menu for the :term:`Delivery Services` table and selecting :menuselection:`Manage Required Server Capabilities` or by navigating to :menuselection:`Services --> Delivery Services --> Delivery Service --> More --> Manage Required Server Cap [...]
 
-Adding a required server capability to a :term:`delivery service` will ensure two things:
+Adding a required server capability to a :term:`Delivery Service` will ensure two things:
 
-1. Only :term:`Edge-tier caches` with the required capability can be assigned to the :term:`delivery service`
-2. Only :term:`Mid-tier caches` with the required capability will handle requests of the :term:`delivery service` (if applicable)
+1. Only :term:`Edge-tier caches` with the required capability can be assigned to the :term:`Delivery Service`
+2. Only :term:`Mid-tier caches` with the required capability will handle requests of the :term:`Delivery Service` (if applicable)
 
 .. figure:: server_capability/ds_server_caps_table.png
 	:align: center
diff --git a/docs/source/admin/traffic_portal/usingtrafficportal.rst b/docs/source/admin/traffic_portal/usingtrafficportal.rst
index 661d6ae..d77442a 100644
--- a/docs/source/admin/traffic_portal/usingtrafficportal.rst
+++ b/docs/source/admin/traffic_portal/usingtrafficportal.rst
@@ -418,33 +418,33 @@ Server management includes the ability to (where applicable):
 
 Origins
 -------
-A table of all :term:`origins`. These are automatically created for the :term:`origins` served by :term:`Delivery Services` throughout all CDNs, but additional ones can be created at will. The table has the following columns:
+A table of all :term:`Origins`. These are automatically created for the :term:`Origins` served by :term:`Delivery Services` throughout all CDNs, but additional ones can be created at will. The table has the following columns:
 
-:Name:             The name of the :term:`origin`. If this :term:`origin` was created automatically for a :term:`Delivery Service`, this will be the :ref:`ds-xmlid` of that :term:`Delivery Service`.
-:Tenant:           The name of the :term:`Tenant` that owns this :term:`origin` - this is not necessarily the same as the :term:`Tenant` that owns the :term:`Delivery Service` to which this :term:`origin` belongs.
-:Primary:          Either ``true`` to indicate that this is the "primary" :term:`origin` for the :term:`Delivery Service` to which it is assigned, or ``false`` otherwise.
-:Delivery Service: The :ref:`ds-xmlid` of the :term:`Delivery Service` to which this :term:`origin` is assigned.
-:FQDN:             The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin server`.
-:IPv4 Address:     The :term:`origin`'s IPv4 address, if configured.
-:IPv6 Address:     The :term:`origin`'s IPv6 address, if configured.
-:Protocol:         The protocol this :term:`origin` uses to serve content. One of
+:Name:             The name of the :term:`Origin`. If this :term:`Origin` was created automatically for a :term:`Delivery Service`, this will be the :ref:`ds-xmlid` of that :term:`Delivery Service`.
+:Tenant:           The name of the :term:`Tenant` that owns this :term:`Origin` - this is not necessarily the same as the :term:`Tenant` that owns the :term:`Delivery Service` to which this :term:`Origin` belongs.
+:Primary:          Either ``true`` to indicate that this is the "primary" :term:`Origin` for the :term:`Delivery Service` to which it is assigned, or ``false`` otherwise.
+:Delivery Service: The :ref:`ds-xmlid` of the :term:`Delivery Service` to which this :term:`Origin` is assigned.
+:FQDN:             The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`.
+:IPv4 Address:     The :term:`Origin`'s IPv4 address, if configured.
+:IPv6 Address:     The :term:`Origin`'s IPv6 address, if configured.
+:Protocol:         The protocol this :term:`Origin` uses to serve content. One of
 
 	- http
 	- https
 
-:Port: The port on which the :term:`origin server` listens for incoming HTTP(S) requests.
+:Port: The port on which the :term:`Origin` listens for incoming HTTP(S) requests.
 
-	.. note:: If this field appears blank in the table, it means that a default was chosen for the :term:`origin` based on its Protocol - ``80`` for "http", ``443`` for "https".
+	.. note:: If this field appears blank in the table, it means that a default was chosen for the :term:`Origin` based on its Protocol - ``80`` for "http", ``443`` for "https".
 
-:Coordinate: The name of the geographic coordinate pair that defines the physical location of this :term:`origin server`. :term:`Origins` created for :term:`Delivery Services` automatically will **not** have associated Coordinates. This can be rectified on the details pages for said :term:`origins`
-:Cachegroup: The :ref:`Name of the Cache Group <cache-group-name>` to which this :term:`origin` belongs, if any.
-:Profile:    The :ref:`profile-name` of a :term:`Profile` used by this :term:`origin`.
+:Coordinate: The name of the geographic coordinate pair that defines the physical location of this :term:`Origin`. :term:`Origins` created for :term:`Delivery Services` automatically will **not** have associated Coordinates. This can be rectified on the details pages for said :term:`Origins`
+:Cachegroup: The :ref:`Name of the Cache Group <cache-group-name>` to which this :term:`Origin` belongs, if any.
+:Profile:    The :ref:`profile-name` of a :term:`Profile` used by this :term:`Origin`.
 
 :term:`Origin` management includes the ability to (where applicable):
 
-- create a new :term:`origin`
-- update an existing :term:`origin`
-- delete an existing :term:`origin`
+- create a new :term:`Origin`
+- update an existing :term:`Origin`
+- delete an existing :term:`Origin`
 
 .. _tp-configure-profiles:
 
@@ -740,7 +740,7 @@ User management includes the ability to (where applicable):
 - update an existing user
 - view :term:`Delivery Services` visible to a user
 
-.. Note:: If OAuth is enabled, the username must exist both here as well as with the OAuth provider. A user's rights are defined by the :term:`role` assigned to the user in Traffic Ops. Creating/deleting a user here will update the user's :term:`role` but the user needs to be created/deleted with the OAuth provider as well.
+.. Note:: If OAuth is enabled, the username must exist both here as well as with the OAuth provider. A user's rights are defined by the :term:`Role` assigned to the user in Traffic Ops. Creating/deleting a user here will update the user's :term:`Role` but the user needs to be created/deleted with the OAuth provider as well.
 
 Tenants
 -------
diff --git a/docs/source/admin/traffic_router.rst b/docs/source/admin/traffic_router.rst
index beee5c5..1a6cd46 100644
--- a/docs/source/admin/traffic_router.rst
+++ b/docs/source/admin/traffic_router.rst
@@ -211,7 +211,7 @@ Much of a Traffic Router's configuration can be obtained through the :term:`Para
 	| deepcoveragezone.polling.url            | CRConfig.json                | The location (URL) where a :term:`Deep Coverage Zone Map` may be found.                                                               |
 	+-----------------------------------------+------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
 	| client.steering.forced.diversity        | CRConfig.json                | When this :term:`Parameter` exists and is exactly "true", it enables the "Client Steering Forced Diversity" feature to diversify      |
-	|                                         |                              | CLIENT_STEERING results by including more unique :term:`Edge-Tier Cache Servers` in the response to the client's request.             |
+	|                                         |                              | CLIENT_STEERING results by including more unique :term:`Edge-tier cache servers` in the response to the client's request.             |
 	+-----------------------------------------+------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
 	| tld.soa.expire                          | CRConfig.json                | The value for the "expire" field the Traffic Router DNS Server will respond with on :abbr:`SOA (Start of Authority)` records.         |
 	+-----------------------------------------+------------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
index 783eb2a..04f84d8 100644
--- a/docs/source/api/index.rst
+++ b/docs/source/api/index.rst
@@ -102,6 +102,17 @@ Object
 ``undefined``
 	No ``response`` object is present in the response payload. Unless the format is otherwise noted, this means that there should be no field list in the "Response Structure" subsection.
 
+Summary
+-------
+The top-level ``summary`` object is used to provide summary statistics about object collections. In general the use of ``summary`` is left to be defined by API endpoints (subject to some restrictions). When an endpoint uses the ``summary`` object, its fields will be explained in a subsection of the "Response Structure" named "Summary Fields".
+
+The following, reserved properties of ``summary`` are guaranteed to always have their herein-described meaning.
+
+.. _reserved-summary-fields:
+
+``count``
+	``count`` contains an unsigned integer that defines the total number of results that could possibly be returned given the non-pagination query parameters supplied by the client.
+
 Using API Endpoints
 ===================
 #. Authenticate with valid Traffic Control user account credentials (the same used by Traffic Portal).
diff --git a/docs/source/api/v1/cdns_name_snapshot.rst b/docs/source/api/v1/cdns_name_snapshot.rst
index 69214a2..746d41d 100644
--- a/docs/source/api/v1/cdns_name_snapshot.rst
+++ b/docs/source/api/v1/cdns_name_snapshot.rst
@@ -116,7 +116,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -186,9 +186,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v1/cdns_name_snapshot_new.rst b/docs/source/api/v1/cdns_name_snapshot_new.rst
index 289ec5a..479fd59 100644
--- a/docs/source/api/v1/cdns_name_snapshot_new.rst
+++ b/docs/source/api/v1/cdns_name_snapshot_new.rst
@@ -115,7 +115,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -185,9 +185,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v1/deliveryservices_id_servers_eligible.rst b/docs/source/api/v1/deliveryservices_id_servers_eligible.rst
index 73e6512..8357a37 100644
--- a/docs/source/api/v1/deliveryservices_id_servers_eligible.rst
+++ b/docs/source/api/v1/deliveryservices_id_servers_eligible.rst
@@ -23,7 +23,7 @@
 
 ``GET``
 =======
-Retrieves properties of :term:`Edge-Tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
+Retrieves properties of :term:`Edge-tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
 
 - The name of the server's :term:`Type` must match one of the glob patterns ``EDGE*``, ``ORG*``
 - The server and :term:`Delivery Service` must belong to the same CDN
diff --git a/docs/source/api/v1/origins.rst b/docs/source/api/v1/origins.rst
index 1ea43ce..df865b2 100644
--- a/docs/source/api/v1/origins.rst
+++ b/docs/source/api/v1/origins.rst
@@ -22,7 +22,7 @@
 
 ``GET``
 =======
-Gets all requested :term:`origins`.
+Gets all requested :term:`Origins`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -35,22 +35,22 @@ Request Structure
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| Name            | Required | Description                                                                                                                                                       |
 	+=================+==========+===================================================================================================================================================================+
-	| cachegroup      | no       | Return only :term:`origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
+	| cachegroup      | no       | Return only :term:`Origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| coordinate      | no       | Return only :term:`origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
+	| coordinate      | no       | Return only :term:`Origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| deliveryservice | no       | Return only :term:`origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
+	| deliveryservice | no       | Return only :term:`Origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| id              | no       | Return only the :term:`origin` that has this integral, unique identifier                                                                                          |
+	| id              | no       | Return only the :term:`Origin` that has this integral, unique identifier                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| name            | no       | Return only :term:`origins` by this name                                                                                                                          |
+	| name            | no       | Return only :term:`Origins` by this name                                                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| profileId       | no       | Return only :term:`origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
+	| profileId       | no       | Return only :term:`Origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| primary         | no       | If ``true``, return only :term:`origins` which are the the primary :term:`origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
-	|                 |          | :term:`origins` which are *not* the primary :term:`origin` of the :term:`Delivery Service` to which they belong                                                   |
+	| primary         | no       | If ``true``, return only :term:`Origins` which are the the primary :term:`Origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
+	|                 |          | :term:`Origins` which are *not* the primary :term:`Origin` of the :term:`Delivery Service` to which they belong                                                   |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| tenant          | no       | Return only :term:`origins` belonging to the tenant identified by this integral, unique identifier                                                                |
+	| tenant          | no       | Return only :term:`Origins` belonging to the tenant identified by this integral, unique identifier                                                                |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| orderby         | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                                                     |
 	|                 |          | array                                                                                                                                                             |
@@ -78,25 +78,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -149,21 +149,21 @@ Creates a new origin definition.
 
 Request Structure
 -----------------
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' :term:`origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' :term:`Origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 
 	.. note:: Though not specifying this field in this request will leave it as ``null`` in the output, Traffic Ops will silently coerce that to its default value: ``false``.
 
 :name:      A human-friendly name of the :term:`Origin`
-:port:      An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`origin`
+:port:      An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`Origin`
 :protocol:  The protocol used by the origin - must be one of 'http' or 'https'
-:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -189,25 +189,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -254,7 +254,7 @@ Response Structure
 
 ``PUT``
 =======
-Updates an :term:`origin` definition.
+Updates an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -267,21 +267,21 @@ Request Structure
 	+------+----------+-------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                   |
 	+======+==========+===============================================================================+
-	| id   | yes      | The integral, unique identifier of the :term:`origin` definition being edited |
+	| id   | yes      | The integral, unique identifier of the :term:`Origin` definition being edited |
 	+------+----------+-------------------------------------------------------------------------------+
 
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 :name:              A human-friendly name of the :term:`Origin`
-:port:              An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`origin`
-:protocol:          The protocol used by the :term:`origin` - must be one of 'http' or 'https'
-:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:port:              An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`Origin`
+:protocol:          The protocol used by the :term:`Origin` - must be one of 'http' or 'https'
+:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -307,25 +307,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -372,7 +372,7 @@ Response Structure
 
 ``DELETE``
 ==========
-Deletes an :term:`origin` definition.
+Deletes an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -385,7 +385,7 @@ Request Structure
 	+------+----------+--------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                    |
 	+======+==========+================================================================================+
-	|  id  | yes      | The integral, unique identifier of the :term:`origin` definition being deleted |
+	|  id  | yes      | The integral, unique identifier of the :term:`Origin` definition being deleted |
 	+------+----------+--------------------------------------------------------------------------------+
 
 .. code-block:: http
diff --git a/docs/source/api/v1/regions_name_phys_locations.rst b/docs/source/api/v1/regions_name_phys_locations.rst
index b8750d2..f770e2d 100644
--- a/docs/source/api/v1/regions_name_phys_locations.rst
+++ b/docs/source/api/v1/regions_name_phys_locations.rst
@@ -22,7 +22,7 @@
 
 ``POST``
 ========
-Creates a new :term:`Physical Location` within the specified :term:`region`.
+Creates a new :term:`Physical Location` within the specified :term:`Region`.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
diff --git a/docs/source/api/v1/server_server_capabilities.rst b/docs/source/api/v1/server_server_capabilities.rst
index ddd6167..8dbf7b1 100644
--- a/docs/source/api/v1/server_server_capabilities.rst
+++ b/docs/source/api/v1/server_server_capabilities.rst
@@ -23,7 +23,7 @@
 
 ``GET``
 =======
-Gets all associations of :term:`Server Capabilities` to :term:`Cache Servers`.
+Gets all associations of :term:`Server Capabilities` to :term:`cache servers`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -115,7 +115,7 @@ Request Structure
 :serverId:         The integral, unique identifier of a server to be associated with a :term:`Server Capability`
 :serverCapability: The :term:`Server Capability`'s name to associate
 
-.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier Cache Server`.
+.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier cache server`.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v1/servers_server_configfiles_ats.rst b/docs/source/api/v1/servers_server_configfiles_ats.rst
index 1bc98ea..2dc4f19 100644
--- a/docs/source/api/v1/servers_server_configfiles_ats.rst
+++ b/docs/source/api/v1/servers_server_configfiles_ats.rst
@@ -21,6 +21,9 @@
 
 .. seealso:: The :ref:`to-api-v1-servers-server-configfiles-ats-filename`, :ref:`to-api-v1-cdns-cdn-configfiles-ats-filename`, and :ref:`to-api-v1-profiles-profile-configfiles-ats-filename` endpoints.
 
+.. deprecated:: 1.4
+	Using the API to retrieve generated configuration files for servers is deprecated, and unavailable in more recent API versions. Also, in ATC version 4.x and higher, it is not guaranteed that configuration files will be output correctly, or even successfully. Instead, configuration file generation is now the responsibility of :ref:`atstccfg`.
+
 ``GET``
 =======
 Gets a list of the configuration files used by ``server``
diff --git a/docs/source/api/v1/servers_server_configfiles_ats_filename.rst b/docs/source/api/v1/servers_server_configfiles_ats_filename.rst
index 69c8849..045c440 100644
--- a/docs/source/api/v1/servers_server_configfiles_ats_filename.rst
+++ b/docs/source/api/v1/servers_server_configfiles_ats_filename.rst
@@ -21,6 +21,9 @@
 
 .. seealso:: The :ref:`to-api-v1-servers-server-configfiles-ats` endpoint
 
+.. deprecated:: 1.4
+	Using the API to retrieve generated configuration files for servers is deprecated, and unavailable in more recent API versions. Also, in ATC version 4.x and higher, it is not guaranteed that configuration files will be output correctly, or even successfully. Instead, configuration file generation is now the responsibility of :ref:`atstccfg`.
+
 ``GET``
 =======
 Returns the requested configuration file for download.
diff --git a/docs/source/api/v2/cdns_name_snapshot.rst b/docs/source/api/v2/cdns_name_snapshot.rst
index e82dd50..bcdc07b 100644
--- a/docs/source/api/v2/cdns_name_snapshot.rst
+++ b/docs/source/api/v2/cdns_name_snapshot.rst
@@ -116,7 +116,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -180,9 +180,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v2/cdns_name_snapshot_new.rst b/docs/source/api/v2/cdns_name_snapshot_new.rst
index ecb20e1..dfe2dbd 100644
--- a/docs/source/api/v2/cdns_name_snapshot_new.rst
+++ b/docs/source/api/v2/cdns_name_snapshot_new.rst
@@ -115,7 +115,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -179,9 +179,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v2/deliveryservices_id_servers_eligible.rst b/docs/source/api/v2/deliveryservices_id_servers_eligible.rst
index f623b0c..33727f5 100644
--- a/docs/source/api/v2/deliveryservices_id_servers_eligible.rst
+++ b/docs/source/api/v2/deliveryservices_id_servers_eligible.rst
@@ -23,7 +23,7 @@
 
 ``GET``
 =======
-Retrieves properties of :term:`Edge-Tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
+Retrieves properties of :term:`Edge-tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
 
 - The name of the server's :term:`Type` must match one of the glob patterns ``EDGE*``, ``ORG*``
 - The server and :term:`Delivery Service` must belong to the same CDN
diff --git a/docs/source/api/v2/origins.rst b/docs/source/api/v2/origins.rst
index 3a94af4..2aa998e 100644
--- a/docs/source/api/v2/origins.rst
+++ b/docs/source/api/v2/origins.rst
@@ -21,7 +21,7 @@
 
 ``GET``
 =======
-Gets all requested :term:`origins`.
+Gets all requested :term:`Origins`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -34,22 +34,22 @@ Request Structure
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| Name            | Required | Description                                                                                                                                                       |
 	+=================+==========+===================================================================================================================================================================+
-	| cachegroup      | no       | Return only :term:`origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
+	| cachegroup      | no       | Return only :term:`Origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| coordinate      | no       | Return only :term:`origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
+	| coordinate      | no       | Return only :term:`Origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| deliveryservice | no       | Return only :term:`origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
+	| deliveryservice | no       | Return only :term:`Origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| id              | no       | Return only the :term:`origin` that has this integral, unique identifier                                                                                          |
+	| id              | no       | Return only the :term:`Origin` that has this integral, unique identifier                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| name            | no       | Return only :term:`origins` by this name                                                                                                                          |
+	| name            | no       | Return only :term:`Origins` by this name                                                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| profileId       | no       | Return only :term:`origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
+	| profileId       | no       | Return only :term:`Origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| primary         | no       | If ``true``, return only :term:`origins` which are the the primary :term:`origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
-	|                 |          | :term:`origins` which are *not* the primary :term:`origin` of the :term:`Delivery Service` to which they belong                                                   |
+	| primary         | no       | If ``true``, return only :term:`Origins` which are the the primary :term:`Origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
+	|                 |          | :term:`Origins` which are *not* the primary :term:`Origin` of the :term:`Delivery Service` to which they belong                                                   |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| tenant          | no       | Return only :term:`origins` belonging to the tenant identified by this integral, unique identifier                                                                |
+	| tenant          | no       | Return only :term:`Origins` belonging to the tenant identified by this integral, unique identifier                                                                |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| orderby         | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                                                     |
 	|                 |          | array                                                                                                                                                             |
@@ -77,25 +77,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -148,21 +148,21 @@ Creates a new origin definition.
 
 Request Structure
 -----------------
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' :term:`origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' :term:`Origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 
 	.. note:: Though not specifying this field in this request will leave it as ``null`` in the output, Traffic Ops will silently coerce that to its default value: ``false``.
 
 :name:      A human-friendly name of the :term:`Origin`
-:port:      An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`origin`
+:port:      An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`Origin`
 :protocol:  The protocol used by the origin - must be one of 'http' or 'https'
-:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -188,25 +188,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -253,7 +253,7 @@ Response Structure
 
 ``PUT``
 =======
-Updates an :term:`origin` definition.
+Updates an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -266,21 +266,21 @@ Request Structure
 	+------+----------+-------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                   |
 	+======+==========+===============================================================================+
-	| id   | yes      | The integral, unique identifier of the :term:`origin` definition being edited |
+	| id   | yes      | The integral, unique identifier of the :term:`Origin` definition being edited |
 	+------+----------+-------------------------------------------------------------------------------+
 
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 :name:              A human-friendly name of the :term:`Origin`
-:port:              An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`origin`
-:protocol:          The protocol used by the :term:`origin` - must be one of 'http' or 'https'
-:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:port:              An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`Origin`
+:protocol:          The protocol used by the :term:`Origin` - must be one of 'http' or 'https'
+:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -306,25 +306,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -371,7 +371,7 @@ Response Structure
 
 ``DELETE``
 ==========
-Deletes an :term:`origin` definition.
+Deletes an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -384,7 +384,7 @@ Request Structure
 	+------+----------+--------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                    |
 	+======+==========+================================================================================+
-	|  id  | yes      | The integral, unique identifier of the :term:`origin` definition being deleted |
+	|  id  | yes      | The integral, unique identifier of the :term:`Origin` definition being deleted |
 	+------+----------+--------------------------------------------------------------------------------+
 
 .. code-block:: http
diff --git a/docs/source/api/v2/server_server_capabilities.rst b/docs/source/api/v2/server_server_capabilities.rst
index b3f10b3..3154538 100644
--- a/docs/source/api/v2/server_server_capabilities.rst
+++ b/docs/source/api/v2/server_server_capabilities.rst
@@ -21,7 +21,7 @@
 
 ``GET``
 =======
-Gets all associations of :term:`Server Capabilities` to :term:`Cache Servers`.
+Gets all associations of :term:`Server Capabilities` to :term:`cache servers`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -113,7 +113,7 @@ Request Structure
 :serverId:         The integral, unique identifier of a server to be associated with a :term:`Server Capability`
 :serverCapability: The :term:`Server Capability`'s name to associate
 
-.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier Cache Server`.
+.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier cache server`.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v3/cdns_name_snapshot.rst b/docs/source/api/v3/cdns_name_snapshot.rst
index c93af85..942f32d 100644
--- a/docs/source/api/v3/cdns_name_snapshot.rst
+++ b/docs/source/api/v3/cdns_name_snapshot.rst
@@ -116,7 +116,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -180,9 +180,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v3/cdns_name_snapshot_new.rst b/docs/source/api/v3/cdns_name_snapshot_new.rst
index e722d2d..0920421 100644
--- a/docs/source/api/v3/cdns_name_snapshot_new.rst
+++ b/docs/source/api/v3/cdns_name_snapshot_new.rst
@@ -115,7 +115,7 @@ Response Structure
 
 		.. seealso:: :ref:`health-proto`
 
-:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-Tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
+:contentServers: An object containing keys which are the (short) hostnames of the :term:`Edge-tier cache servers` in the CDN; the values corresponding to those keys are routing information for said servers
 
 	:cacheGroup:       A string that is the :ref:`cache-group-name` of the :term:`Cache Group` to which the server belongs
 	:deliveryServices: An object containing keys which are the names of :term:`Delivery Services` to which this :term:`cache server` is assigned; the values corresponding to those keys are arrays of :abbr:`FQDNs (Fully Qualified Domain Names)` that resolve to this :term:`cache server`
@@ -179,9 +179,9 @@ Response Structure
 		:shuffled: A string containing a boolean that tells whether the :term:`cache servers` chosen for content dispersion are chosen randomly or based on a consistent hash of the request URL; one of:
 
 			"false"
-				:term:`Cache servers` will be chosen consistently
+				:term:`cache servers` will be chosen consistently
 			"true"
-				:term:`Cache servers` will be chosen at random
+				:term:`cache servers` will be chosen at random
 
 	:domains:             An array of domains served by this :term:`Delivery Service`
 	:ecsEnabled:          A string containing a boolean from :ref:`ds-ecs` that tells whether EDNS0 client subnet is enabled on this :term:`Delivery Service`; one of:
diff --git a/docs/source/api/v3/deliveryservices_id_servers_eligible.rst b/docs/source/api/v3/deliveryservices_id_servers_eligible.rst
index 8cef711..f6bd15b 100644
--- a/docs/source/api/v3/deliveryservices_id_servers_eligible.rst
+++ b/docs/source/api/v3/deliveryservices_id_servers_eligible.rst
@@ -23,7 +23,7 @@
 
 ``GET``
 =======
-Retrieves properties of :term:`Edge-Tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
+Retrieves properties of :term:`Edge-tier cache servers` eligible for assignment to a particular :term:`Delivery Service`. Eligibility is determined based on the following properties:
 
 - The name of the server's :term:`Type` must match one of the glob patterns ``EDGE*``, ``ORG*``
 - The server and :term:`Delivery Service` must belong to the same CDN
diff --git a/docs/source/api/v3/origins.rst b/docs/source/api/v3/origins.rst
index 650b8d5..175abc6 100644
--- a/docs/source/api/v3/origins.rst
+++ b/docs/source/api/v3/origins.rst
@@ -21,7 +21,7 @@
 
 ``GET``
 =======
-Gets all requested :term:`origins`.
+Gets all requested :term:`Origins`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -34,22 +34,22 @@ Request Structure
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| Name            | Required | Description                                                                                                                                                       |
 	+=================+==========+===================================================================================================================================================================+
-	| cachegroup      | no       | Return only :term:`origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
+	| cachegroup      | no       | Return only :term:`Origins` within the :term:`Cache Group` that has this :ref:`cache-group-id`                                                                    |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| coordinate      | no       | Return only :term:`origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
+	| coordinate      | no       | Return only :term:`Origins` located at the geographic coordinates identified by this integral, unique identifier                                                  |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| deliveryservice | no       | Return only :term:`origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
+	| deliveryservice | no       | Return only :term:`Origins` that belong to the :term:`Delivery Service` identified by this integral, unique identifier                                            |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| id              | no       | Return only the :term:`origin` that has this integral, unique identifier                                                                                          |
+	| id              | no       | Return only the :term:`Origin` that has this integral, unique identifier                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| name            | no       | Return only :term:`origins` by this name                                                                                                                          |
+	| name            | no       | Return only :term:`Origins` by this name                                                                                                                          |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| profileId       | no       | Return only :term:`origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
+	| profileId       | no       | Return only :term:`Origins` which use the :term:`Profile` that has this :ref:`profile-id`                                                                         |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| primary         | no       | If ``true``, return only :term:`origins` which are the the primary :term:`origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
-	|                 |          | :term:`origins` which are *not* the primary :term:`origin` of the :term:`Delivery Service` to which they belong                                                   |
+	| primary         | no       | If ``true``, return only :term:`Origins` which are the the primary :term:`Origin` of the :term:`Delivery Service` to which they belong - if ``false`` return only |
+	|                 |          | :term:`Origins` which are *not* the primary :term:`Origin` of the :term:`Delivery Service` to which they belong                                                   |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
-	| tenant          | no       | Return only :term:`origins` belonging to the tenant identified by this integral, unique identifier                                                                |
+	| tenant          | no       | Return only :term:`Origins` belonging to the tenant identified by this integral, unique identifier                                                                |
 	+-----------------+----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 	| orderby         | no       | Choose the ordering of the results - must be the name of one of the fields of the objects in the ``response``                                                     |
 	|                 |          | array                                                                                                                                                             |
@@ -77,25 +77,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   A string that is the :ref:`ds-xmlid` of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` of the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -148,21 +148,21 @@ Creates a new origin definition.
 
 Request Structure
 -----------------
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' :term:`origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the new :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' :term:`Origin` served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 
 	.. note:: Though not specifying this field in this request will leave it as ``null`` in the output, Traffic Ops will silently coerce that to its default value: ``false``.
 
 :name:      A human-friendly name of the :term:`Origin`
-:port:      An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`origin`
+:port:      An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId: An optional :ref:`profile-id` ofa :term:`Profile` that shall be used by this :term:`Origin`
 :protocol:  The protocol used by the origin - must be one of 'http' or 'https'
-:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:tenantId:  An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -188,25 +188,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -253,7 +253,7 @@ Response Structure
 
 ``PUT``
 =======
-Updates an :term:`origin` definition.
+Updates an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -266,21 +266,21 @@ Request Structure
 	+------+----------+-------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                   |
 	+======+==========+===============================================================================+
-	| id   | yes      | The integral, unique identifier of the :term:`origin` definition being edited |
+	| id   | yes      | The integral, unique identifier of the :term:`Origin` definition being edited |
 	+------+----------+-------------------------------------------------------------------------------+
 
-:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`origin` shall belong
-:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`origin`'s geographic location
-:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`origin` shall belong
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:ip6Address:        An optional string containing the IPv6 address of the :term:`origin`
-:ipAddress:         An optional string containing the IPv4 address of the :term:`origin`
-:isPrimary:         An optional boolean which, if ``true`` will set this :term:`origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
+:cachegroupId:      An optional, integer which, if present, should be the :ref:`Cache Group ID <cache-group-id>` that identifies a :term:`Cache Group` to which the new :term:`Origin` shall belong
+:coordinateId:      An optional, integral, unique identifier of a coordinate pair that shall define the :term:`Origin`'s geographic location
+:deliveryServiceId: The integral, unique identifier of the :term:`Delivery Service` to which the :term:`Origin` shall belong
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:ip6Address:        An optional string containing the IPv6 address of the :term:`Origin`
+:ipAddress:         An optional string containing the IPv4 address of the :term:`Origin`
+:isPrimary:         An optional boolean which, if ``true`` will set this :term:`Origin` as the 'primary' origin served by the :term:`Delivery Service` identified by ``deliveryServiceID``
 :name:              A human-friendly name of the :term:`Origin`
-:port:              An optional port number on which the :term:`origin` listens for incoming TCP connections
-:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`origin`
-:protocol:          The protocol used by the :term:`origin` - must be one of 'http' or 'https'
-:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`origin`
+:port:              An optional port number on which the :term:`Origin` listens for incoming TCP connections
+:profileId:         An optional :ref:`profile-id` of the :term:`Profile` that shall be used by this :term:`Origin`
+:protocol:          The protocol used by the :term:`Origin` - must be one of 'http' or 'https'
+:tenantId:          An optional\ [1]_, integral, unique identifier for the :term:`Tenant` which shall own the new :term:`Origin`
 
 .. code-block:: http
 	:caption: Request Example
@@ -306,25 +306,25 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`origin` belongs
-:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`origin` belongs
+:cachegroup:        A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the :term:`Origin` belongs
+:cachegroupId:      An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the :term:`Origin` belongs
 :coordinate:        The name of a coordinate pair that defines the origin's geographic location
-:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`origin`'s geographic location
-:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`origin` belongs
-:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`origin` belongs
-:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`origin`
-:id:                An integral, unique identifier for this :term:`origin`
+:coordinateId:      An integral, unique identifier for the coordinate pair that defines the :term:`Origin`'s geographic location
+:deliveryService:   The 'xml_id' of the :term:`Delivery Service` to which the :term:`Origin` belongs
+:deliveryServiceId: An integral, unique identifier for the :term:`Delivery Service` to which the :term:`Origin` belongs
+:fqdn:              The :abbr:`FQDN (Fully Qualified Domain Name)` of the :term:`Origin`
+:id:                An integral, unique identifier for this :term:`Origin`
 :ip6Address:        The IPv6 address of the :term:`Origin`
 :ipAddress:         The IPv4 address of the :term:`Origin`
-:isPrimary:         A boolean value which, when ``true`` specifies this :term:`origin` as the 'primary' :term:`origin` served by ``deliveryService``
-:lastUpdated:       The date and time at which this :term:`origin` was last modified
-:name:              The name of the :term:`origin`
-:port:              The TCP port on which the :term:`origin` listens
-:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`origin`
-:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`origin`
+:isPrimary:         A boolean value which, when ``true`` specifies this :term:`Origin` as the 'primary' :term:`Origin` served by ``deliveryService``
+:lastUpdated:       The date and time at which this :term:`Origin` was last modified
+:name:              The name of the :term:`Origin`
+:port:              The TCP port on which the :term:`Origin` listens
+:profile:           The :ref:`profile-name` of the :term:`Profile` used by this :term:`Origin`
+:profileId:         The :ref:`profile-id` the :term:`Profile` used by this :term:`Origin`
 :protocol:          The protocol used by this origin - will be one of 'http' or 'https'
-:tenant:            The name of the :term:`Tenant` that owns this :term:`origin`
-:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`origin`
+:tenant:            The name of the :term:`Tenant` that owns this :term:`Origin`
+:tenantId:          An integral, unique identifier for the :term:`Tenant` that owns this :term:`Origin`
 
 .. code-block:: http
 	:caption: Response Example
@@ -371,7 +371,7 @@ Response Structure
 
 ``DELETE``
 ==========
-Deletes an :term:`origin` definition.
+Deletes an :term:`Origin` definition.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
@@ -384,7 +384,7 @@ Request Structure
 	+------+----------+--------------------------------------------------------------------------------+
 	| Name | Required | Description                                                                    |
 	+======+==========+================================================================================+
-	|  id  | yes      | The integral, unique identifier of the :term:`origin` definition being deleted |
+	|  id  | yes      | The integral, unique identifier of the :term:`Origin` definition being deleted |
 	+------+----------+--------------------------------------------------------------------------------+
 
 .. code-block:: http
diff --git a/docs/source/api/v3/server_server_capabilities.rst b/docs/source/api/v3/server_server_capabilities.rst
index 81ef556..f3ea276 100644
--- a/docs/source/api/v3/server_server_capabilities.rst
+++ b/docs/source/api/v3/server_server_capabilities.rst
@@ -21,7 +21,7 @@
 
 ``GET``
 =======
-Gets all associations of :term:`Server Capabilities` to :term:`Cache Servers`.
+Gets all associations of :term:`Server Capabilities` to :term:`cache servers`.
 
 :Auth. Required: Yes
 :Roles Required: None
@@ -113,7 +113,7 @@ Request Structure
 :serverId:         The integral, unique identifier of a server to be associated with a :term:`Server Capability`
 :serverCapability: The :term:`Server Capability`'s name to associate
 
-.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier Cache Server`.
+.. note:: The server referenced must be either an :term:`Edge-tier` or :term:`Mid-tier cache server`.
 
 .. code-block:: http
 	:caption: Request Example
diff --git a/docs/source/api/v3/servers.rst b/docs/source/api/v3/servers.rst
index 508ad99..90cf54a 100644
--- a/docs/source/api/v3/servers.rst
+++ b/docs/source/api/v3/servers.rst
@@ -70,36 +70,55 @@ Request Structure
 
 Response Structure
 ------------------
-:cachegroup:     A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
-:cachegroupId:   An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
-:cdnId:          The integral, unique identifier of the CDN to which the server belongs
-:cdnName:        Name of the CDN to which the server belongs
-:domainName:     The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
-:guid:           An identifier used to uniquely identify the server
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
+:cdnId:        The integral, unique identifier of the CDN to which the server belongs
+:cdnName:      Name of the CDN to which the server belongs
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
 
 	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
 
-:hostName:       The (short) hostname of the server
-:httpsPort:      The port on which the server listens for incoming HTTPS connections/requests
-:id:             An integral, unique identifier for this server
-:iloIpAddress:   The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpGateway:   The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
-:iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:interfaceMtu:   The :abbr:`MTU (Maximum Transmission Unit)` configured on ``interfaceName``
-:interfaceName:  The name of the primary network interface used by the server
-:ip6Address:     The IPv6 address and subnet mask of ``interfaceName``
-:ip6IsService:   A boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.
-:ip6Gateway:     The IPv6 address of the gateway used by ``interfaceName``
-:ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:    A boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.
-:ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
-:ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
-:lastUpdated:    The date and time at which this server description was last modified
-:mgmtIpAddress:  The IPv4 address of some network interface on the server used for 'management'
-:mgmtIpGateway:  The IPv4 address of a gateway used by some network interface on the server used for 'management'
-:mgmtIpNetmask:  The IPv4 subnet mask used by some network interface on the server used for 'management'
+:hostName:     The (short) hostname of the server
+:httpsPort:    The port on which the server listens for incoming HTTPS connections/requests
+:id:           An integral, unique identifier for this server
+:iloIpAddress: The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway: The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask: The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
+:iloUsername:  The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+
+	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address". It is illegal for a server to not have at least one service address. It is also illegal for a server to have more than one service address of the same address family (i.e. more than one IPv4 service address and/or more than one IPv6 address). Finally, all service addresses for a server m [...]
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors will consider the entire server unhealthy - which includes *all* configured network interfaces. If this is ``null``, it has the meaning "no limit". It has no effect if ``monitor`` is not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should be monitored by Traffic Monitor for statistics and health consideration.
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information is either not available or not applicable for this interface.
+	:name:    The name of the interface. No two interfaces of the same server may share a name. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:lastUpdated:   The date and time at which this server description was last modified
+:mgmtIpAddress: The IPv4 address of some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
 :offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
 :physLocation:   The name of the physical location where the server resides
 :physLocationId: An integral, unique identifier for the physical location where the server resides
@@ -132,66 +151,74 @@ Response Structure
 	:caption: Response Example
 
 	HTTP/1.1 200 OK
-	Access-Control-Allow-Credentials: true
-	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
-	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
-	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: WyapQctUIhjzEALka5QbBiZRZ58Mlc6MJSwjBeGyJS2UzbL3W6lN/4kvAZtPrP4qMWQBWz6JjbF7Y5lNRASUmQ==
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 19 May 2020 17:06:25 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Dec 2018 16:13:31 GMT
-	Content-Length: 939
+	Date: Tue, 19 May 2020 16:06:25 GMT
+	Content-Length: 538
 
-	{ "response": [
-		{
-			"cachegroup": "CDN_in_a_Box_Mid",
-			"cachegroupId": 6,
-			"cdnId": 2,
-			"cdnName": "CDN-in-a-Box",
-			"domainName": "infra.ciab.test",
-			"guid": null,
-			"hostName": "mid",
-			"httpsPort": 443,
-			"id": 10,
-			"iloIpAddress": "",
-			"iloIpGateway": "",
-			"iloIpNetmask": "",
-			"iloPassword": "",
-			"iloUsername": "",
-			"interfaceMtu": 1500,
-			"interfaceName": "eth0",
-			"ip6Address": "fc01:9400:1000:8::120",
-			"ip6Gateway": "fc01:9400:1000:8::1",
-			"ipAddress": "172.16.239.120",
-			"ipGateway": "172.16.239.1",
-			"ipNetmask": "255.255.255.0",
-			"lastUpdated": "2018-12-05 18:45:05+00",
-			"mgmtIpAddress": "",
-			"mgmtIpGateway": "",
-			"mgmtIpNetmask": "",
-			"offlineReason": "",
-			"physLocation": "Apachecon North America 2018",
-			"physLocationId": 1,
-			"profile": "ATS_MID_TIER_CACHE",
-			"profileDesc": "Mid Cache - Apache Traffic Server",
-			"profileId": 10,
-			"rack": "",
-			"revalPending": false,
-			"routerHostName": "",
-			"routerPortName": "",
-			"status": "REPORTED",
-			"statusId": 3,
-			"tcpPort": 80,
-			"type": "MID",
-			"typeId": 12,
-			"updPending": false,
-			"xmppId": "mid",
-			"xmppPasswd": "",
-			"ipIsService": true,
-			"ip6IsService": true
-		}
-	]}
+	{ "response": [{
+		"cachegroup": "CDN_in_a_Box_Mid",
+		"cachegroupId": 6,
+		"cdnId": 2,
+		"cdnName": "CDN-in-a-Box",
+		"domainName": "infra.ciab.test",
+		"guid": null,
+		"hostName": "mid",
+		"httpsPort": 443,
+		"id": 12,
+		"iloIpAddress": "",
+		"iloIpGateway": "",
+		"iloIpNetmask": "",
+		"iloPassword": "",
+		"iloUsername": "",
+		"lastUpdated": "2020-05-19 14:49:39+00",
+		"mgmtIpAddress": "",
+		"mgmtIpGateway": "",
+		"mgmtIpNetmask": "",
+		"offlineReason": "",
+		"physLocation": "Apachecon North America 2018",
+		"physLocationId": 1,
+		"profile": "ATS_MID_TIER_CACHE",
+		"profileDesc": "Mid Cache - Apache Traffic Server",
+		"profileId": 10,
+		"rack": "",
+		"revalPending": false,
+		"routerHostName": "",
+		"routerPortName": "",
+		"status": "REPORTED",
+		"statusId": 3,
+		"tcpPort": 80,
+		"type": "MID",
+		"typeId": 12,
+		"updPending": false,
+		"xmppId": "",
+		"xmppPasswd": "",
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "172.26.0.4/16",
+						"gateway": "172.26.0.1",
+						"serviceAddress": true
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": false,
+				"mtu": 1500,
+				"name": "eth0"
+			}
+		]
+	}],
+	"summary": {
+		"count": 13
+	}}
+
+Summary Fields
+""""""""""""""
+The ``summary`` object returned by this method of this endpoint uses only the ``count`` :ref:`standard property <reserved-summary-fields>`.
 
 ``POST``
 ========
@@ -208,26 +235,42 @@ Request Structure
 :domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
 :hostName:     The (short) hostname of the server
 :httpsPort:    An optional port number on which the server listens for incoming HTTPS connections/requests
-:iloIpAddress: An optional IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpGateway: An optional IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpNetmask: An optional IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
-:iloUsername:  An optional string containing the user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:interfaceMtu: The :abbr:`MTU (Maximum Transmission Unit)` configured on ``interfaceName``
-
-	.. note:: In virtually all cases this ought to be 1500. Further note that the only acceptable values are 1500 and 9000.
-
-:interfaceName:  The name of the primary network interface used by the server
-:ip6Address:     An optional IPv6 address and subnet mask of ``interfaceName``
-:ip6IsService:   An optional boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.  Defaults to ``true``.
-:ip6Gateway:     An optional IPv6 address of the gateway used by ``interfaceName``
-:ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:    An optional boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.  Defaults to ``true``.
-:ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
-:ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
-:mgmtIpAddress:  An optional IPv4 address of some network interface on the server used for 'management'
-:mgmtIpGateway:  An optional IPv4 address of a gateway used by some network interface on the server used for 'management'
-:mgmtIpNetmask:  An optional IPv4 subnet mask used by some network interface on the server used for 'management'
+:iloIpAddress: An optional IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway: An optional IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask: An optional IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
+:iloUsername:  An optional string containing the user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be necessary, but it is illegal for this set to be an empty collection.
+
+	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be necessary, but it is illegal for this set to be an empty collection.
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address". It is illegal for a server to not have at least one service address. It is also illegal for a server to have more than one service address of the same address family (i.e. more than one IPv4 service address and/or more than one IPv6 address). Finally, all service addresses for a server m [...]
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors will consider the entire server unhealthy - which includes *all* configured network interfaces. If this is ``null``, it has the meaning "no limit". It has no effect if ``monitor`` is not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should be monitored by Traffic Monitor for statistics and health consideration.
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information is either not available or not applicable for this interface. This unsigned integer may not be less than 1280.
+	:name:    The name of the interface. No two interfaces of the same server may share a name. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:mgmtIpAddress: The IPv4 address of some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
 :physLocationId: An integral, unique identifier for the physical location where the server resides
 :profileId:      The :ref:`profile-id` the :term:`Profile` that shall be used by this server
 :revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
@@ -269,6 +312,26 @@ Request Structure
 		"iloIpNetmask": "",
 		"iloPassword": "",
 		"iloUsername": "",
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "::1",
+						"gateway": "::2",
+						"serviceAddress": true
+					},
+					{
+						"address": "0.0.0.1/24",
+						"gateway": "0.0.0.2",
+						"serviceAddress": true
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": true,
+				"mtu": 1500,
+				"name": "eth0"
+			}
+		],
 		"interfaceMtu": 1500,
 		"interfaceName": "eth0",
 		"ip6Address": "::1",
@@ -287,9 +350,7 @@ Request Structure
 		"statusId": 3,
 		"tcpPort": 80,
 		"typeId": 12,
-		"updPending": false,
-		"ipIsService": true,
-		"ip6IsService": true
+		"updPending": false
 	}
 
 Response Structure
@@ -306,24 +367,43 @@ Response Structure
 :hostName:       The (short) hostname of the server
 :httpsPort:      The port on which the server listens for incoming HTTPS connections/requests
 :id:             An integral, unique identifier for this server
-:iloIpAddress:   The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpGateway:   The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
-:iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:interfaceMtu:   The :abbr:`MTU (Maximum Transmission Unit)` configured on ``interfaceName``
-:interfaceName:  The name of the primary network interface used by the server
-:ip6Address:     The IPv6 address and subnet mask of ``interfaceName``
-:ip6IsService:   A boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.
-:ip6Gateway:     The IPv6 address of the gateway used by ``interfaceName``
-:ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:   A boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.
-:ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
-:ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
-:lastUpdated:    The date and time at which this server description was last modified
-:mgmtIpAddress:  The IPv4 address of some network interface on the server used for 'management'
-:mgmtIpGateway:  The IPv4 address of a gateway used by some network interface on the server used for 'management'
-:mgmtIpNetmask:  The IPv4 subnet mask used by some network interface on the server used for 'management'
+:iloIpAddress:   The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway:   The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
+:iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+
+	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address". It is illegal for a server to not have at least one service address. It is also illegal for a server to have more than one service address of the same address family (i.e. more than one IPv4 service address and/or more than one IPv6 address). Finally, all service addresses for a server m [...]
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors will consider the entire server unhealthy - which includes *all* configured network interfaces. If this is ``null``, it has the meaning "no limit". It has no effect if ``monitor`` is not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should be monitored by Traffic Monitor for statistics and health consideration.
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information is either not available or not applicable for this interface.
+	:name:    The name of the interface. No two interfaces of the same server may share a name. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:lastUpdated:   The date and time at which this server description was last modified
+:mgmtIpAddress: The IPv4 address of some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
 :offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
 :physLocation:   The name of the :term:`Physical Location` where the server resides
 :physLocationId: An integral, unique identifier for the :term:`Physical Location` where the server resides
@@ -355,70 +435,78 @@ Response Structure
 .. code-block:: http
 	:caption: Response Example
 
-	HTTP/1.1 200 OK
-	Access-Control-Allow-Credentials: true
-	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
-	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
-	Access-Control-Allow-Origin: *
+	HTTP/1.1 201 Created
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: mcGmmu5ONDg3jmvlkItcw6jxiT1ecmePYujZfmKiZrn5ThKjsSadeJIynaeOK0XVUjHuYHdtdynSqxr2rdzEyA==
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 19 May 2020 17:34:40 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Dec 2018 17:44:04 GMT
-	Content-Length: 850
+	Date: Tue, 19 May 2020 16:34:40 GMT
+	Content-Length: 562
 
 	{ "alerts": [
 		{
-			"text": "server was created.",
+			"text": "Server created",
 			"level": "success"
 		}
 	],
 	"response": {
-		"cachegroup": null,
+		"cachegroup": "CDN_in_a_Box_Mid",
 		"cachegroupId": 6,
 		"cdnId": 2,
-		"cdnName": null,
+		"cdnName": "CDN-in-a-Box",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "test",
 		"httpsPort": 443,
-		"id": 13,
+		"id": 14,
 		"iloIpAddress": "",
 		"iloIpGateway": "",
 		"iloIpNetmask": "",
 		"iloPassword": "",
 		"iloUsername": "",
-		"interfaceMtu": 1500,
-		"interfaceName": "eth0",
-		"ip6Address": "::1",
-		"ip6Gateway": "::2",
-		"ipAddress": "0.0.0.1",
-		"ipGateway": "0.0.0.2",
-		"ipNetmask": "255.255.255.0",
-		"lastUpdated": "2018-12-10 17:44:04+00",
+		"lastUpdated": "2020-05-19 16:34:40+00",
 		"mgmtIpAddress": "",
 		"mgmtIpGateway": "",
 		"mgmtIpNetmask": "",
 		"offlineReason": "",
-		"physLocation": null,
+		"physLocation": "Apachecon North America 2018",
 		"physLocationId": 1,
-		"profile": null,
-		"profileDesc": null,
+		"profile": "ATS_MID_TIER_CACHE",
+		"profileDesc": "Mid Cache - Apache Traffic Server",
 		"profileId": 10,
 		"rack": null,
-		"revalPending": null,
+		"revalPending": false,
 		"routerHostName": "",
 		"routerPortName": "",
-		"status": null,
+		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
-		"type": "",
+		"type": "MID",
 		"typeId": 12,
 		"updPending": false,
-		"xmppId": "test",
+		"xmppId": null,
 		"xmppPasswd": null,
-		"ipIsService": true,
-		"ip6IsService": true
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "::1",
+						"gateway": "::2",
+						"serviceAddress": true
+					},
+					{
+						"address": "0.0.0.1/24",
+						"gateway": "0.0.0.2",
+						"serviceAddress": true
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": true,
+				"mtu": 1500,
+				"name": "eth0"
+			}
+		]
 	}}
 
-.. [1] For more information see the `Wikipedia page on Lights-Out management <https://en.wikipedia.org/wiki/Out-of-band_management>`_\ .
+.. [#ilo] For more information see the `Wikipedia page on Lights-Out management <https://en.wikipedia.org/wiki/Out-of-band_management>`_\ .
diff --git a/docs/source/api/v3/servers_id.rst b/docs/source/api/v3/servers_id.rst
index c01f468..041eb6e 100644
--- a/docs/source/api/v3/servers_id.rst
+++ b/docs/source/api/v3/servers_id.rst
@@ -42,26 +42,42 @@ Request Structure
 :domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
 :hostName:     The (short) hostname of the server
 :httpsPort:    An optional port number on which the server listens for incoming HTTPS connections/requests
-:iloIpAddress: An optional IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpGateway: An optional IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloIpNetmask: An optional IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
-:iloUsername:  An optional string containing the user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [1]_
-:interfaceMtu: The :abbr:`MTU (Maximum Transmission Unit)` configured on ``interfaceName``
-
-	.. note:: In virtually all cases this ought to be 1500. Further note that the only acceptable values are 1500 and 9000.
-
-:interfaceName:  The name of the primary network interface used by the server
-:ip6Address:     An optional IPv6 address and subnet mask of ``interfaceName``
-:ip6IsService:   An optional boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.  Defaults to ``true``.
-:ip6Gateway:     An optional IPv6 address of the gateway used by ``interfaceName``
-:ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:    An optional boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.  Defaults to ``true``.
-:ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
-:ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
-:mgmtIpAddress:  An optional IPv4 address of some network interface on the server used for 'management'
-:mgmtIpGateway:  An optional IPv4 address of a gateway used by some network interface on the server used for 'management'
-:mgmtIpNetmask:  An optional IPv4 subnet mask used by some network interface on the server used for 'management'
+:iloIpAddress: An optional IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway: An optional IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask: An optional IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:  An optional string containing the password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
+:iloUsername:  An optional string containing the user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be necessary, but it is illegal for this set to be an empty collection.
+
+	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be necessary, but it is illegal for this set to be an empty collection.
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address". It is illegal for a server to not have at least one service address. It is also illegal for a server to have more than one service address of the same address family (i.e. more than one IPv4 service address and/or more than one IPv6 address). Finally, all service addresses for a server m [...]
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors will consider the entire server unhealthy - which includes *all* configured network interfaces. If this is ``null``, it has the meaning "no limit". It has no effect if ``monitor`` is not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should be monitored by Traffic Monitor for statistics and health consideration.
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information is either not available or not applicable for this interface. This unsigned integer must not be less than 1280.
+	:name:    The name of the interface. No two interfaces of the same server may share a name. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:mgmtIpAddress: The IPv4 address of some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
 :physLocationId: An integral, unique identifier for the physical location where the server resides
 :profileId:      The :ref:`profile-id` the :term:`Profile` that shall be used by this server
 :revalPending:   A boolean value which, if ``true`` indicates that this server has pending content invalidation/revalidation
@@ -84,7 +100,7 @@ Request Structure
 .. code-block:: http
 	:caption: Request Example
 
-	PUT /api/3.0/servers/13 HTTP/1.1
+	PUT /api/3.0/servers/14 HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -103,6 +119,26 @@ Request Structure
 		"iloIpNetmask": "",
 		"iloPassword": "",
 		"iloUsername": "",
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "::1",
+						"gateway": "::2",
+						"serviceAddress": true
+					},
+					{
+						"address": "0.0.0.1/24",
+						"gateway": "0.0.0.2",
+						"serviceAddress": false
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": true,
+				"mtu": 1500,
+				"name": "bond0"
+			}
+		],
 		"interfaceMtu": 1500,
 		"interfaceName": "eth0",
 		"ip6Address": "::1",
@@ -121,9 +157,7 @@ Request Structure
 		"statusId": 3,
 		"tcpPort": 80,
 		"typeId": 12,
-		"updPending": true,
-		"ipIsService": true,
-		"ip6IsService": true
+		"updPending": false
 	}
 
 Response Structure
@@ -140,27 +174,46 @@ Response Structure
 :hostName:       The (short) hostname of the server
 :httpsPort:      The port on which the server listens for incoming HTTPS connections/requests
 :id:             An integral, unique identifier for this server
-:iloIpAddress:   The IPv4 address of the server's Integrated Lights-Out (ILO) service\ [1]_
-:iloIpGateway:   The IPv4 gateway address of the server's ILO service\ [1]_
-:iloIpNetmask:   The IPv4 subnet mask of the server's ILO service\ [1]_
-:iloPassword:    The password of the of the server's ILO service user\ [1]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' role(s)
-:iloUsername:    The user name for the server's ILO service\ [1]_
-:interfaceMtu:   The Maximum Transmission Unit (MTU) to configured on ``interfaceName``
-:interfaceName:  The name of the primary network interface used by the server
-:ip6Address:     The IPv6 address and subnet mask of ``interfaceName``
-:ip6IsService:   A boolean value which if ``true`` indicates that the IPv6 address will be used for routing content.
-:ip6Gateway:     The IPv6 address of the gateway used by ``interfaceName``
-:ipAddress:      The IPv4 address of ``interfaceName``
-:ipIsService:    A boolean value which if ``true`` indicates that the IPv4 address will be used for routing content.
-:ipGateway:      The IPv4 address of the gateway used by ``interfaceName``
-:ipNetmask:      The IPv4 subnet mask used by ``interfaceName``
-:lastUpdated:    The date and time at which this server description was last modified
-:mgmtIpAddress:  The IPv4 address of some network interface on the server used for 'management'
-:mgmtIpGateway:  The IPv4 address of a gateway used by some network interface on the server used for 'management'
-:mgmtIpNetmask:  The IPv4 subnet mask used by some network interface on the server used for 'management'
+:iloIpAddress:   The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway:   The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask:   The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:    The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :abbr:`Role(s) <Role>`
+:iloUsername:    The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces in use by the server. In most scenarios, only one will be present, but it is illegal for this set to be an empty collection.
+
+	:ipAddresses: A set of objects representing IP Addresses assigned to this network interface. In most scenarios, only one or two (usually one IPv4 address and one IPv6 address) will be present, but it is illegal for this set to be an empty collection.
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address". It is illegal for a server to not have at least one service address. It is also illegal for a server to have more than one service address of the same address family (i.e. more than one IPv4 service address and/or more than one IPv6 address). Finally, all service addresses for a server m [...]
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors will consider the entire server unhealthy - which includes *all* configured network interfaces. If this is ``null``, it has the meaning "no limit". It has no effect if ``monitor`` is not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should be monitored by Traffic Monitor for statistics and health consideration.
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information is either not available or not applicable for this interface.
+	:name:    The name of the interface. No two interfaces of the same server may share a name. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:lastUpdated:   The date and time at which this server description was last modified
+:mgmtIpAddress: The IPv4 address of some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
 :offlineReason:  A user-entered reason why the server is in ADMIN_DOWN or OFFLINE status
-:physLocation:   The name of the physical location where the server resides
-:physLocationId: An integral, unique identifier for the physical location where the server resides
+:physLocation:   The name of the :term:`Physical Location` where the server resides
+:physLocationId: An integral, unique identifier for the :term:`Physical Location` where the server resides
 :profile:        The :ref:`profile-name` of the :term:`Profile` used by this server
 :profileDesc:    A :ref:`profile-description` of the :term:`Profile` used by this server
 :profileId:      The :ref:`profile-id` the :term:`Profile` used by this server
@@ -194,65 +247,77 @@ Response Structure
 	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
 	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
 	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: 9lGAMCCC9I/bOpuBSyf3ACffjHeRuXCTuxrA/oU78uYzW5FeFTq5PHSSnsnqKG5E0vWg0Rko0CwguGeNc9IT0w==
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 19 May 2020 17:46:33 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Dec 2018 17:58:57 GMT
-	Content-Length: 848
+	Date: Tue, 19 May 2020 16:46:33 GMT
+	Content-Length: 566
 
 	{ "alerts": [
 		{
-			"text": "server was updated.",
+			"text": "Server updated",
 			"level": "success"
 		}
 	],
 	"response": {
-		"cachegroup": null,
+		"cachegroup": "CDN_in_a_Box_Mid",
 		"cachegroupId": 6,
 		"cdnId": 2,
-		"cdnName": null,
+		"cdnName": "CDN-in-a-Box",
 		"domainName": "infra.ciab.test",
 		"guid": null,
 		"hostName": "quest",
 		"httpsPort": 443,
-		"id": 13,
+		"id": 14,
 		"iloIpAddress": "",
 		"iloIpGateway": "",
 		"iloIpNetmask": "",
 		"iloPassword": "",
 		"iloUsername": "",
-		"interfaceMtu": 1500,
-		"interfaceName": "eth0",
-		"ip6Address": "::1",
-		"ip6Gateway": "::2",
-		"ipAddress": "0.0.0.1",
-		"ipGateway": "0.0.0.2",
-		"ipNetmask": "255.255.255.0",
-		"lastUpdated": "2018-12-10 17:58:57+00",
+		"lastUpdated": "2020-05-19 16:46:33+00",
 		"mgmtIpAddress": "",
 		"mgmtIpGateway": "",
 		"mgmtIpNetmask": "",
 		"offlineReason": "",
-		"physLocation": null,
+		"physLocation": "Apachecon North America 2018",
 		"physLocationId": 1,
-		"profile": null,
-		"profileDesc": null,
+		"profile": "ATS_MID_TIER_CACHE",
+		"profileDesc": "Mid Cache - Apache Traffic Server",
 		"profileId": 10,
 		"rack": null,
-		"revalPending": null,
+		"revalPending": false,
 		"routerHostName": "",
 		"routerPortName": "",
-		"status": null,
+		"status": "REPORTED",
 		"statusId": 3,
 		"tcpPort": 80,
-		"type": "",
+		"type": "MID",
 		"typeId": 12,
-		"updPending": true,
+		"updPending": false,
 		"xmppId": null,
 		"xmppPasswd": null,
-		"ipIsService": true,
-		"ip6IsService": true
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "::1",
+						"gateway": "::2",
+						"serviceAddress": true
+					},
+					{
+						"address": "0.0.0.1/24",
+						"gateway": "0.0.0.2",
+						"serviceAddress": false
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": true,
+				"mtu": 1500,
+				"name": "bond0"
+			}
+		]
 	}}
 
 ``DELETE``
@@ -261,7 +326,10 @@ Allow user to delete server through api.
 
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
-:Response Type:  ``undefined``
+:Response Type:  Object
+
+	.. versionchanged:: 3.0
+		In older versions of the API, this endpoint did not return a response object. It now returns a representation of the deleted server.
 
 Request Structure
 -----------------
@@ -276,7 +344,7 @@ Request Structure
 .. code-block:: http
 	:caption: Request Example
 
-	DELETE /api/3.0/servers/13 HTTP/1.1
+	DELETE /api/3.0/servers/14 HTTP/1.1
 	Host: trafficops.infra.ciab.test
 	User-Agent: curl/7.47.0
 	Accept: */*
@@ -284,26 +352,158 @@ Request Structure
 
 Response Structure
 ------------------
+:cachegroup:   A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belonged
+:cachegroupId: An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belonged
+:cdnId:        The integral, unique identifier of the CDN to which the server belonged
+:cdnName:      Name of the CDN to which the server belonged
+:domainName:   The domain part of the server's :abbr:`FQDN (Fully Qualified Domain Name)`
+:guid:         An identifier used to uniquely identify the server
+
+	.. note:: This is a legacy key which only still exists for compatibility reasons - it should always be ``null``
+
+:hostName:     The (short) hostname of the server
+:httpsPort:    The port on which the server listened for incoming HTTPS connections/requests
+:id:           An integral, unique identifier for this server
+:iloIpAddress: The IPv4 address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpGateway: The IPv4 gateway address of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloIpNetmask: The IPv4 subnet mask of the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:iloPassword:  The password of the of the server's :abbr:`ILO (Integrated Lights-Out)` service user\ [#ilo]_ - displays as simply ``******`` if the currently logged-in user does not have the 'admin' or 'operations' :term:`Role(s) <Role>`
+:iloUsername:  The user name for the server's :abbr:`ILO (Integrated Lights-Out)` service\ [#ilo]_
+:interfaces:   A set of the network interfaces that were in use by the server
+
+	:ipAddresses: A set of objects representing IP Addresses that were assigned to this network interface
+
+		:address:        The actual IP address, including any mask as a CIDR-notation suffix
+		:gateway:        Either the IP address of the network gateway for this address, or ``null`` to signify that no such gateway exists
+		:serviceAddress: A boolean that describes whether or not the server's main service is available at this IP address. When this property is ``true``, the IP address is referred to as a "service address".
+
+	:maxBandwidth: The maximum healthy bandwidth allowed for this interface. If bandwidth exceeds this limit, Traffic Monitors would have considered the entire server unhealthy - which includes *all* configured network interfaces. If this was ``null``, it has the meaning "no limit". It had no effect if ``monitor`` was not true for this interface.
+
+		.. seealso:: :ref:`health-proto`
+
+	:monitor: A boolean which describes whether or not this interface should have been monitored by Traffic Monitor for statistics and health consideration
+	:mtu:     The :abbr:`MTU (Maximum Transmission Unit)` of this interface. If it is ``null``, it may be assumed that the information was either not available or not applicable for this interface.
+	:name:    The name of the interface. It is the same as the network interface's device name on the server, e.g. ``eth0``.
+
+:lastUpdated:   The date and time at which this server description was last modified
+:mgmtIpAddress: The IPv4 address of some network interface on the server that was used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpGateway: The IPv4 address of a gateway used by some network interface on the server that was used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:mgmtIpNetmask: The IPv4 subnet mask used by some network interface on the server that was used for 'management'
+
+	.. deprecated:: 3.0
+		This field is deprecated and will be removed in a future API version. Operators should migrate this data into the ``interfaces`` property of the server.
+
+:offlineReason:  A user-entered reason why the server was in ADMIN_DOWN or OFFLINE status
+:physLocation:   The name of the physical location where the server resided
+:physLocationId: An integral, unique identifier for the physical location where the server resided
+:profile:        The :ref:`profile-name` of the :term:`Profile` which was used by this server
+:profileDesc:    A :ref:`profile-description` of the :term:`Profile` which was used by this server
+:profileId:      The :ref:`profile-id` the :term:`Profile` which was used by this server
+:revalPending:   A boolean value which, if ``true`` indicates that this server had pending content invalidation/revalidation
+:rack:           A string indicating "server rack" location
+:routerHostName: The human-readable name of the router responsible for reaching this server
+:routerPortName: The human-readable name of the port used by the router responsible for reaching this server
+:status:         The :term:`Status` of the server
+
+	.. seealso:: :ref:`health-proto`
+
+:statusId: The integral, unique identifier of the status of this server
+
+	.. seealso:: :ref:`health-proto`
+
+:tcpPort: The port on which this server listened for incoming TCP connections
+
+	.. note:: This is typically thought of as synonymous with "HTTP port", as the port specified by ``httpsPort`` may also be used for incoming TCP connections.
+
+:type:       The name of the :term:`Type` of this server
+:typeId:     The integral, unique identifier of the 'type' of this server
+:updPending: A boolean value which, if ``true``, indicates that the server had updates of some kind pending, typically to be acted upon by Traffic Ops :term:`ORT`
+:xmppId:     An identifier to be used in XMPP communications with the server - in nearly all cases this will be the same as ``hostName``
+:xmppPasswd: The password used in XMPP communications with the server
+
 .. code-block:: http
 	:caption: Response Example
 
 	HTTP/1.1 200 OK
-	Access-Control-Allow-Credentials: true
-	Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
-	Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
-	Access-Control-Allow-Origin: *
+	Content-Encoding: gzip
 	Content-Type: application/json
-	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
-	Whole-Content-Sha512: JZdjKJYWN9w9NF6VE/rVkGUqecycKB2ABkkI4LNDmgpJLwu53bRHAA+4uWrow0zuba/4MSEhHKshutziypSxPg==
+	Set-Cookie: mojolicious=...; Path=/; Expires=Tue, 19 May 2020 17:50:13 GMT; Max-Age=3600; HttpOnly
+	Vary: Accept-Encoding
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 10 Dec 2018 18:23:21 GMT
-	Content-Length: 61
+	Date: Tue, 19 May 2020 16:50:13 GMT
+	Content-Length: 568
 
 	{ "alerts": [
 		{
-			"text": "server was deleted.",
+			"text": "Server deleted",
 			"level": "success"
 		}
-	]}
+	],
+	"response": {
+		"cachegroup": "CDN_in_a_Box_Mid",
+		"cachegroupId": 6,
+		"cdnId": 2,
+		"cdnName": "CDN-in-a-Box",
+		"domainName": "infra.ciab.test",
+		"guid": null,
+		"hostName": "quest",
+		"httpsPort": 443,
+		"id": 14,
+		"iloIpAddress": "",
+		"iloIpGateway": "",
+		"iloIpNetmask": "",
+		"iloPassword": "",
+		"iloUsername": "",
+		"lastUpdated": "2020-05-19 16:46:33+00",
+		"mgmtIpAddress": "",
+		"mgmtIpGateway": "",
+		"mgmtIpNetmask": "",
+		"offlineReason": "",
+		"physLocation": "Apachecon North America 2018",
+		"physLocationId": 1,
+		"profile": "ATS_MID_TIER_CACHE",
+		"profileDesc": "Mid Cache - Apache Traffic Server",
+		"profileId": 10,
+		"rack": null,
+		"revalPending": false,
+		"routerHostName": "",
+		"routerPortName": "",
+		"status": "REPORTED",
+		"statusId": 3,
+		"tcpPort": 80,
+		"type": "MID",
+		"typeId": 12,
+		"updPending": false,
+		"xmppId": null,
+		"xmppPasswd": null,
+		"interfaces": [
+			{
+				"ipAddresses": [
+					{
+						"address": "0.0.0.1/24",
+						"gateway": "0.0.0.2",
+						"serviceAddress": false
+					},
+					{
+						"address": "::1",
+						"gateway": "::2",
+						"serviceAddress": true
+					}
+				],
+				"maxBandwidth": null,
+				"monitor": true,
+				"mtu": 1500,
+				"name": "bond0"
+			}
+		]
+	}}
 
-.. [1] For more information see the `Wikipedia page on Lights-Out management <https://en.wikipedia.org/wiki/Out-of-band_management>`_\ .
+.. [#ilo] For more information see the `Wikipedia page on Lights-Out management <https://en.wikipedia.org/wiki/Out-of-band_management>`_\ .
diff --git a/docs/source/basics/content_delivery_networks.rst b/docs/source/basics/content_delivery_networks.rst
index 61b1513..94fd8fa 100644
--- a/docs/source/basics/content_delivery_networks.rst
+++ b/docs/source/basics/content_delivery_networks.rst
@@ -18,7 +18,7 @@ Content Delivery Networks
 *************************
 The vast majority of today's Internet traffic is media files (often video or audio) being sent from a single source (the *Content Provider*) to many thousands or even millions of destinations (the *Content Consumers*). :abbr:`CDN (Content Delivery Network)`\ s are the technology that make that one-to-many distribution efficient. A :abbr:`CDN (Content Delivery Network)` is a distributed system of servers for delivering content over HTTP(S). These servers are deployed in multiple locations [...]
 
-:term:`Cache Servers`
+:term:`cache servers`
 	The :dfn:`cache server` is a server that both proxies the requests and caches the results for reuse. Traffic Control uses `Apache Traffic Server <http://trafficserver.apache.org/>`_ to provide :term:`cache servers`.
 
 Content Router
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index d2c85ca..1c2553b 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -32,9 +32,9 @@ Glossary
 	cache servers
 		The main function of a CDN is to proxy requests from clients to :term:`origin servers` and cache the results. To proxy, in the CDN context, is to obtain content using HTTP from an :term:`origin server` on behalf of a client. To cache is to store the results so they can be reused when other clients are requesting the same content. There are three types of proxies in use on the Internet today:
 
-		- :term:`Reverse Proxy`: Used by Traffic Control for Edge-tier :dfn:`cache servers`.
-		- :term:`Forward Proxy`: Used by Traffic Control for Mid-tier :dfn:`cache servers`.
-		- Transparent Proxy: These are not used by Traffic Control. If you are interested you can learn more about transparent proxies on `wikipedia <http://en.wikipedia.org/wiki/Proxy_server#Transparent_proxy>`_.
+		- :term:`reverse proxy`: Used by Traffic Control for Edge-tier :dfn:`cache servers`.
+		- :term:`forward proxy`: Used by Traffic Control for Mid-tier :dfn:`cache servers`.
+		- transparent proxy: These are not used by Traffic Control. If you are interested you can learn more about transparent proxies on `wikipedia <http://en.wikipedia.org/wiki/Proxy_server#Transparent_proxy>`_.
 
 	Cache Group
 	Cache Groups
@@ -133,7 +133,7 @@ Glossary
 	Federations
 		:dfn:`Federations` allow for other ("federated") CDNs (e.g. at a different :abbr:`ISP (Internet Service Provider)`) to add a list of DNS resolvers and an :abbr:`FQDN (Fully Qualified Domain Name)` to be used in a DNS CNAME record for a :term:`Delivery Service`. When a request is made from one of the federated CDN's clients, Traffic Router will return the CNAME record configured from the federation mapping. This allows the federated CDN to serve the content without the content provider  [...]
 
-		Federations only have meaning to DNS-routed :term:`Delivery Services` - HTTP-routed Delivery services should instead treat the external :abbr:`FQDN (Fully Qualified Domain Name)` as an :term:`origin` to achieve the same effect.
+		Federations only have meaning to DNS-routed :term:`Delivery Services` - HTTP-routed Delivery services should instead treat the external :abbr:`FQDN (Fully Qualified Domain Name)` as an :term:`Origin` to achieve the same effect.
 
 		.. seealso:: Federations are currently only manageable by directly using the :ref:`to-api`. The endpoints related to federations are :ref:`to-api-federations`, :ref:`to-api-federation_resolvers`, :ref:`to-api-federations-id-deliveryservices`, :ref:`to-api-federations-id-deliveryservices-id`, :ref:`to-api-federations-id-federation_resolvers`, :ref:`to-api-federations-id-users`, and :ref:`to-api-federations-id-users-id`.
 
@@ -157,10 +157,10 @@ Glossary
 
 		#. The proxy verifies whether the response for ``http://www-origin-cache.cdn.com/foo/bar/fun.html`` is already in the cache. If it is not in the cache:
 
-			#. The proxy sends the HTTP request to the :term:`origin`.
+			#. The proxy sends the HTTP request to the :term:`Origin`.
 
 				.. code-block:: http
-					:caption: The :dfn:`Forward Proxy` Requests Content from the :term:`Origin Server`
+					:caption: The :dfn:`Forward Proxy` Requests Content from the :term:`origin server`
 
 					GET /foo/bar/fun.html HTTP/1.1
 					Host: www.origin.com
@@ -168,7 +168,7 @@ Glossary
 			#. The :term:`origin server` responds with the requested content.
 
 				.. code-block:: http
-					:caption: The :term:`Origin Server`'s Response
+					:caption: The :term:`origin server`'s Response
 
 					HTTP/1.1 200 OK
 					Date: Sun, 14 Dec 2014 23:22:44 GMT
@@ -234,10 +234,10 @@ Glossary
 	Mid-tier caches
 	Mid-tier cache server
 	Mid-tier cache servers
-		The tier above the edge tier. The mid tier does not directly serves the end-user and is used as an additional layer between the edge and the :term:`origin`. In a Traffic Control CDN the basic function of the mid cache is that of a :term:`forward proxy`.
+		The tier above the edge tier. The mid tier does not directly serves the end-user and is used as an additional layer between the edge and the :term:`Origin`. In a Traffic Control CDN the basic function of the mid cache is that of a :term:`forward proxy`.
 
-	origin
-	origins
+	Origin
+	Origins
 	origin server
 	origin servers
 		The source of content for the CDN. Usually a redundant HTTP/1.1 webserver.
@@ -263,7 +263,7 @@ Glossary
 
 	Profile
 	Profiles
-		A :dfn:`Profile` is, most generally, a group of :term:`Parameters` that will be applied to a server. :dfn:`Profiles` are typically re-used by all :term:`Edge-Tier cache servers` within a CDN or :term:`Cache Group`. A :dfn:`Profile` will, in addition to configuration :term:`Parameters`, define the CDN to which a server belongs and the :ref:`"Type" <profile-type>` of the Profile - which determines some behaviors of Traffic Control components. The allowed :ref:`"Types" <profile-type>` of  [...]
+		A :dfn:`Profile` is, most generally, a group of :term:`Parameters` that will be applied to a server. :dfn:`Profiles` are typically re-used by all :term:`Edge-tier cache servers` within a CDN or :term:`Cache Group`. A :dfn:`Profile` will, in addition to configuration :term:`Parameters`, define the CDN to which a server belongs and the :ref:`"Type" <profile-type>` of the Profile - which determines some behaviors of Traffic Control components. The allowed :ref:`"Types" <profile-type>` of  [...]
 
 		.. tip:: A :dfn:`Profile` of the wrong type assigned to a Traffic Control component *will* (in general) cause it to function incorrectly, regardless of the :term:`Parameters` assigned to it.
 
@@ -288,11 +288,11 @@ Glossary
 
 	reverse proxy
 	reverse proxies
-		A :dfn:`reverse proxy` acts on behalf of the :term:`origin server` such that the client is (potentially) unaware it is not communicating directly with the :term:`origin`. All Edge-tier :term:`cache servers` in a Traffic Control CDN are :dfn:`reverse proxies`. To the end user a Traffic Control-based CDN appears as a :dfn:`reverse proxy` since it retrieves content from the :term:`origin server`, acting on behalf of that :term:`origin server`. The client requests a URL that has a hostname [...]
+		A :dfn:`reverse proxy` acts on behalf of the :term:`origin server` such that the client is (potentially) unaware it is not communicating directly with the :term:`Origin`. All Edge-tier :term:`cache servers` in a Traffic Control CDN are :dfn:`reverse proxies`. To the end user a Traffic Control-based CDN appears as a :dfn:`reverse proxy` since it retrieves content from the :term:`origin server`, acting on behalf of that :term:`origin server`. The client requests a URL that has a hostname [...]
 
 		.. seealso:: `The Apache Traffic Server documentation on reverse proxy <https://docs.trafficserver.apache.org/en/latest/admin/reverse-proxy-http-redirects.en.html#http-reverse-proxy>`_.
 
-		To insert a :dfn:`reverse proxy` into a typical HTTP 1.1 request and response flow, the :dfn:`reverse proxy` needs to be told where the :term:`origin server` can be reached (and which :term:`origin` to use for a given request when it's configured to proxy requests for multiple :term:`origins`). In :abbr:`ATS (Apache Traffic Server)` this is handled by adding rules to `the remap.config configuration file <https://docs.trafficserver.apache.org/en/latest/admin-guide/files/remap.config.en. [...]
+		To insert a :dfn:`reverse proxy` into a typical HTTP 1.1 request and response flow, the :dfn:`reverse proxy` needs to be told where the :term:`origin server` can be reached (and which :term:`Origin` to use for a given request when it's configured to proxy requests for multiple :term:`Origins`). In :abbr:`ATS (Apache Traffic Server)` this is handled by adding rules to `the remap.config configuration file <https://docs.trafficserver.apache.org/en/latest/admin-guide/files/remap.config.en. [...]
 
 		Now, if the client requests ``/foo/bar/fun.html`` from the :dfn:`reverse proxy` the sequence of events is as follows. is given the URL ``http://www-origin-cache.cdn.com/foo/bar/fun.html`` (note the different hostname) and when attempting to obtain that URL, the following occurs:
 
@@ -306,14 +306,14 @@ Glossary
 				GET /foo/bar/fun.html HTTP/1.1
 				Host: www-origin-cache.cdn.com
 
-		#. The :dfn:`reverse proxy` finds out the URL of the true :term:`origin` - in the case of :abbr:`ATS (Apache Traffic Server)` this is done by looking up ``www-origin-cache.cdn.com`` in its remap rules - and finds that it is ``www.origin.com``.
+		#. The :dfn:`reverse proxy` finds out the URL of the true :term:`Origin` - in the case of :abbr:`ATS (Apache Traffic Server)` this is done by looking up ``www-origin-cache.cdn.com`` in its remap rules - and finds that it is ``www.origin.com``.
 		#. The proxy checks its cache to see if the response for ``GET /foo/bar/fun.html HTTP/1.1`` from ``www.origin.com`` is already in the cache.
 		#. If the response is not in the cache:
 
-			#. The proxy sends the request to the actual :term:`origin`
+			#. The proxy sends the request to the actual :term:`Origin`
 
 				.. code-block:: http
-					:caption: :dfn:`Reverse Proxy` Requests Content from the :term:`Origin Server`
+					:caption: :dfn:`Reverse Proxy` Requests Content from the :term:`origin server`
 
 					GET /foo/bar/fun.html HTTP/1.1
 					Host: www.origin.com
@@ -321,7 +321,7 @@ Glossary
 			#. The :term:`origin server` responds with the requested content
 
 				.. code-block:: http
-					:caption: Response from the :term:`Origin Server`
+					:caption: Response from the :term:`origin server`
 
 					HTTP/1.1 200 OK
 					Date: Sun, 14 Dec 2014 23:22:44 GMT
@@ -376,7 +376,7 @@ Glossary
 
 	Server Capability
 	Server Capabilities
-		A :dfn:`Server Capability` (not to be confused with a "Capability") expresses the capacity of a :term:`cache server` to serve a particular kind of traffic. For example, a :dfn:`Server Capability` could be created named "RAM" to be assigned to :term:`cache servers` that have RAM-disks allocated for content caching. :dfn:`Server Capabilities` can also be required by :term:`Delivery Services`, which will prevent :term:`cache servers` without that :dfn:`Server Capability` from being assign [...]
+		A :dfn:`Server Capability` (not to be confused with a "Capability") expresses the capacity of a :term:`cache server` to serve a particular kind of traffic. For example, a :dfn:`Server Capability` could be created named "RAM" to be assigned to :term:`cache servers` that have RAM-disks allocated for content caching. :dfn:`Server Capabilities` can also be required by :term:`Delivery Services`, which will prevent :term:`cache servers` without that :dfn:`Server Capability` from being assign [...]
 
 	Snapshot
 	Snapshots
diff --git a/docs/source/overview/cache_groups.rst b/docs/source/overview/cache_groups.rst
index 53efabd..53b0b0b 100644
--- a/docs/source/overview/cache_groups.rst
+++ b/docs/source/overview/cache_groups.rst
@@ -22,7 +22,7 @@ A :dfn:`Cache Group` is - ostensibly - exactly what it sounds like it is: a grou
 
 The most typical :ref:`Types <cache-group-type>` of Cache Groups are EDGE_LOC_ which contain :term:`Edge-tier cache servers` and MID_LOC_ which contain :term:`Mid-tier cache servers`. The latter are each designated as a Parent_ of one or more of the former to fill out the two-tiered caching hierarchy of an :abbr:`ATC (Apache Traffic Control)` CDN.
 
-Consider the example CDN in :numref:`fig-cg_hierarchy`. Here some country/province/region has been divided into quarters: Northeast, Southeast, Northwest, and Southwest. The arrows in the diagram indicate the flow of requests. If a client in the Northwest, for example, were to make a request to the :term:`Delivery Service`, it would first be directed to some :term:`cache server` in the "Northwest" Edge-tier :dfn:`Cache Group`. Should the requested content not be in cache, the Edge-tier s [...]
+Consider the example CDN in :numref:`fig-cg_hierarchy`. Here some country/province/region has been divided into quarters: Northeast, Southeast, Northwest, and Southwest. The arrows in the diagram indicate the flow of requests. If a client in the Northwest, for example, were to make a request to the :term:`Delivery Service`, it would first be directed to some :term:`cache server` in the "Northwest" Edge-tier :dfn:`Cache Group`. Should the requested content not be in cache, the Edge-tier s [...]
 
 .. _fig-cg_hierarchy:
 
@@ -307,7 +307,7 @@ This :term:`Type` of Cache Group contains :term:`Mid-tier cache servers`
 
 ORG_LOC
 """""""
-This :term:`Type` of Cache Group contains :term:`origins`. The primary purpose of these is to group :term:`origins` for the purposes of "multi-site-origins", and it's suggested that if that doesn't meet your use-case that these be mostly avoided. In general, it's not strictly necessary to create :term:`origin` *servers* in ATC at all, unless you have to support "multi-site-origins".
+This :term:`Type` of Cache Group contains :term:`Origins`. The primary purpose of these is to group :term:`Origins` for the purposes of "multi-site-origins", and it's suggested that if that doesn't meet your use-case that these be mostly avoided. In general, it's not strictly necessary to create :term:`Origin` *servers* in ATC at all, unless you have to support "multi-site-origins".
 
 .. seealso:: :ref:`multi-site-origin-qht`
 
diff --git a/docs/source/overview/delivery_services.rst b/docs/source/overview/delivery_services.rst
index c5ca7a3..bc05c66 100644
--- a/docs/source/overview/delivery_services.rst
+++ b/docs/source/overview/delivery_services.rst
@@ -56,7 +56,7 @@ Cache URL Expression
 .. deprecated:: 3.0
 	This feature is no longer supported by :abbr:`ATS (Apache Traffic Server)` and consequently it will be removed from Traffic Control in the future.
 
-Manipulates the cache key of the incoming requests. Normally, the cache key is the :term:`origin` domain. This can be changed so that multiple services can share a cache key, can also be used to preserve cached content if service origin is changed.
+Manipulates the cache key of the incoming requests. Normally, the cache key is the :term:`Origin` domain. This can be changed so that multiple services can share a cache key, can also be used to preserve cached content if service origin is changed.
 
 .. warning:: This field provides access to a feature that was only present in :abbr:`ATS (Apache Traffic Server)` 6.X and earlier. As :term:`cache servers` must now use :abbr:`ATS (Apache Traffic Server)` 7.1.X, this field **must** be blank unless all :term:`cache servers` can be guaranteed to use that older :abbr:`ATS (Apache Traffic Server)` version (**NOT** recommended).
 
@@ -70,7 +70,7 @@ A CDN to which this Delivery Service belongs. Only :term:`cache servers` within
 
 Check Path
 ----------
-A request path on the :term:`origin server` which is used to by certain :ref:`Traffic Ops Extensions <admin-to-ext-script>` to indicate the "health" of the :term:`origin`.
+A request path on the :term:`origin server` which is used to by certain :ref:`Traffic Ops Extensions <admin-to-ext-script>` to indicate the "health" of the :term:`Origin`.
 
 .. _ds-consistent-hashing-regex:
 
@@ -521,7 +521,7 @@ The Origin Server’s base URL which includes the protocol (http or https). Exam
 
 Origin Shield
 -------------
-An experimental feature that allows administrators to list additional forward proxies that sit between the :term:`Mid-tier` and the :term:`origin`. In most scenarios, this is represented (and required to be input) as a pipe (``|``)-delimited string.
+An experimental feature that allows administrators to list additional forward proxies that sit between the :term:`Mid-tier` and the :term:`Origin`. In most scenarios, this is represented (and required to be input) as a pipe (``|``)-delimited string.
 
 .. _ds-profile:
 
@@ -577,9 +577,9 @@ Query String Handling
 Describes how query strings should be handled by the :term:`Edge-tier cache servers` when serving content for this Delivery Service. This is nearly always expressed as an integral, unique identifier for each behavior, though in Traffic Portal a more descriptive value is typically used, or at least provided in addition to the integral, unique identifier. The allowed values and their meanings are:
 
 0
-	For the purposes of caching, :term:`Edge-tier cache servers` will consider URLs unique if and only if they are unique up to and including any and all query parameters. They will also pass the query parameters in their own requests to :term:`Mid-tier cache servers` (which in turn will exhibit the same caching behavior and pass the query parameters in requests to the :term:`origin`). (Aliased as "USE" in Traffic Portal tables, and "0 - use qstring in cache key, and pass up" in Traffic Por [...]
+	For the purposes of caching, :term:`Edge-tier cache servers` will consider URLs unique if and only if they are unique up to and including any and all query parameters. They will also pass the query parameters in their own requests to :term:`Mid-tier cache servers` (which in turn will exhibit the same caching behavior and pass the query parameters in requests to the :term:`Origin`). (Aliased as "USE" in Traffic Portal tables, and "0 - use qstring in cache key, and pass up" in Traffic Por [...]
 1
-	For the purposes of caching, neither :term:`Edge-tier` nor :term:`Mid-tier cache servers` will consider the query parameter string when determining if a URL is stored in cache. However, the query string will still be passed in upstream requests to :term:`Mid-tier cache servers` and in turn the :term:`origin`. (Aliased as "IGNORE" in Traffic Portal tables and "1 - ignore in cache key, and pass up" in Traffic Portal forms)
+	For the purposes of caching, neither :term:`Edge-tier` nor :term:`Mid-tier cache servers` will consider the query parameter string when determining if a URL is stored in cache. However, the query string will still be passed in upstream requests to :term:`Mid-tier cache servers` and in turn the :term:`Origin`. (Aliased as "IGNORE" in Traffic Portal tables and "1 - ignore in cache key, and pass up" in Traffic Portal forms)
 2
 	The query parameter string will be stripped from URLs immediately when the request is received by an :term:`Edge-tier cache server`. This means it is never considered for the purposes of caching unique URLs and will not be passed in upstream requests. (Aliased as "DROP" in Traffic Portal tables and "2 - drop at edge" in Traffic Portal forms)
 
@@ -657,7 +657,7 @@ For example, if you have an Apache Traffic Server lua plugin which manipulates t
 
 Regex Remap Expression
 ----------------------
-Allows remapping of incoming requests URL using regular expressions to search and replace text. In a more literal sense, this is the raw contents of a configuration file used by the `ATS regex_remap plugin  <https://docs.trafficserver.apache.org/en/7.1.x/admin-guide/plugins/regex_remap.en.html>`_. At its most basic, the contents of this field should consist of ``map`` followed by a regular expression and then a "template URL" - all space-separated. The regular expression matches a client [...]
+Allows remapping of incoming requests URL using regular expressions to search and replace text. In a more literal sense, this is the raw contents of a configuration file used by the `ATS regex_remap plugin  <https://docs.trafficserver.apache.org/en/7.1.x/admin-guide/plugins/regex_remap.en.html>`_. At its most basic, the contents of this field should consist of ``map`` followed by a regular expression and then a "template URL" - all space-separated. The regular expression matches a client [...]
 
 .. seealso:: The `documentation for the regex_remap plugin <https://docs.trafficserver.apache.org/en/7.1.x/admin-guide/plugins/regex_remap.en.html>`_ for :abbr:`ATS (Apache Traffic Server)`
 
@@ -691,7 +691,7 @@ Required Capabilities
 ---------------------
 .. versionadded:: ATCv4
 
-A Delivery Service can be associated with :term:`Server Capabilities` that it requires :term:`cache servers` serving its content to have. When one or more :term:`Server Capability` is required by a Delivery Service, it will block the assignment of :term:`cache servers` to it that do not have those :term:`Server Capabilities`. Additionally, the :term:`Edge-tier Cache Servers` assigned to a Delivery Service that requires a :term:`Server Capability` will only request content they do not hav [...]
+A Delivery Service can be associated with :term:`Server Capabilities` that it requires :term:`cache servers` serving its content to have. When one or more :term:`Server Capability` is required by a Delivery Service, it will block the assignment of :term:`cache servers` to it that do not have those :term:`Server Capabilities`. Additionally, the :term:`Edge-tier cache servers` assigned to a Delivery Service that requires a :term:`Server Capability` will only request content they do not hav [...]
 
 Typically, a required :term:`Server Capability` is represented merely by the name of said :term:`Server Capability`. In fact, there's nothing more to a :term:`Server Capability` than its name; it's the responsibility of CDN operators to ensure that they are assigned and required properly. There is no mechanism to detect whether or not a :term:`cache server` has a given :term:`Server Capability`, it must be assigned manually.
 
@@ -711,7 +711,7 @@ Servers can be assigned to Delivery Services using the :ref:`tp-configure-server
 
 Signing Algorithm
 -----------------
-URLs/URIs may be signed using one of two algorithms before a request for the content to which they refer is sent to the :term:`origin` (which in practice can be any upstream network). At the time of this writing, this field is restricted within the Traffic Ops Database to one of two values (or ``NULL``/"None", to indicate no signing should be done).
+URLs/URIs may be signed using one of two algorithms before a request for the content to which they refer is sent to the :term:`Origin` (which in practice can be any upstream network). At the time of this writing, this field is restricted within the Traffic Ops Database to one of two values (or ``NULL``/"None", to indicate no signing should be done).
 
 .. seealso:: The url_sig `README <https://github.com/apache/trafficserver/blob/master/plugins/experimental/url_sig/README>`_.
 
@@ -866,11 +866,11 @@ CLIENT_STEERING
 	A CLIENT_STEERING Delivery Service is exactly like STEERING except that it provides clients with methods of bypassing the weights, orders, and localizations of targets in order to choose any arbitrary target at will. When utilizing these methods, the client will either directly choose a target immediately or request a list of all available targets from Traffic Router and then choose one to which to send a subsequent request for actual content. CLIENT_STEERING also supports two additiona [...]
 
 	STEERING_GEO_ORDER
-		These targets behave exactly like STEERING_ORDER targets, but Delivery Services are grouped according to the "locations" of their :term:`origins`. Before choosing a Delivery Service to which to direct the client, Traffic Router will first create subsets of choices according to these groupings, and order them by physical distance from the client (closest to farthest). Within these subsets, the values of the targets establish a strict precedence ordering, just like STEERING_ORDER targets.
+		These targets behave exactly like STEERING_ORDER targets, but Delivery Services are grouped according to the "locations" of their :term:`Origins`. Before choosing a Delivery Service to which to direct the client, Traffic Router will first create subsets of choices according to these groupings, and order them by physical distance from the client (closest to farthest). Within these subsets, the values of the targets establish a strict precedence ordering, just like STEERING_ORDER targets.
 	STEERING_GEO_WEIGHT
-		These targets behave exactly like STEERING_WEIGHT targets, but Delivery Services are grouped according to the "locations" of their :term:`origins`. Before choosing a Delivery Service to which to direct the client, Traffic Router will first create subsets of choices according to these groupings, and order them by physical distance from the client (closest to farthest). Within these subsets, the values of the targets establish the likelihood that any given target within the subset will b [...]
+		These targets behave exactly like STEERING_WEIGHT targets, but Delivery Services are grouped according to the "locations" of their :term:`Origins`. Before choosing a Delivery Service to which to direct the client, Traffic Router will first create subsets of choices according to these groupings, and order them by physical distance from the client (closest to farthest). Within these subsets, the values of the targets establish the likelihood that any given target within the subset will b [...]
 
-	.. important:: To make use of the STEERING_GEO_ORDER and/or STEERING_GEO_WEIGHT target types, it is first necessary to ensure that at least the "primary" :term:`origin` of the :term:`Delivery Service` has an associated geographic coordinate pair. This can be done either from the :ref:`tp-configure-origins` page in Traffic Portal, or using the :ref:`to-api-origins` :ref:`to-api` endpoint.
+	.. important:: To make use of the STEERING_GEO_ORDER and/or STEERING_GEO_WEIGHT target types, it is first necessary to ensure that at least the "primary" :term:`Origin` of the :term:`Delivery Service` has an associated geographic coordinate pair. This can be done either from the :ref:`tp-configure-origins` page in Traffic Portal, or using the :ref:`to-api-origins` :ref:`to-api` endpoint.
 
 .. note:: "Steering" is also commonly used to collectively refer to either of the kinds of Delivery Services that can participate in steering behavior (STEERING and CLIENT_STEERING).
 
@@ -888,7 +888,7 @@ CLIENT_STEERING
 
 Use Multi-Site Origin Feature
 -----------------------------
-A boolean value that indicates whether or not this Delivery Service serves content for an :term:`origin` that provides content from two or more redundant servers. There are very few good reasons for this to not be ``false``. When ``true``, Traffic Ops will configure :term:`Mid-tier cache servers` to perform load-balancing and other optimizations for redundant :term:`origin servers`.
+A boolean value that indicates whether or not this Delivery Service serves content for an :term:`Origin` that provides content from two or more redundant servers. There are very few good reasons for this to not be ``false``. When ``true``, Traffic Ops will configure :term:`Mid-tier cache servers` to perform load-balancing and other optimizations for redundant :term:`origin servers`.
 
 Naturally, this assumes that each redundant server is exactly identical, from request paths to actual content. If Multi-Site Origin is configured for servers that are *not* identical, the client's experience is undefined. Furthermore, the :term:`origin servers` may have differing IP addresses, but **must** serve content for a single :abbr:`FQDN (Fully Qualified Domain Name)` - as defined by the Delivery Service's `Origin Server Base URL`_. These redundant servers **must** be configured a [...]
 
diff --git a/docs/source/overview/profiles_and_parameters.rst b/docs/source/overview/profiles_and_parameters.rst
index 6532d73..9b90d5d 100644
--- a/docs/source/overview/profiles_and_parameters.rst
+++ b/docs/source/overview/profiles_and_parameters.rst
@@ -100,7 +100,7 @@ LOGSTASH_PROFILE
 	.. warning:: For legacy reasons, the names of Profiles of this type *must* begin with ``LOGSTASH_``. This is **not** enforced by the :ref:`to-api` or Traffic Portal, but certain Traffic Control operations/components expect this and will fail to work otherwise!
 
 ORG_PROFILE
-	A Profile that may be used by either :term:`origin servers` or :term:`origins` (no, they aren't the same thing).
+	A Profile that may be used by either :term:`origin servers` or :term:`Origins` (no, they aren't the same thing).
 
 	.. warning:: For legacy reasons, the names of Profiles of this type *must* begin with ``MSO``, or contain either ``ORG`` or ``ORIGIN`` anywhere in the name. This is **not** enforced by the :ref:`to-api` or Traffic Portal, but certain Traffic Control operations/components expect this and will fail to work otherwise!
 
@@ -314,7 +314,7 @@ Config Files that match this pattern - where ``anything`` is zero or more charac
 
 hosting.config
 ''''''''''''''
-This configuration file is mainly generated based on the assignments of :term:`cache servers` to :term:`Delivery Services` and the :term:`Cache Group` hierarchy, but there are a couple of Parameter :ref:`Names <parameter-name>` that can affect it when assigned to this Config File. When a Parameter assigned to the ``storage.config`` Config File - **NOT this Config File** - with the :ref:`parameter-name` "RAM_Drive_Prefix" *exists*, it will cause lines to be generated in this configuration [...]
+This configuration file is mainly generated based on the assignments of :term:`cache servers` to :term:`Delivery Services` and the :term:`Cache Group` hierarchy, but there are a couple of Parameter :ref:`Names <parameter-name>` that can affect it when assigned to this Config File. When a Parameter assigned to the ``storage.config`` Config File - **NOT this Config File** - with the :ref:`parameter-name` "RAM_Drive_Prefix" *exists*, it will cause lines to be generated in this configuration [...]
 
 .. caution:: If a Parameter with Config File ``storage.config`` and :ref:`parameter-name` "RAM_Drive_Prefix" does *not* exist on a :ref:`Profile <profiles>`, then the :term:`cache servers` using that :ref:`Profile <profiles>` will **be incapable of serving traffic for** :term:`Delivery Services` **of the aforementioned** :ref:`Types <ds-types>`, **even when a** :ref:`"location" <parameter-name-location>` **Parameter exists**.
 
diff --git a/docs/source/overview/traffic_portal.rst b/docs/source/overview/traffic_portal.rst
index 2e22bb3..1ae20e6 100644
--- a/docs/source/overview/traffic_portal.rst
+++ b/docs/source/overview/traffic_portal.rst
@@ -25,6 +25,6 @@ Features
 - CDN Monitoring
 - CDN Administration
 - :term:`Delivery Service` Configuration
-- :term:`Cache Server` Maintenance
+- :term:`cache server` Maintenance
 
 .. seealso:: See :ref:`usingtrafficportal` for an overview of the Traffic Portal UI.
diff --git a/docs/source/overview/traffic_router.rst b/docs/source/overview/traffic_router.rst
index 697565c..3773c08 100644
--- a/docs/source/overview/traffic_router.rst
+++ b/docs/source/overview/traffic_router.rst
@@ -32,7 +32,7 @@ Traffic routing options are often configured at the :term:`Delivery Service` lev
 
 DNS Content Routing
 ===================
-For a DNS :term:`Delivery Service` the client might receive a URL such as ``http://video.demo1.mycdn.ciab.test/``. When the :abbr:`LDNS (Local Domain Name Server)` is resolving this ``video.demo1.mycdn.ciab.test`` hostname to an IP address, it ends at Traffic Router because it is the authoritative DNS server for ``mycdn.ciab.test`` and the domains below it, and subsequently responds with a list of IP addresses from the eligible :term:`cache servers` based on the location of the :abbr:`LD [...]
+For a DNS :term:`Delivery Service` the client might receive a URL such as ``http://video.demo1.mycdn.ciab.test/``. When the :abbr:`LDNS (Local Domain Name Server)` is resolving this ``video.demo1.mycdn.ciab.test`` hostname to an IP address, it ends at Traffic Router because it is the authoritative DNS server for ``mycdn.ciab.test`` and the domains below it, and subsequently responds with a list of IP addresses from the eligible :term:`cache servers` based on the location of the :abbr:`LD [...]
 
 .. _http-cr:
 
@@ -50,7 +50,7 @@ For an HTTP :term:`Delivery Service` the client might receive a URL such as ``ht
 Traffic Router uses an HTTP ``302 Found`` response to redirect the client to the best :term:`cache server`.
 
 .. code-block:: http
-	:caption: Traffic Router Redirect to Edge-tier :term:`Cache Server`
+	:caption: Traffic Router Redirect to Edge-tier :term:`cache server`
 
 	HTTP/1.1 302 Found
 	Location: http://edge.demo1.mycdn.ciab.test/
diff --git a/lib/go-tc/cachegroups.go b/lib/go-tc/cachegroups.go
index f073d70..7b9d306 100644
--- a/lib/go-tc/cachegroups.go
+++ b/lib/go-tc/cachegroups.go
@@ -95,4 +95,3 @@ type CachegroupQueueUpdatesRequest struct {
 	CDN    *CDNName         `json:"cdn"`
 	CDNID  *util.JSONIntStr `json:"cdnId"`
 }
-
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index 9d8398e..0904a52 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -3,6 +3,7 @@ package tc
 import (
 	"database/sql/driver"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"time"
@@ -29,7 +30,17 @@ import (
  * under the License.
  */
 
-// ServersResponse is a list of Servers as a response.
+// ServersV3Response is the format of a response to a GET request for /servers.
+type ServersV3Response struct {
+	Response []ServerNullable `json:"response"`
+	Summary  struct {
+		Count uint64 `json:"count"`
+	} `json:"summary"`
+	Alerts
+}
+
+// ServersResponse is a list of Servers as a response to an API v2 request.
+// This can't change because it will break ORT. Unfortunately.
 type ServersResponse struct {
 	Response []Server `json:"response"`
 	Alerts
@@ -70,23 +81,23 @@ type ServersV3DetailResponse struct {
 	Alerts
 }
 
-// ServerIpAddress is the data associated with a server's interface's IP address.
-type ServerIpAddress struct {
+// ServerIPAddress is the data associated with a server's interface's IP address.
+type ServerIPAddress struct {
 	Address        string  `json:"address" db:"address"`
 	Gateway        *string `json:"gateway" db:"gateway"`
-	ServiceAddress bool    `json:"service_address" db:"service_address"`
+	ServiceAddress bool    `json:"serviceAddress" db:"service_address"`
 }
 
 // ServerInterfaceInfo is the data associated with a server's interface.
 type ServerInterfaceInfo struct {
-	IpAddresses  []ServerIpAddress `json:"ipAddresses" db:"ipAddresses"`
-	MaxBandwidth *int64            `json:"maxBandwidth" db:"max_bandwidth"`
+	IPAddresses  []ServerIPAddress `json:"ipAddresses" db:"ip_addresses"`
+	MaxBandwidth *uint64           `json:"maxBandwidth" db:"max_bandwidth"`
 	Monitor      bool              `json:"monitor" db:"monitor"`
 	MTU          *uint64           `json:"mtu" db:"mtu"`
 	Name         string            `json:"name" db:"name"`
 }
 
-// Value implements the database/sql/driver.Valuer interface.
+// Value implements the driver.Valuer interface
 // marshals struct to json to pass back as a json.RawMessage
 func (sii *ServerInterfaceInfo) Value() (driver.Value, error) {
 	b, err := json.Marshal(sii)
@@ -115,6 +126,61 @@ type LegacyInterfaceDetails struct {
 	IPNetmask     *string `json:"ipNetmask" db:"ip_netmask"`
 }
 
+// ToInterfaces converts a LegacyInterfaceDetails to a slice of
+// ServerInterfaceInfo structures. No interfaces will be marked for monitoring,
+// and it will generate service addresses according to the passed indicators
+// for each address family.
+func (lid *LegacyInterfaceDetails) ToInterfaces(ipv4IsService, ipv6IsService bool) ([]ServerInterfaceInfo, error) {
+	var iface ServerInterfaceInfo
+	if lid.InterfaceMtu == nil {
+		return nil, errors.New("interfaceMtu is null")
+	}
+	mtu := uint64(*lid.InterfaceMtu)
+	iface.MTU = &mtu
+
+	if lid.InterfaceName == nil {
+		return nil, errors.New("interfaceName is null")
+	}
+	iface.Name = *lid.InterfaceName
+
+	var ips []ServerIPAddress
+	if lid.IPAddress != nil && *lid.IPAddress != "" {
+		if lid.IPGateway != nil && *lid.IPGateway == "" {
+			lid.IPGateway = nil
+		}
+
+		ipStr := *lid.IPAddress
+		if lid.IPNetmask != nil && *lid.IPNetmask != "" {
+			mask := net.ParseIP(*lid.IPNetmask).To4()
+			if mask == nil {
+				return nil, fmt.Errorf("Failed to parse netmask '%s'", *lid.IPNetmask)
+			}
+			cidr, _ := net.IPv4Mask(mask[0], mask[1], mask[2], mask[3]).Size()
+			ipStr = fmt.Sprintf("%s/%d", ipStr, cidr)
+		}
+
+		ips = append(ips, ServerIPAddress{
+			Address:        ipStr,
+			Gateway:        lid.IPGateway,
+			ServiceAddress: ipv4IsService,
+		})
+	}
+
+	if lid.IP6Address != nil && *lid.IP6Address != "" {
+		if lid.IP6Gateway != nil && *lid.IP6Gateway == "" {
+			lid.IP6Gateway = nil
+		}
+		ips = append(ips, ServerIPAddress{
+			Address:        *lid.IP6Address,
+			Gateway:        lid.IP6Gateway,
+			ServiceAddress: ipv6IsService,
+		})
+	}
+
+	iface.IPAddresses = ips
+	return []ServerInterfaceInfo{iface}, nil
+}
+
 // InterfaceInfoToLegacyInterfaces converts a ServerInterfaceInfo to an
 // equivalent LegacyInterfaceDetails structure. It does this by creating the
 // IP address fields using the "service" interface's IP addresses. All others
@@ -123,13 +189,8 @@ func InterfaceInfoToLegacyInterfaces(serverInterfaces []ServerInterfaceInfo) (Le
 	var legacyDetails LegacyInterfaceDetails
 
 	for _, intFace := range serverInterfaces {
-		if intFace.MTU != nil {
-			legacyDetails.InterfaceMtu = util.IntPtr(int(*intFace.MTU))
-		}
-
-		legacyDetails.InterfaceName = &intFace.Name
 
-		for _, addr := range intFace.IpAddresses {
+		for _, addr := range intFace.IPAddresses {
 			if !addr.ServiceAddress {
 				continue
 			}
@@ -137,18 +198,40 @@ func InterfaceInfoToLegacyInterfaces(serverInterfaces []ServerInterfaceInfo) (Le
 			address := addr.Address
 			gateway := addr.Gateway
 
-			parsedIp, mask, err := net.ParseCIDR(address)
+			var parsedIp net.IP
+			var mask *net.IPNet
+			var err error
+			parsedIp, mask, err = net.ParseCIDR(address)
 			if err != nil {
-				return legacyDetails, fmt.Errorf("Failed to parse '%s' as network or CIDR string: %v", address, err)
+				parsedIp = net.ParseIP(address)
+				if parsedIp == nil {
+					return legacyDetails, fmt.Errorf("Failed to parse '%s' as network or CIDR string: %v", address, err)
+				}
 			}
 
 			if parsedIp.To4() == nil {
 				legacyDetails.IP6Address = &address
 				legacyDetails.IP6Gateway = gateway
-			} else {
+			} else if mask != nil {
 				legacyDetails.IPAddress = util.StrPtr(parsedIp.String())
 				legacyDetails.IPGateway = gateway
 				legacyDetails.IPNetmask = util.StrPtr(fmt.Sprintf("%d.%d.%d.%d", mask.Mask[0], mask.Mask[1], mask.Mask[2], mask.Mask[3]))
+			} else {
+				legacyDetails.IPAddress = util.StrPtr(parsedIp.String())
+				legacyDetails.IPGateway = gateway
+				legacyDetails.IPNetmask = new(string)
+			}
+
+			if intFace.MTU != nil {
+				legacyDetails.InterfaceMtu = util.IntPtr(int(*intFace.MTU))
+			}
+
+			legacyDetails.InterfaceName = &intFace.Name
+
+			// we can jump out here since servers can only legally have one
+			// IPv4 and one IPv6 service address
+			if legacyDetails.IPAddress != nil && *legacyDetails.IPAddress != "" && legacyDetails.IP6Address != nil && *legacyDetails.IP6Address != "" {
+				return legacyDetails, nil
 			}
 		}
 	}
@@ -256,7 +339,9 @@ type ServerV1 struct {
 	XMPPPasswd       string              `json:"xmppPasswd" db:"xmpp_passwd"`
 }
 
-type ServerNullableV11 struct {
+// CommonServerProperties is just the collection of properties which are
+// shared by all servers across API versions.
+type CommonServerProperties struct {
 	Cachegroup       *string              `json:"cachegroup" db:"cachegroup"`
 	CachegroupID     *int                 `json:"cachegroupId" db:"cachegroup_id"`
 	CDNID            *int                 `json:"cdnId" db:"cdn_id"`
@@ -274,13 +359,6 @@ type ServerNullableV11 struct {
 	ILOIPNetmask     *string              `json:"iloIpNetmask" db:"ilo_ip_netmask"`
 	ILOPassword      *string              `json:"iloPassword" db:"ilo_password"`
 	ILOUsername      *string              `json:"iloUsername" db:"ilo_username"`
-	InterfaceMtu     *int                 `json:"interfaceMtu" db:"interface_mtu"`
-	InterfaceName    *string              `json:"interfaceName" db:"interface_name"`
-	IP6Address       *string              `json:"ip6Address" db:"ip6_address"`
-	IP6Gateway       *string              `json:"ip6Gateway" db:"ip6_gateway"`
-	IPAddress        *string              `json:"ipAddress" db:"ip_address"`
-	IPGateway        *string              `json:"ipGateway" db:"ip_gateway"`
-	IPNetmask        *string              `json:"ipNetmask" db:"ip_netmask"`
 	LastUpdated      *TimeNoMod           `json:"lastUpdated" db:"last_updated"`
 	MgmtIPAddress    *string              `json:"mgmtIpAddress" db:"mgmt_ip_address"`
 	MgmtIPGateway    *string              `json:"mgmtIpGateway" db:"mgmt_ip_gateway"`
@@ -305,12 +383,48 @@ type ServerNullableV11 struct {
 	XMPPPasswd       *string              `json:"xmppPasswd" db:"xmpp_passwd"`
 }
 
-type ServerNullable struct {
+// ServerNullableV11 is a server as it appeared in API version 1.1.
+type ServerNullableV11 struct {
+	LegacyInterfaceDetails
+	CommonServerProperties
+}
+
+// ServerNullableV2 is a server as it appeared in API v2.
+type ServerNullableV2 struct {
 	ServerNullableV11
 	IPIsService  *bool `json:"ipIsService" db:"ip_address_is_service"`
 	IP6IsService *bool `json:"ip6IsService" db:"ip6_address_is_service"`
 }
 
+// ServerNullable represents an ATC server, as returned by the TO API.
+type ServerNullable struct {
+	CommonServerProperties
+	Interfaces []ServerInterfaceInfo `json:"interfaces" db:"interfaces"`
+}
+
+// ToServerV2 converts the server to an equivalent ServerNullableV2 structure,
+// if possible. If the conversion could not be performed, an error is returned.
+func (s *ServerNullable) ToServerV2() (ServerNullableV2, error) {
+	legacyServer := ServerNullableV2{
+		ServerNullableV11: ServerNullableV11{
+			CommonServerProperties: s.CommonServerProperties,
+		},
+		IPIsService:  new(bool),
+		IP6IsService: new(bool),
+	}
+
+	var err error
+	legacyServer.LegacyInterfaceDetails, err = InterfaceInfoToLegacyInterfaces(s.Interfaces)
+	if err != nil {
+		return legacyServer, err
+	}
+
+	*legacyServer.IPIsService = legacyServer.LegacyInterfaceDetails.IPAddress != nil && *legacyServer.LegacyInterfaceDetails.IPAddress != ""
+	*legacyServer.IP6IsService = legacyServer.LegacyInterfaceDetails.IP6Address != nil && *legacyServer.LegacyInterfaceDetails.IP6Address != ""
+
+	return legacyServer, nil
+}
+
 type ServerUpdateStatus struct {
 	HostName           string `json:"host_name"`
 	UpdatePending      bool   `json:"upd_pending"`
diff --git a/lib/go-tc/servers_test.go b/lib/go-tc/servers_test.go
new file mode 100644
index 0000000..443c585
--- /dev/null
+++ b/lib/go-tc/servers_test.go
@@ -0,0 +1,58 @@
+package tc
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import "fmt"
+
+func ExampleLegacyInterfaceDetails_ToInterfaces() {
+	lid := LegacyInterfaceDetails{
+		InterfaceMtu:  new(int),
+		InterfaceName: new(string),
+		IP6Address:    new(string),
+		IP6Gateway:    new(string),
+		IPAddress:     new(string),
+		IPGateway:     new(string),
+		IPNetmask:     new(string),
+	}
+	*lid.InterfaceMtu = 9000
+	*lid.InterfaceName = "test"
+	*lid.IP6Address = "::14/64"
+	*lid.IP6Gateway = "::15"
+	*lid.IPAddress = "1.2.3.4"
+	*lid.IPGateway = "4.3.2.1"
+	*lid.IPNetmask = "255.255.255.252"
+
+	ifaces, err := lid.ToInterfaces(true, false)
+	if err != nil {
+		fmt.Printf(err.Error())
+		return
+	}
+
+	for _, iface := range ifaces {
+		fmt.Printf("name=%s, monitor=%t\n", iface.Name, iface.Monitor)
+		for _, ip := range iface.IPAddresses {
+			fmt.Printf("\taddr=%s, gateway=%s, service address=%t\n", ip.Address, *ip.Gateway, ip.ServiceAddress)
+		}
+	}
+	// Output: name=test, monitor=false
+	// 	addr=1.2.3.4/30, gateway=4.3.2.1, service address=true
+	// 	addr=::14/64, gateway=::15, service address=false
+	//
+}
diff --git a/traffic_ops/client/capability.go b/traffic_ops/client/capability.go
index cb544a4..43bae24 100644
--- a/traffic_ops/client/capability.go
+++ b/traffic_ops/client/capability.go
@@ -39,7 +39,7 @@ func (to *Session) GetCapabilities() ([]tc.Capability, ReqInf, error) {
 
 // GetCapability retrieves only the capability named 'c'.
 func (to *Session) GetCapability(c string) (tc.Capability, ReqInf, error) {
-	var v url.Values
+	v := url.Values{}
 	v.Add("name", c)
 	endpoint := API_CAPABILITIES + "?" + v.Encode()
 	resp, remoteAddr, err := to.request(http.MethodGet, endpoint, nil)
diff --git a/traffic_ops/client/server.go b/traffic_ops/client/server.go
index 9f688ec..6c60c3d 100644
--- a/traffic_ops/client/server.go
+++ b/traffic_ops/client/server.go
@@ -34,158 +34,142 @@ const (
 	API_SERVER_ASSIGN_DELIVERY_SERVICES = API_SERVER_DELIVERY_SERVICES + "?replace=%t"
 )
 
+func needAndCanFetch(id *int, name *string) bool {
+	return (id == nil || *id == 0) && name != nil && *name != ""
+}
+
 // CreateServer creates a Server.
-func (to *Session) CreateServer(server tc.Server) (tc.Alerts, ReqInf, error) {
+func (to *Session) CreateServer(server tc.ServerNullable) (tc.Alerts, ReqInf, error) {
 
+	var alerts tc.Alerts
 	var remoteAddr net.Addr
 	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
 
-	if server.CachegroupID == 0 && server.Cachegroup != "" {
-		cg, _, err := to.GetCacheGroupNullableByName(server.Cachegroup)
+	if needAndCanFetch(server.CachegroupID, server.Cachegroup) {
+		cg, _, err := to.GetCacheGroupNullableByName(*server.Cachegroup)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no cachegroup named " + server.Cachegroup + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no cachegroup named %s: %v", *server.Cachegroup, err)
 		}
 		if len(cg) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no cachegroup named " + server.Cachegroup)
+			return alerts, reqInf, fmt.Errorf("no cachegroup named %s", *server.Cachegroup)
 		}
 		if cg[0].ID == nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("Cachegroup named " + server.Cachegroup + " has a nil ID")
+			return alerts, reqInf, fmt.Errorf("Cachegroup named %s has a nil ID", *server.Cachegroup)
 		}
-		server.CachegroupID = *cg[0].ID
+		server.CachegroupID = cg[0].ID
 	}
-	if server.CDNID == 0 && server.CDNName != "" {
-		c, _, err := to.GetCDNByName(server.CDNName)
+	if needAndCanFetch(server.CDNID, server.CDNName) {
+		c, _, err := to.GetCDNByName(*server.CDNName)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no CDN named " + server.CDNName + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no CDN named %s: %v", *server.CDNName, err)
 		}
 		if len(c) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no CDN named " + server.CDNName)
+			return alerts, reqInf, fmt.Errorf("no CDN named %s", *server.CDNName)
 		}
-		server.CDNID = c[0].ID
+		server.CDNID = &c[0].ID
 	}
-	if server.PhysLocationID == 0 && server.PhysLocation != "" {
-		ph, _, err := to.GetPhysLocationByName(server.PhysLocation)
+	if needAndCanFetch(server.PhysLocationID, server.PhysLocation) {
+		ph, _, err := to.GetPhysLocationByName(*server.PhysLocation)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no physlocation named " + server.PhysLocation + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no physlocation named %s: %v", *server.PhysLocation, err)
 		}
 		if len(ph) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no physlocation named " + server.PhysLocation)
+			return alerts, reqInf, fmt.Errorf("no physlocation named %s", *server.PhysLocation)
 		}
-		server.PhysLocationID = ph[0].ID
+		server.PhysLocationID = &ph[0].ID
 	}
-	if server.ProfileID == 0 && server.Profile != "" {
-		pr, _, err := to.GetProfileByName(server.Profile)
+	if needAndCanFetch(server.ProfileID, server.Profile) {
+		pr, _, err := to.GetProfileByName(*server.Profile)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no profile named " + server.Profile + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no profile named %s: %v", *server.Profile, err)
 		}
 		if len(pr) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no profile named " + server.Profile)
+			return alerts, reqInf, fmt.Errorf("no profile named %s", *server.Profile)
 		}
-		server.ProfileID = pr[0].ID
+		server.ProfileID = &pr[0].ID
 	}
-	if server.StatusID == 0 && server.Status != "" {
-		st, _, err := to.GetStatusByName(server.Status)
+	if needAndCanFetch(server.StatusID, server.Status) {
+		st, _, err := to.GetStatusByName(*server.Status)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no status named " + server.Status + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no status named %s: %v", *server.Status, err)
 		}
 		if len(st) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no status named " + server.Status)
+			return alerts, reqInf, fmt.Errorf("no status named %s", *server.Status)
 		}
-		server.StatusID = st[0].ID
+		server.StatusID = &st[0].ID
 	}
-	if server.TypeID == 0 && server.Type != "" {
+	if (server.TypeID == nil || *server.TypeID == 0) && server.Type != "" {
 		ty, _, err := to.GetTypeByName(server.Type)
 		if err != nil {
-			return tc.Alerts{}, ReqInf{}, errors.New("no type named " + server.Type + ":" + err.Error())
+			return alerts, reqInf, fmt.Errorf("no type named %s: %v", server.Type, err)
 		}
 		if len(ty) == 0 {
-			return tc.Alerts{}, ReqInf{}, errors.New("no type named " + server.Type)
+			return alerts, reqInf, fmt.Errorf("no type named %s", server.Type)
 		}
-		server.TypeID = ty[0].ID
+		server.TypeID = &ty[0].ID
 	}
+
 	reqBody, err := json.Marshal(server)
 	if err != nil {
-		return tc.Alerts{}, reqInf, err
+		return alerts, reqInf, err
 	}
 
 	resp, remoteAddr, err := to.request(http.MethodPost, API_SERVERS, reqBody)
+	reqInf.RemoteAddr = remoteAddr
 	if err != nil {
-		return tc.Alerts{}, reqInf, err
+		return alerts, reqInf, err
 	}
 	defer resp.Body.Close()
-	var alerts tc.Alerts
+
 	err = json.NewDecoder(resp.Body).Decode(&alerts)
-	return alerts, reqInf, nil
+	return alerts, reqInf, err
 }
 
 // UpdateServerByID updates a Server by ID.
-func (to *Session) UpdateServerByID(id int, server tc.Server) (tc.Alerts, ReqInf, error) {
-
+func (to *Session) UpdateServerByID(id int, server tc.ServerNullable) (tc.Alerts, ReqInf, error) {
+	var alerts tc.Alerts
 	var remoteAddr net.Addr
-	reqBody, err := json.Marshal(server)
 	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+
+	reqBody, err := json.Marshal(server)
 	if err != nil {
-		return tc.Alerts{}, reqInf, err
+		return alerts, reqInf, err
 	}
+
 	route := fmt.Sprintf("%s/%d", API_SERVERS, id)
 	resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody)
+	reqInf.RemoteAddr = remoteAddr
 	if err != nil {
-		return tc.Alerts{}, reqInf, err
+		return alerts, reqInf, err
 	}
 	defer resp.Body.Close()
-	var alerts tc.Alerts
+
 	err = json.NewDecoder(resp.Body).Decode(&alerts)
-	return alerts, reqInf, nil
+	return alerts, reqInf, err
 }
 
 // GetServers returns a list of Servers.
-func (to *Session) GetServers() ([]tc.Server, ReqInf, error) {
-	resp, remoteAddr, err := to.request(http.MethodGet, API_SERVERS, nil)
-	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
-	if err != nil {
-		return nil, reqInf, err
+// The 'params' parameter can be used to optionally pass URL "query string
+// parameters" in the request.
+// It returns, in order, the API response that Traffic Ops returned, a request
+// info object, and any error that occurred.
+func (to *Session) GetServers(params *url.Values) (tc.ServersV3Response, ReqInf, error) {
+	route := API_SERVERS
+	if params != nil {
+		route += "?" + params.Encode()
 	}
-	defer resp.Body.Close()
 
-	var data tc.ServersResponse
-	err = json.NewDecoder(resp.Body).Decode(&data)
-	return data.Response, reqInf, nil
-}
+	var data tc.ServersV3Response
 
-// GetServerByID GETs a Server by the Server ID.
-func (to *Session) GetServerByID(id int) ([]tc.Server, ReqInf, error) {
-	route := fmt.Sprintf("%s?id=%d", API_SERVERS, id)
 	resp, remoteAddr, err := to.request(http.MethodGet, route, nil)
 	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
 	if err != nil {
-		return nil, reqInf, err
-	}
-	defer resp.Body.Close()
-
-	var data tc.ServersResponse
-	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
-		return nil, reqInf, err
-	}
-
-	return data.Response, reqInf, nil
-}
-
-// GetServerByHostName GETs Servers by the Server hostname.
-func (to *Session) GetServerByHostName(hostName string) ([]tc.Server, ReqInf, error) {
-	url := fmt.Sprintf("%s?hostName=%s", API_SERVERS, hostName)
-	resp, remoteAddr, err := to.request(http.MethodGet, url, nil)
-	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
-	if err != nil {
-		return nil, reqInf, err
+		return data, reqInf, err
 	}
 	defer resp.Body.Close()
 
-	var data tc.ServersResponse
-	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
-		return nil, reqInf, err
-	}
-
-	return data.Response, reqInf, nil
+	err = json.NewDecoder(resp.Body).Decode(&data)
+	return data, reqInf, err
 }
 
 // GetServerDetailsByHostName GETs Servers by the Server hostname.
@@ -223,70 +207,58 @@ func (to *Session) DeleteServerByID(id int) (tc.Alerts, ReqInf, error) {
 	return alerts, reqInf, nil
 }
 
-// GetServersByType returns all servers that match the given query parameter filters, NOT
-// - as the name might imply - all servers of a specific type (though type can be specified
-// in the query parameters).
-func (to *Session) GetServersByType(qparams url.Values) ([]tc.Server, ReqInf, error) {
-	url := fmt.Sprintf("%s?%s", API_SERVERS, qparams.Encode())
-	resp, remoteAddr, err := to.request(http.MethodGet, url, nil)
-	reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
-	if err != nil {
-		return nil, reqInf, err
-	}
-	defer resp.Body.Close()
-
-	var data tc.ServersResponse
-	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
-		return nil, reqInf, err
-	}
-
-	return data.Response, reqInf, nil
-}
-
-// GetServerFQDN returns the Fully Qualified Domain Name (FQDN) of the first server found to
-// have the Host Name 'n'.
-func (to *Session) GetServerFQDN(n string) (string, ReqInf, error) {
+// GetServerFQDN returns the Fully Qualified Domain Name (FQDN) of the first
+// server found to have the Host Name 'n'.
+func (to *Session) GetServerFQDN(n string) (string, tc.Alerts, ReqInf, error) {
 	// TODO fix to only request one server
-	fdn := ""
-	servers, reqInf, err := to.GetServers()
+	params := url.Values{}
+	params.Add("hostName", n)
+
+	resp, reqInf, err := to.GetServers(&params)
 	if err != nil {
-		return "Error", reqInf, err
+		return "", resp.Alerts, reqInf, err
 	}
 
-	for _, server := range servers {
-		if server.HostName == n {
-			fdn = fmt.Sprintf("%s.%s", server.HostName, server.DomainName)
+	var fdn string
+	for _, server := range resp.Response {
+		if server.HostName != nil && server.DomainName != nil {
+			fdn = fmt.Sprintf("%s.%s", *server.HostName, *server.DomainName)
 		}
 	}
+
 	if fdn == "" {
-		return "Error", reqInf, fmt.Errorf("No Server %s found", n)
+		err = fmt.Errorf("No Server %s found", n)
 	}
-	return fdn, reqInf, nil
+
+	return fdn, resp.Alerts, reqInf, err
 }
 
-// GetServersShortNameSearch returns all of the Host Names of servers that contain 'shortname'.
-func (to *Session) GetServersShortNameSearch(shortname string) ([]string, ReqInf, error) {
+// GetServersShortNameSearch returns all of the Host Names of servers that
+// contain 'shortname'.
+func (to *Session) GetServersShortNameSearch(shortname string) ([]string, tc.Alerts, ReqInf, error) {
 	var serverlst []string
-	servers, reqInf, err := to.GetServers()
+	resp, reqInf, err := to.GetServers(nil)
 	if err != nil {
-		serverlst = append(serverlst, "N/A")
-		return serverlst, reqInf, err
+		return serverlst, resp.Alerts, reqInf, err
 	}
-	for _, server := range servers {
-		if strings.Contains(server.HostName, shortname) {
-			serverlst = append(serverlst, server.HostName)
+
+	for _, server := range resp.Response {
+		if server.HostName != nil && strings.Contains(*server.HostName, shortname) {
+			serverlst = append(serverlst, *server.HostName)
 		}
 	}
+
 	if len(serverlst) == 0 {
-		serverlst = append(serverlst, "N/A")
-		return serverlst, reqInf, fmt.Errorf("No Servers Found")
+		err = errors.New("No Servers Found")
 	}
-	return serverlst, reqInf, nil
+
+	return serverlst, resp.Alerts, reqInf, err
 }
 
-// AssignDeliveryServiceIDsToServerID assigns a set of Delivery Services to a single server, optionally
-// replacing any and all existing assignments. 'server' should be the requested server's ID, 'dsIDs'
-// should be a slice of the requested Delivery Services' IDs. If 'replace' is 'true', existing
+// AssignDeliveryServiceIDsToServerID assigns a set of Delivery Services to a
+// single server, optionally replacing any and all existing assignments.
+// 'server' should be the requested server's ID, 'dsIDs' should be a slice of
+// the requested Delivery Services' IDs. If 'replace' is 'true', existing
 // assignments to the server will be replaced.
 func (to *Session) AssignDeliveryServiceIDsToServerID(server int, dsIDs []int, replace bool) (tc.Alerts, ReqInf, error) {
 	// datatypes here match the library tc.Server's and tc.DeliveryService's ID fields
diff --git a/traffic_ops/ort/atstccfg/toreqnew/toreqnew.go b/traffic_ops/ort/atstccfg/toreqnew/toreqnew.go
index 9d8f3a8..56bf454 100644
--- a/traffic_ops/ort/atstccfg/toreqnew/toreqnew.go
+++ b/traffic_ops/ort/atstccfg/toreqnew/toreqnew.go
@@ -39,8 +39,8 @@ import (
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	toclient "github.com/apache/trafficcontrol/traffic_ops/v2-client"
 	"github.com/apache/trafficcontrol/traffic_ops/ort/atstccfg/torequtil"
+	toclient "github.com/apache/trafficcontrol/traffic_ops/v2-client"
 )
 
 type TOClient struct {
diff --git a/traffic_ops/testing/api/v1/atsconfig_test.go b/traffic_ops/testing/api/v1/atsconfig_test.go
index 8abea7a..98b96e9 100644
--- a/traffic_ops/testing/api/v1/atsconfig_test.go
+++ b/traffic_ops/testing/api/v1/atsconfig_test.go
@@ -53,12 +53,9 @@ func GetTestATSConfigs(t *testing.T) {
 	}
 
 	serverConfigs := []string{
-		"ip_allow.config",
 		"hosting.config",
 		"packages",
 		"chkconfig",
-		"remap.config",
-		"parent.config",
 	}
 	for _, serverConfig := range serverConfigs {
 		if _, _, err = TOSession.GetATSServerConfig(server.ID, serverConfig); err != nil {
@@ -108,3 +105,81 @@ func GetTestATSConfigs(t *testing.T) {
 		}
 	}
 }
+
+func CreateTestDeliveryServiceServers(t *testing.T) {
+	dses, _, err := TOSession.GetDeliveryServices()
+	if err != nil {
+		t.Errorf("cannot GET DeliveryServices: %v", err)
+	}
+	if len(dses) < 1 {
+		t.Error("GET DeliveryServices returned no dses, must have at least 1 to test ds-servers")
+	}
+
+	servers, _, err := TOSession.GetServers()
+	if err != nil {
+		t.Errorf("cannot GET Servers: %v", err)
+	}
+	if len(servers) < 1 {
+		t.Error("GET Servers returned no servers, must have at least 1 to test ds-servers")
+	}
+
+	for _, ds := range dses {
+		serverIDs := make([]int, 0, len(servers))
+		for _, server := range servers {
+			if server.Type == "EDGE" && server.CDNName == ds.CDNName {
+				serverIDs = append(serverIDs, server.ID)
+			}
+		}
+
+		if len(serverIDs) > 0 {
+			_, err = TOSession.CreateDeliveryServiceServers(ds.ID, serverIDs, true)
+			if err != nil {
+				t.Errorf("POST delivery service servers: %v", err)
+			}
+		}
+	}
+}
+
+// DeleteTestDeliveryServiceServersCreated deletes the dss assignments created by CreateTestDeliveryServiceServers.
+func DeleteTestDeliveryServiceServersCreated(t *testing.T) {
+	// You gotta do this because TOSession.GetDeliveryServiceServers doesn't fetch the complete response.......
+	dssLen := len(testData.Servers) * len(testData.DeliveryServices)
+	dsServers, _, err := TOSession.GetDeliveryServiceServersN(dssLen)
+	if err != nil {
+		t.Fatalf("GET delivery service servers: %v", err)
+	}
+
+	for _, dss := range dsServers.Response {
+		if dss.DeliveryService == nil {
+			t.Error("Found ds-to-server assignment with nil Delivery Service")
+			continue
+		}
+		if dss.Server == nil {
+			t.Error("Found ds-to-server assignment with nil Server")
+			continue
+		}
+
+		_, _, err := TOSession.DeleteDeliveryServiceServer(*dss.DeliveryService, *dss.Server)
+		if err != nil {
+			t.Errorf("Failed to remove assignment of server #%d to DS #%d: %v", *dss.Server, *dss.DeliveryService, err)
+		}
+	}
+
+	dsServers, _, err = TOSession.GetDeliveryServiceServersN(dssLen)
+	if err != nil {
+		t.Fatalf("GET delivery service servers: %v", err)
+	}
+
+	for _, dss := range dsServers.Response {
+		if dss.DeliveryService == nil {
+			t.Error("Found ds-to-server assignment (after supposed deletion) with nil DeliveryService")
+			continue
+		}
+		if dss.Server == nil {
+			t.Error("Found ds-to-server assignment (after supposed deletion) with nil Server")
+			continue
+		}
+
+		t.Errorf("Found ds-to-server assignment {DSID: %d, Server: %d} after deletion", *dss.DeliveryService, *dss.Server)
+	}
+}
diff --git a/traffic_ops/testing/api/v1/ip_allow_dot_config_test.go b/traffic_ops/testing/api/v1/ip_allow_dot_config_test.go
deleted file mode 100644
index 61961a3..0000000
--- a/traffic_ops/testing/api/v1/ip_allow_dot_config_test.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package v1
-
-/*
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-   http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-*/
-
-import (
-	"fmt"
-	"net/url"
-	"strings"
-	"testing"
-
-	"github.com/apache/trafficcontrol/lib/go-util"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
-)
-
-const ipAllow = "ip_allow.config"
-
-var (
-	expectedRules = []string{
-		"src_ip=127.0.0.1 action=ip_allow method=ALL\n",
-		"src_ip=::1 action=ip_allow method=ALL\n",
-	}
-	midExpectedRules = []string{
-		"src_ip=10.0.0.0-10.255.255.255 action=ip_allow method=ALL\n",
-		"src_ip=172.16.0.0-172.31.255.255 action=ip_allow method=ALL\n",
-		"src_ip=192.168.0.0-192.168.255.255 action=ip_allow method=ALL\n",
-		"src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=ALL\n",
-		"src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=ALL\n",
-	}
-	edgeExpectedRules = []string{
-		"src_ip=0.0.0.0-255.255.255.255 action=ip_deny method=PUSH|PURGE|DELETE\n",
-		"src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=PUSH|PURGE|DELETE\n",
-	}
-	rascalServerIP = ""
-	rascalRule     = "src_ip=%v action=ip_allow method=ALL"
-)
-
-func TestIPAllowDotConfig(t *testing.T) {
-	WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, DeliveryServices}, func() {
-		rascalServerIP = getServer(t, "RASCAL").IPAddress
-		// rascalRule = fmt.Sprintf("src_ip=%v action=ip_allow method=ALL", rascalServer.IPAddress)
-		GetTestIPAllowDotConfig(t)
-		GetTestIPAllowMidDotConfig(t)
-	})
-}
-
-func GetTestIPAllowDotConfig(t *testing.T) {
-	// Get edge server
-	s := getServer(t, "EDGE")
-	output, _, err := TOSession.GetATSServerConfig(s.ID, ipAllow)
-	if err != nil {
-		t.Fatalf("cannot GET server %v config %v: %v", s.HostName, ipAllow, err)
-	}
-	for _, r := range append(expectedRules, edgeExpectedRules...) {
-		if !strings.Contains(output, r) {
-			t.Errorf("expected rule %v not found in ip_allow config", r)
-		}
-	}
-	// Make sure edge does not contain rule for rascal server
-	exists, ipRange := getIPRule(output, rascalServerIP)
-	rascalRule := fmt.Sprintf(rascalRule, ipRange)
-	if exists && strings.Contains(output, rascalRule) {
-		t.Errorf("rascal IP was not supposed to be in an allowed rule: %v", rascalRule)
-	}
-}
-
-func GetTestIPAllowMidDotConfig(t *testing.T) {
-	// Get mid server
-	s := getServer(t, "MID")
-	output, _, err := TOSession.GetATSServerConfig(s.ID, ipAllow)
-	if err != nil {
-		t.Errorf("cannot GET server %v config %v: %v", s.HostName, ipAllow, err)
-	}
-	for _, r := range append(expectedRules, midExpectedRules...) {
-		if !strings.Contains(output, r) {
-			t.Errorf("expected rule %v not found in ip_allow config", r)
-		}
-	}
-
-	// Make sure mid contains an allowed rule that includes the rascal server
-	exists, ipRange := getIPRule(output, rascalServerIP)
-	rascalRule := fmt.Sprintf(rascalRule, ipRange)
-	if !(exists && strings.Contains(output, rascalRule)) {
-		t.Errorf("expected rascal to be include as allowed in mid ip allow config")
-	}
-}
-
-func getServer(t *testing.T, serverType string) tc.ServerV1 {
-	v := url.Values{}
-	v.Add("type", serverType)
-	servers, _, err := TOSession.GetServersByType(v)
-	if err != nil {
-		t.Fatalf("cannot GET Server by type %v: %v", serverType, err)
-	}
-	if len(servers) == 0 {
-		t.Fatalf("cannot find any Servers by type %v", serverType)
-	}
-	return servers[0]
-}
-
-// getIPRuleRange returns if the given IP is included in the set of rules and which ip range it is included in
-func getIPRule(rules, ip string) (bool, string) {
-	for _, r := range strings.Split(rules, "\n")[1:] {
-		if !strings.Contains(r, "src_ip") {
-			continue
-		}
-		ipRange := r[7:strings.IndexAny(r, " ")]
-		if exists, _ := util.IP4InRange(ip, ipRange); exists {
-			return true, ipRange
-		}
-	}
-	return false, ""
-}
diff --git a/traffic_ops/testing/api/v1/parentdotconfig_test.go b/traffic_ops/testing/api/v1/parentdotconfig_test.go
deleted file mode 100644
index b39a740..0000000
--- a/traffic_ops/testing/api/v1/parentdotconfig_test.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package v1
-
-/*
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-   http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-*/
-
-import (
-	"net/url"
-	"strconv"
-	"strings"
-	"testing"
-)
-
-func TestParentDotConfig(t *testing.T) {
-	WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, DeliveryServices}, func() {
-		defer DeleteTestDeliveryServiceServersCreated(t)
-		CreateTestDeliveryServiceServers(t)
-		GetTestParentDotConfig(t)
-	})
-}
-
-func GetTestParentDotConfig(t *testing.T) {
-	dsServers, _, err := TOSession.GetDeliveryServiceServers()
-	if err != nil {
-		t.Fatalf("GET delivery service servers: %v", err)
-	}
-	if len(dsServers.Response) == 0 {
-		t.Fatal("GET delivery service servers: no servers found")
-	}
-
-	dss := dsServers.Response[0]
-
-	if dss.Server == nil {
-		t.Fatal("GET delivery service servers: returned nil server")
-	}
-	if dss.DeliveryService == nil {
-		t.Fatal("GET delivery service servers: returned nil ds")
-	}
-
-	ds, _, err := TOSession.GetDeliveryService(strconv.Itoa(*dss.DeliveryService))
-	if err != nil {
-		t.Fatalf("Getting ds %+v: %v", *dss.DeliveryService, err)
-	}
-	if ds == nil {
-		t.Fatalf("Getting ds %+v: got nil response", *dss.DeliveryService)
-	}
-	if ds.OrgServerFQDN == "" {
-		t.Fatalf("Getting ds %+v: got empty ds.OrgServerFQDN", *dss.DeliveryService)
-	}
-
-	originURI, err := url.Parse(ds.OrgServerFQDN)
-	if err != nil {
-		t.Fatalf("Getting ds %+v: ds.OrgServerFQDN '%v' failed to parse as a URL: %v", *dss.DeliveryService, ds.OrgServerFQDN, err)
-	}
-	originHost := originURI.Hostname()
-
-	parentDotConfig, _, err := TOSession.GetATSServerConfig(*dss.Server, "parent.config")
-	if err != nil {
-		t.Fatalf("Getting server %v config parent.config: %v", *dss.Server, err)
-	}
-
-	if !strings.Contains(parentDotConfig, originHost) {
-		t.Errorf("expected: parent.config to contain delivery service origin FQDN '%v' host '%v', actual:\n'''\n%+v\n'''", ds.OrgServerFQDN, originHost, parentDotConfig)
-	}
-}
-
-func CreateTestDeliveryServiceServers(t *testing.T) {
-	dses, _, err := TOSession.GetDeliveryServices()
-	if err != nil {
-		t.Errorf("cannot GET DeliveryServices: %v", err)
-	}
-	if len(dses) < 1 {
-		t.Error("GET DeliveryServices returned no dses, must have at least 1 to test ds-servers")
-	}
-
-	servers, _, err := TOSession.GetServers()
-	if err != nil {
-		t.Errorf("cannot GET Servers: %v", err)
-	}
-	if len(servers) < 1 {
-		t.Error("GET Servers returned no servers, must have at least 1 to test ds-servers")
-	}
-
-	for _, ds := range dses {
-		serverIDs := make([]int, 0, len(servers))
-		for _, server := range servers {
-			if server.Type == "EDGE" && server.CDNName == ds.CDNName {
-				serverIDs = append(serverIDs, server.ID)
-			}
-		}
-
-		if len(serverIDs) > 0 {
-			_, err = TOSession.CreateDeliveryServiceServers(ds.ID, serverIDs, true)
-			if err != nil {
-				t.Errorf("POST delivery service servers: %v", err)
-			}
-		}
-	}
-}
-
-// DeleteTestDeliveryServiceServersCreated deletes the dss assignments created by CreateTestDeliveryServiceServers.
-func DeleteTestDeliveryServiceServersCreated(t *testing.T) {
-	// You gotta do this because TOSession.GetDeliveryServiceServers doesn't fetch the complete response.......
-	dssLen := len(testData.Servers) * len(testData.DeliveryServices)
-	dsServers, _, err := TOSession.GetDeliveryServiceServersN(dssLen)
-	if err != nil {
-		t.Fatalf("GET delivery service servers: %v", err)
-	}
-
-	for _, dss := range dsServers.Response {
-		if dss.DeliveryService == nil {
-			t.Error("Found ds-to-server assignment with nil Delivery Service")
-			continue
-		}
-		if dss.Server == nil {
-			t.Error("Found ds-to-server assignment with nil Server")
-			continue
-		}
-
-		_, _, err := TOSession.DeleteDeliveryServiceServer(*dss.DeliveryService, *dss.Server)
-		if err != nil {
-			t.Errorf("Failed to remove assignment of server #%d to DS #%d: %v", *dss.Server, *dss.DeliveryService, err)
-		}
-	}
-
-	dsServers, _, err = TOSession.GetDeliveryServiceServersN(dssLen)
-	if err != nil {
-		t.Fatalf("GET delivery service servers: %v", err)
-	}
-
-	for _, dss := range dsServers.Response {
-		if dss.DeliveryService == nil {
-			t.Error("Found ds-to-server assignment (after supposed deletion) with nil DeliveryService")
-			continue
-		}
-		if dss.Server == nil {
-			t.Error("Found ds-to-server assignment (after supposed deletion) with nil Server")
-			continue
-		}
-
-		t.Errorf("Found ds-to-server assignment {DSID: %d, Server: %d} after deletion", *dss.DeliveryService, *dss.Server)
-	}
-}
diff --git a/traffic_ops/testing/api/v1/remapdotconfig_test.go b/traffic_ops/testing/api/v1/remapdotconfig_test.go
deleted file mode 100644
index 4e7e229..0000000
--- a/traffic_ops/testing/api/v1/remapdotconfig_test.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package v1
-
-/*
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-   http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
-*/
-
-import (
-	"net/url"
-	"strconv"
-	"strings"
-	"testing"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
-)
-
-func TestRemapDotConfig(t *testing.T) {
-	WithObjs(t, []TCObj{CDNs, Types, Tenants, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, DeliveryServices}, func() {
-		defer DeleteTestDeliveryServiceServersCreated(t)
-		CreateTestDeliveryServiceServers(t)
-		GetTestRemapDotConfig(t)
-	})
-}
-
-func GetTestRemapDotConfig(t *testing.T) {
-	dsServers, _, err := TOSession.GetDeliveryServiceServers()
-	if err != nil {
-		t.Fatalf("GET delivery service servers: %v", err)
-	}
-	if len(dsServers.Response) == 0 {
-		t.Fatal("GET delivery service servers: no servers found")
-	}
-
-	var ds *tc.DeliveryService
-	var serverID int
-	for _, dsServer := range dsServers.Response {
-		if dsServer.Server == nil {
-			t.Error("Found DS-Server assignment with nil server")
-			continue
-		}
-		if dsServer.DeliveryService == nil {
-			t.Error("Found DS-Server assignment with nil Delivery Service")
-			continue
-		}
-
-		serverID = *dsServer.Server
-
-		ds, _, err = TOSession.GetDeliveryService(strconv.Itoa(*dsServer.DeliveryService))
-		if err != nil {
-			t.Errorf("Getting ds %+v: %v", *dsServer.DeliveryService, err)
-			continue
-		}
-		if ds == nil {
-			t.Errorf("Getting ds %+v: got nil response", *dsServer.DeliveryService)
-			continue
-		}
-		if ds.OrgServerFQDN == "" {
-			t.Errorf("Getting ds %+v: got empty ds.OrgServerFQDN", *dsServer.DeliveryService)
-			continue
-		}
-
-		if ds.Type != tc.DSTypeAnyMap {
-			break
-		}
-	}
-
-	if ds == nil || ds.XMLID == "" {
-		t.Fatal("no Delivery Service found with assigned servers that isn't an ANY_MAP service, can't test remap.config")
-	}
-
-	originURI, err := url.Parse(ds.OrgServerFQDN)
-	if err != nil {
-		t.Fatalf("Getting ds %+v: ds.OrgServerFQDN '%+v' failed to parse as a URL: %+v", ds.XMLID, ds.OrgServerFQDN, err)
-	}
-	originHost := originURI.Hostname()
-
-	remapDotConfig, _, err := TOSession.GetATSServerConfig(serverID, "remap.config")
-	if err != nil {
-		t.Fatalf("Getting server %+v config remap.config: %v", serverID, err)
-	}
-
-	if !strings.Contains(remapDotConfig, originHost) {
-		t.Errorf("expected: remap.config to contain delivery service origin FQDN '%v' host '%v', actual:\n'''\n%v\n'''", ds.OrgServerFQDN, originHost, remapDotConfig)
-	}
-
-	remapDotConfigLines := strings.Split(remapDotConfig, "\n")
-	for i, line := range remapDotConfigLines {
-		line = strings.TrimSpace(line)
-		if len(line) > 0 && line[0] != '#' && !strings.HasPrefix(line, "map") {
-			t.Errorf("expected: remap.config line %v to start with 'map', actual: '%v'", i, line)
-		}
-	}
-}
diff --git a/traffic_ops/testing/api/v1/serverupdatestatus_test.go b/traffic_ops/testing/api/v1/serverupdatestatus_test.go
index 3a2bced..b326a5b 100644
--- a/traffic_ops/testing/api/v1/serverupdatestatus_test.go
+++ b/traffic_ops/testing/api/v1/serverupdatestatus_test.go
@@ -86,16 +86,16 @@ func TestServerUpdateStatus(t *testing.T) {
 		// assert that updates were queued for the proper EDGE servers
 		getServers()
 		if !edge1cdn1.UpdPending {
-			t.Errorf("expected: child %s to have updates pending, actual: no updates pending", edge1cdn1.HostName)
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: no updates pending", edge1cdn1.HostName, edge1cdn1.ID)
 		}
 		if !edge2cdn1.UpdPending {
-			t.Errorf("expected: child %s to have updates pending, actual: no updates pending", edge2cdn1.HostName)
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: no updates pending", edge2cdn1.HostName, edge2cdn1.ID)
 		}
 		if mid1cdn1.UpdPending {
-			t.Errorf("expected: server %s with updated status to have no updates pending, actual: updates pending", mid1cdn1.HostName)
+			t.Errorf("expected: server %s (%d) with updated status to have no updates pending, actual: updates pending", mid1cdn1.HostName, mid1cdn1.ID)
 		}
 		if edge1cdn2.UpdPending {
-			t.Errorf("expected: server %s in different CDN than server with updated status to have no updates pending, actual: updates pending", edge2cdn1.HostName)
+			t.Errorf("expected: server %s (%d) in different CDN than server with updated status to have no updates pending, actual: updates pending", edge1cdn2.HostName, edge1cdn2.ID)
 		}
 
 		// update status of MID server to OFFLINE via status ID
diff --git a/traffic_ops/testing/api/v1/todb_test.go b/traffic_ops/testing/api/v1/todb_test.go
index e1b40e5..19e9d36 100644
--- a/traffic_ops/testing/api/v1/todb_test.go
+++ b/traffic_ops/testing/api/v1/todb_test.go
@@ -306,7 +306,6 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM api_capability;
 	DELETE FROM deliveryservices_required_capability;
 	DELETE FROM server_server_capability;
-	DELETE FROM server_server_capability;
 	DELETE FROM server_capability;
 	DELETE FROM to_extension;
 	DELETE FROM staticdnsentry;
@@ -325,6 +324,8 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM deliveryservice_server;
 	DELETE FROM deliveryservice;
 	DELETE FROM origin;
+	DELETE FROM ip_address;
+	DELETE FROM interface;
 	DELETE FROM server;
 	DELETE FROM phys_location;
 	DELETE FROM region;
diff --git a/traffic_ops/testing/api/v2/todb_test.go b/traffic_ops/testing/api/v2/todb_test.go
index 4c54d47..b8e7246 100644
--- a/traffic_ops/testing/api/v2/todb_test.go
+++ b/traffic_ops/testing/api/v2/todb_test.go
@@ -306,7 +306,6 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM api_capability;
 	DELETE FROM deliveryservices_required_capability;
 	DELETE FROM server_server_capability;
-	DELETE FROM server_server_capability;
 	DELETE FROM server_capability;
 	DELETE FROM to_extension;
 	DELETE FROM staticdnsentry;
@@ -325,6 +324,8 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM deliveryservice_server;
 	DELETE FROM deliveryservice;
 	DELETE FROM origin;
+	DELETE FROM ip_address;
+	DELETE FROM interface;
 	DELETE FROM server;
 	DELETE FROM phys_location;
 	DELETE FROM region;
diff --git a/traffic_ops/testing/api/v3/cachegroups_test.go b/traffic_ops/testing/api/v3/cachegroups_test.go
index 726ee39..7fe45f3 100644
--- a/traffic_ops/testing/api/v3/cachegroups_test.go
+++ b/traffic_ops/testing/api/v3/cachegroups_test.go
@@ -45,6 +45,7 @@ func CreateTestCacheGroups(t *testing.T) {
 		resp, _, err = TOSession.CreateCacheGroupNullable(cg)
 		if err != nil {
 			t.Errorf("could not CREATE cachegroups: %v, request: %v", err, cg)
+			continue
 		}
 
 		// Testing 'join' fields during create
diff --git a/traffic_ops/testing/api/v3/cachegroupsdeliveryservices_test.go b/traffic_ops/testing/api/v3/cachegroupsdeliveryservices_test.go
index f245328..5af55dc 100644
--- a/traffic_ops/testing/api/v3/cachegroupsdeliveryservices_test.go
+++ b/traffic_ops/testing/api/v3/cachegroupsdeliveryservices_test.go
@@ -17,6 +17,7 @@ package v3
 
 import (
 	"net/http"
+	"net/url"
 	"testing"
 )
 
@@ -103,16 +104,23 @@ func CreateTestCachegroupsDeliveryServices(t *testing.T) {
 		t.Fatal("setting cachegroup delivery services returned success, but no servers set")
 	}
 
+	params := url.Values{}
 	for _, serverName := range resp.Response.ServerNames {
-		servers, _, err := TOSession.GetServerByHostName(string(serverName))
+		params.Set("hostName", string(serverName))
+		resp, _, err := TOSession.GetServers(&params)
 		if err != nil {
 			t.Fatalf("getting server: %v", err)
 		}
+		servers := resp.Response
 		if len(servers) != 1 {
 			t.Fatalf("getting servers: expected 1 got %v", len(servers))
 		}
 		server := servers[0]
-		serverID := server.ID
+		if server.ID == nil {
+			t.Errorf("Server %s had nil ID", serverName)
+			continue
+		}
+		serverID := *server.ID
 
 		serverDSes, _, err := TOSession.GetDeliveryServicesByServer(serverID)
 
diff --git a/traffic_ops/testing/api/v3/crconfig_test.go b/traffic_ops/testing/api/v3/crconfig_test.go
index d4e3309..5a88c33 100644
--- a/traffic_ops/testing/api/v3/crconfig_test.go
+++ b/traffic_ops/testing/api/v3/crconfig_test.go
@@ -51,14 +51,15 @@ func UpdateTestCRConfigSnapshot(t *testing.T) {
 	}
 
 	// create an ANY_MAP DS assignment to verify that it doesn't show up in the CRConfig
-	servers, _, err := TOSession.GetServers()
+	resp, _, err := TOSession.GetServers(nil)
 	if err != nil {
-		t.Errorf("GetServers err expected nil, actual %+v", err)
+		t.Fatalf("GetServers err expected nil, actual %+v", err)
 	}
+	servers := resp.Response
 	serverID := 0
 	for _, server := range servers {
-		if server.Type == "EDGE" && server.CDNName == "cdn1" {
-			serverID = server.ID
+		if server.Type == "EDGE" && server.CDNName != nil && *server.CDNName == "cdn1" && server.ID != nil {
+			serverID = *server.ID
 			break
 		}
 	}
diff --git a/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go b/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
index 61f6fd4..d478bf6 100644
--- a/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
+++ b/traffic_ops/testing/api/v3/deliveryservices_required_capabilities_test.go
@@ -17,6 +17,7 @@ package v3
 
 import (
 	"fmt"
+	"net/url"
 	"strings"
 	"testing"
 
@@ -181,16 +182,24 @@ func InvalidDeliveryServicesRequiredCapabilityAddition(t *testing.T) {
 	}
 
 	// First assign current capabilities to edge server so we can assign it to the DS
-	servers, _, err := TOSession.GetServerByHostName("atlanta-edge-01")
+	// TODO: DON'T hard-code hostnames!
+	params := url.Values{}
+	params.Add("hostName", "atlanta-edge-01")
+	resp, _, err := TOSession.GetServers(&params)
 	if err != nil {
 		t.Fatalf("cannot GET Server by hostname: %v", err)
 	}
+	servers := resp.Response
 	if len(servers) < 1 {
 		t.Fatal("need at least one server to test invalid ds required capability assignment")
 	}
 
+	if servers[0].ID == nil {
+		t.Fatal("server 'atlanta-edge-01' had nil ID")
+	}
+
 	dsID := capabilities[0].DeliveryServiceID
-	sID := servers[0].ID
+	sID := *servers[0].ID
 	serverCaps := []tc.ServerServerCapability{}
 
 	for _, cap := range capabilities {
diff --git a/traffic_ops/testing/api/v3/deliveryserviceservers_test.go b/traffic_ops/testing/api/v3/deliveryserviceservers_test.go
index 9b0f444..a34ad88 100644
--- a/traffic_ops/testing/api/v3/deliveryserviceservers_test.go
+++ b/traffic_ops/testing/api/v3/deliveryserviceservers_test.go
@@ -18,6 +18,7 @@ package v3
 import (
 	"errors"
 	"net/http"
+	"net/url"
 	"strings"
 	"testing"
 
@@ -50,10 +51,10 @@ func AssignServersToTopologyBasedDeliveryService(t *testing.T) {
 	if ds[0].Topology == nil {
 		t.Fatal("expected delivery service: 'ds-top' to have a non-nil Topology, actual: nil")
 	}
-	serversResp, _, err := TOSession.GetServers()
-	servers := []tc.Server{}
-	for _, s := range serversResp {
-		if s.CDNID == *ds[0].CDNID && s.Type == tc.CacheTypeEdge.String() {
+	serversResp, _, err := TOSession.GetServers(nil)
+	servers := []tc.ServerNullable{}
+	for _, s := range serversResp.Response {
+		if s.CDNID != nil && *s.CDNID == *ds[0].CDNID && s.Type == tc.CacheTypeEdge.String() {
 			servers = append(servers, s)
 		}
 	}
@@ -62,8 +63,8 @@ func AssignServersToTopologyBasedDeliveryService(t *testing.T) {
 	}
 	serverNames := []string{}
 	for _, s := range servers {
-		if s.CDNID == *ds[0].CDNID && s.Type == tc.CacheTypeEdge.String() {
-			serverNames = append(serverNames, s.HostName)
+		if s.CDNID != nil && *s.CDNID == *ds[0].CDNID && s.Type == tc.CacheTypeEdge.String() && s.HostName != nil {
+			serverNames = append(serverNames, *s.HostName)
 		} else {
 			t.Fatalf("expected only EDGE servers in cdn '%s', actual: %v", *ds[0].CDNName, servers)
 		}
@@ -76,7 +77,7 @@ func AssignServersToTopologyBasedDeliveryService(t *testing.T) {
 		t.Fatalf("assigning servers to topology-based delivery service - expected: 400-level status code, actual: %d", reqInf.StatusCode)
 	}
 
-	_, reqInf, err = TOSession.CreateDeliveryServiceServers(*ds[0].ID, []int{servers[0].ID}, false)
+	_, reqInf, err = TOSession.CreateDeliveryServiceServers(*ds[0].ID, []int{*servers[0].ID}, false)
 	if err == nil {
 		t.Fatal("creating deliveryserviceserver assignment for topology-based delivery service - expected: error, actual: nil error")
 	}
@@ -120,24 +121,30 @@ func CreateTestDeliveryServiceServersWithRequiredCapabilities(t *testing.T) {
 
 	for _, ctc := range testCases {
 		t.Run(ctc.description, func(t *testing.T) {
-			servers, _, err := TOSession.GetServerByHostName(ctc.serverName)
+			params := url.Values{}
+			params.Add("hostName", ctc.serverName)
+			resp, _, err := TOSession.GetServers(&params)
 			if err != nil {
 				t.Fatalf("cannot GET Server by hostname: %v", err)
 			}
+			servers := resp.Response
 			server := servers[0]
+			if server.ID == nil {
+				t.Fatalf("server %s had nil ID", ctc.serverName)
+			}
 
 			_, _, err = TOSession.CreateDeliveryServicesRequiredCapability(ctc.capability)
 			if err != nil {
 				t.Fatalf("*POST delivery service required capability: %v", err)
 			}
 
-			ctc.ssc.ServerID = &server.ID
+			ctc.ssc.ServerID = server.ID
 			_, _, err = TOSession.CreateServerServerCapability(ctc.ssc)
 			if err != nil {
 				t.Fatalf("could not POST the server capability %v to server %v: %v", *ctc.ssc.ServerCapability, *ctc.ssc.Server, err)
 			}
 
-			_, _, got := TOSession.CreateDeliveryServiceServers(*ctc.capability.DeliveryServiceID, []int{server.ID}, true)
+			_, _, got := TOSession.CreateDeliveryServiceServers(*ctc.capability.DeliveryServiceID, []int{*server.ID}, true)
 			if (ctc.err == nil && got != nil) || (ctc.err != nil && !strings.Contains(got.Error(), ctc.err.Error())) {
 				t.Fatalf("expected ctc.err to contain %v, got %v", ctc.err, got)
 			}
@@ -161,19 +168,25 @@ func CreateTestMSODSServerWithReqCap(t *testing.T) {
 	}
 
 	// Associate origin server to msods1 even though it does not have req cap
-
-	servers, _, err := TOSession.GetServerByHostName("denver-mso-org-01")
+	// TODO: DON'T hard-code server hostnames!
+	params := url.Values{}
+	params.Add("hostName", "denver-mso-org-01")
+	resp, _, err := TOSession.GetServers(&params)
 	if err != nil {
 		t.Fatalf("GET server denver-mso-org-01: %v", err)
 	}
+	servers := resp.Response
 	if len(servers) != 1 {
 		t.Fatal("expected 1 server with hostname denver-mso-org-01")
 	}
 
 	s := servers[0]
+	if s.ID == nil {
+		t.Fatal("server 'denver-mso-org-01' had nil ID")
+	}
 
 	// Make sure server has no caps to ensure test correctness
-	sccs, _, err := TOSession.GetServerServerCapabilities(&s.ID, nil, nil)
+	sccs, _, err := TOSession.GetServerServerCapabilities(s.ID, nil, nil)
 	if err != nil {
 		t.Fatalf("GET server server capabilities for denver-mso-org-01: %v", err)
 	}
@@ -197,7 +210,7 @@ func CreateTestMSODSServerWithReqCap(t *testing.T) {
 		t.Fatal("expected to find origin server denver-mso-org-01 to be in eligible server return even though it is missing a required capability")
 	}
 
-	if _, _, err = TOSession.CreateDeliveryServiceServers(*dsReqCap[0].DeliveryServiceID, []int{s.ID}, true); err != nil {
+	if _, _, err = TOSession.CreateDeliveryServiceServers(*dsReqCap[0].DeliveryServiceID, []int{*s.ID}, true); err != nil {
 		t.Fatalf("POST delivery service origin servers without capabilities: %v", err)
 	}
 
@@ -228,8 +241,14 @@ func CreateTestMSODSServerWithReqCap(t *testing.T) {
 func DeleteTestDeliveryServiceServers(t *testing.T) {
 	dses, servers := getServersAndDSes(t)
 	ds, server := dses[0], servers[0]
+	if server.ID == nil {
+		t.Fatalf("Got a server with a nil ID: %+v", server)
+	}
+	if ds.ID == nil {
+		t.Fatalf("Got a delivery service with a nil ID %+v", ds)
+	}
 
-	_, _, err := TOSession.CreateDeliveryServiceServers(*ds.ID, []int{server.ID}, true)
+	_, _, err := TOSession.CreateDeliveryServiceServers(*ds.ID, []int{*server.ID}, true)
 	if err != nil {
 		t.Errorf("POST delivery service servers: %v", err)
 	}
@@ -241,7 +260,7 @@ func DeleteTestDeliveryServiceServers(t *testing.T) {
 
 	found := false
 	for _, dss := range dsServers.Response {
-		if *dss.DeliveryService == *ds.ID && *dss.Server == server.ID {
+		if dss.DeliveryService != nil && *dss.DeliveryService == *ds.ID && dss.Server != nil && *dss.Server == *server.ID {
 			found = true
 			break
 		}
@@ -250,7 +269,7 @@ func DeleteTestDeliveryServiceServers(t *testing.T) {
 		t.Error("POST delivery service servers returned success, but ds-server not in GET")
 	}
 
-	if _, _, err := TOSession.DeleteDeliveryServiceServer(*ds.ID, server.ID); err != nil {
+	if _, _, err := TOSession.DeleteDeliveryServiceServer(*ds.ID, *server.ID); err != nil {
 		t.Errorf("DELETE delivery service server: %v", err)
 	}
 
@@ -261,7 +280,7 @@ func DeleteTestDeliveryServiceServers(t *testing.T) {
 
 	found = false
 	for _, dss := range dsServers.Response {
-		if *dss.DeliveryService == *ds.ID && *dss.Server == server.ID {
+		if dss.DeliveryService != nil && *dss.DeliveryService == *ds.ID && dss.Server != nil && *dss.Server == *server.ID {
 			found = true
 			break
 		}
@@ -271,7 +290,7 @@ func DeleteTestDeliveryServiceServers(t *testing.T) {
 	}
 }
 
-func getServersAndDSes(t *testing.T) ([]tc.DeliveryServiceNullable, []tc.Server) {
+func getServersAndDSes(t *testing.T) ([]tc.DeliveryServiceNullable, []tc.ServerNullable) {
 	dses, _, err := TOSession.GetDeliveryServicesNullable()
 	if err != nil {
 		t.Fatalf("cannot GET DeliveryServices: %v", err)
@@ -280,10 +299,11 @@ func getServersAndDSes(t *testing.T) ([]tc.DeliveryServiceNullable, []tc.Server)
 		t.Fatal("GET DeliveryServices returned no dses, must have at least 1 to test ds-servers")
 	}
 
-	servers, _, err := TOSession.GetServers()
+	resp, _, err := TOSession.GetServers(nil)
 	if err != nil {
 		t.Fatalf("cannot GET Servers: %v", err)
 	}
+	servers := resp.Response
 	if len(servers) < 1 {
 		t.Fatal("GET Servers returned no dses, must have at least 1 to test ds-servers")
 	}
diff --git a/traffic_ops/testing/api/v3/servers_test.go b/traffic_ops/testing/api/v3/servers_test.go
index 84b6ae9..ff04d95 100644
--- a/traffic_ops/testing/api/v3/servers_test.go
+++ b/traffic_ops/testing/api/v3/servers_test.go
@@ -16,9 +16,8 @@ package v3
 */
 
 import (
+	"net/url"
 	"testing"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
 func TestServers(t *testing.T) {
@@ -30,9 +29,12 @@ func TestServers(t *testing.T) {
 }
 
 func CreateTestServers(t *testing.T) {
-
 	// loop through servers, assign FKs and create
 	for _, server := range testData.Servers {
+		if server.HostName == nil {
+			t.Errorf("found server with nil hostname: %+v", server)
+			continue
+		}
 		resp, _, err := TOSession.CreateServer(server)
 		t.Log("Response: ", server.HostName, " ", resp)
 		if err != nil {
@@ -43,11 +45,18 @@ func CreateTestServers(t *testing.T) {
 }
 
 func GetTestServers(t *testing.T) {
-
+	params := url.Values{}
 	for _, server := range testData.Servers {
-		resp, _, err := TOSession.GetServerByHostName(server.HostName)
+		if server.HostName == nil {
+			t.Errorf("found server with nil hostname: %+v", server)
+			continue
+		}
+		params.Set("hostName", *server.HostName)
+		resp, _, err := TOSession.GetServers(&params)
 		if err != nil {
-			t.Errorf("cannot GET Server by name: %v - %v", err, resp)
+			t.Errorf("cannot GET Server by name '%s': %v - %v", *server.HostName, err, resp.Alerts)
+		} else if resp.Summary.Count != 1 {
+			t.Errorf("incorrect server count, expected: 1, actual: %d", resp.Summary.Count)
 		}
 	}
 }
@@ -55,7 +64,11 @@ func GetTestServers(t *testing.T) {
 func GetTestServersDetails(t *testing.T) {
 
 	for _, server := range testData.Servers {
-		resp, _, err := TOSession.GetServerDetailsByHostName(server.HostName)
+		if server.HostName == nil {
+			t.Errorf("found server with nil hostname: %+v", server)
+			continue
+		}
+		resp, _, err := TOSession.GetServerDetailsByHostName(*server.HostName)
 		if err != nil {
 			t.Errorf("cannot GET Server Details by name: %v - %v", err, resp)
 		}
@@ -63,38 +76,92 @@ func GetTestServersDetails(t *testing.T) {
 }
 
 func UpdateTestServers(t *testing.T) {
+	if len(testData.Servers) < 1 {
+		t.Fatal("Need at least one server to test updating")
+	}
 
 	firstServer := testData.Servers[0]
-	hostName := firstServer.HostName
-	// Retrieve the server by hostname so we can get the id for the Update
-	resp, _, err := TOSession.GetServerByHostName(hostName)
+	if firstServer.HostName == nil {
+		t.Fatalf("First test server had nil hostname: %+v", firstServer)
+	}
 
+	hostName := *firstServer.HostName
+	params := url.Values{}
+	params.Add("hostName", hostName)
+
+	// Retrieve the server by hostname so we can get the id for the Update
+	resp, _, err := TOSession.GetServers(&params)
 	if err != nil {
-		t.Errorf("cannot GET Server by hostname: %v - %v", firstServer.HostName, err)
+		t.Fatalf("cannot GET Server by hostname '%s': %v - %v", hostName, err, resp.Alerts)
+	}
+	if len(resp.Response) < 1 {
+		t.Fatalf("Expected at least one server to exist by hostname '%s'", hostName)
+	}
+	if len(resp.Response) > 1 {
+		t.Errorf("Expected exactly one server to exist by hostname '%s' - actual: %d", hostName, len(resp.Response))
+		t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+	}
+
+	remoteServer := resp.Response[0]
+	if remoteServer.ID == nil {
+		t.Fatalf("Got null ID for server '%s'", hostName)
+	}
+
+	infs := remoteServer.Interfaces
+	if len(infs) < 1 {
+		t.Fatalf("Expected server '%s' to have at least one network interface", hostName)
 	}
-	remoteServer := resp[0]
+	inf := infs[0]
+
 	updatedServerInterface := "bond1"
 	updatedServerRack := "RR 119.03"
 
 	// update rack and interfaceName values on server
-	remoteServer.InterfaceName = updatedServerInterface
-	remoteServer.Rack = updatedServerRack
-	var alert tc.Alerts
-	alert, _, err = TOSession.UpdateServerByID(remoteServer.ID, remoteServer)
+	inf.Name = updatedServerInterface
+	infs[0] = inf
+	remoteServer.Interfaces = infs
+	remoteServer.Rack = &updatedServerRack
+
+	alerts, _, err := TOSession.UpdateServerByID(*remoteServer.ID, remoteServer)
 	if err != nil {
-		t.Errorf("cannot UPDATE Server by hostname: %v - %v", err, alert)
+		t.Fatalf("cannot UPDATE Server by ID %d (hostname '%s'): %v - %v", *remoteServer.ID, hostName, err, alerts)
 	}
 
 	// Retrieve the server to check rack and interfaceName values were updated
-	resp, _, err = TOSession.GetServerByID(remoteServer.ID)
+	resp, _, err = TOSession.GetServers(&params)
 	if err != nil {
 		t.Errorf("cannot GET Server by ID: %v - %v", remoteServer.HostName, err)
 	}
+	if len(resp.Response) < 1 {
+		t.Fatalf("Expected at least one server to exist by hostname '%s'", hostName)
+	}
+	if len(resp.Response) > 1 {
+		t.Errorf("Expected exactly one server to exist by hostname '%s' - actual: %d", hostName, len(resp.Response))
+		t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+	}
+
+	respServer := resp.Response[0]
+	infs = respServer.Interfaces
+	found := false
+	for _, inf = range infs {
+		if inf.Name == updatedServerInterface {
+			found = true
+			break
+		}
+	}
+	if !found {
+		t.Errorf("Expected server '%s' to have an interface named '%s' after update", hostName, updatedServerInterface)
+		t.Logf("Actual interfaces: %+v", infs)
+	}
 
-	respServer := resp[0]
-	if respServer.InterfaceName != updatedServerInterface || respServer.Rack != updatedServerRack {
-		t.Errorf("results do not match actual: %s, expected: %s", respServer.InterfaceName, updatedServerInterface)
-		t.Errorf("results do not match actual: %s, expected: %s", respServer.Rack, updatedServerRack)
+	if respServer.Rack == nil {
+		t.Errorf("results do not match actual: null, expected: '%s'", updatedServerRack)
+	} else if *respServer.Rack != updatedServerRack {
+		t.Errorf("results do not match actual: '%s', expected: '%s'", *respServer.Rack, updatedServerRack)
+	}
+
+	if remoteServer.TypeID == nil {
+		t.Fatalf("Cannot test server type change update; server '%s' had nil type ID", hostName)
 	}
 
 	// Assign server to DS and then attempt to update to a different type
@@ -114,48 +181,69 @@ func UpdateTestServers(t *testing.T) {
 		t.Fatal("GET Server Types returned less then 2 types, must have at least 2 to test invalid type server update")
 	}
 	for _, t := range serverTypes {
-		if t.ID != remoteServer.TypeID {
-			remoteServer.TypeID = t.ID
+		if t.ID != *remoteServer.TypeID {
+			remoteServer.TypeID = &t.ID
 			break
 		}
 	}
 
 	// Assign server to DS
-	_, _, err = TOSession.CreateDeliveryServiceServers(*dses[0].ID, []int{remoteServer.ID}, true)
+	_, _, err = TOSession.CreateDeliveryServiceServers(*dses[0].ID, []int{*remoteServer.ID}, true)
 	if err != nil {
 		t.Fatalf("POST delivery service servers: %v", err)
 	}
 
 	// Attempt Update - should fail
-	_, _, err = TOSession.UpdateServerByID(remoteServer.ID, remoteServer)
+	alerts, _, err = TOSession.UpdateServerByID(*remoteServer.ID, remoteServer)
 	if err == nil {
 		t.Errorf("expected error when updating Server Type of a server assigned to DSes")
+	} else {
+		t.Logf("type change update alerts: %+v", alerts)
 	}
 
 }
 
 func DeleteTestServers(t *testing.T) {
+	params := url.Values{}
 
 	for _, server := range testData.Servers {
-		resp, _, err := TOSession.GetServerByHostName(server.HostName)
+		if server.HostName == nil {
+			t.Errorf("found server with nil hostname: %+v", server)
+			continue
+		}
+
+		params.Set("hostName", *server.HostName)
+
+		resp, _, err := TOSession.GetServers(&params)
 		if err != nil {
-			t.Errorf("cannot GET Server by hostname: %v - %v", server.HostName, err)
+			t.Errorf("cannot GET Server by hostname '%s': %v - %v", *server.HostName, err, resp.Alerts)
+			continue
 		}
-		if len(resp) > 0 {
-			respServer := resp[0]
+		if len(resp.Response) > 0 {
+			if len(resp.Response) > 1 {
+				t.Errorf("Expected exactly one server by hostname '%s' - actual: %d", *server.HostName, len(resp.Response))
+				t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+			}
+			respServer := resp.Response[0]
+
+			if respServer.ID == nil {
+				t.Errorf("Server '%s' had nil ID", *server.HostName)
+				continue
+			}
 
-			delResp, _, err := TOSession.DeleteServerByID(respServer.ID)
+			delResp, _, err := TOSession.DeleteServerByID(*respServer.ID)
 			if err != nil {
-				t.Errorf("cannot DELETE Server by ID: %v - %v", err, delResp)
+				t.Errorf("cannot DELETE Server by ID %d: %v - %v", *respServer.ID, err, delResp)
+				continue
 			}
 
 			// Retrieve the Server to see if it got deleted
-			serv, _, err := TOSession.GetServerByHostName(server.HostName)
+			resp, _, err := TOSession.GetServers(&params)
 			if err != nil {
-				t.Errorf("error deleting Server hostname: %s", err.Error())
+				t.Errorf("error deleting Server hostname '%s': %v - %v", *server.HostName, err, resp.Alerts)
 			}
-			if len(serv) > 0 {
-				t.Errorf("expected Server hostname: %s to be deleted", server.HostName)
+			if len(resp.Response) > 0 {
+				t.Errorf("expected Server hostname: %s to be deleted", *server.HostName)
 			}
 		}
 	}
diff --git a/traffic_ops/testing/api/v3/servers_to_deliveryservice_assignment_test.go b/traffic_ops/testing/api/v3/servers_to_deliveryservice_assignment_test.go
index 8afd7ff..dee4472 100644
--- a/traffic_ops/testing/api/v3/servers_to_deliveryservice_assignment_test.go
+++ b/traffic_ops/testing/api/v3/servers_to_deliveryservice_assignment_test.go
@@ -16,6 +16,7 @@ package v3
 
 import (
 	"net/http"
+	"net/url"
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
@@ -30,13 +31,28 @@ func TestAssignments(t *testing.T) {
 }
 
 func AssignTestDeliveryService(t *testing.T) {
-	rs, _, err := TOSession.GetServerByHostName(testData.Servers[0].HostName)
+	if len(testData.Servers) < 1 {
+		t.Fatal("Need at least one test server to test delivery service assignment")
+	}
+
+	server := testData.Servers[0]
+	if server.HostName == nil {
+		t.Fatalf("First server had nil hostname: %+v", server)
+	}
+
+	params := url.Values{}
+	params.Add("hostName", *server.HostName)
+
+	rs, _, err := TOSession.GetServers(&params)
 	if err != nil {
 		t.Fatalf("Failed to fetch server information: %v", err)
-	} else if len(rs) == 0 {
+	} else if len(rs.Response) == 0 {
 		t.Fatalf("Failed to fetch server information: No results returned!")
 	}
-	firstServer := rs[0]
+	firstServer := rs.Response[0]
+	if firstServer.ID == nil {
+		t.Fatalf("Server '%s' had nil ID", *server.HostName)
+	}
 
 	rd, _, err := TOSession.GetDeliveryServiceByXMLIDNullable(*testData.DeliveryServices[0].XMLID)
 	if err != nil {
@@ -49,13 +65,13 @@ func AssignTestDeliveryService(t *testing.T) {
 	if firstDS.ID == nil {
 		t.Fatal("Fetch DS information returned unknown ID")
 	}
-	alerts, _, err := TOSession.AssignDeliveryServiceIDsToServerID(firstServer.ID, []int{*firstDS.ID}, true)
+	alerts, _, err := TOSession.AssignDeliveryServiceIDsToServerID(*firstServer.ID, []int{*firstDS.ID}, true)
 	if err != nil {
 		t.Errorf("Couldn't assign DS '%+v' to server '%+v': %v (alerts: %v)", firstDS, firstServer, err, alerts)
 	}
 	t.Logf("alerts: %+v", alerts)
 
-	response, _, err := TOSession.GetServerIDDeliveryServices(firstServer.ID)
+	response, _, err := TOSession.GetServerIDDeliveryServices(*firstServer.ID)
 	t.Logf("response: %+v", response)
 	if err != nil {
 		t.Fatalf("Couldn't get Delivery Services assigned to Server '%+v': %v", firstServer, err)
@@ -75,24 +91,33 @@ func AssignTestDeliveryService(t *testing.T) {
 }
 
 func AssignIncorrectTestDeliveryService(t *testing.T) {
-	var server *tc.Server
+	var server *tc.ServerNullable
 	for _, s := range testData.Servers {
-		if s.CDNName == "cdn2" {
+		if s.CDNName != nil && *s.CDNName == "cdn2" {
 			server = &s
 			break
 		}
 	}
 	if server == nil {
-		t.Fatalf("Couldn't find a server in CDN 'cdn2'!")
+		t.Fatal("Couldn't find a server in CDN 'cdn2'!")
 	}
+	if server.HostName == nil {
+		t.Fatalf("Server found with nil hostname: %+v", *server)
+	}
+	hostname := *server.HostName
 
-	rs, _, err := TOSession.GetServerByHostName(server.HostName)
+	params := url.Values{}
+	params.Add("hostName", hostname)
+	rs, _, err := TOSession.GetServers(&params)
 	if err != nil {
-		t.Fatalf("Failed to fetch server information: %v", err)
-	} else if len(rs) == 0 {
+		t.Fatalf("Failed to fetch server information: %v - %v", err, rs.Alerts)
+	} else if len(rs.Response) == 0 {
 		t.Fatalf("Failed to fetch server information: No results returned!")
 	}
-	server = &rs[0]
+	server = &rs.Response[0]
+	if server.ID == nil {
+		t.Fatalf("Server '%s' has nil ID", hostname)
+	}
 
 	rd, _, err := TOSession.GetDeliveryServiceByXMLIDNullable(*testData.DeliveryServices[0].XMLID)
 	if err != nil {
@@ -105,12 +130,12 @@ func AssignIncorrectTestDeliveryService(t *testing.T) {
 	if firstDS.ID == nil {
 		t.Fatal("Fetch DS information returned unknown ID")
 	}
-	alerts, _, err := TOSession.AssignDeliveryServiceIDsToServerID(server.ID, []int{*firstDS.ID}, false)
+	alerts, _, err := TOSession.AssignDeliveryServiceIDsToServerID(*server.ID, []int{*firstDS.ID}, false)
 	if err == nil {
 		t.Errorf("Expected bad assignment to fail, but it didn't! (alerts: %v)", alerts)
 	}
 
-	response, _, err := TOSession.GetServerIDDeliveryServices(server.ID)
+	response, _, err := TOSession.GetServerIDDeliveryServices(*server.ID)
 	t.Logf("response: %+v", response)
 	if err != nil {
 		t.Fatalf("Couldn't get Delivery Services assigned to Server '%+v': %v", *server, err)
@@ -131,24 +156,29 @@ func AssignIncorrectTestDeliveryService(t *testing.T) {
 }
 
 func AssignTopologyBasedDeliveryService(t *testing.T) {
-	var server *tc.Server
+	var server *tc.ServerNullable
 	for _, s := range testData.Servers {
-		if s.CDNName == "cdn1" && s.Type == string(tc.CacheTypeEdge) {
+		if s.CDNName != nil && *s.CDNName == "cdn1" && s.Type == string(tc.CacheTypeEdge) {
 			server = &s
 			break
 		}
 	}
-	if server == nil {
+	if server == nil || server.HostName == nil {
 		t.Fatalf("Couldn't find an EDGE server in CDN 'cdn1'!")
 	}
 
-	rs, _, err := TOSession.GetServerByHostName(server.HostName)
+	params := url.Values{}
+	params.Add("hostName", *server.HostName)
+	rs, _, err := TOSession.GetServers(&params)
 	if err != nil {
 		t.Fatalf("Failed to fetch server information: %v", err)
-	} else if len(rs) == 0 {
+	} else if len(rs.Response) == 0 {
 		t.Fatalf("Failed to fetch server information: No results returned!")
 	}
-	server = &rs[0]
+	server = &rs.Response[0]
+	if server.ID == nil {
+		t.Fatal("Server had nil ID")
+	}
 
 	rd, _, err := TOSession.GetDeliveryServiceByXMLIDNullable("ds-top")
 	if err != nil {
@@ -161,7 +191,7 @@ func AssignTopologyBasedDeliveryService(t *testing.T) {
 	if firstDS.ID == nil {
 		t.Fatal("Fetch DS information returned unknown ID")
 	}
-	alerts, reqInf, err := TOSession.AssignDeliveryServiceIDsToServerID(server.ID, []int{*firstDS.ID}, false)
+	alerts, reqInf, err := TOSession.AssignDeliveryServiceIDsToServerID(*server.ID, []int{*firstDS.ID}, false)
 	if err == nil {
 		t.Errorf("Expected bad assignment to fail, but it didn't! (alerts: %v)", alerts)
 	}
@@ -169,7 +199,7 @@ func AssignTopologyBasedDeliveryService(t *testing.T) {
 		t.Fatalf("assigning Topology-based delivery service to server - expected: 400-level status code, actual: %d", reqInf.StatusCode)
 	}
 
-	response, _, err := TOSession.GetServerIDDeliveryServices(server.ID)
+	response, _, err := TOSession.GetServerIDDeliveryServices(*server.ID)
 	t.Logf("response: %+v", response)
 	if err != nil {
 		t.Fatalf("Couldn't get Delivery Services assigned to Server '%+v': %v", *server, err)
diff --git a/traffic_ops/testing/api/v3/serverservercapability_test.go b/traffic_ops/testing/api/v3/serverservercapability_test.go
index 457117e..c2dbe7a 100644
--- a/traffic_ops/testing/api/v3/serverservercapability_test.go
+++ b/traffic_ops/testing/api/v3/serverservercapability_test.go
@@ -16,6 +16,7 @@ package v3
 */
 
 import (
+	"net/url"
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
@@ -32,21 +33,27 @@ func CreateTestServerServerCapabilities(t *testing.T) {
 	// Valid POSTs
 
 	// loop through server ServerCapabilities, assign FKs and create
+	params := url.Values{}
 	for _, ssc := range testData.ServerServerCapabilities {
-		servResp, _, err := TOSession.GetServerByHostName(*ssc.Server)
+		if ssc.Server == nil {
+			t.Fatalf("server-server-capability structure had nil server")
+		}
+		params.Set("hostName", *ssc.Server)
+		resp, _, err := TOSession.GetServers(&params)
 		if err != nil {
-			t.Fatalf("cannot GET Server by hostname: %v - %v", *ssc.Server, err)
+			t.Fatalf("cannot GET Server by hostname '%s': %v - %v", *ssc.Server, err, resp.Alerts)
 		}
+		servResp := resp.Response
 		if len(servResp) != 1 {
 			t.Fatalf("cannot GET Server by hostname: %v. Response did not include record.", *ssc.Server)
 		}
 		server := servResp[0]
-		ssc.ServerID = &server.ID
-		resp, _, err := TOSession.CreateServerServerCapability(ssc)
+		ssc.ServerID = server.ID
+		createResp, _, err := TOSession.CreateServerServerCapability(ssc)
 		if err != nil {
 			t.Errorf("could not POST the server capability %v to server %v: %v", *ssc.ServerCapability, *ssc.Server, err)
 		}
-		t.Log("Response: ", server.HostName, " ", resp)
+		t.Log("Response: ", *ssc.Server, " ", createResp)
 	}
 
 	// Invalid POSTs
@@ -98,16 +105,19 @@ func CreateTestServerServerCapabilities(t *testing.T) {
 	}
 
 	// Attempt to assign a server capability to a non MID/EDGE server
-	servers, _, err := TOSession.GetServerByHostName("riak")
+	// TODO: DON'T hard-code server hostnames!
+	params.Set("hostName", "riak")
+	resp, _, err := TOSession.GetServers(&params)
 	if err != nil {
-		t.Fatalf("cannot GET Server by hostname: %v - %v", *ssc.Server, err)
+		t.Fatalf("cannot GET Server by hostname 'riak': %v - %v", err, resp.Alerts)
 	}
+	servers := resp.Response
 	if len(servers) < 1 {
 		t.Fatal("need at least one server to test invalid server type assignment")
 	}
 
 	sscInvalidType := tc.ServerServerCapability{
-		ServerID:         &servers[0].ID,
+		ServerID:         servers[0].ID,
 		ServerCapability: ssc.ServerCapability,
 	}
 	_, _, err = TOSession.CreateServerServerCapability(sscInvalidType)
diff --git a/traffic_ops/testing/api/v3/serverupdatestatus_test.go b/traffic_ops/testing/api/v3/serverupdatestatus_test.go
index 6794925..5382260 100644
--- a/traffic_ops/testing/api/v3/serverupdatestatus_test.go
+++ b/traffic_ops/testing/api/v3/serverupdatestatus_test.go
@@ -19,6 +19,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"net/url"
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
@@ -27,15 +28,18 @@ import (
 
 func TestServerUpdateStatus(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Types, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers}, func() {
-		edge1cdn1 := tc.Server{}
-		edge2cdn1 := tc.Server{}
-		mid1cdn1 := tc.Server{}
-		edge1cdn2 := tc.Server{}
+		//TODO: DON'T hard-code server hostnames!
+		var edge1cdn1 tc.ServerNullable
+		var edge2cdn1 tc.ServerNullable
+		var mid1cdn1 tc.ServerNullable
+		var edge1cdn2 tc.ServerNullable
+
+		params := url.Values{}
 
 		getServers := func() {
 			for _, s := range []struct {
 				name   string
-				server *tc.Server
+				server *tc.ServerNullable
 			}{
 				{
 					"atlanta-edge-01",
@@ -54,29 +58,45 @@ func TestServerUpdateStatus(t *testing.T) {
 					&edge1cdn2,
 				},
 			} {
-				resp, _, err := TOSession.GetServerByHostName(s.name)
+				params.Set("hostName", s.name)
+				resp, _, err := TOSession.GetServers(&params)
 				if err != nil {
-					t.Errorf("cannot GET Server by hostname: %v - %v", s.name, err)
+					t.Errorf("cannot GET Server by hostname '%s': %v - %v", s.name, err, resp.Alerts)
+				}
+				if len(resp.Response) < 1 {
+					t.Fatalf("Expected a server named '%s' to exist", s.name)
+				}
+				if len(resp.Response) > 1 {
+					t.Errorf("Expected exactly one server named '%s' to exist - actual: %d", s.name, len(resp.Response))
+					t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+				}
+				*s.server = resp.Response[0]
+				if s.server.ID == nil {
+					t.Fatalf("server '%s' was returned with nil ID", s.name)
+				}
+				if s.server.HostName == nil {
+					t.Fatalf("server '%s' was returned with nil HostName", s.name)
 				}
-				*s.server = resp[0]
 			}
 		}
 		getServers()
 
 		// assert that servers don't have updates pending
-		for _, s := range []tc.Server{
+		for _, s := range []tc.ServerNullable{
 			edge1cdn1,
 			edge2cdn1,
 			mid1cdn1,
 			edge1cdn2,
 		} {
-			if s.UpdPending {
+			if s.UpdPending == nil {
+				t.Error("expected UpdPending: false, actual: null")
+			} else if *s.UpdPending {
 				t.Error("expected UpdPending: false, actual: true")
 			}
 		}
 
 		// update status of MID server to OFFLINE
-		_, _, err := TOSession.UpdateServerStatus(mid1cdn1.ID, tc.ServerPutStatus{
+		_, _, err := TOSession.UpdateServerStatus(*mid1cdn1.ID, tc.ServerPutStatus{
 			Status:        util.JSONNameOrIDStr{Name: util.StrPtr("OFFLINE")},
 			OfflineReason: util.StrPtr("testing")})
 		if err != nil {
@@ -85,17 +105,27 @@ func TestServerUpdateStatus(t *testing.T) {
 
 		// assert that updates were queued for the proper EDGE servers
 		getServers()
-		if !edge1cdn1.UpdPending {
-			t.Errorf("expected: child %s to have updates pending, actual: no updates pending", edge1cdn1.HostName)
+		if edge1cdn1.UpdPending == nil {
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: property was null (or missing)", *edge1cdn1.HostName, *edge1cdn1.ID)
+		} else if !*edge1cdn1.UpdPending {
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: no updates pending", *edge1cdn1.HostName, *edge1cdn1.ID)
 		}
-		if !edge2cdn1.UpdPending {
-			t.Errorf("expected: child %s to have updates pending, actual: no updates pending", edge2cdn1.HostName)
+
+		if edge2cdn1.UpdPending == nil {
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: property was null (or missing)", *edge2cdn1.HostName, *edge2cdn1.ID)
+		} else if !*edge2cdn1.UpdPending {
+			t.Errorf("expected: child %s (%d) to have updates pending, actual: no updates pending", *edge2cdn1.HostName, *edge2cdn1.ID)
 		}
-		if mid1cdn1.UpdPending {
-			t.Errorf("expected: server %s with updated status to have no updates pending, actual: updates pending", mid1cdn1.HostName)
+		if mid1cdn1.UpdPending == nil {
+			t.Errorf("expected: server %s (%d) with updated status to have no updates pending, actual: property was null (or missing)", *mid1cdn1.HostName, *mid1cdn1.ID)
+		} else if *mid1cdn1.UpdPending {
+			t.Errorf("expected: server %s (%d) with updated status to have no updates pending, actual: updates pending", *mid1cdn1.HostName, *mid1cdn1.ID)
 		}
-		if edge1cdn2.UpdPending {
-			t.Errorf("expected: server %s in different CDN than server with updated status to have no updates pending, actual: updates pending", edge2cdn1.HostName)
+
+		if edge1cdn2.UpdPending == nil {
+			t.Errorf("expected: server %s (%d) in different CDN than server with updated status to have no updates pending, actual: updates pending", *edge1cdn2.HostName, *edge1cdn2.ID)
+		} else if *edge1cdn2.UpdPending {
+			t.Errorf("expected: server %s (%d) in different CDN than server with updated status to have no updates pending, actual: updates pending", *edge1cdn2.HostName, *edge1cdn2.ID)
 		}
 
 		// update status of MID server to OFFLINE via status ID
@@ -104,7 +134,7 @@ func TestServerUpdateStatus(t *testing.T) {
 			t.Fatalf("cannot GET status by name: %v", err)
 		}
 		_, _, err = TOSession.UpdateServerStatus(
-			mid1cdn1.ID,
+			*mid1cdn1.ID,
 			tc.ServerPutStatus{
 				Status:        util.JSONNameOrIDStr{ID: util.IntPtr(status[0].ID)},
 				OfflineReason: util.StrPtr("testing"),
@@ -129,7 +159,7 @@ func TestServerUpdateStatus(t *testing.T) {
 
 		// status does not exist
 		_, _, err = TOSession.UpdateServerStatus(
-			mid1cdn1.ID,
+			*mid1cdn1.ID,
 			tc.ServerPutStatus{
 				Status:        util.JSONNameOrIDStr{Name: util.StrPtr("NOT_A_REAL_STATUS")},
 				OfflineReason: util.StrPtr("testing"),
@@ -141,7 +171,7 @@ func TestServerUpdateStatus(t *testing.T) {
 
 		// offlineReason required for OFFLINE status
 		_, _, err = TOSession.UpdateServerStatus(
-			mid1cdn1.ID,
+			*mid1cdn1.ID,
 			tc.ServerPutStatus{
 				Status:        util.JSONNameOrIDStr{Name: util.StrPtr("OFFLINE")},
 				OfflineReason: nil,
@@ -153,7 +183,7 @@ func TestServerUpdateStatus(t *testing.T) {
 
 		// offlineReason required for ADMIN_DOWN status
 		_, _, err = TOSession.UpdateServerStatus(
-			mid1cdn1.ID,
+			*mid1cdn1.ID,
 			tc.ServerPutStatus{
 				Status:        util.JSONNameOrIDStr{Name: util.StrPtr("ADMIN_DOWN")},
 				OfflineReason: nil,
@@ -167,6 +197,7 @@ func TestServerUpdateStatus(t *testing.T) {
 
 func TestServerQueueUpdate(t *testing.T) {
 	WithObjs(t, []TCObj{Divisions, Regions, PhysLocations, Statuses, Types, CacheGroups, CDNs, Profiles, Servers}, func() {
+		// TODO: DON'T hard-code server hostnames!
 		const serverName = "atlanta-edge-01"
 
 		queueUpdateActions := map[bool]string{
@@ -174,26 +205,42 @@ func TestServerQueueUpdate(t *testing.T) {
 			true:  "queue",
 		}
 
-		var s tc.Server
-		resp, _, err := TOSession.GetServerByHostName(serverName)
+		var s tc.ServerNullable
+		params := url.Values{}
+		params.Add("hostName", serverName)
+		resp, _, err := TOSession.GetServers(&params)
 		if err != nil {
-			t.Fatalf("failed to GET Server by hostname: %v - %v", serverName, err)
+			t.Fatalf("failed to GET Server by hostname '%s': %v - %v", serverName, err, resp.Alerts)
 		}
-		s = resp[0]
+		if len(resp.Response) < 1 {
+			t.Fatalf("Expected a server named '%s' to exist", serverName)
+		}
+		if len(resp.Response) > 1 {
+			t.Errorf("Expected exactly one server named '%s' to exist", serverName)
+			t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+		}
+		s = resp.Response[0]
 
 		// assert that servers don't have updates pending
-		if got, want := s.UpdPending, false; got != want {
+		if s.UpdPending == nil {
+			t.Fatalf("Server '%s' had null (or missing) updPending property", serverName)
+		}
+		if got, want := *s.UpdPending, false; got != want {
 			t.Fatalf("unexpected UpdPending, got: %v, want: %v", got, want)
 		}
 
+		if s.ID == nil {
+			t.Fatalf("Server '%s' had nil ID", serverName)
+		}
+
 		for _, setVal := range [...]bool{true, false} {
 			t.Run(fmt.Sprint(setVal), func(t *testing.T) {
 				// queue update and check response
-				quResp, _, err := TOSession.SetServerQueueUpdate(s.ID, setVal)
+				quResp, _, err := TOSession.SetServerQueueUpdate(*s.ID, setVal)
 				if err != nil {
 					t.Fatalf("failed to set queue update for server with ID %v to %v: %v", s.ID, setVal, err)
 				}
-				if got, want := int(quResp.Response.ServerID), s.ID; got != want {
+				if got, want := int(quResp.Response.ServerID), *s.ID; got != want {
 					t.Errorf("wrong serverId in response, got: %v, want: %v", got, want)
 				}
 				if got, want := quResp.Response.Action, queueUpdateActions[setVal]; got != want {
@@ -201,12 +248,22 @@ func TestServerQueueUpdate(t *testing.T) {
 				}
 
 				// assert that the server has updates queued
-				resp, _, err = TOSession.GetServerByID(s.ID)
+				resp, _, err = TOSession.GetServers(&params)
 				if err != nil {
-					t.Fatalf("failed to GET Server by ID: %v - %v", s.ID, err)
+					t.Fatalf("failed to GET Server by hostname '%s': %v - %v", serverName, err, resp.Alerts)
+				}
+				if len(resp.Response) < 1 {
+					t.Fatalf("Expected a server named '%s' to exist", serverName)
 				}
-				s = resp[0]
-				if got, want := s.UpdPending, setVal; got != want {
+				if len(resp.Response) > 1 {
+					t.Errorf("Expected exactly one server named '%s' to exist", serverName)
+					t.Logf("Testing will proceed with server: %+v", resp.Response[0])
+				}
+				s = resp.Response[0]
+				if s.UpdPending == nil {
+					t.Fatalf("Server '%s' had null (or missing) updPending property", serverName)
+				}
+				if got, want := *s.UpdPending, setVal; got != want {
 					t.Errorf("unexpected UpdPending, got: %v, want: %v", got, want)
 				}
 			})
@@ -224,7 +281,9 @@ func TestServerQueueUpdate(t *testing.T) {
 			if err != nil {
 				t.Fatalf("failed to encode request body: %v", err)
 			}
-			path := fmt.Sprintf(TestAPIBase+"/servers/%d/queue_update", s.ID)
+
+			// TODO: don't construct URLs like this, nor use "RawRequest"
+			path := fmt.Sprintf(TestAPIBase+"/servers/%d/queue_update", *s.ID)
 			httpResp, _, err := TOSession.RawRequest(http.MethodPost, path, req)
 			if err != nil {
 				t.Fatalf("POST request failed: %v", err)
@@ -242,42 +301,65 @@ func TestSetServerUpdateStatuses(t *testing.T) {
 			t.Fatal("cannot GET Server: no test data")
 		}
 		testServer := testData.Servers[0]
+		if testServer.HostName == nil {
+			t.Fatalf("First test server had nil hostname: %+v", testServer)
+		}
 
+		params := url.Values{}
+		params.Add("hostName", *testServer.HostName)
 		testVals := func(queue *bool, reval *bool) {
-			existingServer, _, err := TOSession.GetServerByHostName(testServer.HostName)
+			resp, _, err := TOSession.GetServers(&params)
 			if err != nil {
-				t.Errorf("cannot GET Server by name: %v - %v", err, existingServer)
-			} else if len(existingServer) != 1 {
-				t.Fatalf("GET Server expected 1, actual %v", len(existingServer))
+				t.Errorf("cannot GET Server by name '%s': %v - %v", *testServer.HostName, err, resp.Alerts)
+			} else if len(resp.Response) != 1 {
+				t.Fatalf("GET Server expected 1, actual %v", len(resp.Response))
+			}
+
+			existingServer := resp.Response
+
+			if existingServer[0].UpdPending == nil {
+				t.Fatalf("Server '%s' had nil UpdPending before update status change", *testServer.HostName)
+			}
+			if existingServer[0].RevalPending == nil {
+				t.Fatalf("Server '%s' had nil RevalPending before update status change", *testServer.HostName)
 			}
 
-			if _, err := TOSession.SetUpdateServerStatuses(testServer.HostName, queue, reval); err != nil {
+			if _, err := TOSession.SetUpdateServerStatuses(*testServer.HostName, queue, reval); err != nil {
 				t.Fatalf("UpdateServerStatuses error expected: nil, actual: %v", err)
 			}
 
-			newServer, _, err := TOSession.GetServerByHostName(testServer.HostName)
+			resp, _, err = TOSession.GetServers(&params)
 			if err != nil {
-				t.Errorf("cannot GET Server by name: %v - %v", err, existingServer)
-			} else if len(newServer) != 1 {
-				t.Fatalf("GET Server expected 1, actual %v", len(newServer))
+				t.Errorf("cannot GET Server by name '%s': %v - %v", *testServer.HostName, err, resp.Alerts)
+			} else if len(resp.Response) != 1 {
+				t.Fatalf("GET Server expected 1, actual %v", len(resp.Response))
+			}
+
+			newServer := resp.Response
+
+			if newServer[0].UpdPending == nil {
+				t.Fatalf("Server '%s' had nil UpdPending after update status change", *testServer.HostName)
+			}
+			if newServer[0].RevalPending == nil {
+				t.Fatalf("Server '%s' had nil RevalPending after update status change", *testServer.HostName)
 			}
 
 			if queue != nil {
-				if newServer[0].UpdPending != *queue {
-					t.Errorf("set queue update pending to %v, but then got server %v", *queue, newServer[0].UpdPending)
+				if *newServer[0].UpdPending != *queue {
+					t.Errorf("set queue update pending to %v, but then got server %v", *queue, *newServer[0].UpdPending)
 				}
 			} else {
-				if newServer[0].UpdPending != existingServer[0].UpdPending {
-					t.Errorf("set queue update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", newServer[0].UpdPending, existingServer[0].UpdPending)
+				if *newServer[0].UpdPending != *existingServer[0].UpdPending {
+					t.Errorf("set queue update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", *newServer[0].UpdPending, *existingServer[0].UpdPending)
 				}
 			}
 			if reval != nil {
-				if newServer[0].RevalPending != *reval {
-					t.Errorf("set reval update pending to %v, but then got server %v", *reval, newServer[0].RevalPending)
+				if *newServer[0].RevalPending != *reval {
+					t.Errorf("set reval update pending to %v, but then got server %v", *reval, *newServer[0].RevalPending)
 				}
 			} else {
-				if newServer[0].RevalPending != existingServer[0].RevalPending {
-					t.Errorf("set reval update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", newServer[0].RevalPending, existingServer[0].RevalPending)
+				if *newServer[0].RevalPending != *existingServer[0].RevalPending {
+					t.Errorf("set reval update pending with nil (don't update), but then got server %v which didn't match pre-update value %v", *newServer[0].RevalPending, *existingServer[0].RevalPending)
 				}
 			}
 		}
@@ -290,7 +372,7 @@ func TestSetServerUpdateStatuses(t *testing.T) {
 		testVals(util.BoolPtr(true), nil)
 		testVals(util.BoolPtr(false), nil)
 
-		if _, err := TOSession.SetUpdateServerStatuses(testServer.HostName, nil, nil); err == nil {
+		if _, err := TOSession.SetUpdateServerStatuses(*testServer.HostName, nil, nil); err == nil {
 			t.Errorf("UpdateServerStatuses with (nil,nil) expected error, actual nil")
 		}
 	})
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json b/traffic_ops/testing/api/v3/tc-fixtures.json
index b1007a3..b2117cf 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -1629,14 +1629,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:8::1/64",
-            "ip6Gateway": "2345:1234:12:8::1",
-            "ipAddress": "127.0.0.21",
-            "ipGateway": "127.0.0.21",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.21/30",
+                            "gateway": "127.0.0.21",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:8::1/64",
+                            "gateway": "2345:1234:12:8::1",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1667,14 +1679,21 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 1500,
-            "interfaceName": "eth0",
-            "ip6Address": "",
-            "ip6Gateway": "",
-            "ipAddress": "0.0.0.0",
-            "ipGateway": "0.0.0.0",
-            "ipIsService": true,
-            "ipNetmask": "0.0.0.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "0.0.0.0/0",
+                            "gateway": "0.0.0.0",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 1500,
+                    "name": "eth0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1705,14 +1724,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 1500,
-            "interfaceName": "eth1",
-            "ip6Address": "2345:1234:12:8::2/64",
-            "ip6Gateway": "2345:1234:12:8::2",
-            "ipAddress": "127.0.0.11",
-            "ipGateway": "127.0.0.11",
-            "ipIsService": true,
-            "ipNetmask": "255.255.252.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.11/22",
+                            "gateway": "127.0.0.11",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:8::2/64",
+                            "gateway": "2345:1234:12:8::2",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 1500,
+                    "name": "eth1"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1743,14 +1774,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:8::3/64",
-            "ip6Gateway": "2345:1234:12:8::3",
-            "ipAddress": "127.0.0.12",
-            "ipGateway": "127.0.0.1",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.12/30",
+                            "gateway": "127.0.0.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:8::3/64",
+                            "gateway": "2345:1234:12:8::3",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1781,14 +1824,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:2::4/64",
-            "ip6Gateway": "2345:1234:12:8::4",
-            "ipAddress": "127.0.0.13",
-            "ipGateway": "127.0.0.1",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2345:1234:12:2::4/64",
+                            "gateway": "2345:1234:12:2::4",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.13/30",
+                            "gateway": "127.0.0.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1819,14 +1874,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:8::5/64",
-            "ip6Gateway": "2345:1234:12:8::5",
-            "ipAddress": "127.0.0.14",
-            "ipGateway": "127.0.0.1",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2345:1234:12:8::5/64",
+                            "gateway": "2345:1234:12:8::5",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.14/30",
+                            "gateway": "127.0.0.1",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1857,13 +1924,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:d::6/64",
-            "ip6Gateway": "2345:1234:12:d::6",
-            "ipAddress": "127.0.0.15",
-            "ipGateway": "127.0.0.7",
-            "ipIsService": true,
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2345:1234:12:d::6/64",
+                            "gateway": "2345:1234:12:d::6",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.15/30",
+                            "gateway": "127.0.0.7",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "ipNetmask": "255.255.255.252",
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
@@ -1895,14 +1975,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:d::7/64",
-            "ip6Gateway": "2345:1234:12:d::7",
-            "ipAddress": "127.0.0.16",
-            "ipGateway": "127.0.0.7",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2345:1234:12:d::7/64",
+                            "gateway": "2345:1234:12:d::7",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.16/30",
+                            "gateway": "127.0.0.7",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1933,14 +2025,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:d::8/64",
-            "ip6Gateway": "2345:1234:12:d::8",
-            "ipAddress": "127.0.0.17",
-            "ipGateway": "127.0.0.17",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "2345:1234:12:d::8/64",
+                            "gateway": "2345:1234:12:d::8",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.17/30",
+                            "gateway": "127.0.0.17",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -1971,14 +2075,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:d::9/64",
-            "ip6Gateway": "2345:1234:12:d::9",
-            "ipAddress": "127.0.0.18",
-            "ipGateway": "127.0.0.18",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.18/30",
+                            "gateway": "127.0.0.18",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:d::9/64",
+                            "gateway": "2345:1234:12:d::9",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -2009,14 +2125,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:9::10/64",
-            "ip6Gateway": "2345:1234:12:9::10",
-            "ipAddress": "127.0.0.2",
-            "ipGateway": "127.0.0.2",
-            "ipIsService": true,
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.2/30",
+                            "gateway": "127.0.0.2",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:9::10/64",
+                            "gateway": "2345:1234:12:9::10",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -2047,14 +2175,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:b::11/64",
-            "ip6Gateway": "2345:1234:12:b::11",
-            "ipAddress": "127.0.0.4",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.4",
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.4/30",
+                            "gateway": "127.0.0.4",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:b::11/64",
+                            "gateway": "2345:1234:12:b::11",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -2085,14 +2225,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:b::13/64",
-            "ip6Gateway": "2345:1234:12:b::13",
-            "ipAddress": "127.0.0.31",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.4",
-            "ipNetmask": "255.255.255.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.31/24",
+                            "gateway": "127.0.0.4",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:b::13/64",
+                            "gateway": "2345:1234:12:b::13",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2122,14 +2274,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 1500,
-            "interfaceName": "eth1",
-            "ip6Address": "2345:1234:12:b::12/64",
-            "ip6Gateway": "2345:1234:12:b::12",
-            "ipAddress": "127.0.0.1",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.1",
-            "ipNetmask": "255.255.252.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.1/22",
+                            "gateway": "127.0.0.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:b::12/64",
+                            "gateway": "2345:1234:12:b::12",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 1500,
+                    "name": "eth1"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -2160,14 +2324,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "2345:1234:12:8::20/64",
-            "ip6Gateway": "2345:1234:12:8::20",
-            "ipAddress": "127.0.0.1",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.1",
-            "ipNetmask": "255.255.255.252",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "127.0.0.1/30",
+                            "gateway": "127.0.0.1",
+                            "serviceAddress": true
+                        },
+                        {
+                            "address": "2345:1234:12:8::20/64",
+                            "gateway": "2345:1234:12:8::20",
+                            "serviceAddress": false
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "lastUpdated": "2018-03-28T17:30:00.220351+00:00",
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
@@ -2198,14 +2374,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "::13/64",
-            "ip6Gateway": "2345:1234:12:b::13",
-            "ipAddress": "127.0.0.100",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.4",
-            "ipNetmask": "255.255.255.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "::13/64",
+                            "gateway": "2345:1234:12:b::13",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.100/24",
+                            "gateway": "127.0.0.4",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
@@ -2235,14 +2423,26 @@
             "iloIpNetmask": "",
             "iloPassword": "",
             "iloUsername": "",
-            "interfaceMtu": 9000,
-            "interfaceName": "bond0",
-            "ip6Address": "::14/64",
-            "ip6Gateway": "2345:1234:12:b::13",
-            "ipAddress": "127.0.0.101",
-            "ipIsService": true,
-            "ipGateway": "127.0.0.4",
-            "ipNetmask": "255.255.255.0",
+            "interfaces": [
+                {
+                    "ipAddresses": [
+                        {
+                            "address": "::14/64",
+                            "gateway": "2345:1234:12:b::13",
+                            "serviceAddress": false
+                        },
+                        {
+                            "address": "127.0.0.101/24",
+                            "gateway": "127.0.0.4",
+                            "serviceAddress": true
+                        }
+                    ],
+                    "maxBandwidth": null,
+                    "monitor": true,
+                    "mtu": 9000,
+                    "name": "bond0"
+                }
+            ],
             "mgmtIpAddress": "",
             "mgmtIpGateway": "",
             "mgmtIpNetmask": "",
diff --git a/traffic_ops/testing/api/v3/todb_test.go b/traffic_ops/testing/api/v3/todb_test.go
index d0511d1..abec74a 100644
--- a/traffic_ops/testing/api/v3/todb_test.go
+++ b/traffic_ops/testing/api/v3/todb_test.go
@@ -306,7 +306,6 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM api_capability;
 	DELETE FROM deliveryservices_required_capability;
 	DELETE FROM server_server_capability;
-	DELETE FROM server_server_capability;
 	DELETE FROM server_capability;
 	DELETE FROM to_extension;
 	DELETE FROM staticdnsentry;
@@ -325,6 +324,8 @@ func Teardown(db *sql.DB) error {
 	DELETE FROM deliveryservice_server;
 	DELETE FROM deliveryservice;
 	DELETE FROM origin;
+	DELETE FROM ip_address;
+	DELETE FROM interface;
 	DELETE FROM server;
 	DELETE FROM phys_location;
 	DELETE FROM region;
diff --git a/traffic_ops/testing/api/v3/traffic_control_test.go b/traffic_ops/testing/api/v3/traffic_control_test.go
index 4c400f2..e090021 100644
--- a/traffic_ops/testing/api/v3/traffic_control_test.go
+++ b/traffic_ops/testing/api/v3/traffic_control_test.go
@@ -42,7 +42,7 @@ type TrafficControl struct {
 	PhysLocations                        []tc.PhysLocation                       `json:"physLocations"`
 	Regions                              []tc.Region                             `json:"regions"`
 	Roles                                []tc.Role                               `json:"roles"`
-	Servers                              []tc.Server                             `json:"servers"`
+	Servers                              []tc.ServerNullable                     `json:"servers"`
 	ServerServerCapabilities             []tc.ServerServerCapability             `json:"serverServerCapabilities"`
 	ServerCapabilities                   []tc.ServerCapability                   `json:"serverCapabilities"`
 	Statuses                             []tc.StatusNullable                     `json:"statuses"`
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index af138f1..6c28d7d 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -71,12 +71,21 @@ WHERE type in ( SELECT id
 AND status=(SELECT id FROM status WHERE name='ONLINE')
 `
 
+type APIResponse struct {
+	Response interface{} `json:"response"`
+}
+
+type APIResponseWithSummary struct {
+	Response interface{} `json:"response"`
+	Summary  struct {
+		Count uint64 `json:"count"`
+	} `json:"summary"`
+}
+
 // WriteResp takes any object, serializes it as JSON, and writes that to w. Any errors are logged and written to w via tc.GetHandleErrorsFunc.
 // This is a helper for the common case; not using this in unusual cases is perfectly acceptable.
 func WriteResp(w http.ResponseWriter, r *http.Request, v interface{}) {
-	resp := struct {
-		Response interface{} `json:"response"`
-	}{v}
+	resp := APIResponse{v}
 	WriteRespRaw(w, r, resp)
 }
 
@@ -98,6 +107,18 @@ func WriteRespRaw(w http.ResponseWriter, r *http.Request, v interface{}) {
 	w.Write(append(bts, '\n'))
 }
 
+// WriteRespWithSummary writes a JSON-encoded representation of an arbitrary
+// object to the provided writer, and cleans up the corresponding request
+// object. It also provides a "summary" section to the response object that
+// contains the given "count".
+func WriteRespWithSummary(w http.ResponseWriter, r *http.Request, v interface{}, count uint64) {
+	var resp APIResponseWithSummary
+	resp.Response = v
+	resp.Summary.Count = count
+
+	WriteRespRaw(w, r, resp)
+}
+
 // WriteRespVals is like WriteResp, but also takes a map of root-level values to write. The API most commonly needs these for meta-parameters, like size, limit, and orderby.
 // This is a helper for the common case; not using this in unusual cases is perfectly acceptable.
 func WriteRespVals(w http.ResponseWriter, r *http.Request, v interface{}, vals map[string]interface{}) {
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/chkconfig.go b/traffic_ops/traffic_ops_golang/ats/atsserver/chkconfig.go
index db58f24..b445781 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/chkconfig.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsserver/chkconfig.go
@@ -25,7 +25,9 @@ import (
 
 	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-rfc"
+
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 )
 
 func GetChkconfig(w http.ResponseWriter, r *http.Request) {
@@ -45,7 +47,7 @@ func GetChkconfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	params, err := GetServerParams(inf.Tx.Tx, serverName, atscfg.ChkconfigParamConfigFile)
+	params, err := ats.GetServerParams(inf.Tx.Tx, serverName, atscfg.ChkconfigParamConfigFile)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server '"+string(serverName)+"' + chkconfig parameters: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/hostingdotconfig.go b/traffic_ops/traffic_ops_golang/ats/atsserver/hostingdotconfig.go
index e3525be..c441687 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/hostingdotconfig.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsserver/hostingdotconfig.go
@@ -55,7 +55,7 @@ func GetHostingDotConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	multiParams, err := GetServerParams(inf.Tx.Tx, serverName, atscfg.HostingConfigParamConfigFile)
+	multiParams, err := ats.GetServerParams(inf.Tx.Tx, serverName, atscfg.HostingConfigParamConfigFile)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server '"+string(serverName)+"' + hosting parameters: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/ipallowdotconfig.go b/traffic_ops/traffic_ops_golang/ats/atsserver/ipallowdotconfig.go
deleted file mode 100644
index 520200b..0000000
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/ipallowdotconfig.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package atsserver
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"database/sql"
-	"errors"
-	"net/http"
-	"strings"
-
-	"github.com/apache/trafficcontrol/lib/go-atscfg"
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-)
-
-func GetIPAllowDotConfig(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id-or-host"}, nil)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	serverName, serverType, ok, err := GetServerNameAndTypeFromNameOrID(inf.Tx.Tx, inf.Params["id-or-host"])
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server name from ID: "+err.Error()))
-		return
-	} else if !ok {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("not found"), nil)
-		return
-	}
-
-	toToolName, toURL, err := ats.GetToolNameAndURL(inf.Tx.Tx)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting tool name and url: "+err.Error()))
-		return
-	}
-
-	params, err := GetServerParams(inf.Tx.Tx, serverName, atscfg.IPAllowConfigFileName)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server '"+string(serverName)+"' + ip_allow parameters: "+err.Error()))
-		return
-	}
-
-	childServers := map[tc.CacheName]atscfg.IPAllowServer{}
-	if strings.HasPrefix(string(serverType), tc.MidTypePrefix) {
-		if childServers, err = GetChildServers(inf.Tx.Tx, serverName); err != nil {
-			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting child servers from mid '"+string(serverName)+"': "+err.Error()))
-			return
-		}
-	}
-
-	txt := atscfg.MakeIPAllowDotConfig(serverName, serverType, toToolName, toURL, params, childServers)
-
-	w.Header().Set("Content-Type", "text/plain")
-	w.Write([]byte(txt))
-}
-
-// GetChildServers returns the child servers of the given Mid serverName. This should not be called with an Edge server.
-func GetChildServers(tx *sql.Tx, serverName tc.CacheName) (map[tc.CacheName]atscfg.IPAllowServer, error) {
-	qry := `
-SELECT
-  s.host_name,
-  s.ip_address,
-  COALESCE(s.ip6_address, '')
-FROM
-  server s
-  JOIN type tp on tp.id = s.type
-  JOIN cachegroup cg on cg.id = s.cachegroup
-WHERE
-  (tp.name = '` + tc.MonitorTypeName + `' OR ( tp.name LIKE '` + tc.EdgeTypePrefix + `%')
-  AND cg.id IN (
-    SELECT
-      cg2.id
-    FROM
-     server s2
-     JOIN cachegroup cg2 ON (cg2.parent_cachegroup_id = s2.cachegroup OR cg2.secondary_parent_cachegroup_id = s2.cachegroup)
-    WHERE
-      s2.host_name = $1 )
-  )
-`
-	rows, err := tx.Query(qry, serverName)
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	servers := map[tc.CacheName]atscfg.IPAllowServer{}
-	for rows.Next() {
-		svName := tc.CacheName("")
-		sv := atscfg.IPAllowServer{}
-		if err := rows.Scan(&svName, &sv.IPAddress, &sv.IP6Address); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		servers[svName] = sv
-	}
-	return servers, nil
-}
-
-func GetServerParams(tx *sql.Tx, serverName tc.CacheName, configFile string) (map[string][]string, error) {
-	qry := `
-SELECT
-  pa.name,
-  pa.value
-FROM
-  parameter pa
-  JOIN profile_parameter pp ON pp.parameter = pa.id
-  JOIN profile pr ON pr.id = pp.profile
-  JOIN server s ON s.profile = pr.id
-WHERE
-  s.host_name = $1
-  AND pa.config_file = $2
-`
-	rows, err := tx.Query(qry, serverName, configFile)
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	params := map[string][]string{}
-	for rows.Next() {
-		name := ""
-		val := ""
-		if err := rows.Scan(&name, &val); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		params[name] = append(params[name], val)
-	}
-	return params, nil
-}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/meta.go b/traffic_ops/traffic_ops_golang/ats/atsserver/meta.go
index 26e741d..d6d94c3 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/meta.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsserver/meta.go
@@ -25,6 +25,8 @@ import (
 
 	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-rfc"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 )
@@ -82,7 +84,7 @@ func GetConfigMetaData(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	txt := atscfg.MakeMetaConfig(serverName, server, tmParams.URL, tmParams.ReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dsNames)
+	txt := atscfg.MakeMetaConfig(tc.CacheName(serverName), server, tmParams.URL, tmParams.ReverseProxyURL, locationParams, uriSignedDSes, scopeParams, dsNames)
 	w.Header().Set(rfc.ContentType, rfc.ApplicationJSON)
 	w.Write([]byte(txt))
 }
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/packages.go b/traffic_ops/traffic_ops_golang/ats/atsserver/packages.go
index 65b4025..7da7e42 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/packages.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsserver/packages.go
@@ -26,6 +26,7 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-atscfg"
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
 )
 
 func GetPackages(w http.ResponseWriter, r *http.Request) {
@@ -45,7 +46,7 @@ func GetPackages(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	params, err := GetServerParams(inf.Tx.Tx, serverName, atscfg.PackagesParamConfigFile)
+	params, err := ats.GetServerParams(inf.Tx.Tx, serverName, atscfg.PackagesParamConfigFile)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server '"+string(serverName)+"' + package parameters: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/parentdotconfig.go b/traffic_ops/traffic_ops_golang/ats/atsserver/parentdotconfig.go
deleted file mode 100644
index c3223f1..0000000
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/parentdotconfig.go
+++ /dev/null
@@ -1,832 +0,0 @@
-package atsserver
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"database/sql"
-	"errors"
-	"net/http"
-	"strconv"
-	"strings"
-
-	"github.com/apache/trafficcontrol/lib/go-atscfg"
-	"github.com/apache/trafficcontrol/lib/go-log"
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-
-	"github.com/lib/pq"
-)
-
-func GetParentDotConfig(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"server-name-or-id"}, nil)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	idOrHost := strings.TrimSuffix(inf.Params["server-name-or-id"], ".json")
-	hostName := ""
-	isHost := false
-	id, err := strconv.Atoi(idOrHost)
-	if err != nil {
-		isHost = true
-		hostName = idOrHost
-	}
-
-	serverInfo, ok, err := &atscfg.ServerInfo{}, false, error(nil)
-	if isHost {
-		serverInfo, ok, err = getServerInfoByHost(inf.Tx.Tx, hostName)
-	} else {
-		serverInfo, ok, err = getServerInfoByID(inf.Tx.Tx, id)
-	}
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server info: "+err.Error()))
-		return
-	}
-	if !ok {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("server not found"), nil)
-		return
-	}
-
-	atsMajorVer, err := GetATSMajorVersion(inf.Tx.Tx, int(serverInfo.ProfileID))
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting ATS major version: "+err.Error()))
-		return
-	}
-
-	toolName, toURL, err := ats.GetToolNameAndURL(inf.Tx.Tx)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting toolname and TO url parameters: "+err.Error()))
-		return
-	}
-
-	parentConfigDSes := []atscfg.ParentConfigDSTopLevel{}
-	if serverInfo.IsTopLevelCache() {
-		parentConfigDSes, err = getParentConfigDSTopLevel(inf.Tx.Tx, serverInfo.CDN)
-	} else {
-		parentConfigDSes, err = getParentConfigDS(inf.Tx.Tx, serverInfo.ID)
-	}
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server params: "+err.Error()))
-		return
-	}
-
-	serverParams, err := getParentConfigServerProfileParams(inf.Tx.Tx, serverInfo.ID)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server params: "+err.Error()))
-		return
-	}
-
-	parentInfos, err := getParentInfo(inf.Tx.Tx, serverInfo)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server parent info: "+err.Error()))
-		return
-	}
-
-	text := atscfg.MakeParentDotConfig(serverInfo, atsMajorVer, toolName, toURL, parentConfigDSes, serverParams, parentInfos)
-
-	w.Header().Set("Content-Type", "text/plain")
-	w.Write([]byte(text))
-}
-
-type ParentConfigDSSortByName []atscfg.ParentConfigDS
-
-func (s ParentConfigDSSortByName) Len() int      { return len(([]atscfg.ParentConfigDS)(s)) }
-func (s ParentConfigDSSortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
-func (s ParentConfigDSSortByName) Less(i, j int) bool {
-	// TODO make this match the Perl sort "foreach my $ds ( sort @{ $data->{dslist} } )" ?
-	return strings.Compare(string(s[i].Name), string(s[j].Name)) < 0
-}
-
-// getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfoByID(tx *sql.Tx, id int) (*atscfg.ServerInfo, bool, error) {
-	return getServerInfo(tx, ServerInfoQuery()+`WHERE s.id = $1`, []interface{}{id})
-}
-
-// getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfoByHost(tx *sql.Tx, host string) (*atscfg.ServerInfo, bool, error) {
-	return getServerInfo(tx, ServerInfoQuery()+` WHERE s.host_name = $1 `, []interface{}{host})
-}
-
-// getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfo(tx *sql.Tx, qry string, qryParams []interface{}) (*atscfg.ServerInfo, bool, error) {
-	s := atscfg.ServerInfo{}
-	if err := tx.QueryRow(qry, qryParams...).Scan(&s.CDN, &s.CDNID, &s.ID, &s.HostName, &s.DomainName, &s.IP, &s.ProfileID, &s.ProfileName, &s.Port, &s.Type, &s.CacheGroupID, &s.ParentCacheGroupID, &s.SecondaryParentCacheGroupID, &s.ParentCacheGroupType, &s.SecondaryParentCacheGroupType); err != nil {
-		if err == sql.ErrNoRows {
-			return nil, false, nil
-		}
-		return nil, false, errors.New("querying server info: " + err.Error())
-	}
-	return &s, true, nil
-}
-
-func ServerInfoQuery() string {
-	return `
-SELECT
-  c.name as cdn,
-  s.cdn_id,
-  s.id,
-  s.host_name,
-  c.domain_name,
-  s.ip_address,
-  s.profile AS profile_id,
-  p.name AS profile_name,
-  s.tcp_port,
-  t.name as type,
-  s.cachegroup,
-  COALESCE(cg.parent_cachegroup_id, ` + strconv.Itoa(atscfg.InvalidID) + `) as parent_cachegroup_id,
-  COALESCE(cg.secondary_parent_cachegroup_id, ` + strconv.Itoa(atscfg.InvalidID) + `) as secondary_parent_cachegroup_id,
-  COALESCE(parentt.name, '') as parent_cachegroup_type,
-  COALESCE(sparentt.name, '') as secondary_parent_cachegroup_type
-FROM
-  server s
-  JOIN cdn c ON s.cdn_id = c.id
-  JOIN type t ON s.type = t.id
-  JOIN profile p ON p.id = s.profile
-  JOIN cachegroup cg on s.cachegroup = cg.id
-  LEFT JOIN type parentt on parentt.id = (select type from cachegroup where id = cg.parent_cachegroup_id)
-  LEFT JOIN type sparentt on sparentt.id = (select type from cachegroup where id = cg.secondary_parent_cachegroup_id)
-`
-}
-
-// GetATSMajorVersion returns the major version of the given profile's package trafficserver parameter.
-// If no parameter exists, this does not return an error, but rather logs a warning and uses DefaultATSVersion.
-func GetATSMajorVersion(tx *sql.Tx, serverProfileID int) (int, error) {
-	atsVersion, _, err := ats.GetProfileParamValue(tx, serverProfileID, "package", "trafficserver")
-	if err != nil {
-		return 0, errors.New("getting profile param value: " + err.Error())
-	}
-	if len(atsVersion) == 0 {
-		atsVersion = atscfg.DefaultATSVersion
-		log.Warnln("Parameter package.trafficserver missing for profile " + strconv.Itoa(int(serverProfileID)) + ". Assuming version " + atsVersion)
-	}
-	return atscfg.GetATSMajorVersionFromATSVersion(atsVersion)
-}
-
-type ParentConfigDS struct {
-	Name            tc.DeliveryServiceName
-	QStringIgnore   tc.QStringIgnore
-	OriginFQDN      string
-	MultiSiteOrigin bool
-	OriginShield    string
-	Type            tc.DSType
-
-	QStringHandling string
-}
-
-type ParentConfigDSTopLevel struct {
-	ParentConfigDS
-	MSOAlgorithm                       string
-	MSOParentRetry                     string
-	MSOUnavailableServerRetryResponses string
-	MSOMaxSimpleRetries                string
-	MSOMaxUnavailableServerRetries     string
-}
-
-func ParentConfigDSQuerySelect() string {
-	return `
-SELECT
-  ds.xml_id,
-  COALESCE(ds.qstring_ignore, ` + tc.QStringIgnoreUseInCacheKeyAndPassUp.String() + `),
-  COALESCE((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,
-  COALESCE(ds.multi_site_origin, false),
-  COALESCE(ds.origin_shield, ''),
-  ARRAY(SELECT required_capability FROM deliveryservices_required_capability dsrc WHERE dsrc.deliveryservice_id = ds.id),
-  dt.name AS ds_type
-`
-}
-
-const ParentConfigDSQueryFromTopLevel = `
-FROM
-  deliveryservice ds
-  JOIN type as dt ON ds.type = dt.id
-  JOIN cdn ON cdn.id = ds.cdn_id
-` // TODO Perl does 'JOIN deliveryservice_regex dsr ON dsr.deliveryservice = ds.id   JOIN regex r ON dsr.regex = r.id   JOIN type as rt ON r.type = rt.id' and orders by, but doesn't use; ensure it isn't necessary
-
-const ParentConfigDSQueryFrom = ParentConfigDSQueryFromTopLevel + `
-`
-
-const ParentConfigDSQueryOrder = `
-ORDER BY ds.id
-` // TODO: perl does 'ORDER BY ds.id, rt.name, dsr.set_number' - but doesn't actually use regexes - ensure it isn't necessary
-
-const ParentConfigDSQueryWhere = `
-WHERE ds.id in (SELECT DISTINCT(dss.deliveryservice) FROM deliveryservice_server dss where dss.server = $1)
-`
-
-const ParentConfigDSQueryWhereTopLevel = `
-WHERE
-  cdn.name = $1
-  AND ds.id in (SELECT deliveryservice_server.deliveryservice FROM deliveryservice_server)
-  AND ds.active = true
-`
-
-func ParentConfigDSQuery() string {
-	return ParentConfigDSQuerySelect() +
-		ParentConfigDSQueryFrom +
-		ParentConfigDSQueryWhere +
-		ParentConfigDSQueryOrder
-}
-
-func ParentConfigDSQueryTopLevel() string {
-	return ParentConfigDSQuerySelect() +
-		ParentConfigDSQueryFromTopLevel +
-		ParentConfigDSQueryWhereTopLevel +
-		ParentConfigDSQueryOrder
-}
-
-func getParentConfigDSTopLevel(tx *sql.Tx, cdnName tc.CDNName) ([]atscfg.ParentConfigDSTopLevel, error) {
-	dses, err := getParentConfigDSRaw(tx, ParentConfigDSQueryTopLevel(), []interface{}{cdnName})
-	if err != nil {
-		return nil, errors.New("getting top level raw parent config ds: " + err.Error())
-	}
-	topDSes := []atscfg.ParentConfigDSTopLevel{}
-	for _, ds := range dses {
-		topDSes = append(topDSes, ds)
-	}
-
-	dsesWithParams, err := getParentConfigDSParamsTopLevel(tx, topDSes)
-	if err != nil {
-		return nil, errors.New("getting top level ds params: " + err.Error())
-	}
-
-	return dsesWithParams, nil
-}
-
-func getParentConfigDS(tx *sql.Tx, serverID int) ([]atscfg.ParentConfigDSTopLevel, error) {
-	dses, err := getParentConfigDSRaw(tx, ParentConfigDSQuery(), []interface{}{serverID})
-	if err != nil {
-		return nil, errors.New("getting raw parent config ds: " + err.Error())
-	}
-
-	dsesWithParams, err := getParentConfigDSParams(tx, dses)
-	if err != nil {
-		return nil, errors.New("getting ds params: " + err.Error())
-	}
-	return dsesWithParams, nil
-}
-
-func getParentConfigServerProfileParams(tx *sql.Tx, serverID int) (map[string]string, error) {
-	qry := `
-SELECT
-  pa.name,
-  pa.value
-FROM
-  parameter pa
-  JOIN profile_parameter pp ON pp.parameter = pa.id
-  JOIN profile pr ON pr.id = pp.profile
-  JOIN server s on s.profile = pr.id
-WHERE
-  s.id = $1
-  AND pa.config_file = 'parent.config'
-  AND pa.name IN (
-    '` + atscfg.ParentConfigParamQStringHandling + `',
-    '` + atscfg.ParentConfigParamAlgorithm + `',
-    '` + atscfg.ParentConfigParamQString + `'
-  )
-`
-	rows, err := tx.Query(qry, serverID)
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-	params := map[string]string{}
-	for rows.Next() {
-		name := ""
-		val := ""
-		if err := rows.Scan(&name, &val); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		params[name] = val
-	}
-	return params, nil
-}
-
-// getParentConfigDSRaw returns a ParentConfigDSTopLevel, but all fields in addition to ParentConfigDS will be defaulted. This is because a ParentConfigDSTopLevel is returned to share the same interface, but it doesn't actually have top level data.
-func getParentConfigDSRaw(tx *sql.Tx, qry string, qryParams []interface{}) ([]atscfg.ParentConfigDSTopLevel, error) {
-	rows, err := tx.Query(qry, qryParams...)
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-	dses := []atscfg.ParentConfigDSTopLevel{}
-	for rows.Next() {
-		d := atscfg.ParentConfigDS{RequiredCapabilities: map[atscfg.ServerCapability]struct{}{}}
-		requiredCaps := []string{}
-		if err := rows.Scan(&d.Name, &d.QStringIgnore, &d.OriginFQDN, &d.MultiSiteOrigin, &d.OriginShield, pq.Array(&requiredCaps), &d.Type); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		if d.OriginFQDN == "" {
-			// TODO skip ANY_MAP DSes? Why? Did Perl, I didn't see it?
-			log.Errorf("parent.config generation: getting parent config ds: server %+v has no origin, skipping!\n", d.Name)
-			continue
-		}
-		d.Type = tc.DSTypeFromString(string(d.Type))
-		for _, cap := range requiredCaps {
-
-			d.RequiredCapabilities[atscfg.ServerCapability(cap)] = struct{}{}
-		}
-		dses = append(dses, atscfg.ParentConfigDSTopLevel{ParentConfigDS: d})
-	}
-
-	return dses, nil
-}
-
-func parentConfigDSesToNamesTopLevel(dses []atscfg.ParentConfigDSTopLevel) []string {
-	names := []string{}
-	for _, ds := range dses {
-		names = append(names, string(ds.Name))
-	}
-	return names
-}
-
-const ParentConfigDSParamsQuerySelect = `
-SELECT
-  ds.xml_id,
-  pa.name,
-  pa.value
-`
-const ParentConfigDSParamsQueryFrom = `
-FROM
-  parameter pa
-  JOIN profile_parameter pp ON pp.parameter = pa.id
-  JOIN profile pr ON pr.id = pp.profile
-  JOIN deliveryservice ds on ds.profile = pr.id
-`
-const ParentConfigDSParamsQueryWhere = `
-WHERE
-  pa.config_file = 'parent.config'
-  AND ds.xml_id = ANY($1)
-  AND pa.name IN (
-    '` + atscfg.ParentConfigParamQStringHandling + `'
-  )
-`
-
-var ParentConfigDSParamsQueryWhereTopLevel = `
-WHERE
-  pa.config_file = 'parent.config'
-  AND ds.xml_id = ANY($1)
-  AND pa.name IN (
-    '` + atscfg.ParentConfigParamQStringHandling + `',
-    '` + atscfg.ParentConfigParamMSOAlgorithm + `',
-    '` + atscfg.ParentConfigParamMSOParentRetry + `',
-    '` + atscfg.ParentConfigParamUnavailableServerRetryResponses + `',
-    '` + atscfg.ParentConfigParamMaxSimpleRetries + `',
-    '` + atscfg.ParentConfigParamMaxUnavailableServerRetries + `'
-  )
-`
-
-const ParentConfigDSParamsQuery = ParentConfigDSParamsQuerySelect + ParentConfigDSParamsQueryFrom + ParentConfigDSParamsQueryWhere
-
-var ParentConfigDSParamsQueryTopLevel = ParentConfigDSParamsQuerySelect + ParentConfigDSParamsQueryFrom + ParentConfigDSParamsQueryWhereTopLevel
-
-func getParentConfigDSParams(tx *sql.Tx, dses []atscfg.ParentConfigDSTopLevel) ([]atscfg.ParentConfigDSTopLevel, error) {
-	params, err := getParentConfigDSParamsRaw(tx, ParentConfigDSParamsQuery, parentConfigDSesToNamesTopLevel(dses))
-	if err != nil {
-		return nil, err
-	}
-	for i, ds := range dses {
-		dsParams, ok := params[ds.Name]
-		if !ok {
-			continue
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamQStringHandling]; ok {
-			ds.QStringHandling = v
-			dses[i] = ds
-		}
-	}
-	return dses, nil
-}
-
-func getParentConfigDSParamsTopLevel(tx *sql.Tx, dses []atscfg.ParentConfigDSTopLevel) ([]atscfg.ParentConfigDSTopLevel, error) {
-	params, err := getParentConfigDSParamsRaw(tx, ParentConfigDSParamsQueryTopLevel, parentConfigDSesToNamesTopLevel(dses))
-	if err != nil {
-		return nil, err
-	}
-	for i, ds := range dses {
-		dsParams := params[ds.Name] // it's acceptable for this to not exist, if there are no params for the DS. If so, we still need to continue below, to set all the defaults.
-		if v, ok := dsParams[atscfg.ParentConfigParamQStringHandling]; ok {
-			ds.QStringHandling = v
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamMSOAlgorithm]; ok && strings.TrimSpace(v) != "" {
-			ds.MSOAlgorithm = v
-		} else {
-			ds.MSOAlgorithm = atscfg.ParentConfigDSParamDefaultMSOAlgorithm
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamMSOParentRetry]; ok {
-			ds.MSOParentRetry = v
-		} else {
-			ds.MSOParentRetry = atscfg.ParentConfigDSParamDefaultMSOParentRetry
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamUnavailableServerRetryResponses]; ok {
-			ds.MSOUnavailableServerRetryResponses = v
-		} else {
-			ds.MSOUnavailableServerRetryResponses = atscfg.ParentConfigDSParamDefaultMSOUnavailableServerRetryResponses
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamMaxSimpleRetries]; ok {
-			ds.MSOMaxSimpleRetries = v
-		} else {
-			ds.MSOMaxSimpleRetries = atscfg.ParentConfigDSParamDefaultMaxSimpleRetries
-		}
-		if v, ok := dsParams[atscfg.ParentConfigParamMaxUnavailableServerRetries]; ok {
-			ds.MSOMaxUnavailableServerRetries = v
-		} else {
-			ds.MSOMaxUnavailableServerRetries = atscfg.ParentConfigDSParamDefaultMaxUnavailableServerRetries
-		}
-		dses[i] = ds
-	}
-	return dses, nil
-}
-
-func getParentConfigDSParamsRaw(tx *sql.Tx, qry string, dsNames []string) (map[tc.DeliveryServiceName]map[string]string, error) {
-	rows, err := tx.Query(qry, pq.Array(dsNames))
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	params := map[tc.DeliveryServiceName]map[string]string{}
-	for rows.Next() {
-		dsName := tc.DeliveryServiceName("")
-		pName := ""
-		pVal := ""
-		if err := rows.Scan(&dsName, &pName, &pVal); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		if _, ok := params[dsName]; !ok {
-			params[dsName] = map[string]string{}
-		}
-		params[dsName][pName] = pVal
-	}
-	return params, nil
-}
-
-func getParentInfo(tx *sql.Tx, server *atscfg.ServerInfo) (map[atscfg.OriginHost][]atscfg.ParentInfo, error) {
-	parentInfos := map[atscfg.OriginHost][]atscfg.ParentInfo{}
-
-	serverDomain, ok, err := getCDNDomainByProfileID(tx, server.ProfileID)
-	if err != nil {
-		return nil, errors.New("getting CDN domain from profile ID: " + err.Error())
-	} else if !ok || serverDomain == "" {
-		return parentInfos, nil // TODO warn? Perl doesn't.
-	}
-
-	profileCaches, originServers, err := getServerParentCacheGroupProfiles(tx, server)
-	if err != nil {
-		return nil, errors.New("getting server parent cachegroup profiles: " + err.Error())
-	}
-
-	return atscfg.MakeParentInfo(server, serverDomain, profileCaches, originServers), nil
-}
-
-// getServerParentCacheGroupProfiles gets the profile information for servers belonging to the parent cachegroup, and secondary parent cachegroup, of the cachegroup of each server.
-func getServerParentCacheGroupProfiles(tx *sql.Tx, server *atscfg.ServerInfo) (map[atscfg.ProfileID]atscfg.ProfileCache, map[atscfg.OriginHost][]atscfg.CGServer, error) {
-	// TODO make this more efficient - should be a single query - this was transliterated from Perl - it's extremely inefficient.
-
-	profileCaches := map[atscfg.ProfileID]atscfg.ProfileCache{}
-	originServers := map[atscfg.OriginHost][]atscfg.CGServer{} // "deliveryServices" in Perl
-
-	qry := ""
-	if server.IsTopLevelCache() {
-		// multisite origins take all the org groups in to account
-		qry = `
-WITH parent_cachegroup_ids AS (
-  SELECT cg.id as v
-  FROM cachegroup cg
-  JOIN type on type.id = cg.type
-  WHERE type.name = '` + tc.CacheGroupOriginTypeName + `'
-)
-`
-	} else {
-		qry = `
-WITH server_cachegroup_ids AS (
-  SELECT cachegroup as v FROM server WHERE id = $2
-),
-parent_cachegroup_ids AS (
-  SELECT parent_cachegroup_id as v
-  FROM cachegroup WHERE id IN (SELECT v from server_cachegroup_ids)
-  UNION ALL
-  SELECT secondary_parent_cachegroup_id as v
-  FROM cachegroup WHERE id IN (SELECT v from server_cachegroup_ids)
-)
-`
-	}
-
-	qry += `
-SELECT
-  s.id,
-  s.host_name,
-  s.ip_address,
-  s.tcp_port,
-  s.cachegroup,
-  s.status,
-  s.type,
-  s.profile,
-  s.cdn_id,
-  stype.name as type_name,
-  ARRAY(SELECT server_capability FROM server_server_capability ssc WHERE ssc.server = s.id),
-  s.domain_name
-FROM
-  server s
-  JOIN type stype ON s.type = stype.id
-  JOIN cachegroup cg ON cg.id = s.cachegroup
-  JOIN cdn on s.cdn_id = cdn.id
-  JOIN status st ON st.id = s.status
-WHERE
-  cg.id IN (SELECT v FROM parent_cachegroup_ids)
-  AND (stype.name = '` + tc.OriginTypeName + `' OR stype.name LIKE '` + tc.EdgeTypePrefix + `%' OR stype.name LIKE '` + tc.MidTypePrefix + `%')
-  AND (st.name = '` + string(tc.CacheStatusReported) + `' OR st.name = '` + string(tc.CacheStatusOnline) + `')
-  AND cdn.name = $1
-`
-
-	// TODO move qry, qryParams to separate funcs/consts
-	qryParams := []interface{}{}
-	if server.IsTopLevelCache() {
-		qryParams = []interface{}{server.CDN}
-	} else {
-		qryParams = []interface{}{server.CDN, server.ID}
-	}
-
-	rows, err := tx.Query(qry, qryParams...)
-	if err != nil {
-		return nil, nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	cgServerIDs := []int{}
-	cgServers := []atscfg.CGServer{}
-	for rows.Next() {
-		s := atscfg.CGServer{Capabilities: map[atscfg.ServerCapability]struct{}{}}
-		caps := []string{}
-		if err := rows.Scan(&s.ServerID, &s.ServerHost, &s.ServerIP, &s.ServerPort, &s.CacheGroupID, &s.Status, &s.Type, &s.ProfileID, &s.CDN, &s.TypeName, pq.Array(&caps), &s.Domain); err != nil {
-			return nil, nil, errors.New("scanning: " + err.Error())
-		}
-		for _, cap := range caps {
-			s.Capabilities[atscfg.ServerCapability(cap)] = struct{}{}
-		}
-		cgServers = append(cgServers, s)
-		cgServerIDs = append(cgServerIDs, int(s.ServerID))
-	}
-
-	serverCapabilities, err := ats.GetServerCapabilitiesByID(tx, cgServerIDs)
-	if err != nil {
-		return nil, nil, errors.New("getting server capabilities: " + err.Error())
-	}
-
-	cgServerDSes, err := getServerDSes(tx, cgServerIDs)
-	if err != nil {
-		return nil, nil, errors.New("getting cachegroup server deliveryservices: " + err.Error())
-	}
-
-	profileParams, err := getParentConfigServerCacheProfileParams(tx, cgServerIDs) // TODO change to take cg IDs directly?
-	if err != nil {
-		return nil, nil, errors.New("getting cachegroup server profile params: " + err.Error())
-	}
-
-	allDSMap := map[atscfg.DeliveryServiceID]struct{}{}
-	for _, dses := range cgServerDSes {
-		for _, ds := range dses {
-			allDSMap[ds] = struct{}{}
-		}
-	}
-	allDSes := []int{}
-	for ds, _ := range allDSMap {
-		allDSes = append(allDSes, int(ds))
-	}
-
-	dsRequiredCapabilities, err := ats.GetDeliveryServiceRequiredCapabilities(tx, allDSes)
-	if err != nil {
-		return nil, nil, errors.New("getting DS required capabilities: " + err.Error())
-	}
-
-	dsOrigins, err := getDSOrigins(tx, allDSes)
-	if err != nil {
-		return nil, nil, errors.New("getting deliveryservice origins: " + err.Error())
-	}
-
-	for _, cgServer := range cgServers {
-		if cgServer.TypeName == tc.OriginTypeName {
-			dses := cgServerDSes[cgServer.ServerID]
-			for _, ds := range dses {
-				orgURI := dsOrigins[ds]
-				if atscfg.HasRequiredCapabilities(serverCapabilities[int(cgServer.ServerID)], dsRequiredCapabilities[int(ds)]) {
-					originServers[atscfg.OriginHost(orgURI.Host)] = append(originServers[atscfg.OriginHost(orgURI.Host)], cgServer)
-				}
-			}
-		} else {
-			originServers[atscfg.DeliveryServicesAllParentsKey] = append(originServers[atscfg.DeliveryServicesAllParentsKey], cgServer)
-		}
-
-		if _, profileCachesHasProfile := profileCaches[cgServer.ProfileID]; !profileCachesHasProfile {
-			defaultProfileCache := atscfg.DefaultProfileCache()
-			if profileCache, profileParamsHasProfile := profileParams[cgServer.ProfileID]; !profileParamsHasProfile {
-				log.Warnf("cachegroup has server with profile %+v but that profile has no parameters", cgServer.ProfileID)
-				profileCaches[cgServer.ProfileID] = defaultProfileCache
-			} else {
-				profileCaches[cgServer.ProfileID] = profileCache
-			}
-		}
-	}
-	return profileCaches, originServers, nil
-}
-
-func getServerDSes(tx *sql.Tx, serverIDs []int) (map[atscfg.ServerID][]atscfg.DeliveryServiceID, error) {
-	sds := map[atscfg.ServerID][]atscfg.DeliveryServiceID{}
-	if len(serverIDs) == 0 {
-		return sds, nil
-	}
-	qry := `
-SELECT
-  dss.server,
-  dss.deliveryservice
-FROM
-  deliveryservice_server dss
-WHERE
-  dss.server = ANY($1)
-`
-	rows, err := tx.Query(qry, pq.Array(serverIDs))
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		sID := atscfg.ServerID(0)
-		dsID := atscfg.DeliveryServiceID(0)
-		if err := rows.Scan(&sID, &dsID); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		sds[sID] = append(sds[sID], dsID)
-	}
-	return sds, nil
-}
-
-func getDSOrigins(tx *sql.Tx, dsIDs []int) (map[atscfg.DeliveryServiceID]*atscfg.OriginURI, error) {
-	origins := map[atscfg.DeliveryServiceID]*atscfg.OriginURI{}
-	if len(dsIDs) == 0 {
-		return origins, nil
-	}
-	qry := `
-SELECT
-  ds.id,
-  o.protocol::text,
-  o.fqdn,
-  COALESCE(o.port::text, '')
-FROM
-  deliveryservice ds
-  JOIN origin o ON o.deliveryservice = ds.id
-WHERE
-  ds.id = ANY($1)
-  AND o.is_primary
-`
-	rows, err := tx.Query(qry, pq.Array(dsIDs))
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		id := atscfg.DeliveryServiceID(0)
-		uri := &atscfg.OriginURI{}
-		if err := rows.Scan(&id, &uri.Scheme, &uri.Host, &uri.Port); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		if uri.Port == "" {
-			if uri.Scheme == "http" {
-				uri.Port = "80"
-			} else if uri.Scheme == "https" {
-				uri.Port = "443"
-			} else {
-				log.Warnf("parent.config generation: origin had unknown scheme '" + uri.Scheme + "' and no port; leaving port empty")
-			}
-		}
-		origins[id] = uri
-	}
-	return origins, nil
-}
-
-func getParentConfigServerCacheProfileParams(tx *sql.Tx, serverIDs []int) (map[atscfg.ProfileID]atscfg.ProfileCache, error) {
-	qry := `
-SELECT
-  pr.id,
-  pa.name,
-  pa.value
-FROM
-  parameter pa
-  JOIN profile_parameter pp ON pp.parameter = pa.id
-  JOIN profile pr ON pr.id = pp.profile
-  JOIN server s on s.profile = pr.id
-WHERE
-  s.id = ANY($1)
-  AND pa.config_file = 'parent.config'
-  AND pa.name IN (
-    '` + atscfg.ParentConfigCacheParamWeight + `',
-    '` + atscfg.ParentConfigCacheParamPort + `',
-    '` + atscfg.ParentConfigCacheParamUseIP + `',
-    '` + atscfg.ParentConfigCacheParamRank + `',
-    '` + atscfg.ParentConfigCacheParamNotAParent + `'
-  )
-`
-	rows, err := tx.Query(qry, pq.Array(serverIDs))
-	if err != nil {
-		return nil, errors.New("querying: " + err.Error())
-	}
-	defer rows.Close()
-
-	type Param struct {
-		ProfileID atscfg.ProfileID
-		Name      string
-		Val       string
-	}
-
-	params := []Param{}
-	for rows.Next() {
-		p := Param{}
-		if err := rows.Scan(&p.ProfileID, &p.Name, &p.Val); err != nil {
-			return nil, errors.New("scanning: " + err.Error())
-		}
-		params = append(params, p)
-	}
-
-	sParams := map[atscfg.ProfileID]atscfg.ProfileCache{} // TODO change to map of pointers? Does efficiency matter?
-	for _, param := range params {
-		profileCache, ok := sParams[param.ProfileID]
-		if !ok {
-			profileCache = atscfg.DefaultProfileCache()
-		}
-		switch param.Name {
-		case atscfg.ParentConfigCacheParamWeight:
-			// f, err := strconv.ParseFloat(param.Val, 64)
-			// if err != nil {
-			// 	log.Errorln("parent.config generation: weight param is not a float, skipping! : " + err.Error())
-			// } else {
-			// 	profileCache.Weight = f
-			// }
-			// TODO validate float?
-			profileCache.Weight = param.Val
-		case atscfg.ParentConfigCacheParamPort:
-			i, err := strconv.ParseInt(param.Val, 10, 64)
-			if err != nil {
-				log.Errorln("parent.config generation: port param is not an integer, skipping! : " + err.Error())
-			} else {
-				profileCache.Port = int(i)
-			}
-		case atscfg.ParentConfigCacheParamUseIP:
-			profileCache.UseIP = param.Val == "1"
-		case atscfg.ParentConfigCacheParamRank:
-			i, err := strconv.ParseInt(param.Val, 10, 64)
-			if err != nil {
-				log.Errorln("parent.config generation: rank param is not an integer, skipping! : " + err.Error())
-			} else {
-				profileCache.Rank = int(i)
-			}
-
-		case atscfg.ParentConfigCacheParamNotAParent:
-			profileCache.NotAParent = param.Val != "false"
-		default:
-			return nil, errors.New("query returned unexpected param: " + param.Name)
-		}
-		sParams[param.ProfileID] = profileCache
-	}
-	return sParams, nil
-}
-
-type ParentConfigServerParams struct {
-	QString         string
-	Algorithm       string
-	QStringHandling bool
-}
-
-func getCDNDomainByProfileID(tx *sql.Tx, profileID atscfg.ProfileID) (string, bool, error) {
-	qry := `SELECT domain_name from cdn where id = (select cdn from profile where id = $1)`
-	val := ""
-	if err := tx.QueryRow(qry, profileID).Scan(&val); err != nil {
-		if err == sql.ErrNoRows {
-			return "", false, nil
-		}
-		return "", false, errors.New("querying: " + err.Error())
-	}
-	return val, true, nil
-}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/remapdotconfig.go b/traffic_ops/traffic_ops_golang/ats/atsserver/remapdotconfig.go
deleted file mode 100644
index 9ce112b..0000000
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/remapdotconfig.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package atsserver
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"errors"
-	"io"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/lib/go-atscfg"
-	"github.com/apache/trafficcontrol/lib/go-rfc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/ats"
-)
-
-func GetServerConfigRemap(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"server-name-or-id"}, nil)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	serverName, userErr, sysErr, errCode := ats.GetServerNameFromNameOrID(inf.Tx.Tx, inf.Params["server-name-or-id"])
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-
-	toToolName, toURL, err := ats.GetToolNameAndURL(inf.Tx.Tx)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting tool name and url: "+err.Error()))
-		return
-	}
-
-	atsMajorVersion, err := ats.GetATSMajorVersionFromServerName(inf.Tx.Tx, serverName)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting ATS major version: "+err.Error()))
-		return
-	}
-
-	cacheURLConfigParams, err := ats.GetServerProfileParamData(inf.Tx.Tx, serverName, "cacheurl.config")
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cacheurl.config params: "+err.Error()))
-		return
-	}
-
-	serverInfo, ok, err := ats.GetServerInfoByHost(inf.Tx.Tx, serverName)
-	if !ok {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("server not found"), nil)
-		return
-	}
-
-	remapDSData, err := ats.GetRemapDSData(inf.Tx.Tx, serverInfo)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting remap ds data: "+err.Error()))
-		return
-	}
-
-	dsProfilesCacheKeyConfigParams, err := ats.GetProfilesParamData(inf.Tx.Tx, atscfg.DSProfileIDs(remapDSData), "cachekey.config")
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting profiles param data for cachekey: "+err.Error()))
-		return
-	}
-
-	serverPackageParamData, err := ats.GetServerParamData(inf.Tx.Tx, int(serverInfo.ProfileID), "package", serverInfo.HostName, serverInfo.DomainName)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("Getting server package param data: "+err.Error()))
-		return
-	}
-
-	txt := atscfg.MakeRemapDotConfig(serverName, toToolName, toURL, atsMajorVersion, cacheURLConfigParams, dsProfilesCacheKeyConfigParams, serverPackageParamData, serverInfo, remapDSData)
-
-	w.Header().Set(rfc.ContentType, rfc.ContentTypeTextPlain)
-	io.WriteString(w, txt)
-}
diff --git a/traffic_ops/traffic_ops_golang/ats/atsserver/unknown.go b/traffic_ops/traffic_ops_golang/ats/atsserver/unknown.go
index 0a578cf..e77582b 100644
--- a/traffic_ops/traffic_ops_golang/ats/atsserver/unknown.go
+++ b/traffic_ops/traffic_ops_golang/ats/atsserver/unknown.go
@@ -52,7 +52,7 @@ func GetUnknown(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	params, err := GetServerParams(inf.Tx.Tx, serverName, inf.Params["file"])
+	params, err := ats.GetServerParams(inf.Tx.Tx, serverName, inf.Params["file"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting server '"+string(serverName)+"' + package parameters: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/ats/db.go b/traffic_ops/traffic_ops_golang/ats/db.go
index 3df8787..95062ef 100644
--- a/traffic_ops/traffic_ops_golang/ats/db.go
+++ b/traffic_ops/traffic_ops_golang/ats/db.go
@@ -37,6 +37,38 @@ import (
 // RemapDotConfigIncludeInactiveDeliveryServices is whether delivery services with 'active' false are included in the remap.config.
 const RemapDotConfigIncludeInactiveDeliveryServices = true
 
+func GetServerParams(tx *sql.Tx, serverName tc.CacheName, configFile string) (map[string][]string, error) {
+	qry := `
+	SELECT
+		pa.name,
+		pa.value
+	FROM
+		parameter pa
+		JOIN profile_parameter pp ON pp.parameter = pa.id
+		JOIN profile pr ON pr.id = pp.profile
+		JOIN server s ON s.profile = pr.id
+	WHERE
+		s.host_name = $1
+		AND pa.config_file = $2
+	`
+	rows, err := tx.Query(qry, serverName, configFile)
+	if err != nil {
+		return nil, errors.New("querying: " + err.Error())
+	}
+	defer rows.Close()
+
+	params := map[string][]string{}
+	for rows.Next() {
+		name := ""
+		val := ""
+		if err := rows.Scan(&name, &val); err != nil {
+			return nil, errors.New("scanning: " + err.Error())
+		}
+		params[name] = append(params[name], val)
+	}
+	return params, nil
+}
+
 // GetProfilesParamData returns a map[profileID][paramName]paramVal
 func GetProfilesParamData(tx *sql.Tx, profileIDs []int, configFile string) (map[int]map[string]string, error) {
 	qry := `
@@ -541,7 +573,7 @@ func GetServerNameFromID(tx *sql.Tx, id int) (tc.CacheName, bool, error) {
 // GetServerNameFromNameOrID returns the server name from a parameter which may be the name or ID.
 // This also checks and verifies the existence of the given server, and returns an appropriate user error if it doesn't exist.
 // Returns the name, any user error, any system error, and any error code.
-func GetServerNameFromNameOrID(tx *sql.Tx, serverNameOrID string) (tc.CacheName, error, error, int) {
+func GetServerNameFromNameOrID(tx *sql.Tx, serverNameOrID string) (string, error, error, int) {
 	if serverID, err := strconv.Atoi(serverNameOrID); err == nil {
 		serverName, ok, err := dbhelpers.GetServerNameFromID(tx, serverID)
 		if err != nil {
@@ -549,37 +581,51 @@ func GetServerNameFromNameOrID(tx *sql.Tx, serverNameOrID string) (tc.CacheName,
 		} else if !ok {
 			return "", errors.New("server not found"), nil, http.StatusNotFound
 		}
-		return tc.CacheName(serverName), nil, nil, http.StatusOK
+		return serverName, nil, nil, http.StatusOK
 	}
 
-	serverName := tc.CacheName(serverNameOrID)
-	if _, ok, err := dbhelpers.GetServerIDFromName(string(serverName), tx); err != nil {
-		return "", nil, fmt.Errorf("checking server name '%v' existence: %v", serverName, err), http.StatusInternalServerError
+	if _, ok, err := dbhelpers.GetServerIDFromName(serverNameOrID, tx); err != nil {
+		return "", nil, fmt.Errorf("checking server name '%v' existence: %v", serverNameOrID, err), http.StatusInternalServerError
 	} else if !ok {
 		return "", errors.New("server not found"), nil, http.StatusNotFound
 	}
-	return serverName, nil, nil, http.StatusOK
+	return serverNameOrID, nil, nil, http.StatusOK
 }
 
 // GetServerInfoByID returns the necessary info about the server, whether the server exists, and any error.
 func GetServerInfoByID(tx *sql.Tx, id int) (*atscfg.ServerInfo, bool, error) {
-	return getServerInfo(tx, ServerInfoQuery()+`WHERE s.id = $1`, []interface{}{id})
+	return GetServerInfo(tx, ServerInfoQuery()+`WHERE s.id = $1`, []interface{}{id})
 }
 
 // GetServerInfoByHost returns the necessary info about the server, whether the server exists, and any error.
-func GetServerInfoByHost(tx *sql.Tx, host tc.CacheName) (*atscfg.ServerInfo, bool, error) {
-	return getServerInfo(tx, ServerInfoQuery()+` WHERE s.host_name = $1 `, []interface{}{host})
+func GetServerInfoByHost(tx *sql.Tx, host string) (*atscfg.ServerInfo, bool, error) {
+	return GetServerInfo(tx, ServerInfoQuery()+` WHERE s.host_name = $1 `, []interface{}{host})
 }
 
 // getServerInfo returns the necessary info about the server, whether the server exists, and any error.
-func getServerInfo(tx *sql.Tx, qry string, qryParams []interface{}) (*atscfg.ServerInfo, bool, error) {
+func GetServerInfo(tx *sql.Tx, qry string, qryParams []interface{}) (*atscfg.ServerInfo, bool, error) {
 	s := atscfg.ServerInfo{}
-	if err := tx.QueryRow(qry, qryParams...).Scan(&s.CDN, &s.CDNID, &s.ID, &s.HostName, &s.DomainName, &s.IP, &s.ProfileID, &s.ProfileName, &s.Port, &s.HTTPSPort, &s.Type, &s.CacheGroupID, &s.ParentCacheGroupID, &s.SecondaryParentCacheGroupID, &s.ParentCacheGroupType, &s.SecondaryParentCacheGroupType); err != nil {
+	if err := tx.QueryRow(qry, qryParams...).Scan(&s.CDN, &s.CDNID, &s.ID, &s.HostName, &s.DomainName, &s.ProfileID, &s.ProfileName, &s.Port, &s.HTTPSPort, &s.Type, &s.CacheGroupID, &s.ParentCacheGroupID, &s.SecondaryParentCacheGroupID, &s.ParentCacheGroupType, &s.SecondaryParentCacheGroupType); err != nil {
 		if err == sql.ErrNoRows {
 			return nil, false, nil
 		}
 		return nil, false, errors.New("querying server info: " + err.Error())
 	}
+
+	infs, err := dbhelpers.GetServerInterfaces(s.ID, tx)
+	if err != nil {
+		return nil, false, fmt.Errorf("querying server info interfaces: %v", err)
+	}
+
+	legacyInfo, err := tc.InterfaceInfoToLegacyInterfaces(infs)
+	if err != nil {
+		return nil, false, fmt.Errorf("converting server info interfaces to legacy: %v", err)
+	}
+
+	if legacyInfo.IPAddress != nil {
+		s.IP = *legacyInfo.IPAddress
+	}
+
 	return &s, true, nil
 }
 
@@ -591,7 +637,6 @@ SELECT
   s.id,
   s.host_name,
   c.domain_name,
-  s.ip_address,
   s.profile AS profile_id,
   p.name AS profile_name,
   s.tcp_port,
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 0ce7be9..8ee6c28 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -519,6 +519,52 @@ WHERE s.id = $1
 	return row, true, nil
 }
 
+// GetServerInterfaces, given the ID of a server, returns all of its network
+// interfaces, or an error if one occurs during retrieval.
+func GetServerInterfaces(id int, tx *sql.Tx) ([]tc.ServerInterfaceInfo, error) {
+	q := `
+	SELECT (
+		json_build_object (
+			'ipAddresses',
+			ARRAY (
+				SELECT (
+					json_build_object (
+						'address', ip_address.address,
+						'gateway', ip_address.gateway,
+						'service_address', ip_address.service_address
+					)
+				)
+				FROM ip_address
+				WHERE ip_address.interface = interface.name
+				AND ip_address.server = $1
+			),
+			'max_bandwidth', interface.max_bandwidth,
+			'monitor', interface.monitor,
+			'mtu', interface.mtu,
+			'name', interface.name
+		)
+	)
+	FROM interface
+	WHERE interface.server = $1
+	`
+	rows, err := tx.Query(q, id)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	infs := []tc.ServerInterfaceInfo{}
+	for rows.Next() {
+		var inf tc.ServerInterfaceInfo
+		if err = rows.Scan(&inf); err != nil {
+			return nil, err
+		}
+		infs = append(infs, inf)
+	}
+
+	return infs, nil
+}
+
 // GetStatusByID returns a Status struct, a bool for whether or not a status of the given ID exists, and an error (if one occurs).
 func GetStatusByID(id int, tx *sql.Tx) (tc.StatusNullable, bool, error) {
 	q := `
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 1763f71..4ab9f0a 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -319,10 +319,10 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{3, 0}, http.MethodPost, `servers/{id-or-name}/update$`, server.UpdateHandler, auth.PrivLevelOperations, Authenticated, nil, 143813233, noPerlBypass},
 
 		//Server: CRUD
-		{api.Version{3, 0}, http.MethodGet, `servers/?$`, api.ReadHandler(&server.TOServer{}), auth.PrivLevelReadOnly, Authenticated, nil, 27209592853, noPerlBypass},
-		{api.Version{3, 0}, http.MethodPut, `servers/{id}$`, api.UpdateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 2586341033, noPerlBypass},
-		{api.Version{3, 0}, http.MethodPost, `servers/?$`, api.CreateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 22255580613, noPerlBypass},
-		{api.Version{3, 0}, http.MethodDelete, `servers/{id}$`, api.DeleteHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 2923222333, noPerlBypass},
+		{api.Version{3, 0}, http.MethodGet, `servers/?$`, server.Read, auth.PrivLevelReadOnly, Authenticated, nil, 27209592853, noPerlBypass},
+		{api.Version{3, 0}, http.MethodPut, `servers/{id}$`, server.Update, auth.PrivLevelOperations, Authenticated, nil, 2586341033, noPerlBypass},
+		{api.Version{3, 0}, http.MethodPost, `servers/?$`, server.Create, auth.PrivLevelOperations, Authenticated, nil, 22255580613, noPerlBypass},
+		{api.Version{3, 0}, http.MethodDelete, `servers/{id}$`, server.Delete, auth.PrivLevelOperations, Authenticated, nil, 2923222333, noPerlBypass},
 
 		//Server Capability
 		{api.Version{3, 0}, http.MethodGet, `server_capabilities$`, api.ReadHandler(&servercapability.TOServerCapability{}), auth.PrivLevelReadOnly, Authenticated, nil, 2104073913, noPerlBypass},
@@ -683,10 +683,10 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{2, 0}, http.MethodPost, `servers/{id-or-name}/update$`, server.UpdateHandler, auth.PrivLevelOperations, Authenticated, nil, 14381323, noPerlBypass},
 
 		//Server: CRUD
-		{api.Version{2, 0}, http.MethodGet, `servers/?$`, api.ReadHandler(&server.TOServer{}), auth.PrivLevelReadOnly, Authenticated, nil, 2720959285, noPerlBypass},
-		{api.Version{2, 0}, http.MethodPut, `servers/{id}$`, api.UpdateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 258634103, noPerlBypass},
-		{api.Version{2, 0}, http.MethodPost, `servers/?$`, api.CreateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 2225558061, noPerlBypass},
-		{api.Version{2, 0}, http.MethodDelete, `servers/{id}$`, api.DeleteHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 292322233, noPerlBypass},
+		{api.Version{2, 0}, http.MethodGet, `servers/?$`, server.Read, auth.PrivLevelReadOnly, Authenticated, nil, 2720959285, noPerlBypass},
+		{api.Version{2, 0}, http.MethodPut, `servers/{id}$`, server.Update, auth.PrivLevelOperations, Authenticated, nil, 258634103, noPerlBypass},
+		{api.Version{2, 0}, http.MethodPost, `servers/?$`, server.Create, auth.PrivLevelOperations, Authenticated, nil, 2225558061, noPerlBypass},
+		{api.Version{2, 0}, http.MethodDelete, `servers/{id}$`, server.Delete, auth.PrivLevelOperations, Authenticated, nil, 292322233, noPerlBypass},
 
 		//Server Capability
 		{api.Version{2, 0}, http.MethodGet, `server_capabilities$`, api.ReadHandler(&servercapability.TOServerCapability{}), auth.PrivLevelReadOnly, Authenticated, nil, 210407391, noPerlBypass},
@@ -1080,11 +1080,11 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{1, 3}, http.MethodGet, `servers/{host_name}/update_status$`, server.GetServerUpdateStatusHandler, auth.PrivLevelReadOnly, Authenticated, nil, 438451599, noPerlBypass},
 
 		//Server: CRUD
-		{api.Version{1, 1}, http.MethodGet, `servers/?(\.json)?$`, api.ReadHandler(&server.TOServer{}), auth.PrivLevelReadOnly, Authenticated, nil, 1720959285, noPerlBypass},
-		{api.Version{1, 1}, http.MethodGet, `servers/{id}$`, api.DeprecatedReadHandler(&server.TOServer{}, util.StrPtr("GET /servers with query parameter id")), auth.PrivLevelReadOnly, Authenticated, nil, 1543122028, noPerlBypass},
-		{api.Version{1, 1}, http.MethodPut, `servers/{id}$`, api.UpdateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 958634103, noPerlBypass},
-		{api.Version{1, 1}, http.MethodPost, `servers/?$`, api.CreateHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 2025558061, noPerlBypass},
-		{api.Version{1, 1}, http.MethodDelete, `servers/{id}$`, api.DeleteHandler(&server.TOServer{}), auth.PrivLevelOperations, Authenticated, nil, 192322233, noPerlBypass},
+		{api.Version{1, 1}, http.MethodGet, `servers/?(\.json)?$`, server.Read, auth.PrivLevelReadOnly, Authenticated, nil, 1720959285, noPerlBypass},
+		{api.Version{1, 1}, http.MethodGet, `servers/{id}$`, server.ReadID, auth.PrivLevelReadOnly, Authenticated, nil, 1543122028, noPerlBypass},
+		{api.Version{1, 1}, http.MethodPut, `servers/{id}$`, server.Update, auth.PrivLevelOperations, Authenticated, nil, 958634103, noPerlBypass},
+		{api.Version{1, 1}, http.MethodPost, `servers/?$`, server.Create, auth.PrivLevelOperations, Authenticated, nil, 2025558061, noPerlBypass},
+		{api.Version{1, 1}, http.MethodDelete, `servers/{id}$`, server.Delete, auth.PrivLevelOperations, Authenticated, nil, 192322233, noPerlBypass},
 
 		//Server Capability
 		{api.Version{1, 4}, http.MethodGet, `server_capabilities$`, api.ReadHandler(&servercapability.TOServerCapability{}), auth.PrivLevelReadOnly, Authenticated, nil, 610407391, noPerlBypass},
@@ -1251,11 +1251,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{api.Version{1, 1}, http.MethodGet, `profiles/{profile-name-or-id}/configfiles/ats/volume\.config/?$`, atsprofile.GetVolume, auth.PrivLevelOperations, Authenticated, nil, 792704719, perlBypass},
 		{api.Version{1, 1}, http.MethodGet, `profiles/{profile-name-or-id}/configfiles/ats/{file}/?$`, atsprofile.GetUnknown, auth.PrivLevelOperations, Authenticated, nil, 1651257268, perlBypass},
 
-		{api.Version{1, 1}, http.MethodGet, `servers/{server-name-or-id}/configfiles/ats/parent\.config/?(\.json)?$`, atsserver.GetParentDotConfig, auth.PrivLevelOperations, Authenticated, nil, 645056066, perlBypass},
-		{api.Version{1, 1}, http.MethodGet, `servers/{server-name-or-id}/configfiles/ats/remap\.config/?(\.json)?$`, atsserver.GetServerConfigRemap, auth.PrivLevelOperations, Authenticated, nil, 2038454899, perlBypass},
-
 		{api.Version{1, 1}, http.MethodGet, `servers/{id-or-host}/configfiles/ats/cache\.config/?(\.json)?$`, atsserver.GetCacheDotConfig, auth.PrivLevelOperations, Authenticated, nil, 34686861, perlBypass},
-		{api.Version{1, 1}, http.MethodGet, `servers/{id-or-host}/configfiles/ats/ip_allow\.config/?(\.json)?$`, atsserver.GetIPAllowDotConfig, auth.PrivLevelOperations, Authenticated, nil, 724651786, perlBypass},
 		{api.Version{1, 1}, http.MethodGet, `servers/{id-or-host}/configfiles/ats/hosting\.config/?(\.json)?$`, atsserver.GetHostingDotConfig, auth.PrivLevelOperations, Authenticated, nil, 1387459113, perlBypass},
 		{api.Version{1, 1}, http.MethodGet, `servers/{id-or-host}/configfiles/ats/packages/?(\.json)?$`, atsserver.GetPackages, auth.PrivLevelOperations, Authenticated, nil, 245024839, perlBypass},
 		{api.Version{1, 1}, http.MethodGet, `servers/{id-or-host}/configfiles/ats/chkconfig/?(\.json)?$`, atsserver.GetChkconfig, auth.PrivLevelOperations, Authenticated, nil, 1012457987, perlBypass},
diff --git a/traffic_ops/traffic_ops_golang/server/detail.go b/traffic_ops/traffic_ops_golang/server/detail.go
index 4faea0b..a80f83c 100644
--- a/traffic_ops/traffic_ops_golang/server/detail.go
+++ b/traffic_ops/traffic_ops_golang/server/detail.go
@@ -141,40 +141,40 @@ func GetDetailParamHandler(w http.ResponseWriter, r *http.Request) {
 func getDetailServers(tx *sql.Tx, user *auth.CurrentUser, hostName string, physLocationID int, orderBy string, limit int, reqVersion api.Version) ([]tc.ServerDetailV30, error) {
 	allowedOrderByCols := map[string]string{
 		"":                 "",
-		"cachegroup":       "s.cachegroup",
+		"cachegroup":       "server.cachegroup",
 		"cdn_name":         "cdn.name",
-		"domain_name":      "s.domain_name",
-		"guid":             "s.guid",
-		"host_name":        "s.host_name",
-		"https_port":       "s.https_port",
-		"id":               "s.id",
-		"ilo_ip_address":   "s.ilo_ip_address",
-		"ilo_ip_gateway":   "s.ilo_ip_gateway",
-		"ilo_ip_netmask":   "s.ilo_ip_netmask",
-		"ilo_password":     "s.ilo_password",
-		"ilo_username":     "s.ilo_username",
+		"domain_name":      "server.domain_name",
+		"guid":             "server.guid",
+		"host_name":        "server.host_name",
+		"https_port":       "server.https_port",
+		"id":               "server.id",
+		"ilo_ip_address":   "server.ilo_ip_address",
+		"ilo_ip_gateway":   "server.ilo_ip_gateway",
+		"ilo_ip_netmask":   "server.ilo_ip_netmask",
+		"ilo_password":     "server.ilo_password",
+		"ilo_username":     "server.ilo_username",
 		"interface_mtu":    "interface_mtu",
-		"interface_name":   "s.interface_name",
-		"ip6_address":      "s.ip6_address",
-		"ip6_gateway":      "s.ip6_gateway",
-		"ip_address":       "s.ip_address",
-		"ip_gateway":       "s.ip_gateway",
-		"ip_netmask":       "s.ip_netmask",
-		"mgmt_ip_address":  "s.mgmt_ip_address",
-		"mgmt_ip_gateway":  "s.mgmt_ip_gateway",
-		"mgmt_ip_netmask":  "s.mgmt_ip_netmask",
-		"offline_reason":   "s.offline_reason",
+		"interface_name":   "server.interface_name",
+		"ip6_address":      "server.ip6_address",
+		"ip6_gateway":      "server.ip6_gateway",
+		"ip_address":       "server.ip_address",
+		"ip_gateway":       "server.ip_gateway",
+		"ip_netmask":       "server.ip_netmask",
+		"mgmt_ip_address":  "server.mgmt_ip_address",
+		"mgmt_ip_gateway":  "server.mgmt_ip_gateway",
+		"mgmt_ip_netmask":  "server.mgmt_ip_netmask",
+		"offline_reason":   "server.offline_reason",
 		"phys_location":    "pl.name",
 		"profile":          "p.name",
 		"profile_desc":     "p.description",
-		"rack":             "s.rack",
-		"router_host_name": "s.router_host_name",
-		"router_port_name": "s.router_port_name",
+		"rack":             "server.rack",
+		"router_host_name": "server.router_host_name",
+		"router_port_name": "server.router_port_name",
 		"status":           "st.name",
-		"tcp_port":         "s.tcp_port",
+		"tcp_port":         "server.tcp_port",
 		"server_type":      "t.name",
-		"xmpp_id":          "s.xmpp_id",
-		"xmpp_passwd":      "s.xmpp_passwd",
+		"xmpp_id":          "server.xmpp_id",
+		"xmpp_passwd":      "server.xmpp_passwd",
 	}
 	orderBy, ok := allowedOrderByCols[orderBy]
 	if !ok {
@@ -182,62 +182,43 @@ func getDetailServers(tx *sql.Tx, user *auth.CurrentUser, hostName string, physL
 	}
 	const JumboFrameBPS = 9000
 	q := `
-SELECT 
+SELECT
 	cg.name AS cachegroup,
 	cdn.name AS cdn_name,
-	ARRAY(select deliveryservice from deliveryservice_server where server = s.id),
-	s.domain_name,
-	s.guid,
-	s.host_name,
-	s.https_port,
-	s.id,
-	s.ilo_ip_address,
-	s.ilo_ip_gateway,
-	s.ilo_ip_netmask,
-	s.ilo_password,
-	s.ilo_username,
-	ARRAY (
-		SELECT ( json_build_object (
-			'ipAddresses', ARRAY (
-				SELECT ( json_build_object (
-					'address', ip_address.address,
-					'gateway', ip_address.gateway,
-					'service_address', ip_address.service_address
-				))
-				FROM ip_address
-				WHERE ip_address.interface = interface.name
-				AND ip_address.server = s.id
-			),
-			'max_bandwidth', interface.max_bandwidth,
-			'monitor', interface.monitor,
-			'mtu', COALESCE (interface.mtu, 9000),
-			'name', interface.name
-		))
-		FROM interface
-		WHERE interface.server = s.id
-	) AS interfaces,
-	s.mgmt_ip_address,
-	s.mgmt_ip_gateway,
-	s.mgmt_ip_netmask,
-	s.offline_reason,
+	ARRAY(select deliveryservice from deliveryservice_server where server = server.id),
+	server.domain_name,
+	server.guid,
+	server.host_name,
+	server.https_port,
+	server.id,
+	server.ilo_ip_address,
+	server.ilo_ip_gateway,
+	server.ilo_ip_netmask,
+	server.ilo_password,
+	server.ilo_username,
+	` + InterfacesArray + ` AS interfaces,
+	server.mgmt_ip_address,
+	server.mgmt_ip_gateway,
+	server.mgmt_ip_netmask,
+	server.offline_reason,
 	pl.name as phys_location,
 	p.name as profile,
 	p.description as profile_desc,
-	s.rack,
-	s.router_host_name,
-	s.router_port_name,
+	server.rack,
+	server.router_host_name,
+	server.router_port_name,
 	st.name as status,
-	s.tcp_port,
+	server.tcp_port,
 	t.name as server_type,
-	s.xmpp_id,
-	s.xmpp_passwd
-FROM server AS s
-JOIN cachegroup cg ON s.cachegroup = cg.id
-JOIN cdn ON s.cdn_id = cdn.id
-JOIN phys_location pl ON s.phys_location = pl.id
-JOIN profile p ON s.profile = p.id
-JOIN status st ON s.status = st.id
-JOIN type t ON s.type = t.id
+	server.xmpp_id,
+	server.xmpp_passwd
+FROM server
+JOIN cachegroup cg ON server.cachegroup = cg.id
+JOIN cdn ON server.cdn_id = cdn.id
+JOIN phys_location pl ON server.phys_location = pl.id
+JOIN profile p ON server.profile = p.id
+JOIN status st ON server.status = st.id
+JOIN type t ON server.type = t.id
 `
 	limitStr := ""
 	if limit != 0 {
@@ -250,13 +231,13 @@ JOIN type t ON s.type = t.id
 	rows := (*sql.Rows)(nil)
 	err := error(nil)
 	if hostName != "" && physLocationID != -1 {
-		q += ` WHERE s.host_name = $1::text AND s.phys_location = $2::bigint` + orderByStr + limitStr
+		q += ` WHERE server.host_name = $1::text AND server.phys_location = $2::bigint` + orderByStr + limitStr
 		rows, err = tx.Query(q, hostName, physLocationID)
 	} else if hostName != "" {
-		q += ` WHERE s.host_name = $1::text` + orderByStr + limitStr
+		q += ` WHERE server.host_name = $1::text` + orderByStr + limitStr
 		rows, err = tx.Query(q, hostName)
 	} else if physLocationID != -1 {
-		q += ` WHERE s.phys_location = $1::int` + orderByStr + limitStr
+		q += ` WHERE server.phys_location = $1::int` + orderByStr + limitStr
 		rows, err = tx.Query(q, physLocationID)
 	} else {
 		q += orderByStr + limitStr
diff --git a/traffic_ops/traffic_ops_golang/server/detail_test.go b/traffic_ops/traffic_ops_golang/server/detail_test.go
index d88b4b5..3b937f8 100644
--- a/traffic_ops/traffic_ops_golang/server/detail_test.go
+++ b/traffic_ops/traffic_ops_golang/server/detail_test.go
@@ -82,7 +82,7 @@ func TestGetDetailServers(t *testing.T) {
 			sd.ILOIPNetmask,
 			sd.ILOPassword,
 			sd.ILOUsername,
-			[]byte(`{"{\"ipAddresses\" : [{\"address\" : \"127.0.0.0\", \"gateway\" : null, \"service_address\" : true}], \"max_bandwidth\" : null, \"monitor\" : true, \"mtu\" : 1500, \"name\" : \"eth0\"}"}`),
+			[]byte(`{"{\"ipAddresses\" : [{\"address\" : \"127.0.0.0\", \"gateway\" : null, \"serviceAddress\" : true}], \"max_bandwidth\" : null, \"monitor\" : true, \"mtu\" : 1500, \"name\" : \"eth0\"}"}`),
 			sd.MgmtIPAddress,
 			sd.MgmtIPGateway,
 			sd.MgmtIPNetmask,
@@ -126,16 +126,16 @@ func TestGetDetailServers(t *testing.T) {
 		t.Fatalf("servers.read expected len(srvInts) == 1, actual = %v", len(srvInts))
 	}
 
-	if len(srvInts[0].IpAddresses) != 1 {
-		t.Fatalf("servers.read expected len(srvInts[0].IpAddresses) == 1, actual = %v", len(srvInts[0].IpAddresses))
+	if len(srvInts[0].IPAddresses) != 1 {
+		t.Fatalf("servers.read expected len(srvInts[0].IpAddresses) == 1, actual = %v", len(srvInts[0].IPAddresses))
 	}
 
 	if len(actualSrvs[0].HardwareInfo) != 3 {
 		t.Fatalf("servers.read expected len(actualSrvs[0].HardwareInfo) == 3, actual = %v", len(actualSrvs[0].HardwareInfo))
 	}
 
-	if !srvInts[0].IpAddresses[0].ServiceAddress {
-		t.Fatalf("srvInts[0].IpAddresses[0].ServiceAddress expected to be true, actual = %v", srvInts[0].IpAddresses[0].ServiceAddress)
+	if !srvInts[0].IPAddresses[0].ServiceAddress {
+		t.Fatalf("srvInts[0].IpAddresses[0].ServiceAddress expected to be true, actual = %v", srvInts[0].IPAddresses[0].ServiceAddress)
 	}
 }
 
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index 1ed7968..8d72737 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -23,8 +23,10 @@ package server
 
 import (
 	"database/sql"
+	"encoding/json"
 	"errors"
 	"fmt"
+	"net"
 	"net/http"
 	"strconv"
 	"strings"
@@ -33,95 +35,273 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
 	"github.com/apache/trafficcontrol/lib/go-util"
+
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-	validation "github.com/go-ozzo/ozzo-validation"
+
+	"github.com/go-ozzo/ozzo-validation"
 	"github.com/go-ozzo/ozzo-validation/is"
 	"github.com/jmoiron/sqlx"
 	"github.com/lib/pq"
 )
 
-// TOServer combines data about a server with metadata from an API request and
-// provides methods that implement several interfaces from the api package.
-type TOServer struct {
-	api.APIInfoImpl `json:"-"`
-	tc.ServerNullable
-}
-
-func (s *TOServer) SetLastUpdated(t tc.TimeNoMod) { s.LastUpdated = &t }
-func (*TOServer) InsertQuery() string             { return insertQuery() }
-func (*TOServer) UpdateQuery() string             { return updateQuery() }
-func (*TOServer) DeleteQuery() string             { return deleteQuery() }
-
-func (TOServer) GetKeyFieldsInfo() []api.KeyFieldInfo {
-	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
+const serverCountQuery = `
+SELECT COUNT(s.id)
+FROM server AS s
+`
 
-func (s TOServer) GetKeys() (map[string]interface{}, bool) {
-	if s.ID == nil {
-		return map[string]interface{}{"id": 0}, false
-	}
-	return map[string]interface{}{"id": *s.ID}, true
-}
+const selectQuery = `
+SELECT
+	cg.name AS cachegroup,
+	s.cachegroup AS cachegroup_id,
+	s.cdn_id,
+	cdn.name AS cdn_name,
+	s.domain_name,
+	s.guid,
+	s.host_name,
+	s.https_port,
+	s.id,
+	s.ilo_ip_address,
+	s.ilo_ip_gateway,
+	s.ilo_ip_netmask,
+	s.ilo_password,
+	s.ilo_username,
+	s.last_updated,
+	s.mgmt_ip_address,
+	s.mgmt_ip_gateway,
+	s.mgmt_ip_netmask,
+	s.offline_reason,
+	pl.name AS phys_location,
+	s.phys_location AS phys_location_id,
+	p.name AS profile,
+	p.description AS profile_desc,
+	s.profile AS profile_id,
+	s.rack,
+	s.reval_pending,
+	s.router_host_name,
+	s.router_port_name,
+	st.name AS status,
+	s.status AS status_id,
+	s.tcp_port,
+	t.name AS server_type,
+	s.type AS server_type_id,
+	s.upd_pending AS upd_pending,
+	s.xmpp_id,
+	s.xmpp_passwd
+FROM server AS s
+JOIN cachegroup cg ON s.cachegroup = cg.id
+JOIN cdn cdn ON s.cdn_id = cdn.id
+JOIN phys_location pl ON s.phys_location = pl.id
+JOIN profile p ON s.profile = p.id
+JOIN status st ON s.status = st.id
+JOIN type t ON s.type = t.id
+`
+const InterfacesArray = `
+ARRAY ( SELECT (
+		json_build_object (
+			'ipAddresses',
+			ARRAY (
+				SELECT (
+					json_build_object (
+						'address', ip_address.address,
+						'gateway', ip_address.gateway,
+						'serviceAddress', ip_address.service_address
+					)
+				)
+				FROM ip_address
+				WHERE ip_address.interface = interface.name
+				AND ip_address.server = server.id
+			),
+			'maxBandwidth', interface.max_bandwidth,
+			'monitor', interface.monitor,
+			'mtu', interface.mtu,
+			'name', interface.name
+		)
+	)
+	FROM interface
+	WHERE interface.server = server.id
+)`
+const SelectInterfacesQuery = `
+SELECT ( ` + InterfacesArray + `
+	) AS interfaces,
+server.id
+FROM server
+WHERE server.id = ANY ($1)
+`
 
-func (s *TOServer) SetKeys(keys map[string]interface{}) {
-	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
-	s.ID = &i
-}
+const insertQuery = `
+INSERT INTO server (
+	cachegroup,
+	cdn_id,
+	domain_name,
+	host_name,
+	https_port,
+	ilo_ip_address,
+	ilo_ip_netmask,
+	ilo_ip_gateway,
+	ilo_username,
+	ilo_password,
+	interface_name,
+	mgmt_ip_address,
+	mgmt_ip_netmask,
+	mgmt_ip_gateway,
+	offline_reason,
+	phys_location,
+	profile,
+	rack,
+	router_host_name,
+	router_port_name,
+	status,
+	tcp_port,
+	type,
+	upd_pending,
+	xmpp_id,
+	xmpp_passwd
+) VALUES (
+	:cachegroup_id,
+	:cdn_id,
+	:domain_name,
+	:host_name,
+	:https_port,
+	:ilo_ip_address,
+	:ilo_ip_netmask,
+	:ilo_ip_gateway,
+	:ilo_username,
+	:ilo_password,
+	:interface_name,
+	:mgmt_ip_address,
+	:mgmt_ip_netmask,
+	:mgmt_ip_gateway,
+	:offline_reason,
+	:phys_location_id,
+	:profile_id,
+	:rack,
+	:router_host_name,
+	:router_port_name,
+	:status_id,
+	:tcp_port,
+	:server_type_id,
+	:upd_pending,
+	:xmpp_id,
+	:xmpp_passwd
+) RETURNING
+	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
+	cachegroup AS cachegroup_id,
+	cdn_id,
+	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
+	domain_name,
+	guid,
+	host_name,
+	https_port,
+	id,
+	ilo_ip_address,
+	ilo_ip_gateway,
+	ilo_ip_netmask,
+	ilo_password,
+	ilo_username,
+	last_updated,
+	mgmt_ip_address,
+	mgmt_ip_gateway,
+	mgmt_ip_netmask,
+	offline_reason,
+	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
+	phys_location AS phys_location_id,
+	profile AS profile_id,
+	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
+	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
+	rack,
+	reval_pending,
+	router_host_name,
+	router_port_name,
+	(SELECT name FROM status WHERE status.id=server.status) AS status,
+	status AS status_id,
+	tcp_port,
+	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
+	type AS server_type_id,
+	upd_pending
+`
 
-func (s *TOServer) GetAuditName() string {
-	if s.DomainName != nil {
-		return *s.DomainName
-	}
-	if s.ID != nil {
-		return strconv.Itoa(*s.ID)
-	}
-	return "unknown"
-}
+const updateQuery = `
+UPDATE server SET
+	cachegroup=:cachegroup_id,
+	cdn_id=:cdn_id,
+	domain_name=:domain_name,
+	host_name=:host_name,
+	https_port=:https_port,
+	ilo_ip_address=:ilo_ip_address,
+	ilo_ip_netmask=:ilo_ip_netmask,
+	ilo_ip_gateway=:ilo_ip_gateway,
+	ilo_username=:ilo_username,
+	ilo_password=:ilo_password,
+	mgmt_ip_address=:mgmt_ip_address,
+	mgmt_ip_netmask=:mgmt_ip_netmask,
+	mgmt_ip_gateway=:mgmt_ip_gateway,
+	offline_reason=:offline_reason,
+	phys_location=:phys_location_id,
+	profile=:profile_id,
+	rack=:rack,
+	router_host_name=:router_host_name,
+	router_port_name=:router_port_name,
+	status=:status_id,
+	tcp_port=:tcp_port,
+	type=:server_type_id,
+	upd_pending=:upd_pending,
+	xmpp_id=:xmpp_id,
+	xmpp_passwd=:xmpp_passwd
+WHERE id=:id
+RETURNING
+	(SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS cachegroup,
+	cachegroup AS cachegroup_id,
+	cdn_id,
+	(SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
+	domain_name,
+	guid,
+	host_name,
+	https_port,
+	id,
+	ilo_ip_address,
+	ilo_ip_gateway,
+	ilo_ip_netmask,
+	ilo_password,
+	ilo_username,
+	last_updated,
+	mgmt_ip_address,
+	mgmt_ip_gateway,
+	mgmt_ip_netmask,
+	offline_reason,
+	(SELECT name FROM phys_location WHERE phys_location.id=server.phys_location) AS phys_location,
+	phys_location AS phys_location_id,
+	profile AS profile_id,
+	(SELECT description FROM profile WHERE profile.id=server.profile) AS profile_desc,
+	(SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
+	rack,
+	reval_pending,
+	router_host_name,
+	router_port_name,
+	(SELECT name FROM status WHERE status.id=server.status) AS status,
+	status AS status_id,
+	tcp_port,
+	(SELECT name FROM type WHERE type.id=server.type) AS server_type,
+	type AS server_type_id,
+	upd_pending
+`
 
-func (s *TOServer) GetType() string {
-	return "server"
-}
+const deleteServerQuery = `DELETE FROM server WHERE id=$1`
+const deleteInterfacesQuery = `DELETE FROM interface WHERE server=$1`
+const deleteIPsQuery = `DELETE FROM ip_address WHERE server = $1`
 
-func (s *TOServer) Sanitize() {
-	if s.IP6Address != nil && *s.IP6Address == "" {
-		s.IP6Address = nil
-	}
-}
+func validateCommon(s *tc.CommonServerProperties, tx *sql.Tx) []error {
 
-func (s *TOServer) Validate() error {
-	s.Sanitize()
-	version := s.APIInfo().Version
 	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
 
-	errs := []error{}
-	if (s.IPAddress == nil || *s.IPAddress == "") && (s.IP6Address == nil || *s.IP6Address == "") {
-		errs = append(errs, tc.NeedsAtLeastOneIPError)
-	}
-
-	if s.IPIsService != nil && *s.IPIsService && (s.IPAddress == nil || *s.IPAddress == "") {
-		errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
-	}
-
-	if s.IP6IsService != nil && *s.IP6IsService && (s.IP6Address == nil || *s.IP6Address == "") {
-		errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
-	}
-
-	if version.Major >= 2 {
-		if (s.IPIsService == nil || !*s.IPIsService) && (s.IP6IsService == nil || !*s.IP6IsService) {
-			errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
-		}
-	}
-
-	validateErrs := validation.Errors{
+	errs := tovalidate.ToErrors(validation.Errors{
 		"cachegroupId":   validation.Validate(s.CachegroupID, validation.NotNil),
 		"cdnId":          validation.Validate(s.CDNID, validation.NotNil),
 		"domainName":     validation.Validate(s.DomainName, validation.NotNil, noSpaces),
 		"hostName":       validation.Validate(s.HostName, validation.NotNil, noSpaces),
-		"interfaceMtu":   validation.Validate(s.InterfaceMtu, validation.NotNil),
-		"interfaceName":  validation.Validate(s.InterfaceName, validation.NotNil),
 		"physLocationId": validation.Validate(s.PhysLocationID, validation.NotNil),
 		"profileId":      validation.Validate(s.ProfileID, validation.NotNil),
 		"statusId":       validation.Validate(s.StatusID, validation.NotNil),
@@ -129,7 +309,55 @@ func (s *TOServer) Validate() error {
 		"updPending":     validation.Validate(s.UpdPending, validation.NotNil),
 		"httpsPort":      validation.Validate(s.HTTPSPort, validation.By(tovalidate.IsValidPortNumber)),
 		"tcpPort":        validation.Validate(s.TCPPort, validation.By(tovalidate.IsValidPortNumber)),
+	})
+
+	if len(errs) > 0 {
+		return errs
 	}
+
+	if s.XMPPID == nil || *s.XMPPID == "" {
+		hostName := *s.HostName
+		s.XMPPID = &hostName
+	}
+
+	if _, err := tc.ValidateTypeID(tx, s.TypeID, "server"); err != nil {
+		errs = append(errs, err)
+	}
+
+	var cdnID int
+	if err := tx.QueryRow("SELECT cdn from profile WHERE id=$1", s.ProfileID).Scan(&cdnID); err != nil {
+		log.Errorf("could not execute select cdnID from profile: %s\n", err)
+		if err == sql.ErrNoRows {
+			errs = append(errs, errors.New("associated profile must have a cdn associated"))
+		} else {
+			errs = append(errs, tc.DBError)
+		}
+		return errs
+	}
+
+	log.Infof("got cdn id: %d from profile and cdn id: %d from server", cdnID, *s.CDNID)
+	if cdnID != *s.CDNID {
+		errs = append(errs, fmt.Errorf("CDN id '%d' for profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID))
+	}
+
+	return errs
+}
+
+func validateV1(s *tc.ServerNullableV11, tx *sql.Tx) error {
+	if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
+		s.IP6Address = nil
+	}
+
+	errs := []error{}
+	if (s.IPAddress == nil || *s.IPAddress == "") && s.IP6Address == nil {
+		errs = append(errs, tc.NeedsAtLeastOneIPError)
+	}
+
+	validateErrs := validation.Errors{
+		"interfaceMtu":  validation.Validate(s.InterfaceMtu, validation.NotNil),
+		"interfaceName": validation.Validate(s.InterfaceName, validation.NotNil),
+	}
+
 	if s.IPAddress != nil && *s.IPAddress != "" {
 		validateErrs["ipAddress"] = validation.Validate(s.IPAddress, is.IPv4)
 		validateErrs["ipNetmask"] = validation.Validate(s.IPNetmask, validation.NotNil)
@@ -139,94 +367,212 @@ func (s *TOServer) Validate() error {
 		validateErrs["ip6Address"] = validation.Validate(s.IP6Address, validation.By(tovalidate.IsValidIPv6CIDROrAddress))
 	}
 	errs = append(errs, tovalidate.ToErrors(validateErrs)...)
-	if len(errs) > 0 {
-		return util.JoinErrs(errs)
-	}
+	errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...)
+
+	return util.JoinErrs(errs)
+}
 
-	if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); err != nil {
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+	var errs []error
+
+	if err := validateV1(&s.ServerNullableV11, tx); err != nil {
 		return err
 	}
 
-	rows, err := s.ReqInfo.Tx.Tx.Query("select cdn from profile where id=$1", s.ProfileID)
-	if err != nil {
-		log.Error.Printf("could not execute select cdnID from profile: %s\n", err)
-		errs = append(errs, tc.DBError)
-		return util.JoinErrs(errs)
+	// default boolean value is false
+	if s.IPIsService == nil {
+		s.IPIsService = new(bool)
 	}
-	defer rows.Close()
-	var cdnID int
-	for rows.Next() {
-		if err := rows.Scan(&cdnID); err != nil {
-			log.Error.Printf("could not scan cdnID from profile: %s\n", err)
-			errs = append(errs, errors.New("associated profile must have a cdn associated"))
-			return util.JoinErrs(errs)
-		}
+	if s.IP6IsService == nil {
+		s.IP6IsService = new(bool)
 	}
-	log.Infof("got cdn id: %d from profile and cdn id: %d from server", cdnID, *s.CDNID)
-	if cdnID != *s.CDNID {
-		errs = append(errs, errors.New(fmt.Sprintf("CDN id '%d' for profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID)))
+
+	if !*s.IPIsService && !*s.IP6IsService {
+		errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
 	}
-	return util.JoinErrs(errs)
-}
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log message
-func (s TOServer) ChangeLogMessage(action string) (string, error) {
+	if *s.IPIsService && s.IPAddress == nil {
+		errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
+	}
 
-	var status string
-	if s.Status != nil {
-		status = *s.Status
+	if *s.IP6IsService && s.IP6Address == nil {
+		errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
 	}
+	return util.JoinErrs(errs)
+}
 
-	var hostName string
-	if s.HostName != nil {
-		hostName = *s.HostName
+func validateMTU(mtu interface{}) error {
+	m, ok := mtu.(*uint64)
+	if !ok {
+		return errors.New("must be an unsigned integer with 64-bit precision")
+	}
+	if m == nil {
+		return nil
 	}
 
-	var domainName string
-	if s.DomainName != nil {
-		domainName = *s.DomainName
+	if *m < 1280 {
+		return errors.New("must be at least 1280")
 	}
+	return nil
+}
+
+func validateV3(s *tc.ServerNullable, tx *sql.Tx) (string, error) {
 
-	var serverID string
-	if s.ID != nil {
-		serverID = strconv.Itoa(*s.ID)
+	if len(s.Interfaces) == 0 {
+		return "", errors.New("a server must have at least one interface")
 	}
+	var errs []error
+	var serviceAddrV4Found bool
+	var serviceAddrV6Found bool
+	var serviceInterface string
+	for _, iface := range s.Interfaces {
+
+		ruleName := fmt.Sprintf("interface '%s' ", iface.Name)
+		errs = append(errs, tovalidate.ToErrors(validation.Errors{
+			ruleName + "name":        validation.Validate(iface.Name, validation.Required),
+			ruleName + "mtu":         validation.Validate(iface.MaxBandwidth, validation.By(validateMTU)),
+			ruleName + "ipAddresses": validation.Validate(iface.IPAddresses, validation.Required),
+		})...)
 
-	message := action + ` ` + status + ` server: { "hostName":"` + hostName + `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+		for _, addr := range iface.IPAddresses {
+			ruleName += fmt.Sprintf("address '%s'", addr.Address)
 
-	return message, nil
+			var parsedIP net.IP
+			var err error
+			if parsedIP, _, err = net.ParseCIDR(addr.Address); err != nil {
+				if parsedIP = net.ParseIP(addr.Address); parsedIP == nil {
+					errs = append(errs, fmt.Errorf("%s: address: %v", ruleName, err))
+					continue
+				}
+			}
+
+			if addr.Gateway != nil {
+				if gateway := net.ParseIP(*addr.Gateway); gateway == nil {
+					errs = append(errs, fmt.Errorf("%s: gateway: could not parse '%s' as a network gateway", ruleName, *addr.Gateway))
+				} else if (gateway.To4() == nil && parsedIP.To4() != nil) || (gateway.To4() != nil && parsedIP.To4() == nil) {
+					errs = append(errs, errors.New(ruleName+": address family mismatch between address and gateway"))
+				}
+			}
+
+			if addr.ServiceAddress {
+				if serviceInterface != "" && serviceInterface != iface.Name {
+					errs = append(errs, fmt.Errorf("interfaces: both %s and %s interfaces contain service addresses - only one service-address-containing-interface is allowed", serviceInterface, iface.Name))
+				}
+				serviceInterface = iface.Name
+				if parsedIP.To4() != nil {
+					if serviceAddrV4Found {
+						errs = append(errs, fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service address, but an IPv4 service address appears earlier in the list", addr.Address, iface.Name))
+					}
+					serviceAddrV4Found = true
+				} else {
+					if serviceAddrV6Found {
+						errs = append(errs, fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service address, but an IPv6 service address appears earlier in the list", addr.Address, iface.Name))
+					}
+					serviceAddrV6Found = true
+				}
+			}
+		}
+	}
+
+	errs = append(errs, validateCommon(&s.CommonServerProperties, tx)...)
+	return serviceInterface, util.JoinErrs(errs)
 }
 
-func (s *TOServer) Read() ([]interface{}, error, error, int) {
-	version := s.APIInfo().Version
-	if version == nil {
-		return nil, nil, errors.New("TOServer.Read called with nil API version"), http.StatusInternalServerError
+func Read(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
+	defer inf.Close()
 
-	returnable := []interface{}{}
+	// Middleware should've already handled this, so idk why this is a pointer at all tbh
+	version := inf.Version
+	if version == nil {
+		middleware.NotImplementedHandler().ServeHTTP(w, r)
+		return
+	}
 
-	servers, userErr, sysErr, errCode := getServers(s.ReqInfo.Params, s.ReqInfo.Tx, s.ReqInfo.User)
+	servers := []tc.ServerNullable{}
+	var serverCount uint64
+	servers, serverCount, userErr, sysErr, errCode = getServers(inf.Params, inf.Tx, inf.User)
 
 	if userErr != nil || sysErr != nil {
-		return nil, userErr, sysErr, errCode
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
 
+	if version.Major >= 3 {
+		api.WriteRespWithSummary(w, r, servers, serverCount)
+		return
+	}
+
+	if version.Major <= 1 {
+		legacyServers := make([]tc.ServerNullableV11, 0, len(servers))
+		for _, server := range servers {
+			legacyServer, err := server.ToServerV2()
+			if err != nil {
+				api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to convert servers to legacy format: %v", err))
+				return
+			}
+			legacyServers = append(legacyServers, legacyServer.ServerNullableV11)
+		}
+		api.WriteResp(w, r, legacyServers)
+		return
+	}
+
+	legacyServers := make([]tc.ServerNullableV2, 0, len(servers))
 	for _, server := range servers {
-		switch {
-		// NOTE: it's required to handle minor version cases in a descending >= manner
-		case version.Major >= 2:
-			returnable = append(returnable, server)
-		case version.Major == 1 && version.Minor >= 1:
-			returnable = append(returnable, server.ServerNullableV11)
-		default:
-			return nil, nil, fmt.Errorf("TOServer.Read called with invalid API version: %d.%d", version.Major, version.Minor), http.StatusInternalServerError
+		legacyServer, err := server.ToServerV2()
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to convert servers to legacy format: %v", err))
+			return
 		}
+		legacyServers = append(legacyServers, legacyServer)
+	}
+	api.WriteResp(w, r, legacyServers)
+}
+
+func ReadID(w http.ResponseWriter, r *http.Request) {
+	alternative := "GET /servers with query parameter id"
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleDeprecatedErr(w, r, tx, errCode, userErr, sysErr, &alternative)
+		return
+	}
+	defer inf.Close()
+
+	servers := []tc.ServerNullable{}
+	servers, _, userErr, sysErr, errCode = getServers(inf.Params, inf.Tx, inf.User)
+
+	if len(servers) > 1 {
+		api.HandleDeprecatedErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("ID '%d' matched more than one server (%d total)", inf.IntParams["id"], len(servers)), &alternative)
+		return
+	}
+
+	deprecationAlerts := api.CreateDeprecationAlerts(&alternative)
+
+	// No need to bother converting if there's no data
+	if len(servers) < 1 {
+		api.WriteAlertsObj(w, r, http.StatusOK, deprecationAlerts, servers)
 	}
 
-	return returnable, nil, nil, http.StatusOK
+	legacyServers := make([]tc.ServerNullableV11, 0, len(servers))
+	for _, server := range servers {
+		legacyServer, err := server.ToServerV2()
+		if err != nil {
+			api.HandleDeprecatedErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("failed to convert servers to legacy format: %v", err), &alternative)
+			return
+		}
+		legacyServers = append(legacyServers, legacyServer.ServerNullableV11)
+	}
+	api.WriteAlertsObj(w, r, http.StatusOK, deprecationAlerts, legacyServers)
 }
 
-func getServers(params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser) ([]tc.ServerNullable, error, error, int) {
+func getServers(params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser) ([]tc.ServerNullable, uint64, error, error, int) {
+
 	// Query Parameters to Database Query column mappings
 	// see the fields mapped in the SQL query
 	queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{
@@ -248,23 +594,23 @@ func getServers(params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser) (
 		// don't allow query on ds outside user's tenant
 		dsID, err := strconv.Atoi(dsIDStr)
 		if err != nil {
-			return nil, errors.New("dsId must be an integer"), nil, http.StatusNotFound
+			return nil, 0, errors.New("dsId must be an integer"), nil, http.StatusNotFound
 		}
 		userErr, sysErr, _ := tenant.CheckID(tx.Tx, user, dsID)
 		if userErr != nil || sysErr != nil {
-			return nil, errors.New("Forbidden"), sysErr, http.StatusForbidden
+			return nil, 0, errors.New("forbidden"), sysErr, http.StatusForbidden
 		}
 		// only if dsId is part of params: add join on deliveryservice_server table
 		queryAddition = `
-FULL OUTER JOIN deliveryservice_server dss ON dss.server = s.id
-`
+			FULL OUTER JOIN deliveryservice_server dss ON dss.server = s.id
+		`
 		// depending on ds type, also need to add mids
 		dsType, exists, err := dbhelpers.GetDeliveryServiceType(dsID, tx.Tx)
 		if err != nil {
-			return nil, nil, err, http.StatusInternalServerError
+			return nil, 0, nil, err, http.StatusInternalServerError
 		}
 		if !exists {
-			return nil, fmt.Errorf("a deliveryservice with id %v was not found", dsID), nil, http.StatusBadRequest
+			return nil, 0, fmt.Errorf("a deliveryservice with id %v was not found", dsID), nil, http.StatusBadRequest
 		}
 		usesMids = dsType.UsesMidCache()
 		log.Debugf("Servers for ds %d; uses mids? %v\n", dsID, usesMids)
@@ -272,47 +618,148 @@ FULL OUTER JOIN deliveryservice_server dss ON dss.server = s.id
 
 	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToSQLCols)
 	if len(errs) > 0 {
-		return nil, util.JoinErrs(errs), nil, http.StatusBadRequest
+		return nil, 0, util.JoinErrs(errs), nil, http.StatusBadRequest
+	}
+
+	// TODO there's probably a cleaner way to do this by preparing a NamedStmt first and using its QueryRow method
+	var serverCount uint64
+	countRows, err := tx.NamedQuery(serverCountQuery+where, queryValues)
+	if err != nil {
+		return nil, 0, nil, fmt.Errorf("failed to get servers count: %v", err), http.StatusInternalServerError
+	}
+	defer countRows.Close()
+	rowsAffected := 0
+	for countRows.Next() {
+		if err = countRows.Scan(&serverCount); err != nil {
+			return nil, 0, nil, fmt.Errorf("failed to read servers count: %v", err), http.StatusInternalServerError
+		}
+		rowsAffected++
+	}
+	if rowsAffected != 1 {
+		return nil, 0, nil, fmt.Errorf("incorrect rows returned for server count, want: 1 got: %v", rowsAffected), http.StatusInternalServerError
 	}
 
-	query := selectQuery() + queryAddition + where + orderBy + pagination
+	query := selectQuery + queryAddition + where + orderBy + pagination
 	log.Debugln("Query is ", query)
 
 	rows, err := tx.NamedQuery(query, queryValues)
 	if err != nil {
-		return nil, nil, errors.New("querying: " + err.Error()), http.StatusInternalServerError
+		return nil, serverCount, nil, errors.New("querying: " + err.Error()), http.StatusInternalServerError
 	}
 	defer rows.Close()
 
-	servers := []tc.ServerNullable{}
-
 	HiddenField := "********"
 
+	servers := make(map[int]tc.ServerNullable)
+	ids := []int{}
 	for rows.Next() {
 		var s tc.ServerNullable
 		if err = rows.StructScan(&s); err != nil {
-			return nil, nil, errors.New("getting servers: " + err.Error()), http.StatusInternalServerError
+			return nil, serverCount, nil, errors.New("getting servers: " + err.Error()), http.StatusInternalServerError
 		}
 		if user.PrivLevel < auth.PrivLevelOperations {
 			s.ILOPassword = &HiddenField
 			s.XMPPPasswd = &HiddenField
 		}
-		servers = append(servers, s)
+
+		if s.ID == nil {
+			return nil, serverCount, nil, errors.New("found server with nil ID"), http.StatusInternalServerError
+		}
+		if _, ok := servers[*s.ID]; ok {
+			return nil, serverCount, nil, fmt.Errorf("found more than one server with ID #%d", *s.ID), http.StatusInternalServerError
+		}
+		servers[*s.ID] = s
+		ids = append(ids, *s.ID)
 	}
 
 	// if ds requested uses mid-tier caches, add those to the list as well
 	if usesMids {
-		mids, userErr, sysErr, errCode := getMidServers(servers, tx)
+		midIDs, userErr, sysErr, errCode := getMidServers(ids, servers, tx)
 
 		log.Debugf("getting mids: %v, %v, %s\n", userErr, sysErr, http.StatusText(errCode))
 
 		if userErr != nil || sysErr != nil {
-			return nil, userErr, sysErr, errCode
+			return nil, serverCount, userErr, sysErr, errCode
+		}
+		ids = append(ids, midIDs...)
+	}
+
+	if len(ids) < 1 {
+		return []tc.ServerNullable{}, serverCount, nil, nil, http.StatusOK
+	}
+
+	query, args, err := sqlx.In(`SELECT max_bandwidth, monitor, mtu, name, server FROM interface WHERE server IN (?)`, ids)
+	if err != nil {
+		return nil, serverCount, nil, fmt.Errorf("building interfaces query: %v", err), http.StatusInternalServerError
+	}
+	query = tx.Rebind(query)
+	interfaces := map[int]map[string]tc.ServerInterfaceInfo{}
+	interfaceRows, err := tx.Queryx(query, args...)
+	if err != nil {
+		return nil, serverCount, nil, fmt.Errorf("querying for interfaces: %v", err), http.StatusInternalServerError
+	}
+	defer interfaceRows.Close()
+
+	for interfaceRows.Next() {
+		iface := tc.ServerInterfaceInfo{
+			IPAddresses: []tc.ServerIPAddress{},
+		}
+		var server int
+
+		if err = interfaceRows.Scan(&iface.MaxBandwidth, &iface.Monitor, &iface.MTU, &iface.Name, &server); err != nil {
+			return nil, serverCount, nil, fmt.Errorf("getting server interfaces: %v", err), http.StatusInternalServerError
+		}
+
+		if _, ok := servers[server]; !ok  {
+			continue
+		}
+
+		if _, ok := interfaces[server]; !ok {
+			interfaces[server] = map[string]tc.ServerInterfaceInfo{}
+		}
+		interfaces[server][iface.Name] = iface
+	}
+
+	query, args, err = sqlx.In(`SELECT address, gateway, service_address, server, interface FROM ip_address WHERE server IN (?)`, ids)
+	if err != nil {
+		return nil, serverCount, nil, fmt.Errorf("building IP addresses query: %v", err), http.StatusInternalServerError
+	}
+	query = tx.Rebind(query)
+	ipRows, err := tx.Tx.Query(query, args...)
+	if err != nil {
+		return nil, serverCount, nil, fmt.Errorf("querying for IP addresses: %v", err), http.StatusInternalServerError
+	}
+	defer ipRows.Close()
+
+	for ipRows.Next() {
+		var ip tc.ServerIPAddress
+		var server int
+		var iface string
+
+		if err = ipRows.Scan(&ip.Address, &ip.Gateway, &ip.ServiceAddress, &server, &iface); err != nil {
+			return nil, serverCount, nil, fmt.Errorf("getting server IP addresses: %v", err), http.StatusInternalServerError
+		}
+
+		if _, ok := interfaces[server]; !ok {
+			continue
+		}
+		if i, ok := interfaces[server][iface]; !ok {
+			log.Warnf("IP addresses query returned addresses for an interface that was not found in interfaces query: %s", iface)
+		} else {
+			i.IPAddresses = append(i.IPAddresses, ip)
+			interfaces[server][iface] = i
+		}
+	}
+
+	returnable := make([]tc.ServerNullable, 0, len(servers))
+	for _, server := range servers {
+		for _, iface := range interfaces[*server.ID] {
+			server.Interfaces = append(server.Interfaces, iface)
 		}
-		servers = append(servers, mids...)
+		returnable = append(returnable, server)
 	}
 
-	return servers, nil, nil, http.StatusOK
+	return returnable, serverCount, nil, nil, http.StatusOK
 }
 
 // getMidServers gets the mids used by the servers in this DS.
@@ -323,303 +770,533 @@ FULL OUTER JOIN deliveryservice_server dss ON dss.server = s.id
 // pulling the cachegroups of the edges and finding those cachegroups parent
 // cachegroup... then we see which servers have cachegroup in parent cachegroup
 // list...that's how we find mids for the ds :)
-func getMidServers(servers []tc.ServerNullable, tx *sqlx.Tx) ([]tc.ServerNullable, error, error, int) {
-	if len(servers) == 0 {
+func getMidServers(edgeIDs []int, servers map[int]tc.ServerNullable, tx *sqlx.Tx) ([]int, error, error, int) {
+	if len(edgeIDs) == 0 {
 		return nil, nil, nil, http.StatusOK
 	}
-	var ids []string
-	for _, s := range servers {
-		ids = append(ids, strconv.Itoa(*s.ID))
-	}
 
-	edgeIDs := strings.Join(ids, ",")
 	// TODO: include secondary parent?
-	q := selectQuery() + `
-WHERE t.name = 'MID' AND s.cachegroup IN (
-SELECT cg.parent_cachegroup_id FROM cachegroup AS cg
-WHERE cg.id IN (
-SELECT s.cachegroup FROM server AS s
-WHERE s.id IN (` + edgeIDs + `)))
-`
-	rows, err := tx.Queryx(q)
+	q := selectQuery + `
+	WHERE t.name = 'MID' AND s.cachegroup IN (
+	SELECT cg.parent_cachegroup_id FROM cachegroup AS cg
+	WHERE cg.id IN (
+	SELECT s.cachegroup FROM server AS s
+	WHERE s.id IN (?)))
+	`
+
+	query, args, err := sqlx.In(q, edgeIDs)
+	if err != nil {
+		return nil, nil, fmt.Errorf("constructing mid servers query: %v", err), http.StatusInternalServerError
+	}
+	query = tx.Rebind(query)
+
+	rows, err := tx.Queryx(query, args...)
 	if err != nil {
 		return nil, err, nil, http.StatusBadRequest
 	}
 	defer rows.Close()
 
-	var mids []tc.ServerNullable
+	ids := []int{}
 	for rows.Next() {
 		var s tc.ServerNullable
 		if err := rows.StructScan(&s); err != nil {
 			log.Error.Printf("could not scan mid servers: %s\n", err)
 			return nil, nil, err, http.StatusInternalServerError
 		}
-		mids = append(mids, s)
-	}
-	return mids, nil, nil, http.StatusOK
-}
+		if s.ID == nil {
+			return nil, nil, errors.New("found server with nil ID"), http.StatusInternalServerError
+		}
 
-func (s *TOServer) Update() (error, error, int) {
-	if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-		s.IP6Address = nil
+		// This may mean that the server was caught by other query parameters,
+		// so not technically an error, unlike earlier in 'getServers'.
+		if _, ok := servers[*s.ID]; ok {
+			continue
+		}
+
+		servers[*s.ID] = s
+		ids = append(ids, *s.ID)
 	}
-	// see if type changed
-	typeID := -1
-	// see if cdn changed
-	cdnId := -1
 
-	if err := s.APIInfo().Tx.QueryRow("SELECT type, cdn_id FROM server WHERE id = $1", s.ID).Scan(&typeID, &cdnId); err != nil {
+	return ids, nil, nil, http.StatusOK
+}
+
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) (error, error, int) {
+	// see if cdn or type changed
+	var cdnID int
+	var typeID int
+	if err := tx.QueryRow("SELECT type, cdn_id FROM server WHERE id = $1", *server.ID).Scan(&typeID, &cdnID); err != nil {
 		if err == sql.ErrNoRows {
-			return errors.New("no server found with this id"), nil, http.StatusNotFound
+			return errors.New("no server found with this ID"), nil, http.StatusNotFound
 		}
 		return nil, fmt.Errorf("getting current server type: %v", err), http.StatusInternalServerError
 	}
 
-	dsIDs := []int64{}
-	if err := s.APIInfo().Tx.QueryRowx("SELECT ARRAY(SELECT deliveryservice FROM deliveryservice_server WHERE server = $1)", s.ID).Scan(pq.Array(&dsIDs)); err != nil && err != sql.ErrNoRows {
+	var dsIDs []int64
+	if err := tx.QueryRowx("SELECT ARRAY(SELECT deliveryservice FROM deliveryservice_server WHERE server = $1)", server.ID).Scan(pq.Array(&dsIDs)); err != nil && err != sql.ErrNoRows {
 		return nil, fmt.Errorf("getting server assigned delivery services: %v", err), http.StatusInternalServerError
 	}
+	// If type is changing ensure it isn't assigned to any DSes.
+	if typeID != *server.TypeID {
+		if len(dsIDs) != 0 {
+			return errors.New("server type can not be updated when it is currently assigned to Delivery Services"), nil, http.StatusConflict
+		}
+	}
 	// Check to see if the user is trying to change the CDN of a server, which is already linked with a DS
-	if cdnId != *s.CDNID && len(dsIDs) != 0 {
+	if cdnID != *server.CDNID && len(dsIDs) != 0 {
 		return errors.New("server cdn can not be updated when it is currently assigned to delivery services"), nil, http.StatusConflict
 	}
-	// If type is changing ensure it isn't assigned to any DSes.
-	if typeID != *s.TypeID {
-		if len(dsIDs) != 0 {
-			return errors.New("server type can not be updated when it is currently assigned to delivery services"), nil, http.StatusConflict
+
+	return nil, nil, http.StatusOK
+}
+
+func createInterfaces(id int, interfaces []tc.ServerInterfaceInfo, tx *sql.Tx) (error, error, int) {
+	ifaceQry := `
+	INSERT INTO interface (
+		max_bandwidth,
+		monitor,
+		mtu,
+		name,
+		server
+	) VALUES
+	`
+	ipQry := `
+	INSERT INTO ip_address (
+		address,
+		gateway,
+		interface,
+		server,
+		service_address
+	) VALUES
+	`
+
+	ifaceQueryParts := make([]string, 0, len(interfaces))
+	ipQueryParts := make([]string, 0, len(interfaces))
+	ifaceArgs := make([]interface{}, 0, len(interfaces))
+	ipArgs := make([]interface{}, 0, len(interfaces))
+	for i, iface := range interfaces {
+		argStart := i * 5
+		ifaceQueryParts = append(ifaceQueryParts, fmt.Sprintf("($%d, $%d, $%d, $%d, $%d)", argStart+1, argStart+2, argStart+3, argStart+4, argStart+5))
+		ifaceArgs = append(ifaceArgs, iface.MaxBandwidth, iface.Monitor, iface.MTU, iface.Name, id)
+		for _, ip := range iface.IPAddresses {
+			argStart = len(ipArgs)
+			ipQueryParts = append(ipQueryParts, fmt.Sprintf("($%d, $%d, $%d, $%d, $%d)", argStart+1, argStart+2, argStart+3, argStart+4, argStart+5))
+			ipArgs = append(ipArgs, ip.Address, ip.Gateway, iface.Name, id, ip.ServiceAddress)
 		}
 	}
 
-	current := TOServer{}
-	err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+	ifaceQry += strings.Join(ifaceQueryParts, ",")
+	log.Debugf("Inserting interfaces for new server, query is: %s", ifaceQry)
+
+	_, err := tx.Exec(ifaceQry, ifaceArgs...)
 	if err != nil {
 		return api.ParseDBError(err)
 	}
-	defaultIsService := true
-	if s.IPIsService == nil {
-		if current.IPIsService != nil {
-			s.IPIsService = current.IPIsService
-		} else {
-			s.IPIsService = &defaultIsService
+
+	ipQry += strings.Join(ipQueryParts, ",")
+	log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+	_, err = tx.Exec(ipQry, ipArgs...)
+	if err != nil {
+		return api.ParseDBError(err)
+	}
+
+	return nil, nil, http.StatusOK
+}
+
+func deleteInterfaces(id int, tx *sql.Tx) (error, error, int) {
+	if _, err := tx.Exec(deleteIPsQuery, id); err != nil && err != sql.ErrNoRows {
+		return api.ParseDBError(err)
+	}
+
+	if _, err := tx.Exec(deleteInterfacesQuery, id); err != nil && err != sql.ErrNoRows {
+		return api.ParseDBError(err)
+	}
+
+	return nil, nil, http.StatusOK
+}
+
+func Update(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	var server tc.ServerNullableV2
+	var interfaces []tc.ServerInterfaceInfo
+	if inf.Version.Major >= 3 {
+		var newServer tc.ServerNullable
+		if err := json.NewDecoder(r.Body).Decode(&newServer); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+		serviceInterface, err := validateV3(&newServer, tx)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+
+		server, err = newServer.ToServerV2()
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("converting v3 server to v2 for update: %v", err))
+			return
+		}
+		server.InterfaceName = util.StrPtr(serviceInterface)
+		interfaces = newServer.Interfaces
+	} else if inf.Version.Major == 2 {
+		if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+
+		err := validateV2(&server, tx)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+
+		interfaces, err = server.LegacyInterfaceDetails.ToInterfaces(*server.IPIsService, *server.IP6IsService)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("converting server legacy interfaces to interface array: %v", err))
+			return
+		}
+	} else {
+		var legacyServer tc.ServerNullableV11
+		if err := json.NewDecoder(r.Body).Decode(&legacyServer); err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+
+		err := validateV1(&legacyServer, tx)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+			return
+		}
+
+		interfaces, err = legacyServer.LegacyInterfaceDetails.ToInterfaces(true, true)
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("converting server legacy interfaces to interface array: %v", err))
+			return
+		}
+		server = tc.ServerNullableV2{
+			ServerNullableV11: legacyServer,
+			IPIsService:       util.BoolPtr(true),
+			IP6IsService:      util.BoolPtr(true),
 		}
 	}
-	if s.IP6IsService == nil {
-		if current.IP6IsService != nil {
-			s.IP6IsService = current.IP6IsService
-		} else {
-			s.IP6IsService = &defaultIsService
+
+	server.ID = new(int)
+	*server.ID = inf.IntParams["id"]
+
+	if userErr, sysErr, errCode = checkTypeChangeSafety(server.CommonServerProperties, inf.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	rows, err := inf.Tx.NamedQuery(updateQuery, server)
+	if err != nil {
+		userErr, sysErr, errCode = api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer rows.Close()
+
+	rowsAffected := 0
+	for rows.Next() {
+		if err := rows.StructScan(&server); err != nil {
+			api.HandleErr(w, r, tx, http.StatusNotFound, nil, fmt.Errorf("scanning lastUpdated from server insert: %v", err))
+			return
 		}
+		rowsAffected++
+	}
+
+	if rowsAffected < 1 {
+		api.HandleErr(w, r, tx, http.StatusNotFound, errors.New("no server found with this id"), nil)
+		return
+	}
+	if rowsAffected > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("update for server #%d affected too many rows (%d)", *server.ID, rowsAffected))
+		return
+	}
+
+	if userErr, sysErr, errCode = deleteInterfaces(inf.IntParams["id"], tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	if userErr, sysErr, errCode = createInterfaces(inf.IntParams["id"], interfaces, tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
 
-	return api.GenericUpdate(s)
+	if inf.Version.Major >= 3 {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", tc.ServerNullable{CommonServerProperties: server.CommonServerProperties, Interfaces: interfaces})
+	} else if inf.Version.Minor <= 1 {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", server.ServerNullableV11)
+	} else {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", server)
+	}
+
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: updated", *server.HostName, *server.DomainName, *server.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func (s *TOServer) Create() (error, error, int) {
-	// TODO put in Validate()
-	if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-		s.IP6Address = nil
+func createV1(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+	var server tc.ServerNullableV11
+
+	tx := inf.Tx.Tx
+
+	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
 	}
-	if s.XMPPID == nil || *s.XMPPID == "" {
-		hostName := *s.HostName
-		s.XMPPID = &hostName
+
+	if err := validateV1(&server, tx); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
 	}
 
-	// default the is service field to true if omitted and to upgrade version < 1.4
-	defaultIsService := true
-	if s.IPIsService == nil {
-		s.IPIsService = &defaultIsService
+	resultRows, err := inf.Tx.NamedQuery(insertQuery, server)
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
 	}
-	if s.IP6IsService == nil {
-		s.IP6IsService = &defaultIsService
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
+			return
+		}
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
+		return
+	} else if rowsAffected > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
 	}
 
-	return api.GenericCreate(s)
-}
+	ifaces, err := server.LegacyInterfaceDetails.ToInterfaces(true, true)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, tx); err != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
 
-func (s *TOServer) Delete() (error, error, int) { return api.GenericDelete(s) }
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
 
-func selectV20UpdatesQuery() string {
-	return `SELECT 
-sv.ip_address_is_service, 
-sv.ip6_address_is_service 
-FROM 
-	server sv`
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func selectQuery() string {
-	const JumboFrameBPS = 9000
-	return `SELECT
-cg.name as cachegroup,
-s.cachegroup as cachegroup_id,
-s.cdn_id,
-cdn.name as cdn_name,
-s.domain_name,
-s.guid,
-s.host_name,
-s.https_port,
-s.id,
-s.ilo_ip_address,
-s.ilo_ip_gateway,
-s.ilo_ip_netmask,
-s.ilo_password,
-s.ilo_username,
-COALESCE(s.interface_mtu, ` + strconv.Itoa(JumboFrameBPS) + `) as interface_mtu,
-s.interface_name,
-s.ip6_address,
-s.ip6_address_is_service,
-s.ip6_gateway,
-s.ip_address,
-s.ip_address_is_service,
-s.ip_gateway,
-s.ip_netmask,
-s.last_updated,
-s.mgmt_ip_address,
-s.mgmt_ip_gateway,
-s.mgmt_ip_netmask,
-s.offline_reason,
-pl.name as phys_location,
-s.phys_location as phys_location_id,
-p.name as profile,
-p.description as profile_desc,
-s.profile as profile_id,
-s.rack,
-s.reval_pending,
-s.router_host_name,
-s.router_port_name,
-st.name as status,
-s.status as status_id,
-s.tcp_port,
-t.name as server_type,
-s.type as server_type_id,
-s.upd_pending as upd_pending,
-s.xmpp_id,
-s.xmpp_passwd
-FROM
-  server s
-JOIN cachegroup cg ON s.cachegroup = cg.id
-JOIN cdn cdn ON s.cdn_id = cdn.id
-JOIN phys_location pl ON s.phys_location = pl.id
-JOIN profile p ON s.profile = p.id
-JOIN status st ON s.status = st.id
-JOIN type t ON s.type = t.id`
+func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+	var server tc.ServerNullableV2
+
+	tx := inf.Tx.Tx
+
+	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	if err := validateV2(&server, tx); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	resultRows, err := inf.Tx.NamedQuery(insertQuery, server)
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
+			return
+		}
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
+		return
+	} else if rowsAffected > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
+	}
+
+	ifaces, err := server.LegacyInterfaceDetails.ToInterfaces(*server.IPIsService, *server.IP6IsService)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+	}
+
+	if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, tx); err != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func insertQuery() string {
-	query := `INSERT INTO server (
-cachegroup,
-cdn_id,
-domain_name,
-host_name,
-https_port,
-ilo_ip_address,
-ilo_ip_netmask,
-ilo_ip_gateway,
-ilo_username,
-ilo_password,
-interface_mtu,
-interface_name,
-ip6_address,
-ip6_address_is_service,
-ip6_gateway,
-ip_address,
-ip_address_is_service,
-ip_netmask,
-ip_gateway,
-mgmt_ip_address,
-mgmt_ip_netmask,
-mgmt_ip_gateway,
-offline_reason,
-phys_location,
-profile,
-rack,
-router_host_name,
-router_port_name,
-status,
-tcp_port,
-type,
-upd_pending,
-xmpp_id,
-xmpp_passwd
-) VALUES (
-:cachegroup_id,
-:cdn_id,
-:domain_name,
-:host_name,
-:https_port,
-:ilo_ip_address,
-:ilo_ip_netmask,
-:ilo_ip_gateway,
-:ilo_username,
-:ilo_password,
-:interface_mtu,
-:interface_name,
-:ip6_address,
-:ip6_address_is_service,
-:ip6_gateway,
-:ip_address,
-:ip_address_is_service,
-:ip_netmask,
-:ip_gateway,
-:mgmt_ip_address,
-:mgmt_ip_netmask,
-:mgmt_ip_gateway,
-:offline_reason,
-:phys_location_id,
-:profile_id,
-:rack,
-:router_host_name,
-:router_port_name,
-:status_id,
-:tcp_port,
-:server_type_id,
-:upd_pending,
-:xmpp_id,
-:xmpp_passwd
-) RETURNING id,last_updated`
-	return query
+func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+	var server tc.ServerNullable
+
+	tx := inf.Tx.Tx
+
+	if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	serviceInterface, err := validateV3(&server, tx)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+		return
+	}
+
+	v2Server, err := server.ToServerV2()
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	v2Server.InterfaceName = &serviceInterface
+
+	resultRows, err := inf.Tx.NamedQuery(insertQuery, v2Server)
+	if err != nil {
+		userErr, sysErr, errCode := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer resultRows.Close()
+
+	rowsAffected := 0
+	for resultRows.Next() {
+		rowsAffected++
+		if err := resultRows.StructScan(&server.CommonServerProperties); err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("server create scanning: %v", err))
+			return
+		}
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("server create: no server was inserted, no id was returned"))
+		return
+	} else if rowsAffected > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("too many ids returned from server insert"))
+		return
+	}
+
+	userErr, sysErr, errCode := createInterfaces(*server.ID, server.Interfaces, tx)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *server.HostName, *server.DomainName, *server.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func updateQuery() string {
-	query := `UPDATE
-server SET
-cachegroup=:cachegroup_id,
-cdn_id=:cdn_id,
-domain_name=:domain_name,
-host_name=:host_name,
-https_port=:https_port,
-ilo_ip_address=:ilo_ip_address,
-ilo_ip_netmask=:ilo_ip_netmask,
-ilo_ip_gateway=:ilo_ip_gateway,
-ilo_username=:ilo_username,
-ilo_password=:ilo_password,
-interface_mtu=:interface_mtu,
-interface_name=:interface_name,
-ip6_address=:ip6_address,
-ip6_address_is_service=:ip6_address_is_service,
-ip6_gateway=:ip6_gateway,
-ip_address=:ip_address,
-ip_address_is_service=:ip_address_is_service,
-ip_netmask=:ip_netmask,
-ip_gateway=:ip_gateway,
-mgmt_ip_address=:mgmt_ip_address,
-mgmt_ip_netmask=:mgmt_ip_netmask,
-mgmt_ip_gateway=:mgmt_ip_gateway,
-offline_reason=:offline_reason,
-phys_location=:phys_location_id,
-profile=:profile_id,
-rack=:rack,
-router_host_name=:router_host_name,
-router_port_name=:router_port_name,
-status=:status_id,
-tcp_port=:tcp_port,
-type=:server_type_id,
-upd_pending=:upd_pending,
-xmpp_id=:xmpp_id,
-xmpp_passwd=:xmpp_passwd
-WHERE id=:id RETURNING last_updated`
-	return query
+func Create(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	switch {
+	case inf.Version.Major <= 1:
+		createV1(inf, w, r)
+	case inf.Version.Major == 2:
+		createV2(inf, w, r)
+	default:
+		createV3(inf, w, r)
+	}
 }
 
-func deleteQuery() string {
-	return `DELETE FROM server WHERE id = :id`
+func Delete(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	id := inf.IntParams["id"]
+
+	var servers []tc.ServerNullable
+	servers, _, userErr, sysErr, errCode = getServers(map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	if len(servers) < 1 {
+		api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no server exists by id #%d", id), nil)
+		return
+	}
+	if len(servers) > 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("there are somehow two servers with id %d - cannot delete", id))
+		return
+	}
+
+	userErr, sysErr, errCode = deleteInterfaces(id, tx)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	if result, err := tx.Exec(deleteServerQuery, id); err != nil {
+		log.Errorf("Raw error: %v", err)
+		userErr, sysErr, errCode = api.ParseDBError(err)
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	} else if rowsAffected, err := result.RowsAffected(); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("getting rows affected by server delete: %v", err))
+		return
+	} else if rowsAffected != 1 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("incorrect number of rows affected: %d", rowsAffected))
+		return
+	}
+
+	server := servers[0]
+
+	if inf.Version.Major >= 3 {
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", server)
+	} else {
+
+		serverV2, err := server.ToServerV2()
+		if err != nil {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+			return
+		}
+
+		if inf.Version.Major <= 1 {
+			api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", serverV2.ServerNullableV11)
+		} else {
+			api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", serverV2)
+		}
+	}
+	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: deleted", *server.HostName, *server.DomainName, *server.ID)
+	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index a8c91f2..2f80a8a 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -20,21 +20,27 @@ package server
  */
 
 import (
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"net/http"
 	"testing"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/test"
-	"github.com/jmoiron/sqlx"
 
+	"github.com/jmoiron/sqlx"
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
-func getTestServers() []tc.Server {
-	servers := []tc.Server{}
+type ServerAndInterfaces struct {
+	Server    tc.Server
+	Interface tc.ServerInterfaceInfo
+}
+
+func getTestServers() []ServerAndInterfaces {
+	servers := []ServerAndInterfaces{}
 	testServer := tc.Server{
 		Cachegroup:     "Cachegroup",
 		CachegroupID:   1,
@@ -82,17 +88,36 @@ func getTestServers() []tc.Server {
 		XMPPID:         "xmppId",
 		XMPPPasswd:     "xmppPasswd",
 	}
-	servers = append(servers, testServer)
+
+	mtu := uint64(testServer.InterfaceMtu)
+
+	iface := tc.ServerInterfaceInfo{
+		IPAddresses: []tc.ServerIPAddress{
+			tc.ServerIPAddress{
+				Address: testServer.IPAddress,
+				Gateway: nil,
+				ServiceAddress: true,
+			},
+		},
+		MaxBandwidth: nil,
+		Monitor: true,
+		MTU: &mtu,
+		Name: testServer.InterfaceName,
+	}
+
+	servers = append(servers, ServerAndInterfaces{Server: testServer, Interface: iface})
 
 	testServer2 := testServer
 	testServer2.Cachegroup = "cachegroup2"
 	testServer2.HostName = "server2"
-	servers = append(servers, testServer2)
+	testServer2.ID = 2
+	servers = append(servers, ServerAndInterfaces{Server: testServer2, Interface: iface})
 
 	testServer3 := testServer
 	testServer3.Cachegroup = "cachegroup3"
 	testServer3.HostName = "server3"
-	servers = append(servers, testServer2)
+	testServer3.ID = 3
+	servers = append(servers, ServerAndInterfaces{Server: testServer3, Interface: iface})
 
 	return servers
 }
@@ -112,7 +137,7 @@ func TestUpdateServer(t *testing.T) {
 
 	rows := sqlmock.NewRows([]string{"type", "cdn_id"})
 	// note here that the cdnid is 5, which is not the same as the initial cdnid of the fist traffic server
-	rows.AddRow(testServers[0].TypeID, 5)
+	rows.AddRow(testServers[0].Server.TypeID, 5)
 	// Make it return a list of atleast one associated ds
 	dsrows := sqlmock.NewRows([]string{"array"})
 	dsrows.AddRow("{3}")
@@ -120,33 +145,14 @@ func TestUpdateServer(t *testing.T) {
 	mock.ExpectQuery("SELECT").WillReturnRows(rows)
 	mock.ExpectQuery("SELECT ARRAY").WillReturnRows(dsrows)
 
-	snv := tc.ServerNullableV11{
-		CDNID:    &testServers[0].CDNID,
+	s := tc.CommonServerProperties{
+		CDNID:    &testServers[0].Server.CDNID,
 		FqdnTime: time.Time{},
-		TypeID:   &testServers[0].TypeID,
-	}
-	sn := tc.ServerNullable{
-		ServerNullableV11: snv,
-		IPIsService:       nil,
-		IP6IsService:      nil,
-	}
-
-	s := &TOServer{
-		APIInfoImpl: api.APIInfoImpl{
-			ReqInfo: &api.APIInfo{
-				Params:    nil,
-				IntParams: nil,
-				User:      nil,
-				ReqID:     0,
-				Version:   nil,
-				Tx:        db.MustBegin(),
-				Config:    nil,
-			},
-		},
-		ServerNullable: sn,
+		TypeID:   &testServers[0].Server.TypeID,
+		ID:       &testServers[0].Server.ID,
 	}
 
-	userErr, _, errCode := s.Update()
+	userErr, _, errCode := checkTypeChangeSafety(s, db.MustBegin())
 	if errCode != 409 {
 		t.Errorf("Update servers: Expected error code of %v, but got %v", 409, errCode)
 	}
@@ -167,12 +173,22 @@ func TestGetServersByCachegroup(t *testing.T) {
 	defer db.Close()
 
 	testServers := getTestServers()
-	cols := test.ColsFromStructByTag("db", tc.Server{})
+
+	unfilteredCols := []string{"count"}
+	unfilteredRows := sqlmock.NewRows(unfilteredCols).AddRow(len(testServers))
+
+	cols := test.ColsFromStructByTag("db", tc.CommonServerProperties{})
+	interfaceCols := []string{"max_bandwidth", "monitor", "mtu", "name", "server"}
 	rows := sqlmock.NewRows(cols)
+	interfaceRows := sqlmock.NewRows(interfaceCols)
+
+	ipCols := []string{"address", "gateway", "service_address", "server", "interface"}
+	ipRows := sqlmock.NewRows(ipCols)
 
 	//TODO: drichardson - build helper to add these Rows from the struct values
 	//                    or by CSV if types get in the way
-	for _, ts := range testServers {
+	for _, srv := range testServers {
+		ts := srv.Server
 		rows = rows.AddRow(
 			ts.Cachegroup,
 			ts.CachegroupID,
@@ -188,15 +204,6 @@ func TestGetServersByCachegroup(t *testing.T) {
 			ts.ILOIPNetmask,
 			ts.ILOPassword,
 			ts.ILOUsername,
-			ts.InterfaceMtu,
-			ts.InterfaceName,
-			ts.IP6Address,
-			ts.IP6IsService,
-			ts.IP6Gateway,
-			ts.IPAddress,
-			ts.IPIsService,
-			ts.IPNetmask,
-			ts.IPGateway,
 			ts.LastUpdated,
 			ts.MgmtIPAddress,
 			ts.MgmtIPGateway,
@@ -220,14 +227,36 @@ func TestGetServersByCachegroup(t *testing.T) {
 			ts.XMPPID,
 			ts.XMPPPasswd,
 		)
+		interfaceRows = interfaceRows.AddRow(
+			srv.Interface.MaxBandwidth,
+			srv.Interface.Monitor,
+			srv.Interface.MTU,
+			srv.Interface.Name,
+			ts.ID,
+		)
+
+		for _, ip := range srv.Interface.IPAddresses {
+			ipRows = ipRows.AddRow(
+				ip.Address,
+				ip.Gateway,
+				ip.ServiceAddress,
+				ts.ID,
+				srv.Interface.Name,
+			)
+		}
 	}
+
 	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT COUNT\\(s.id\\) FROM s").WillReturnRows(unfilteredRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+	mock.ExpectQuery("SELECT").WillReturnRows(interfaceRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(ipRows)
+
 	v := map[string]string{"cachegroup": "2"}
 
 	user := auth.CurrentUser{}
 
-	servers, userErr, sysErr, errCode := getServers(v, db.MustBegin(), &user)
+	servers, _, userErr, sysErr, errCode := getServers(v, db.MustBegin(), &user)
 	if userErr != nil || sysErr != nil {
 		t.Errorf("getServers expected: no errors, actual: %v %v with status: %s", userErr, sysErr, http.StatusText(errCode))
 	}
@@ -235,7 +264,6 @@ func TestGetServersByCachegroup(t *testing.T) {
 	if len(servers) != 3 {
 		t.Errorf("getServers expected: len(servers) == 3, actual: %v", len(servers))
 	}
-
 }
 
 func TestGetMidServers(t *testing.T) {
@@ -251,14 +279,23 @@ func TestGetMidServers(t *testing.T) {
 	testServers := getTestServers()
 	testServers = testServers[0:2]
 
-	testServers[1].Cachegroup = "parentCacheGroup"
-	testServers[1].CachegroupID = 2
-	testServers[1].Type = "MID"
+	testServers[1].Server.Cachegroup = "parentCacheGroup"
+	testServers[1].Server.CachegroupID = 2
+	testServers[1].Server.Type = "MID"
 
-	cols := test.ColsFromStructByTag("db", tc.Server{})
+	unfilteredCols := []string{"count"}
+	unfilteredRows := sqlmock.NewRows(unfilteredCols).AddRow(len(testServers))
+
+	cols := test.ColsFromStructByTag("db", tc.CommonServerProperties{})
+	interfaceCols := []string{"max_bandwidth", "monitor", "mtu", "name", "server"}
 	rows := sqlmock.NewRows(cols)
+	interfaceRows := sqlmock.NewRows(interfaceCols)
+
+	ipCols := []string{"address", "gateway", "service_address", "server", "interface"}
+	ipRows := sqlmock.NewRows(ipCols)
 
-	for _, ts := range testServers {
+	for _, srv := range testServers {
+		ts := srv.Server
 		rows = rows.AddRow(
 			ts.Cachegroup,
 			ts.CachegroupID,
@@ -274,15 +311,6 @@ func TestGetMidServers(t *testing.T) {
 			ts.ILOIPNetmask,
 			ts.ILOPassword,
 			ts.ILOUsername,
-			ts.InterfaceMtu,
-			ts.InterfaceName,
-			ts.IP6Address,
-			ts.IP6IsService,
-			ts.IP6Gateway,
-			ts.IPAddress,
-			ts.IPIsService,
-			ts.IPNetmask,
-			ts.IPGateway,
 			ts.LastUpdated,
 			ts.MgmtIPAddress,
 			ts.MgmtIPGateway,
@@ -306,20 +334,40 @@ func TestGetMidServers(t *testing.T) {
 			ts.XMPPID,
 			ts.XMPPPasswd,
 		)
+		interfaceRows = interfaceRows.AddRow(
+			srv.Interface.MaxBandwidth,
+			srv.Interface.Monitor,
+			srv.Interface.MTU,
+			srv.Interface.Name,
+			ts.ID,
+		)
+
+		for _, ip := range srv.Interface.IPAddresses {
+			ipRows = ipRows.AddRow(
+				ip.Address,
+				ip.Gateway,
+				ip.ServiceAddress,
+				ts.ID,
+				srv.Interface.Name,
+			)
+		}
 	}
 	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT COUNT\\(s.id\\) FROM s").WillReturnRows(unfilteredRows)
 	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+	mock.ExpectQuery("SELECT").WillReturnRows(interfaceRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(ipRows)
 	v := map[string]string{}
 
 	user := auth.CurrentUser{}
 
-	servers, userErr, sysErr, errCode := getServers(v, db.MustBegin(), &user)
+	servers, _, userErr, sysErr, errCode := getServers(v, db.MustBegin(), &user)
 
 	if userErr != nil || sysErr != nil {
 		t.Errorf("getServers expected: no errors, actual: %v %v with status: %s", userErr, sysErr, http.StatusText(errCode))
 	}
 
-	cols2 := test.ColsFromStructByTag("db", tc.Server{})
+	cols2 := test.ColsFromStructByTag("db", tc.CommonServerProperties{})
 	rows2 := sqlmock.NewRows(cols2)
 
 	cgs := []tc.CacheGroup{}
@@ -365,7 +413,18 @@ func TestGetMidServers(t *testing.T) {
 	}
 	cgs = append(cgs, testCG2)
 
+	serverMap := make(map[int]tc.ServerNullable, len(servers))
+	serverIDs := make([]int, 0, len(servers))
+	for _, server := range servers {
+		if server.ID == nil {
+			t.Fatal("Found server with nil ID")
+		}
+		serverIDs = append(serverIDs, *server.ID)
+		serverMap[*server.ID] = server
+	}
+
 	ts := servers[1]
+	*ts.ID = *ts.ID + 1
 	rows2 = rows2.AddRow(
 		ts.Cachegroup,
 		ts.CachegroupID,
@@ -381,15 +440,6 @@ func TestGetMidServers(t *testing.T) {
 		ts.ILOIPNetmask,
 		ts.ILOPassword,
 		ts.ILOUsername,
-		ts.InterfaceMtu,
-		ts.InterfaceName,
-		ts.IP6Address,
-		ts.IP6IsService,
-		ts.IP6Gateway,
-		ts.IPAddress,
-		ts.IPIsService,
-		ts.IPNetmask,
-		ts.IPGateway,
 		ts.LastUpdated,
 		ts.MgmtIPAddress,
 		ts.MgmtIPGateway,
@@ -416,7 +466,7 @@ func TestGetMidServers(t *testing.T) {
 
 	mock.ExpectBegin()
 	mock.ExpectQuery("SELECT").WillReturnRows(rows2)
-	mid, userErr, sysErr, errCode := getMidServers(servers, db.MustBegin())
+	mid, userErr, sysErr, errCode := getMidServers(serverIDs, serverMap, db.MustBegin())
 
 	if userErr != nil || sysErr != nil {
 		t.Fatalf("getMidServers expected: no errors, actual: %v %v with status: %s", userErr, sysErr, http.StatusText(errCode))
@@ -424,10 +474,191 @@ func TestGetMidServers(t *testing.T) {
 	if len(mid) != 1 {
 		t.Fatalf("getMidServers expected: len(mid) == 1, actual: %v", len(mid))
 	}
-	if mid[0].Type != "MID" || *(mid[0].CachegroupID) != 2 || *(mid[0].Cachegroup) != "parentCacheGroup" {
-		t.Fatalf("getMidServers expected: Type == MID, actual: %v", mid[0].Type)
-		t.Fatalf("getMidServers expected: CachegroupID == 2, actual: %v", *(mid[0].CachegroupID))
-		t.Fatalf("getMidServers expected: Cachegroup == parentCacheGroup, actual: %v", *(mid[0].Cachegroup))
+	if serverMap[mid[0]].Type != "MID" {
+		t.Errorf("getMidServers expected: Type == MID, actual: %v", serverMap[mid[0]].Type)
+	}
+
+	if serverMap[mid[0]].Cachegroup == nil {
+		t.Error("getMidServers expected: Cachegroup == parentCacheGroup, actual: nil")
+	} else if *(serverMap[mid[0]].Cachegroup) != "parentCacheGroup" {
+		t.Errorf("getMidServers expected: Cachegroup == parentCacheGroup, actual: %v", *(serverMap[mid[0]].Cachegroup))
+	}
+
+	if serverMap[mid[0]].CachegroupID == nil {
+		t.Error("getMidServers expected: CachegroupID == 2, actual: nil")
+	} else if *(serverMap[mid[0]].CachegroupID) != 2  {
+		t.Errorf("getMidServers expected: CachegroupID == 2, actual: %v", *(serverMap[mid[0]].CachegroupID))
+	}
+}
+
+func TestV3Validations(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	goodInterface := tc.ServerInterfaceInfo{
+		IPAddresses: []tc.ServerIPAddress{
+			tc.ServerIPAddress{
+				Address:        "127.0.0.1/32",
+				Gateway:        nil,
+				ServiceAddress: true,
+			},
+		},
+		MaxBandwidth: nil,
+		Monitor:      true,
+		MTU:          nil,
+		Name:         "eth0",
+	}
+
+	testServer := tc.ServerNullable{
+		CommonServerProperties: tc.CommonServerProperties{
+			CDNID:          util.IntPtr(1),
+			HostName:       util.StrPtr("test"),
+			DomainName:     util.StrPtr("quest"),
+			PhysLocationID: new(int),
+			ProfileID:      new(int),
+			StatusID:       new(int),
+			TypeID:         new(int),
+			UpdPending:     new(bool),
+			CachegroupID:   new(int),
+		},
+		Interfaces: []tc.ServerInterfaceInfo{goodInterface},
+	}
+
+	typeCols := []string{"name", "use_in_table"}
+	cdnCols := []string{"cdn"}
+	typeRows := sqlmock.NewRows(typeCols).AddRow("EDGE", "server")
+	cdnRows := sqlmock.NewRows(cdnCols).AddRow(*testServer.CDNID)
+
+	mock.ExpectBegin()
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	tx := db.MustBegin().Tx
+
+	_, err = validateV3(&testServer, tx)
+	if err != nil {
+		t.Errorf("Unexpected error validating test server: %v", err)
+	}
+
+	testServer.Interfaces = []tc.ServerInterfaceInfo{}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with no interfaces to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with no interfaces: %v", err)
+	}
+
+	testServer.Interfaces = nil
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with nil interfaces to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with nil interfaces: %v", err)
+	}
+
+	badIface := goodInterface
+	var badMTU uint64 = 1279
+	badIface.MTU = &badMTU
+	testServer.Interfaces = []tc.ServerInterfaceInfo{badIface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server an MTU < 1280 to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with an MTU < 1280: %v", err)
+	}
+
+	badIface.MTU = nil
+	badIface.IPAddresses = []tc.ServerIPAddress{}
+	testServer.Interfaces = []tc.ServerInterfaceInfo{badIface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with no IP addresses to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with no IP addresses: %v", err)
+	}
+
+	badIface.IPAddresses = nil
+	testServer.Interfaces = []tc.ServerInterfaceInfo{badIface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with nil IP addresses to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with nil IP addresses: %v", err)
+	}
+
+	badIface = goodInterface
+	badIP := tc.ServerIPAddress{
+		Address:        "127.0.0.1/32",
+		Gateway:        nil,
+		ServiceAddress: false,
+	}
+	badIface.IPAddresses = []tc.ServerIPAddress{badIP}
+	testServer.Interfaces = []tc.ServerInterfaceInfo{badIface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with no service addresses to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with no service addresses: %v", err)
+	}
+
+	testServer.Interfaces = []tc.ServerInterfaceInfo{goodInterface, goodInterface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with too many interfaces with service addresses to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with too many interfaces with service addresses: %v", err)
+	}
+
+	badIface = goodInterface
+	badIface.IPAddresses = append(badIface.IPAddresses, tc.ServerIPAddress{
+		Address:        "1.2.3.4/1",
+		Gateway:        nil,
+		ServiceAddress: true,
+	})
+	testServer.Interfaces = []tc.ServerInterfaceInfo{badIface}
+
+	mock.ExpectQuery("SELECT name, use_in_table").WillReturnRows(typeRows)
+	mock.ExpectQuery("SELECT").WillReturnRows(cdnRows)
+
+	_, err = validateV3(&testServer, tx)
+	if err == nil {
+		t.Errorf("Expected a server with no service addresses to be invalid")
+	} else {
+		t.Logf("Got expected error validating server with no service addresses: %v", err)
 	}
 }
 
diff --git a/traffic_ops/v1-client/capability.go b/traffic_ops/v1-client/capability.go
index 100c0fa..d2ba6ed 100644
--- a/traffic_ops/v1-client/capability.go
+++ b/traffic_ops/v1-client/capability.go
@@ -62,7 +62,7 @@ func (to *Session) GetCapabilities() ([]tc.Capability, ReqInf, error) {
 
 // GetCapability retrieves only the capability named 'c'
 func (to *Session) GetCapability(c string) (tc.Capability, ReqInf, error) {
-	var v url.Values
+	v := url.Values{}
 	v.Add("name", c)
 	endpoint := API_v14_CAPABILITIES + "?" + v.Encode()
 	resp, remoteAddr, err := to.request(http.MethodGet, endpoint, nil)
diff --git a/traffic_ops/v2-client/capability.go b/traffic_ops/v2-client/capability.go
index cb544a4..43bae24 100644
--- a/traffic_ops/v2-client/capability.go
+++ b/traffic_ops/v2-client/capability.go
@@ -39,7 +39,7 @@ func (to *Session) GetCapabilities() ([]tc.Capability, ReqInf, error) {
 
 // GetCapability retrieves only the capability named 'c'.
 func (to *Session) GetCapability(c string) (tc.Capability, ReqInf, error) {
-	var v url.Values
+	v := url.Values{}
 	v.Add("name", c)
 	endpoint := API_CAPABILITIES + "?" + v.Encode()
 	resp, remoteAddr, err := to.request(http.MethodGet, endpoint, nil)
diff --git a/traffic_stats/traffic_stats.go b/traffic_stats/traffic_stats.go
index 0ee4076..dc15582 100644
--- a/traffic_stats/traffic_stats.go
+++ b/traffic_stats/traffic_stats.go
@@ -38,7 +38,7 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 
-	"github.com/apache/trafficcontrol/traffic_ops/client"
+	"github.com/apache/trafficcontrol/traffic_ops/v2-client"
 	log "github.com/cihub/seelog"
 	influx "github.com/influxdata/influxdb/client/v2"
 )