rawlinp commented on a change in pull request #4700:
URL: https://github.com/apache/trafficcontrol/pull/4700#discussion_r432733101



##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -33,103 +35,328 @@ 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() }
+const unfilteredServersQuery = `
+SELECT COUNT(server.id)
+FROM server
+`
 
-func (TOServer) GetKeyFieldsInfo() []api.KeyFieldInfo {
-       return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
+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
+`
 
-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 SelectInterfacesQuery = `

Review comment:
       Can we share some of this query with the other N places it's duplicated?

##########
File path: docs/source/glossary.rst
##########
@@ -302,22 +302,22 @@ 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``.

Review comment:
       I really think these changes should've been out of scope for this PR, 
but it is what it is.

##########
File path: traffic_ops/traffic_ops_golang/ats/atsserver/ipallowdotconfig.go
##########
@@ -1,148 +0,0 @@
-package atsserver

Review comment:
       Removing these three ATS config gen routes from 1.x could be its own PR 
as well (probably including the removal of _all_ ATS config gen routes).

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +366,213 @@ 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)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       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 := mtu.(*uint64)

Review comment:
       this should probably use the non-panicking type assertion `m, ok := 
mtu.(*uint64)` and return an error `if !ok`

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +366,213 @@ 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)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       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 := mtu.(*uint64)
+       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),
+               })...)
+
+               for _, addr := range iface.IPAddresses {
+                       ruleName += fmt.Sprintf("address '%s'", addr.Address)
+
+                       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
+                               }
+                       }
 
-       message := action + ` ` + status + ` server: { "hostName":"` + hostName 
+ `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+                       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"))
+                               }
+                       }
 
-       return message, nil
+                       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 unfiltered uint64
+       servers, unfiltered, 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, unfiltered)
+               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)
 
-       return returnable, nil, nil, http.StatusOK
+       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)
+       }
+
+       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) {
+       var unfiltered uint64

Review comment:
       this var (and the associated query var) would probably make more sense 
with `serverCount` or something similar in the name

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(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
+       defer ifaceRows.Close()
+       insertedIfaces := 0
+       for ifaceRows.Next() {
+               insertedIfaces++
+       }
+       log.Debugf("Inserted %d interfaces", insertedIfaces)
+
+       ipQry += strings.Join(ipQueryParts, ",")
+       log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+       ipRows, err := tx.Query(ipQry, ipArgs...)
+       if err != nil {
+               return api.ParseDBError(err)
+       }
+       defer ipRows.Close()
+
+       return nil, nil, http.StatusOK
+}
+
+func deleteInterfaces(id int, tx *sql.Tx) (error, error, int) {
+       if err := tx.QueryRow(deleteIPsQuery, id).Scan(); err != nil && err != 
sql.ErrNoRows {
+               return api.ParseDBError(err)
+       }
+
+       if err := tx.QueryRow(deleteInterfacesQuery, id).Scan(); 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
+       }
+
+       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)
        }
 
-       return api.GenericUpdate(s)
+       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)
+       }
 
-func (s *TOServer) Delete() (error, error, int) { return api.GenericDelete(s) }
+       if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, 
tx); err != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
 
-func selectV20UpdatesQuery() string {
-       return `SELECT 
-sv.ip_address_is_service, 
-sv.ip6_address_is_service 
-FROM 
-       server sv`
+       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 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 err := tx.QueryRow(deleteServerQuery, id).Scan(); err != nil && err 
!= sql.ErrNoRows {
+               userErr, sysErr, errCode = api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }

Review comment:
       Should we also handle the `sql.ErrNoRows` error here? 

##########
File path: infrastructure/cdn-in-a-box/enroller/Dockerfile
##########
@@ -20,7 +20,7 @@ FROM golang:1.14.2 AS enroller-builder
 # enroller source and dependencies
 COPY ./lib/ /go/src/github.com/apache/trafficcontrol/lib/
 COPY ./vendor/ /go/src/github.com/apache/trafficcontrol/vendor/
-COPY ./traffic_ops/client/ 
/go/src/github.com/apache/trafficcontrol/traffic_ops/v2-client/
+COPY ./traffic_ops/v2-client/ 
/go/src/github.com/apache/trafficcontrol/traffic_ops/v2-client/

Review comment:
       This probably should've been a separate PR as well -- others ran into 
this issue too: https://github.com/apache/trafficcontrol/pull/4739

##########
File path: traffic_ops/traffic_ops_golang/server/detail.go
##########
@@ -202,7 +202,7 @@ SELECT
                                SELECT ( json_build_object (
                                        'address', ip_address.address,
                                        'gateway', ip_address.gateway,
-                                       'service_address', 
ip_address.service_address
+                                       'serviceAddress', 
ip_address.service_address

Review comment:
       This whole query chunk for `interfaces` seems like it should be moved 
into a shared constant, since it's already out of sync here w/ the same query 
in dbhelpers.go. Also, why change this from snake_case but leave 
`max_bandwidth` as-is?

##########
File path: traffic_ops/client/server.go
##########
@@ -33,158 +33,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) {

Review comment:
       These non-nullable -> nullable client changes would've been nice to 
split into another PR as well, as a bit of a "step 1", then have this PR 
implement just the v3-specific conversions.

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -33,103 +35,328 @@ 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() }
+const unfilteredServersQuery = `

Review comment:
       this doesn't seem to be the right name for this query

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +366,213 @@ 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)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       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 := mtu.(*uint64)
+       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),
+               })...)
+
+               for _, addr := range iface.IPAddresses {
+                       ruleName += fmt.Sprintf("address '%s'", addr.Address)
+
+                       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
+                               }
+                       }
 
-       message := action + ` ` + status + ` server: { "hostName":"` + hostName 
+ `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+                       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"))
+                               }
+                       }
 
-       return message, nil
+                       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 unfiltered uint64
+       servers, unfiltered, 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, unfiltered)
+               return
        }
 
+       if version.Major <= 1 {
+               legacyServers := make([]tc.ServerNullableV11, 0, len(servers))
+               for _, server := range servers {
+                       legacyServer, err := server.ToServerV2()

Review comment:
       Is it possible that server data changes could be made using the V3 API 
that would prevent the ability to use the V2/V1 APIs? Basically, is there any 
valid reason why a V3 server couldn't be converted to a V2/V1 server?

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(ifaceQry, ifaceArgs...)

Review comment:
       This seems like it should be an `Exec` instead if we really only care 
about the number of rows affected.

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -248,71 +594,103 @@ 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, unfiltered, 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, unfiltered, 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
-`
+               queryAddition = "\nFULL OUTER JOIN deliveryservice_server dss 
ON dss.server = s.id\n"

Review comment:
       I actually prefer the previous format -- it more clearly shows the query 
addition without the newlines.

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(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
+       defer ifaceRows.Close()
+       insertedIfaces := 0
+       for ifaceRows.Next() {
+               insertedIfaces++
+       }
+       log.Debugf("Inserted %d interfaces", insertedIfaces)
+
+       ipQry += strings.Join(ipQueryParts, ",")
+       log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+       ipRows, err := tx.Query(ipQry, ipArgs...)

Review comment:
       Similar to the previous comment, this seems like it should be an `Exec`. 
It's also a bit of a red flag to not iterate over all of the returned `Rows` 
since later queries in the same transaction will fail if there are `Rows` that 
haven't been `Next()`'d from a previous query in the transaction.

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(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
+       defer ifaceRows.Close()
+       insertedIfaces := 0
+       for ifaceRows.Next() {
+               insertedIfaces++
+       }
+       log.Debugf("Inserted %d interfaces", insertedIfaces)
+
+       ipQry += strings.Join(ipQueryParts, ",")
+       log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+       ipRows, err := tx.Query(ipQry, ipArgs...)
+       if err != nil {
+               return api.ParseDBError(err)
+       }
+       defer ipRows.Close()
+
+       return nil, nil, http.StatusOK
+}
+
+func deleteInterfaces(id int, tx *sql.Tx) (error, error, int) {
+       if err := tx.QueryRow(deleteIPsQuery, id).Scan(); err != nil && err != 
sql.ErrNoRows {

Review comment:
       Similar to the previous comment, this seems like it should be an `Exec` 
since the `Scan()` does nothing. 

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(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
+       defer ifaceRows.Close()
+       insertedIfaces := 0
+       for ifaceRows.Next() {
+               insertedIfaces++
+       }
+       log.Debugf("Inserted %d interfaces", insertedIfaces)
+
+       ipQry += strings.Join(ipQueryParts, ",")
+       log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+       ipRows, err := tx.Query(ipQry, ipArgs...)
+       if err != nil {
+               return api.ParseDBError(err)
+       }
+       defer ipRows.Close()
+
+       return nil, nil, http.StatusOK
+}
+
+func deleteInterfaces(id int, tx *sql.Tx) (error, error, int) {
+       if err := tx.QueryRow(deleteIPsQuery, id).Scan(); err != nil && err != 
sql.ErrNoRows {
+               return api.ParseDBError(err)
+       }
+
+       if err := tx.QueryRow(deleteInterfacesQuery, id).Scan(); err != nil && 
err != sql.ErrNoRows {

Review comment:
       Similar to the previous comment, this seems like it should be an `Exec` 
since the `Scan()` does nothing. 

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,267 +737,482 @@ WHERE s.id IN (` + edgeIDs + `)))
        return mids, nil, nil, http.StatusOK
 }
 
-func (s *TOServer) Update() (error, error, int) {
-       if s.IP6Address != nil && len(strings.TrimSpace(*s.IP6Address)) == 0 {
-               s.IP6Address = nil
-       }
-       // 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 {
+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)
+
+       ifaceRows, err := tx.Query(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
+       defer ifaceRows.Close()
+       insertedIfaces := 0
+       for ifaceRows.Next() {
+               insertedIfaces++
+       }
+       log.Debugf("Inserted %d interfaces", insertedIfaces)
+
+       ipQry += strings.Join(ipQueryParts, ",")
+       log.Debugf("Inserting IP addresses for new server, query is: %s", ipQry)
+
+       ipRows, err := tx.Query(ipQry, ipArgs...)
+       if err != nil {
+               return api.ParseDBError(err)
+       }
+       defer ipRows.Close()
+
+       return nil, nil, http.StatusOK
+}
+
+func deleteInterfaces(id int, tx *sql.Tx) (error, error, int) {
+       if err := tx.QueryRow(deleteIPsQuery, id).Scan(); err != nil && err != 
sql.ErrNoRows {
+               return api.ParseDBError(err)
+       }
+
+       if err := tx.QueryRow(deleteInterfacesQuery, id).Scan(); 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
+       }
+
+       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)
        }
 
-       return api.GenericUpdate(s)
+       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)
+       }
 
-func (s *TOServer) Delete() (error, error, int) { return api.GenericDelete(s) }
+       if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, 
tx); err != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
 
-func selectV20UpdatesQuery() string {
-       return `SELECT 
-sv.ip_address_is_service, 
-sv.ip6_address_is_service 
-FROM 
-       server sv`
+       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 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)

Review comment:
       This seems like it should be handled via `ON DELETE CASCADE` on the 
interface/ip_address tables, but I see that they are actually `ON DELETE 
RESTRICT`. At this point I don't think it's a big enough deal to require 
modifying the db migration.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to