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



##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +398,224 @@ 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)...)
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       return util.JoinErrs(errs)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
+
+       if err := validateV1(s.ServerNullableV11, tx); err != nil {
                return err
        }
 
-       rows, err := s.ReqInfo.Tx.Tx.Query("select cdn from profile where 
id=$1", s.ProfileID)
-       if err != nil {
-               log.Error.Printf("could not execute select cdnID from profile: 
%s\n", err)
-               errs = append(errs, tc.DBError)
-               return util.JoinErrs(errs)
+       // default boolean value is false
+       if s.IPIsService == nil {
+               s.IPIsService = new(bool)
        }
-       defer rows.Close()
-       var cdnID int
-       for rows.Next() {
-               if err := rows.Scan(&cdnID); err != nil {
-                       log.Error.Printf("could not scan cdnID from profile: 
%s\n", err)
-                       errs = append(errs, errors.New("associated profile must 
have a cdn associated"))
-                       return util.JoinErrs(errs)
-               }
+       if s.IP6IsService == nil {
+               s.IP6IsService = new(bool)
        }
-       log.Infof("got cdn id: %d from profile and cdn id: %d from server", 
cdnID, *s.CDNID)
-       if cdnID != *s.CDNID {
-               errs = append(errs, errors.New(fmt.Sprintf("CDN id '%d' for 
profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID)))
+
+       if !*s.IPIsService && !*s.IP6IsService {
+               errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
+       }
+
+       if *s.IPIsService && s.IPAddress == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
+       }
+
+       if *s.IP6IsService && s.IP6Address == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
        }
        return util.JoinErrs(errs)
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log 
message
-func (s TOServer) ChangeLogMessage(action string) (string, error) {
+func validateMTU(mtu interface{}) error {
+       m := mtu.(*uint64)
+       if m == nil {
+               return nil
+       }
 
-       var status string
-       if s.Status != nil {
-               status = *s.Status
+       if *m < 1280 {
+               return errors.New("must be at least 1280")
        }
+       return nil
+}
 
-       var hostName string
-       if s.HostName != nil {
-               hostName = *s.HostName
+func validateGateway(g interface{}) error {

Review comment:
       Looks unused?

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -33,103 +35,360 @@ import (
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
        "github.com/apache/trafficcontrol/lib/go-util"
+
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-       validation "github.com/go-ozzo/ozzo-validation"
+
+       "github.com/go-ozzo/ozzo-validation"
        "github.com/go-ozzo/ozzo-validation/is"
        "github.com/jmoiron/sqlx"
        "github.com/lib/pq"
 )
 
-// TOServer combines data about a server with metadata from an API request and
-// provides methods that implement several interfaces from the api package.
-type TOServer struct {
-       api.APIInfoImpl `json:"-"`
-       tc.ServerNullable
-}
-
-func (s *TOServer) SetLastUpdated(t tc.TimeNoMod) { s.LastUpdated = &t }
-func (*TOServer) InsertQuery() string             { return insertQuery() }
-func (*TOServer) UpdateQuery() string             { return updateQuery() }
-func (*TOServer) DeleteQuery() string             { return deleteQuery() }
-
-func (TOServer) GetKeyFieldsInfo() []api.KeyFieldInfo {
-       return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
-
-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 unfilteredServersQuery = `
+SELECT COUNT(server.id)
+FROM server
+`
 
-func (s *TOServer) SetKeys(keys map[string]interface{}) {
-       i, _ := keys["id"].(int) //this utilizes the non panicking type 
assertion, if the thrown away ok variable is false i will be the zero of the 
type, 0 here.
-       s.ID = &i
-}
+const 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) GetAuditName() string {
-       if s.DomainName != nil {
-               return *s.DomainName
-       }
-       if s.ID != nil {
-               return strconv.Itoa(*s.ID)
-       }
-       return "unknown"
-}
+const SelectInterfacesQuery = `
+SELECT (
+       ARRAY ( SELECT (
+               json_build_object (
+                       'ipAddresses',
+                       ARRAY (
+                               SELECT (
+                                       json_build_object (
+                                               'address', ip_address.address,
+                                               'gateway', ip_address.gateway,
+                                               'serviceAddress', 
ip_address.service_address
+                                       )
+                               )
+                               FROM ip_address
+                               WHERE ip_address.interface = interface.name
+                               AND ip_address.server = server.id
+                       ),
+                       'maxBandwidth', interface.max_bandwidth,
+                       'monitor', interface.monitor,
+                       'mtu', interface.mtu,
+                       'name', interface.name
+               )
+       )
+       FROM interface
+       WHERE interface.server = server.id
+)) AS interfaces,
+server.id
+FROM server
+WHERE server.id = ANY ($1)
+`
 
-func (s *TOServer) GetType() string {
-       return "server"
-}
+const insertQuery = `
+INSERT INTO server (
+       cachegroup,
+       cdn_id,
+       domain_name,
+       host_name,
+       https_port,
+       ilo_ip_address,
+       ilo_ip_netmask,
+       ilo_ip_gateway,
+       ilo_username,
+       ilo_password,
+       interface_name,
+       mgmt_ip_address,
+       mgmt_ip_netmask,
+       mgmt_ip_gateway,
+       offline_reason,
+       phys_location,
+       profile,
+       rack,
+       router_host_name,
+       router_port_name,
+       status,
+       tcp_port,
+       type,
+       upd_pending,
+       xmpp_id,
+       xmpp_passwd
+) VALUES (
+       :cachegroup_id,
+       :cdn_id,
+       :domain_name,
+       :host_name,
+       :https_port,
+       :ilo_ip_address,
+       :ilo_ip_netmask,
+       :ilo_ip_gateway,
+       :ilo_username,
+       :ilo_password,
+       :interface_name,
+       :mgmt_ip_address,
+       :mgmt_ip_netmask,
+       :mgmt_ip_gateway,
+       :offline_reason,
+       :phys_location_id,
+       :profile_id,
+       :rack,
+       :router_host_name,
+       :router_port_name,
+       :status_id,
+       :tcp_port,
+       :server_type_id,
+       :upd_pending,
+       :xmpp_id,
+       :xmpp_passwd
+) RETURNING
+       (SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS 
cachegroup,
+       cachegroup AS cachegroup_id,
+       cdn_id,
+       (SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
+       domain_name,
+       guid,
+       host_name,
+       https_port,
+       id,
+       ilo_ip_address,
+       ilo_ip_gateway,
+       ilo_ip_netmask,
+       ilo_password,
+       ilo_username,
+       last_updated,
+       mgmt_ip_address,
+       mgmt_ip_gateway,
+       mgmt_ip_netmask,
+       offline_reason,
+       (SELECT name FROM phys_location WHERE 
phys_location.id=server.phys_location) AS phys_location,
+       phys_location AS phys_location_id,
+       profile AS profile_id,
+       (SELECT description FROM profile WHERE profile.id=server.profile) AS 
profile_desc,
+       (SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
+       rack,
+       reval_pending,
+       router_host_name,
+       router_port_name,
+       (SELECT name FROM status WHERE status.id=server.status) AS status,
+       status AS status_id,
+       tcp_port,
+       (SELECT name FROM type WHERE type.id=server.type) AS server_type,
+       type AS server_type_id,
+       upd_pending
+`
 
-func (s *TOServer) Sanitize() {
-       if s.IP6Address != nil && *s.IP6Address == "" {
-               s.IP6Address = nil
-       }
-}
+const insertInterfaceQuery = `

Review comment:
       Looks unused?

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +398,224 @@ 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)...)
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       return util.JoinErrs(errs)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
+
+       if err := validateV1(s.ServerNullableV11, tx); err != nil {
                return err
        }
 
-       rows, err := s.ReqInfo.Tx.Tx.Query("select cdn from profile where 
id=$1", s.ProfileID)
-       if err != nil {
-               log.Error.Printf("could not execute select cdnID from profile: 
%s\n", err)
-               errs = append(errs, tc.DBError)
-               return util.JoinErrs(errs)
+       // default boolean value is false
+       if s.IPIsService == nil {
+               s.IPIsService = new(bool)
        }
-       defer rows.Close()
-       var cdnID int
-       for rows.Next() {
-               if err := rows.Scan(&cdnID); err != nil {
-                       log.Error.Printf("could not scan cdnID from profile: 
%s\n", err)
-                       errs = append(errs, errors.New("associated profile must 
have a cdn associated"))
-                       return util.JoinErrs(errs)
-               }
+       if s.IP6IsService == nil {
+               s.IP6IsService = new(bool)
        }
-       log.Infof("got cdn id: %d from profile and cdn id: %d from server", 
cdnID, *s.CDNID)
-       if cdnID != *s.CDNID {
-               errs = append(errs, errors.New(fmt.Sprintf("CDN id '%d' for 
profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID)))
+
+       if !*s.IPIsService && !*s.IP6IsService {
+               errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
+       }
+
+       if *s.IPIsService && s.IPAddress == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
+       }
+
+       if *s.IP6IsService && s.IP6Address == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
        }
        return util.JoinErrs(errs)
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log 
message
-func (s TOServer) ChangeLogMessage(action string) (string, error) {
+func validateMTU(mtu interface{}) error {
+       m := mtu.(*uint64)
+       if m == nil {
+               return nil
+       }
 
-       var status string
-       if s.Status != nil {
-               status = *s.Status
+       if *m < 1280 {
+               return errors.New("must be at least 1280")
        }
+       return nil
+}
 
-       var hostName string
-       if s.HostName != nil {
-               hostName = *s.HostName
+func validateGateway(g interface{}) error {
+       if g == nil {
+               return nil
        }
 
-       var domainName string
-       if s.DomainName != nil {
-               domainName = *s.DomainName
+       if gtwy := net.ParseIP(*g.(*string)); gtwy == nil {
+               return errors.New("gateway not a valid IP address")
        }
+       return nil
+}
 
-       var serverID string
-       if s.ID != nil {
-               serverID = strconv.Itoa(*s.ID)
+func validateV3(s tc.ServerNullable, tx *sql.Tx) (string, error) {
+
+       if len(s.Interfaces) == 0 {
+               return "", errors.New("a server must have at least one 
interface")
        }
+       var errs []error
+       var serviceAddrV4Found bool
+       var serviceAddrV6Found bool
+       var serviceInterface string
+       for _, iface := range s.Interfaces {
+
+               ruleName := fmt.Sprintf("interface '%s' ", iface.Name)
+               errs = append(errs, tovalidate.ToErrors(validation.Errors{
+                       ruleName + "name":        
validation.Validate(iface.Name, validation.Required),
+                       ruleName + "mtu":         
validation.Validate(iface.MaxBandwidth, validation.By(validateMTU)),
+                       ruleName + "ipAddresses": 
validation.Validate(iface.IPAddresses, validation.Required),
+               })...)
 
-       message := action + ` ` + status + ` server: { "hostName":"` + hostName 
+ `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+               for _, addr := range iface.IPAddresses {
+                       ruleName += fmt.Sprintf("address '%s'", addr.Address)
+
+                       var parsedIP net.IP
+                       var err error
+                       if parsedIP, _, err = net.ParseCIDR(addr.Address); err 
!= nil {
+                               if parsedIP = net.ParseIP(addr.Address); 
parsedIP == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
address: %v", ruleName, err))
+                                       continue
+                               }
+                       }
+
+                       if addr.Gateway != nil {
+                               if gateway := net.ParseIP(*addr.Gateway); 
gateway == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
gateway: could not parse '%s' as a network gateway", ruleName, *addr.Gateway))
+                               } else if (gateway.To4() == nil && 
parsedIP.To4() != nil) || (gateway.To4() != nil && parsedIP.To4() == nil) {
+                                       errs = append(errs, 
errors.New(ruleName+": address family mismatch between address and gateway"))
+                               }
+                       }
+
+                       if addr.ServiceAddress {
+                               if serviceInterface != "" && serviceInterface 
!= iface.Name {
+                                       errs = append(errs, 
fmt.Errorf("interfaces: both %s and %s interfaces contain service addresses - 
only one service-address-containing-interface is allowed", serviceInterface, 
iface.Name))
+                               }
+                               serviceInterface = iface.Name
+                               if parsedIP.To4() != nil {
+                                       if serviceAddrV4Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv4 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV4Found = true
+                               } else {
+                                       if serviceAddrV6Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv6 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV6Found = true
+                               }
+                       }
+               }
+       }
 
-       return message, nil
+       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()
 
-       return returnable, nil, nil, http.StatusOK
+       servers := []tc.ServerNullable{}
+       servers, _, userErr, sysErr, errCode = getServers(inf.Params, inf.Tx, 
inf.User)
+
+       if len(servers) > 1 {
+               api.HandleDeprecatedErr(w, r, tx, 
http.StatusInternalServerError, nil, fmt.Errorf("ID '%d' matched more than one 
server (%d total)", inf.IntParams["id"], len(servers)), &alternative)
+               return
+       }
+
+       deprecationAlerts := api.CreateDeprecationAlerts(&alternative)
+
+       // No need to bother converting if there's no data
+       if len(servers) < 1 {
+               api.WriteAlertsObj(w, r, http.StatusOK, deprecationAlerts, 
servers)
+       }
+
+       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
+       if err := tx.QueryRow(unfilteredServersQuery).Scan(&unfiltered); err != 
nil {
+               return nil, 0, nil, fmt.Errorf("Failed to get servers count: 
%v", err), http.StatusInternalServerError

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict
                }
        }
 
-       current := TOServer{}
-       err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE 
sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+       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)
+               }
+       }
+
+       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))

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict
                }
        }
 
-       current := TOServer{}
-       err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE 
sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+       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)
+               }
+       }
+
+       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++
        }
 
-       return api.GenericUpdate(s)
+       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)
+       }
+
+       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
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
 
-func selectV20UpdatesQuery() string {
-       return `SELECT 
-sv.ip_address_is_service, 
-sv.ip6_address_is_service 
-FROM 
-       server sv`
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func selectQuery() string {
-       const JumboFrameBPS = 9000
-       return `SELECT
-cg.name as cachegroup,
-s.cachegroup as cachegroup_id,
-s.cdn_id,
-cdn.name as cdn_name,
-s.domain_name,
-s.guid,
-s.host_name,
-s.https_port,
-s.id,
-s.ilo_ip_address,
-s.ilo_ip_gateway,
-s.ilo_ip_netmask,
-s.ilo_password,
-s.ilo_username,
-COALESCE(s.interface_mtu, ` + strconv.Itoa(JumboFrameBPS) + `) as 
interface_mtu,
-s.interface_name,
-s.ip6_address,
-s.ip6_address_is_service,
-s.ip6_gateway,
-s.ip_address,
-s.ip_address_is_service,
-s.ip_gateway,
-s.ip_netmask,
-s.last_updated,
-s.mgmt_ip_address,
-s.mgmt_ip_gateway,
-s.mgmt_ip_netmask,
-s.offline_reason,
-pl.name as phys_location,
-s.phys_location as phys_location_id,
-p.name as profile,
-p.description as profile_desc,
-s.profile as profile_id,
-s.rack,
-s.reval_pending,
-s.router_host_name,
-s.router_port_name,
-st.name as status,
-s.status as status_id,
-s.tcp_port,
-t.name as server_type,
-s.type as server_type_id,
-s.upd_pending as upd_pending,
-s.xmpp_id,
-s.xmpp_passwd
-FROM
-  server s
-JOIN cachegroup cg ON s.cachegroup = cg.id
-JOIN cdn cdn ON s.cdn_id = cdn.id
-JOIN phys_location pl ON s.phys_location = pl.id
-JOIN profile p ON s.profile = p.id
-JOIN status st ON s.status = st.id
-JOIN type t ON s.type = t.id`
+func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+       var server tc.ServerNullableV2
+
+       tx := inf.Tx.Tx
+
+       if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       if err := validateV2(&server, tx); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       resultRows, err := inf.Tx.NamedQuery(insertQuery, server)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := 
resultRows.StructScan(&server.CommonServerProperties); err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("server create scanning: %v", err))
+                       return
+               }
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("server create: no server was inserted, no id was returned"))
+               return
+       } else if rowsAffected > 1 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("too many ids returned from server insert"))
+       }
+
+       ifaces, err := 
server.LegacyInterfaceDetails.ToInterfaces(*server.IPIsService, 
*server.IP6IsService)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+       }
+
+       if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, 
tx); err != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func insertQuery() string {
-       query := `INSERT INTO server (
-cachegroup,
-cdn_id,
-domain_name,
-host_name,
-https_port,
-ilo_ip_address,
-ilo_ip_netmask,
-ilo_ip_gateway,
-ilo_username,
-ilo_password,
-interface_mtu,
-interface_name,
-ip6_address,
-ip6_address_is_service,
-ip6_gateway,
-ip_address,
-ip_address_is_service,
-ip_netmask,
-ip_gateway,
-mgmt_ip_address,
-mgmt_ip_netmask,
-mgmt_ip_gateway,
-offline_reason,
-phys_location,
-profile,
-rack,
-router_host_name,
-router_port_name,
-status,
-tcp_port,
-type,
-upd_pending,
-xmpp_id,
-xmpp_passwd
-) VALUES (
-:cachegroup_id,
-:cdn_id,
-:domain_name,
-:host_name,
-:https_port,
-:ilo_ip_address,
-:ilo_ip_netmask,
-:ilo_ip_gateway,
-:ilo_username,
-:ilo_password,
-:interface_mtu,
-:interface_name,
-:ip6_address,
-:ip6_address_is_service,
-:ip6_gateway,
-:ip_address,
-:ip_address_is_service,
-:ip_netmask,
-:ip_gateway,
-:mgmt_ip_address,
-:mgmt_ip_netmask,
-:mgmt_ip_gateway,
-:offline_reason,
-:phys_location_id,
-:profile_id,
-:rack,
-:router_host_name,
-:router_port_name,
-:status_id,
-:tcp_port,
-:server_type_id,
-:upd_pending,
-:xmpp_id,
-:xmpp_passwd
-) RETURNING id,last_updated`
-       return query
+func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+       var server tc.ServerNullable
+
+       tx := inf.Tx.Tx
+
+       if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       serviceInterface, err := validateV3(server, tx)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       v2Server, err := server.ToServerV2()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+               return
+       }
+
+       v2Server.InterfaceName = &serviceInterface
+
+       resultRows, err := inf.Tx.NamedQuery(insertQuery, v2Server)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := 
resultRows.StructScan(&server.CommonServerProperties); err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("server create scanning: %v", err))
+                       return
+               }
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("server create: no server was inserted, no id was returned"))
+               return
+       } else if rowsAffected > 1 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("too many ids returned from server insert"))
+               return
+       }
+
+       userErr, sysErr, errCode := createInterfaces(*server.ID, 
server.Interfaces, tx)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func updateQuery() string {
-       query := `UPDATE
-server SET
-cachegroup=:cachegroup_id,
-cdn_id=:cdn_id,
-domain_name=:domain_name,
-host_name=:host_name,
-https_port=:https_port,
-ilo_ip_address=:ilo_ip_address,
-ilo_ip_netmask=:ilo_ip_netmask,
-ilo_ip_gateway=:ilo_ip_gateway,
-ilo_username=:ilo_username,
-ilo_password=:ilo_password,
-interface_mtu=:interface_mtu,
-interface_name=:interface_name,
-ip6_address=:ip6_address,
-ip6_address_is_service=:ip6_address_is_service,
-ip6_gateway=:ip6_gateway,
-ip_address=:ip_address,
-ip_address_is_service=:ip_address_is_service,
-ip_netmask=:ip_netmask,
-ip_gateway=:ip_gateway,
-mgmt_ip_address=:mgmt_ip_address,
-mgmt_ip_netmask=:mgmt_ip_netmask,
-mgmt_ip_gateway=:mgmt_ip_gateway,
-offline_reason=:offline_reason,
-phys_location=:phys_location_id,
-profile=:profile_id,
-rack=:rack,
-router_host_name=:router_host_name,
-router_port_name=:router_port_name,
-status=:status_id,
-tcp_port=:tcp_port,
-type=:server_type_id,
-upd_pending=:upd_pending,
-xmpp_id=:xmpp_id,
-xmpp_passwd=:xmpp_passwd
-WHERE id=:id RETURNING last_updated`
-       return query
+func Create(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       switch {
+       case inf.Version.Major <= 1:
+               createV1(inf, w, r)
+       case inf.Version.Major == 2:
+               createV2(inf, w, r)
+       default:
+               createV3(inf, w, r)
+       }
 }
 
-func deleteQuery() string {
-       return `DELETE FROM server WHERE id = :id`
+func Delete(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, 
[]string{"id"})
+       tx := inf.Tx.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       id := inf.IntParams["id"]
+
+       var servers []tc.ServerNullable
+       servers, _, userErr, sysErr, errCode = 
getServers(map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       if len(servers) < 1 {
+               api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("No 
server exists by id #%d", id), nil)

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -33,103 +35,360 @@ import (
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
        "github.com/apache/trafficcontrol/lib/go-util"
+
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
-       validation "github.com/go-ozzo/ozzo-validation"
+
+       "github.com/go-ozzo/ozzo-validation"
        "github.com/go-ozzo/ozzo-validation/is"
        "github.com/jmoiron/sqlx"
        "github.com/lib/pq"
 )
 
-// TOServer combines data about a server with metadata from an API request and
-// provides methods that implement several interfaces from the api package.
-type TOServer struct {
-       api.APIInfoImpl `json:"-"`
-       tc.ServerNullable
-}
-
-func (s *TOServer) SetLastUpdated(t tc.TimeNoMod) { s.LastUpdated = &t }
-func (*TOServer) InsertQuery() string             { return insertQuery() }
-func (*TOServer) UpdateQuery() string             { return updateQuery() }
-func (*TOServer) DeleteQuery() string             { return deleteQuery() }
-
-func (TOServer) GetKeyFieldsInfo() []api.KeyFieldInfo {
-       return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
-
-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 unfilteredServersQuery = `
+SELECT COUNT(server.id)
+FROM server
+`
 
-func (s *TOServer) SetKeys(keys map[string]interface{}) {
-       i, _ := keys["id"].(int) //this utilizes the non panicking type 
assertion, if the thrown away ok variable is false i will be the zero of the 
type, 0 here.
-       s.ID = &i
-}
+const 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) GetAuditName() string {
-       if s.DomainName != nil {
-               return *s.DomainName
-       }
-       if s.ID != nil {
-               return strconv.Itoa(*s.ID)
-       }
-       return "unknown"
-}
+const SelectInterfacesQuery = `
+SELECT (
+       ARRAY ( SELECT (
+               json_build_object (
+                       'ipAddresses',
+                       ARRAY (
+                               SELECT (
+                                       json_build_object (
+                                               'address', ip_address.address,
+                                               'gateway', ip_address.gateway,
+                                               'serviceAddress', 
ip_address.service_address
+                                       )
+                               )
+                               FROM ip_address
+                               WHERE ip_address.interface = interface.name
+                               AND ip_address.server = server.id
+                       ),
+                       'maxBandwidth', interface.max_bandwidth,
+                       'monitor', interface.monitor,
+                       'mtu', interface.mtu,
+                       'name', interface.name
+               )
+       )
+       FROM interface
+       WHERE interface.server = server.id
+)) AS interfaces,
+server.id
+FROM server
+WHERE server.id = ANY ($1)
+`
 
-func (s *TOServer) GetType() string {
-       return "server"
-}
+const insertQuery = `
+INSERT INTO server (
+       cachegroup,
+       cdn_id,
+       domain_name,
+       host_name,
+       https_port,
+       ilo_ip_address,
+       ilo_ip_netmask,
+       ilo_ip_gateway,
+       ilo_username,
+       ilo_password,
+       interface_name,
+       mgmt_ip_address,
+       mgmt_ip_netmask,
+       mgmt_ip_gateway,
+       offline_reason,
+       phys_location,
+       profile,
+       rack,
+       router_host_name,
+       router_port_name,
+       status,
+       tcp_port,
+       type,
+       upd_pending,
+       xmpp_id,
+       xmpp_passwd
+) VALUES (
+       :cachegroup_id,
+       :cdn_id,
+       :domain_name,
+       :host_name,
+       :https_port,
+       :ilo_ip_address,
+       :ilo_ip_netmask,
+       :ilo_ip_gateway,
+       :ilo_username,
+       :ilo_password,
+       :interface_name,
+       :mgmt_ip_address,
+       :mgmt_ip_netmask,
+       :mgmt_ip_gateway,
+       :offline_reason,
+       :phys_location_id,
+       :profile_id,
+       :rack,
+       :router_host_name,
+       :router_port_name,
+       :status_id,
+       :tcp_port,
+       :server_type_id,
+       :upd_pending,
+       :xmpp_id,
+       :xmpp_passwd
+) RETURNING
+       (SELECT name FROM cachegroup WHERE cachegroup.id=server.cachegroup) AS 
cachegroup,
+       cachegroup AS cachegroup_id,
+       cdn_id,
+       (SELECT name FROM cdn WHERE cdn.id=server.cdn_id) AS cdn_name,
+       domain_name,
+       guid,
+       host_name,
+       https_port,
+       id,
+       ilo_ip_address,
+       ilo_ip_gateway,
+       ilo_ip_netmask,
+       ilo_password,
+       ilo_username,
+       last_updated,
+       mgmt_ip_address,
+       mgmt_ip_gateway,
+       mgmt_ip_netmask,
+       offline_reason,
+       (SELECT name FROM phys_location WHERE 
phys_location.id=server.phys_location) AS phys_location,
+       phys_location AS phys_location_id,
+       profile AS profile_id,
+       (SELECT description FROM profile WHERE profile.id=server.profile) AS 
profile_desc,
+       (SELECT name FROM profile WHERE profile.id=server.profile) AS profile,
+       rack,
+       reval_pending,
+       router_host_name,
+       router_port_name,
+       (SELECT name FROM status WHERE status.id=server.status) AS status,
+       status AS status_id,
+       tcp_port,
+       (SELECT name FROM type WHERE type.id=server.type) AS server_type,
+       type AS server_type_id,
+       upd_pending
+`
 
-func (s *TOServer) Sanitize() {
-       if s.IP6Address != nil && *s.IP6Address == "" {
-               s.IP6Address = nil
-       }
-}
+const insertInterfaceQuery = `
+INSERT INTO interface (
+       max_bandwidth,
+       monitor,
+       mtu,
+       name,
+       server
+) VALUES (
+       $1,
+       $2,
+       $3,
+       $4,
+       $5
+)
+`
 
-func (s *TOServer) Validate() error {
-       s.Sanitize()
-       version := s.APIInfo().Version
-       noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot 
contain spaces")
+const insertIPQuery = `

Review comment:
       Looks unused?

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +398,224 @@ 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)...)
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       return util.JoinErrs(errs)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
+
+       if err := validateV1(s.ServerNullableV11, tx); err != nil {
                return err
        }
 
-       rows, err := s.ReqInfo.Tx.Tx.Query("select cdn from profile where 
id=$1", s.ProfileID)
-       if err != nil {
-               log.Error.Printf("could not execute select cdnID from profile: 
%s\n", err)
-               errs = append(errs, tc.DBError)
-               return util.JoinErrs(errs)
+       // default boolean value is false
+       if s.IPIsService == nil {
+               s.IPIsService = new(bool)
        }
-       defer rows.Close()
-       var cdnID int
-       for rows.Next() {
-               if err := rows.Scan(&cdnID); err != nil {
-                       log.Error.Printf("could not scan cdnID from profile: 
%s\n", err)
-                       errs = append(errs, errors.New("associated profile must 
have a cdn associated"))
-                       return util.JoinErrs(errs)
-               }
+       if s.IP6IsService == nil {
+               s.IP6IsService = new(bool)
        }
-       log.Infof("got cdn id: %d from profile and cdn id: %d from server", 
cdnID, *s.CDNID)
-       if cdnID != *s.CDNID {
-               errs = append(errs, errors.New(fmt.Sprintf("CDN id '%d' for 
profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID)))
+
+       if !*s.IPIsService && !*s.IP6IsService {
+               errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
+       }
+
+       if *s.IPIsService && s.IPAddress == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
+       }
+
+       if *s.IP6IsService && s.IP6Address == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
        }
        return util.JoinErrs(errs)
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log 
message
-func (s TOServer) ChangeLogMessage(action string) (string, error) {
+func validateMTU(mtu interface{}) error {
+       m := mtu.(*uint64)
+       if m == nil {
+               return nil
+       }
 
-       var status string
-       if s.Status != nil {
-               status = *s.Status
+       if *m < 1280 {
+               return errors.New("must be at least 1280")
        }
+       return nil
+}
 
-       var hostName string
-       if s.HostName != nil {
-               hostName = *s.HostName
+func validateGateway(g interface{}) error {
+       if g == nil {
+               return nil
        }
 
-       var domainName string
-       if s.DomainName != nil {
-               domainName = *s.DomainName
+       if gtwy := net.ParseIP(*g.(*string)); gtwy == nil {
+               return errors.New("gateway not a valid IP address")
        }
+       return nil
+}
 
-       var serverID string
-       if s.ID != nil {
-               serverID = strconv.Itoa(*s.ID)
+func validateV3(s tc.ServerNullable, tx *sql.Tx) (string, error) {
+
+       if len(s.Interfaces) == 0 {
+               return "", errors.New("a server must have at least one 
interface")
        }
+       var errs []error
+       var serviceAddrV4Found bool
+       var serviceAddrV6Found bool
+       var serviceInterface string
+       for _, iface := range s.Interfaces {
+
+               ruleName := fmt.Sprintf("interface '%s' ", iface.Name)
+               errs = append(errs, tovalidate.ToErrors(validation.Errors{
+                       ruleName + "name":        
validation.Validate(iface.Name, validation.Required),
+                       ruleName + "mtu":         
validation.Validate(iface.MaxBandwidth, validation.By(validateMTU)),
+                       ruleName + "ipAddresses": 
validation.Validate(iface.IPAddresses, validation.Required),
+               })...)
 
-       message := action + ` ` + status + ` server: { "hostName":"` + hostName 
+ `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+               for _, addr := range iface.IPAddresses {
+                       ruleName += fmt.Sprintf("address '%s'", addr.Address)
+
+                       var parsedIP net.IP
+                       var err error
+                       if parsedIP, _, err = net.ParseCIDR(addr.Address); err 
!= nil {
+                               if parsedIP = net.ParseIP(addr.Address); 
parsedIP == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
address: %v", ruleName, err))
+                                       continue
+                               }
+                       }
+
+                       if addr.Gateway != nil {
+                               if gateway := net.ParseIP(*addr.Gateway); 
gateway == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
gateway: could not parse '%s' as a network gateway", ruleName, *addr.Gateway))
+                               } else if (gateway.To4() == nil && 
parsedIP.To4() != nil) || (gateway.To4() != nil && parsedIP.To4() == nil) {
+                                       errs = append(errs, 
errors.New(ruleName+": address family mismatch between address and gateway"))
+                               }
+                       }
+
+                       if addr.ServiceAddress {
+                               if serviceInterface != "" && serviceInterface 
!= iface.Name {
+                                       errs = append(errs, 
fmt.Errorf("interfaces: both %s and %s interfaces contain service addresses - 
only one service-address-containing-interface is allowed", serviceInterface, 
iface.Name))
+                               }
+                               serviceInterface = iface.Name
+                               if parsedIP.To4() != nil {
+                                       if serviceAddrV4Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv4 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV4Found = true
+                               } else {
+                                       if serviceAddrV6Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv6 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV6Found = true
+                               }
+                       }
+               }
+       }
 
-       return message, nil
+       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))

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -248,71 +637,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

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict
                }
        }
 
-       current := TOServer{}
-       err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE 
sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+       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)
+               }
+       }
+
+       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))

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -139,94 +398,224 @@ 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)...)
 
-       if _, err := tc.ValidateTypeID(s.ReqInfo.Tx.Tx, s.TypeID, "server"); 
err != nil {
+       return util.JoinErrs(errs)
+}
+
+func validateV2(s *tc.ServerNullableV2, tx *sql.Tx) error {
+       var errs []error
+
+       if err := validateV1(s.ServerNullableV11, tx); err != nil {
                return err
        }
 
-       rows, err := s.ReqInfo.Tx.Tx.Query("select cdn from profile where 
id=$1", s.ProfileID)
-       if err != nil {
-               log.Error.Printf("could not execute select cdnID from profile: 
%s\n", err)
-               errs = append(errs, tc.DBError)
-               return util.JoinErrs(errs)
+       // default boolean value is false
+       if s.IPIsService == nil {
+               s.IPIsService = new(bool)
        }
-       defer rows.Close()
-       var cdnID int
-       for rows.Next() {
-               if err := rows.Scan(&cdnID); err != nil {
-                       log.Error.Printf("could not scan cdnID from profile: 
%s\n", err)
-                       errs = append(errs, errors.New("associated profile must 
have a cdn associated"))
-                       return util.JoinErrs(errs)
-               }
+       if s.IP6IsService == nil {
+               s.IP6IsService = new(bool)
        }
-       log.Infof("got cdn id: %d from profile and cdn id: %d from server", 
cdnID, *s.CDNID)
-       if cdnID != *s.CDNID {
-               errs = append(errs, errors.New(fmt.Sprintf("CDN id '%d' for 
profile '%d' does not match Server CDN '%d'", cdnID, *s.ProfileID, *s.CDNID)))
+
+       if !*s.IPIsService && !*s.IP6IsService {
+               errs = append(errs, tc.NeedsAtLeastOneServiceAddressError)
+       }
+
+       if *s.IPIsService && s.IPAddress == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
+       }
+
+       if *s.IP6IsService && s.IP6Address == nil {
+               errs = append(errs, tc.EmptyAddressCannotBeAServiceAddressError)
        }
        return util.JoinErrs(errs)
 }
 
-// ChangeLogMessage implements the api.ChangeLogger interface for a custom log 
message
-func (s TOServer) ChangeLogMessage(action string) (string, error) {
+func validateMTU(mtu interface{}) error {
+       m := mtu.(*uint64)
+       if m == nil {
+               return nil
+       }
 
-       var status string
-       if s.Status != nil {
-               status = *s.Status
+       if *m < 1280 {
+               return errors.New("must be at least 1280")
        }
+       return nil
+}
 
-       var hostName string
-       if s.HostName != nil {
-               hostName = *s.HostName
+func validateGateway(g interface{}) error {
+       if g == nil {
+               return nil
        }
 
-       var domainName string
-       if s.DomainName != nil {
-               domainName = *s.DomainName
+       if gtwy := net.ParseIP(*g.(*string)); gtwy == nil {
+               return errors.New("gateway not a valid IP address")
        }
+       return nil
+}
 
-       var serverID string
-       if s.ID != nil {
-               serverID = strconv.Itoa(*s.ID)
+func validateV3(s tc.ServerNullable, tx *sql.Tx) (string, error) {
+
+       if len(s.Interfaces) == 0 {
+               return "", errors.New("a server must have at least one 
interface")
        }
+       var errs []error
+       var serviceAddrV4Found bool
+       var serviceAddrV6Found bool
+       var serviceInterface string
+       for _, iface := range s.Interfaces {
+
+               ruleName := fmt.Sprintf("interface '%s' ", iface.Name)
+               errs = append(errs, tovalidate.ToErrors(validation.Errors{
+                       ruleName + "name":        
validation.Validate(iface.Name, validation.Required),
+                       ruleName + "mtu":         
validation.Validate(iface.MaxBandwidth, validation.By(validateMTU)),
+                       ruleName + "ipAddresses": 
validation.Validate(iface.IPAddresses, validation.Required),
+               })...)
 
-       message := action + ` ` + status + ` server: { "hostName":"` + hostName 
+ `", "domainName":"` + domainName + `", id:` + serverID + ` }`
+               for _, addr := range iface.IPAddresses {
+                       ruleName += fmt.Sprintf("address '%s'", addr.Address)
+
+                       var parsedIP net.IP
+                       var err error
+                       if parsedIP, _, err = net.ParseCIDR(addr.Address); err 
!= nil {
+                               if parsedIP = net.ParseIP(addr.Address); 
parsedIP == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
address: %v", ruleName, err))
+                                       continue
+                               }
+                       }
+
+                       if addr.Gateway != nil {
+                               if gateway := net.ParseIP(*addr.Gateway); 
gateway == nil {
+                                       errs = append(errs, fmt.Errorf("%s: 
gateway: could not parse '%s' as a network gateway", ruleName, *addr.Gateway))
+                               } else if (gateway.To4() == nil && 
parsedIP.To4() != nil) || (gateway.To4() != nil && parsedIP.To4() == nil) {
+                                       errs = append(errs, 
errors.New(ruleName+": address family mismatch between address and gateway"))
+                               }
+                       }
+
+                       if addr.ServiceAddress {
+                               if serviceInterface != "" && serviceInterface 
!= iface.Name {
+                                       errs = append(errs, 
fmt.Errorf("interfaces: both %s and %s interfaces contain service addresses - 
only one service-address-containing-interface is allowed", serviceInterface, 
iface.Name))
+                               }
+                               serviceInterface = iface.Name
+                               if parsedIP.To4() != nil {
+                                       if serviceAddrV4Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv4 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV4Found = true
+                               } else {
+                                       if serviceAddrV6Found {
+                                               errs = append(errs, 
fmt.Errorf("interfaces: address '%s' of interface '%s' is marked as a service 
address, but an IPv6 service address appears earlier in the list", 
addr.Address, iface.Name))
+                                       }
+                                       serviceAddrV6Found = true
+                               }
+                       }
+               }
+       }
 
-       return message, nil
+       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))

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict
                }
        }
 
-       current := TOServer{}
-       err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE 
sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+       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)
+               }
+       }
+
+       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))

Review comment:
       Error string starts with capital

##########
File path: traffic_ops/traffic_ops_golang/server/servers.go
##########
@@ -359,261 +780,478 @@ 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
-       }
-
+func checkTypeChangeSafety(server tc.CommonServerProperties, tx *sqlx.Tx) 
(error, error, int) {
        // see if type changed
-       typeID := -1
-       if err := s.APIInfo().Tx.QueryRow("SELECT type FROM server WHERE id = 
$1", s.ID).Scan(&typeID); err != nil {
+       var typeID int
+       if err := tx.QueryRow("SELECT type FROM server WHERE id = $1", 
*server.ID).Scan(&typeID); 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
        }
 
        // If type is changing ensure it isn't assigned to any DSes.
-       if typeID != *s.TypeID {
+       if typeID != *server.TypeID {
                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 {
+               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 len(dsIDs) != 0 {
-                       return errors.New("server type can not be updated when 
it is currently assigned to delivery services"), nil, http.StatusConflict
+                       return errors.New("Server type can not be updated when 
it is currently assigned to Delivery Services"), nil, http.StatusConflict
                }
        }
 
-       current := TOServer{}
-       err := s.ReqInfo.Tx.QueryRowx(selectV20UpdatesQuery()+` WHERE 
sv.id=$1`, strconv.Itoa(*s.ID)).StructScan(&current)
+       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)
+               }
+       }
+
+       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++
        }
 
-       return api.GenericUpdate(s)
+       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)
+       }
+
+       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
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
 
-func selectV20UpdatesQuery() string {
-       return `SELECT 
-sv.ip_address_is_service, 
-sv.ip6_address_is_service 
-FROM 
-       server sv`
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func selectQuery() string {
-       const JumboFrameBPS = 9000
-       return `SELECT
-cg.name as cachegroup,
-s.cachegroup as cachegroup_id,
-s.cdn_id,
-cdn.name as cdn_name,
-s.domain_name,
-s.guid,
-s.host_name,
-s.https_port,
-s.id,
-s.ilo_ip_address,
-s.ilo_ip_gateway,
-s.ilo_ip_netmask,
-s.ilo_password,
-s.ilo_username,
-COALESCE(s.interface_mtu, ` + strconv.Itoa(JumboFrameBPS) + `) as 
interface_mtu,
-s.interface_name,
-s.ip6_address,
-s.ip6_address_is_service,
-s.ip6_gateway,
-s.ip_address,
-s.ip_address_is_service,
-s.ip_gateway,
-s.ip_netmask,
-s.last_updated,
-s.mgmt_ip_address,
-s.mgmt_ip_gateway,
-s.mgmt_ip_netmask,
-s.offline_reason,
-pl.name as phys_location,
-s.phys_location as phys_location_id,
-p.name as profile,
-p.description as profile_desc,
-s.profile as profile_id,
-s.rack,
-s.reval_pending,
-s.router_host_name,
-s.router_port_name,
-st.name as status,
-s.status as status_id,
-s.tcp_port,
-t.name as server_type,
-s.type as server_type_id,
-s.upd_pending as upd_pending,
-s.xmpp_id,
-s.xmpp_passwd
-FROM
-  server s
-JOIN cachegroup cg ON s.cachegroup = cg.id
-JOIN cdn cdn ON s.cdn_id = cdn.id
-JOIN phys_location pl ON s.phys_location = pl.id
-JOIN profile p ON s.profile = p.id
-JOIN status st ON s.status = st.id
-JOIN type t ON s.type = t.id`
+func createV2(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+       var server tc.ServerNullableV2
+
+       tx := inf.Tx.Tx
+
+       if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       if err := validateV2(&server, tx); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       resultRows, err := inf.Tx.NamedQuery(insertQuery, server)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := 
resultRows.StructScan(&server.CommonServerProperties); err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("server create scanning: %v", err))
+                       return
+               }
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("server create: no server was inserted, no id was returned"))
+               return
+       } else if rowsAffected > 1 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("too many ids returned from server insert"))
+       }
+
+       ifaces, err := 
server.LegacyInterfaceDetails.ToInterfaces(*server.IPIsService, 
*server.IP6IsService)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+       }
+
+       if userErr, sysErr, errCode := createInterfaces(*server.ID, ifaces, 
tx); err != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func insertQuery() string {
-       query := `INSERT INTO server (
-cachegroup,
-cdn_id,
-domain_name,
-host_name,
-https_port,
-ilo_ip_address,
-ilo_ip_netmask,
-ilo_ip_gateway,
-ilo_username,
-ilo_password,
-interface_mtu,
-interface_name,
-ip6_address,
-ip6_address_is_service,
-ip6_gateway,
-ip_address,
-ip_address_is_service,
-ip_netmask,
-ip_gateway,
-mgmt_ip_address,
-mgmt_ip_netmask,
-mgmt_ip_gateway,
-offline_reason,
-phys_location,
-profile,
-rack,
-router_host_name,
-router_port_name,
-status,
-tcp_port,
-type,
-upd_pending,
-xmpp_id,
-xmpp_passwd
-) VALUES (
-:cachegroup_id,
-:cdn_id,
-:domain_name,
-:host_name,
-:https_port,
-:ilo_ip_address,
-:ilo_ip_netmask,
-:ilo_ip_gateway,
-:ilo_username,
-:ilo_password,
-:interface_mtu,
-:interface_name,
-:ip6_address,
-:ip6_address_is_service,
-:ip6_gateway,
-:ip_address,
-:ip_address_is_service,
-:ip_netmask,
-:ip_gateway,
-:mgmt_ip_address,
-:mgmt_ip_netmask,
-:mgmt_ip_gateway,
-:offline_reason,
-:phys_location_id,
-:profile_id,
-:rack,
-:router_host_name,
-:router_port_name,
-:status_id,
-:tcp_port,
-:server_type_id,
-:upd_pending,
-:xmpp_id,
-:xmpp_passwd
-) RETURNING id,last_updated`
-       return query
+func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
+       var server tc.ServerNullable
+
+       tx := inf.Tx.Tx
+
+       if err := json.NewDecoder(r.Body).Decode(&server); err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       serviceInterface, err := validateV3(server, tx)
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil)
+               return
+       }
+
+       v2Server, err := server.ToServerV2()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
err)
+               return
+       }
+
+       v2Server.InterfaceName = &serviceInterface
+
+       resultRows, err := inf.Tx.NamedQuery(insertQuery, v2Server)
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer resultRows.Close()
+
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := 
resultRows.StructScan(&server.CommonServerProperties); err != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("server create scanning: %v", err))
+                       return
+               }
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("server create: no server was inserted, no id was returned"))
+               return
+       } else if rowsAffected > 1 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
errors.New("too many ids returned from server insert"))
+               return
+       }
+
+       userErr, sysErr, errCode := createInterfaces(*server.ID, 
server.Interfaces, tx)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, server)
+
+       changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", 
*server.HostName, *server.DomainName, *server.ID)
+       api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
 }
 
-func updateQuery() string {
-       query := `UPDATE
-server SET
-cachegroup=:cachegroup_id,
-cdn_id=:cdn_id,
-domain_name=:domain_name,
-host_name=:host_name,
-https_port=:https_port,
-ilo_ip_address=:ilo_ip_address,
-ilo_ip_netmask=:ilo_ip_netmask,
-ilo_ip_gateway=:ilo_ip_gateway,
-ilo_username=:ilo_username,
-ilo_password=:ilo_password,
-interface_mtu=:interface_mtu,
-interface_name=:interface_name,
-ip6_address=:ip6_address,
-ip6_address_is_service=:ip6_address_is_service,
-ip6_gateway=:ip6_gateway,
-ip_address=:ip_address,
-ip_address_is_service=:ip_address_is_service,
-ip_netmask=:ip_netmask,
-ip_gateway=:ip_gateway,
-mgmt_ip_address=:mgmt_ip_address,
-mgmt_ip_netmask=:mgmt_ip_netmask,
-mgmt_ip_gateway=:mgmt_ip_gateway,
-offline_reason=:offline_reason,
-phys_location=:phys_location_id,
-profile=:profile_id,
-rack=:rack,
-router_host_name=:router_host_name,
-router_port_name=:router_port_name,
-status=:status_id,
-tcp_port=:tcp_port,
-type=:server_type_id,
-upd_pending=:upd_pending,
-xmpp_id=:xmpp_id,
-xmpp_passwd=:xmpp_passwd
-WHERE id=:id RETURNING last_updated`
-       return query
+func Create(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       switch {
+       case inf.Version.Major <= 1:
+               createV1(inf, w, r)
+       case inf.Version.Major == 2:
+               createV2(inf, w, r)
+       default:
+               createV3(inf, w, r)
+       }
 }
 
-func deleteQuery() string {
-       return `DELETE FROM server WHERE id = :id`
+func Delete(w http.ResponseWriter, r *http.Request) {
+       inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, 
[]string{"id"})
+       tx := inf.Tx.Tx
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       defer inf.Close()
+
+       id := inf.IntParams["id"]
+
+       var servers []tc.ServerNullable
+       servers, _, userErr, sysErr, errCode = 
getServers(map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User)
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       if len(servers) < 1 {
+               api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("No 
server exists by id #%d", id), nil)
+               return
+       }
+       if len(servers) > 1 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("There are somehow two servers with id %d - cannot delete", id))

Review comment:
       Error string starts with capital




----------------------------------------------------------------
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