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

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


The following commit(s) were added to refs/heads/master by this push:
     new 225f91c5fe Layered Server Feature for `GET` API endpoint 
`servers/details` (#6593)
225f91c5fe is described below

commit 225f91c5febe8155ed01cb36b501c213de420ad8
Author: Rima Shah <[email protected]>
AuthorDate: Tue Apr 26 13:24:55 2022 -0600

    Layered Server Feature for `GET` API endpoint `servers/details` (#6593)
    
    * Updated server struct and type for V4 and added commonpropertiesV40 
function and struct
    
    * Added db migration files
    
    * Added backward compatibility to get (GET) and update (PUT) /server
    
    * Updated docs for GET /server and PUT /server/{id}
    
    * Updated migration script, doc, /server API endpoint for GET and PUT
    
    * Updated query for profile_names
    
    * Updated profile_names type
    
    * Passing in the original transaction id
    
    * Update servers.go for PUT servers/{id} API
    
    * Moved function having db transactions to dbhelpers.
    
    * Fixed go vet
    
    * Fixed testing/api/v4 test
    
    * Updated cache config and lib test
    
    * Fixed fmt to log
    
    * Fixed server profile to profiles
    
    * Updated docs and tests(cache-config).
    
    * Updated tests to accept first profile to ensure current tests pass.
    
    * Updated tests to accept first profile to ensure current tests pass-1.
    
    * Added changelog entry
    
    * Fixed unit tests
    
    * Removed fmt statement.
    
    * Tc_fixture updated for profiles (v4).
    
    * Added logic for POST servers/
    
    * Updated server_profile foreign key constraint for delete servers/{id} API
    
    * Updated db_helper functions to return err
    
    * Updated parentdotconfig.go and server.go for profile
    
    * Fixed test related to profile
    
    * Updated API/v4 test related to profile.
    Updated tc_fixtures tp remove duplicate interface ip.
    
    * Removed extra if clause.
    
    * Removed database transaction from api/v4/ tests
    
    * Updated changelog and server params
    
    * Removed fmt statements
    
    * updated select statement for v2/v3 and added logic to fill in 
server_profile table for v2/v3 when creating a server
    
    * replaced profileId with profileNames in traffic portal data
    
    * replaced profileId with profileNames in traffic portal data-1
    
    * Updated ServerDetail struct and getDetailServers() to handle array of 
profile_names for V4
    
    * Removed changes from checkTypeChangeSafety() in servers.go
    
    * Updated unit test for servers_test.go and added multiple profiles to one 
of the servers in v4/tc_fixtures
    
    * Changed enroller server_template.json for CIAB
    
    * Traffic Portal changes
    
    * Removed db_helpers function from cache-config
    
    * Updated TP server's integration test
    
    * Removed get profile name request by profile ID since we already have a 
profile name.
    
    * updated profile field in server
    
    * updated unit test
    
    * updated error message in validatedCommonV40 and server integration tests
    
    * updated API.ts to add random characters for profileNames
    
    * updated APIv4 docs
    
    * Made profile-names singular to match current documentation.
    
    * Update based on review comments.
    
    * Changed Profiles field in go struct to ProfileNames to match JSON.
    
    * Fixed unit test.
    
    * Changed ProfileNames field type in struct ServerV40 to []string from 
pq.StringArray
    
    * Removed additional code from TP.
    
    * Updated code and queries for PUT and POST call.
    
    * Updated function call param
    
    * Changed CommonServerPropertiesV40 field ProfileNames from a pointer of 
string slice to a slice of string. Created new migration file to make them as 
latest date.
    
    * Created new migration file to make them as latest date.
    
    * Updated conversion.go
    
    * Fixed queries and scan() to correct GH failures.
    
    * Updated profile to a []string from a string and removed profileDesc from 
ServerDetailsV40
    
    * Added error check and renamed Profiles back to Profile for V3/V2
    
    * Added changelog entry and updated doc
    
    * removed incorrect migration file and revert changes in v4/tc_fixtures.json
    
    * removed ServerDetailsV40{}.
    
    * updated based on review comments.
---
 CHANGELOG.md                                       |  1 +
 docs/source/api/v4/servers_details.rst             |  6 +--
 lib/go-tc/servers.go                               | 29 ++++++++++++-
 traffic_ops/testing/api/v4/tc-fixtures.json        | 32 +++++++--------
 .../traffic_ops_golang/dbhelpers/db_helpers.go     | 37 +++++++++++++++++
 traffic_ops/traffic_ops_golang/server/detail.go    | 48 +++++++++++++++++++---
 .../traffic_ops_golang/server/detail_test.go       | 16 +++-----
 7 files changed, 132 insertions(+), 37 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aaee21caa..043163cbf3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - [Traffic Ops | Traffic Go Clients | T3C] Add additional timestamp fields to 
server for queuing and dequeueing config and revalidate updates.
 - Added layered profile feature to 4.0 for `GET` /servers/, `POST` /servers/, 
`PUT` /servers/{id} and `DELETE` /servers/{id}.
 - Added a Traffic Ops endpoint and Traffic Portal page to view all CDNi 
configuration update requests and approve or deny.
+- Added layered profile feature to 4.0 for `GET` /servers/details.
 - Added layered profile feature to 4.0 for `GET` 
/deliveryservices/{id}/servers/ and /deliveryservices/{id}/servers/eligible.
 
 ### Fixed
diff --git a/docs/source/api/v4/servers_details.rst 
b/docs/source/api/v4/servers_details.rst
index afc95ed6ae..dc0a917b50 100644
--- a/docs/source/api/v4/servers_details.rst
+++ b/docs/source/api/v4/servers_details.rst
@@ -98,8 +98,7 @@ Response Structure
        :mgmtIpNetmask:  The IPv4 subnet mask of the server's management port
        :offlineReason:         A user-entered reason why the server is in 
ADMIN_DOWN or OFFLINE status
        :physLocation:          The name of the physical location where the 
server resides
-       :profile:               The :ref:`profile-name` of the :term:`Profile` 
used by this server
-       :profileDesc:           A :ref:`profile-description` of the 
:term:`Profile` used by this server
+       :profileNames:          List of :ref:`profile-name` of the 
:term:`Profiles` used by this server
        :rack:  A string indicating "server rack" location
        :status:                The status of the server
 
@@ -157,8 +156,7 @@ Response Structure
                                "mgmtIpNetmask": "",
                                "offlineReason": "",
                                "physLocation": "Apachecon North America 2018",
-                               "profile": "ATS_EDGE_TIER_CACHE",
-                               "profileDesc": "Edge Cache - Apache Traffic 
Server",
+                               "profileNames": ["ATS_EDGE_TIER_CACHE"],
                                "rack": "",
                                "status": "REPORTED",
                                "tcpPort": 80,
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index 23971316b5..2f435cdb3f 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -87,8 +87,33 @@ type ServerDetailV30 struct {
 
 // ServerDetailV40 is the details for a server for API v4.
 type ServerDetailV40 struct {
-       ServerDetail
-       ServerInterfaces []ServerInterfaceInfoV40 `json:"interfaces"`
+       CacheGroup         *string                  `json:"cachegroup" 
db:"cachegroup"`
+       CDNName            *string                  `json:"cdnName" 
db:"cdn_name"`
+       DeliveryServiceIDs []int64                  
`json:"deliveryservices,omitempty"`
+       DomainName         *string                  `json:"domainName" 
db:"domain_name"`
+       GUID               *string                  `json:"guid" db:"guid"`
+       HardwareInfo       map[string]string        `json:"hardwareInfo"`
+       HostName           *string                  `json:"hostName" 
db:"host_name"`
+       HTTPSPort          *int                     `json:"httpsPort" 
db:"https_port"`
+       ID                 *int                     `json:"id" db:"id"`
+       ILOIPAddress       *string                  `json:"iloIpAddress" 
db:"ilo_ip_address"`
+       ILOIPGateway       *string                  `json:"iloIpGateway" 
db:"ilo_ip_gateway"`
+       ILOIPNetmask       *string                  `json:"iloIpNetmask" 
db:"ilo_ip_netmask"`
+       ILOPassword        *string                  `json:"iloPassword" 
db:"ilo_password"`
+       ILOUsername        *string                  `json:"iloUsername" 
db:"ilo_username"`
+       MgmtIPAddress      *string                  `json:"mgmtIpAddress" 
db:"mgmt_ip_address"`
+       MgmtIPGateway      *string                  `json:"mgmtIpGateway" 
db:"mgmt_ip_gateway"`
+       MgmtIPNetmask      *string                  `json:"mgmtIpNetmask" 
db:"mgmt_ip_netmask"`
+       OfflineReason      *string                  `json:"offlineReason" 
db:"offline_reason"`
+       PhysLocation       *string                  `json:"physLocation" 
db:"phys_location"`
+       ProfileNames       []string                 `json:"profileNames" 
db:"profile_name"`
+       Rack               *string                  `json:"rack" db:"rack"`
+       Status             *string                  `json:"status" db:"status"`
+       TCPPort            *int                     `json:"tcpPort" 
db:"tcp_port"`
+       Type               string                   `json:"type" 
db:"server_type"`
+       XMPPID             *string                  `json:"xmppId" db:"xmpp_id"`
+       XMPPPasswd         *string                  `json:"xmppPasswd" 
db:"xmpp_passwd"`
+       ServerInterfaces   []ServerInterfaceInfoV40 `json:"interfaces"`
 }
 
 // ServersV1DetailResponse is the JSON object returned for a single server for 
v1.
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json 
b/traffic_ops/testing/api/v4/tc-fixtures.json
index 011a1a8042..ebf1fab2aa 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -5009,8 +5009,8 @@
         {
             "cachegroup": "cachegroup1",
             "cdnName": "cdn1",
-                       "configUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "configApplyTime": "1969-12-31T17:00:00-07:00",
+            "configUpdateTime": "2022-01-01T17:00:00-07:00",
+            "configApplyTime": "1969-12-31T17:00:00-07:00",
             "domainName": "ga.atlanta.kabletown.net",
             "guid": null,
             "hostName": "config-update-time",
@@ -5060,8 +5060,8 @@
         {
             "cachegroup": "cachegroup1",
             "cdnName": "cdn1",
-                       "configUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "configApplyTime": "1969-12-31T17:00:00-07:00",
+            "configUpdateTime": "2022-01-01T17:00:00-07:00",
+            "configApplyTime": "1969-12-31T17:00:00-07:00",
             "domainName": "ga.atlanta.kabletown.net",
             "guid": null,
             "hostName": "config-update-time-no-updpend",
@@ -5149,8 +5149,8 @@
             "profileNames": ["EDGE1"],
             "rack": "RR 119.02",
             "revalPending": false,
-                       "revalUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "revalUpdateTime": "2022-01-01T17:00:00-07:00",
+            "revalApplyTime": "1969-12-31T17:00:00-07:00",
             "status": "REPORTED",
             "tcpPort": 80,
             "type": "EDGE",
@@ -5199,8 +5199,8 @@
             "physLocation": "Denver",
             "profileNames": ["EDGE1"],
             "rack": "RR 119.02",
-                       "revalUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "revalUpdateTime": "2022-01-01T17:00:00-07:00",
+            "revalApplyTime": "1969-12-31T17:00:00-07:00",
             "status": "REPORTED",
             "tcpPort": 80,
             "type": "EDGE",
@@ -5211,8 +5211,8 @@
         {
             "cachegroup": "cachegroup1",
             "cdnName": "cdn1",
-                       "configUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "configApplyTime": "1969-12-31T17:00:00-07:00",
+            "configUpdateTime": "2022-01-01T17:00:00-07:00",
+            "configApplyTime": "1969-12-31T17:00:00-07:00",
             "domainName": "ga.atlanta.kabletown.net",
             "guid": null,
             "hostName": "config-reval-update-time",
@@ -5252,8 +5252,8 @@
             "profileNames": ["EDGE1"],
             "rack": "RR 119.02",
             "revalPending": false,
-                       "revalUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "revalUpdateTime": "2022-01-01T17:00:00-07:00",
+            "revalApplyTime": "1969-12-31T17:00:00-07:00",
             "status": "REPORTED",
             "tcpPort": 80,
             "type": "EDGE",
@@ -5264,8 +5264,8 @@
         {
             "cachegroup": "cachegroup1",
             "cdnName": "cdn1",
-                       "configUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "configApplyTime": "1969-12-31T17:00:00-07:00",
+            "configUpdateTime": "2022-01-01T17:00:00-07:00",
+            "configApplyTime": "1969-12-31T17:00:00-07:00",
             "domainName": "ga.atlanta.kabletown.net",
             "guid": null,
             "hostName": "config-reval-update-time-only",
@@ -5304,8 +5304,8 @@
             "physLocation": "Denver",
             "profileNames": ["EDGE1"],
             "rack": "RR 119.02",
-                       "revalUpdateTime": "2022-01-01T17:00:00-07:00",
-                       "revalApplyTime": "1969-12-31T17:00:00-07:00",
+            "revalUpdateTime": "2022-01-01T17:00:00-07:00",
+            "revalApplyTime": "1969-12-31T17:00:00-07:00",
             "status": "REPORTED",
             "tcpPort": 80,
             "type": "EDGE",
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go 
b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index e024126d01..dc029086bf 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2091,6 +2091,43 @@ func UpdateServerProfileTableForV2V3(id *int, newProfile 
*string, origProfile st
        return profileName, nil
 }
 
+// GetServerDetailFromV4 function converts server details from V4 to V3/V2
+func GetServerDetailFromV4(sd tc.ServerDetailV40, tx *sql.Tx) 
(tc.ServerDetail, error) {
+       var profileDesc *string
+       if err := tx.QueryRow(`SELECT p.description FROM profile p WHERE 
p.name=$1`, sd.ProfileNames[0]).Scan(&profileDesc); err != nil {
+               return tc.ServerDetail{}, fmt.Errorf("querying profile 
description by profile name: %w", err)
+       }
+       return tc.ServerDetail{
+               CacheGroup:         sd.CacheGroup,
+               CDNName:            sd.CDNName,
+               DeliveryServiceIDs: sd.DeliveryServiceIDs,
+               DomainName:         sd.DomainName,
+               GUID:               sd.GUID,
+               HardwareInfo:       sd.HardwareInfo,
+               HostName:           sd.HostName,
+               HTTPSPort:          sd.HTTPSPort,
+               ID:                 sd.ID,
+               ILOIPAddress:       sd.ILOIPAddress,
+               ILOIPGateway:       sd.ILOIPGateway,
+               ILOIPNetmask:       sd.ILOIPNetmask,
+               ILOPassword:        sd.ILOPassword,
+               ILOUsername:        sd.ILOUsername,
+               MgmtIPAddress:      sd.MgmtIPAddress,
+               MgmtIPGateway:      sd.MgmtIPGateway,
+               MgmtIPNetmask:      sd.MgmtIPNetmask,
+               OfflineReason:      sd.OfflineReason,
+               PhysLocation:       sd.PhysLocation,
+               Profile:            &sd.ProfileNames[0],
+               ProfileDesc:        profileDesc,
+               Rack:               sd.Rack,
+               Status:             sd.Status,
+               TCPPort:            sd.TCPPort,
+               Type:               sd.Type,
+               XMPPID:             sd.XMPPID,
+               XMPPPasswd:         sd.XMPPPasswd,
+       }, nil
+}
+
 // GetProfileIDDesc gets profile ID and desc for V3 servers
 func GetProfileIDDesc(tx *sql.Tx, name string) (id int, desc string) {
        err := tx.QueryRow(`SELECT id, description from "profile" p WHERE 
p.name=$1`, name).Scan(&id, &desc)
diff --git a/traffic_ops/traffic_ops_golang/server/detail.go 
b/traffic_ops/traffic_ops_golang/server/detail.go
index 7f2eb9b1f7..aa45dd9552 100644
--- a/traffic_ops/traffic_ops_golang/server/detail.go
+++ b/traffic_ops/traffic_ops_golang/server/detail.go
@@ -91,7 +91,11 @@ func GetDetailParamHandler(w http.ResponseWriter, r 
*http.Request) {
                                routerPortName = interfaces[0].RouterPortName
                        }
                        v11server := tc.ServerDetailV11{}
-                       v11server.ServerDetail = server.ServerDetail
+                       v11server.ServerDetail, err = 
dbhelpers.GetServerDetailFromV4(server, inf.Tx.Tx)
+                       if err != nil {
+                               api.HandleErr(w, r, inf.Tx.Tx, 
http.StatusInternalServerError, nil, fmt.Errorf("failed to 
GetServerDetailFromV4: %w", err))
+                               return
+                       }
                        v11server.RouterHostName = &routerHostName
                        v11server.RouterPortName = &routerPortName
                        legacyInterface, err := 
tc.V4InterfaceInfoToLegacyInterfaces(interfaces)
@@ -117,7 +121,11 @@ func GetDetailParamHandler(w http.ResponseWriter, r 
*http.Request) {
                                routerHostName = interfaces[0].RouterHostName
                                routerPortName = interfaces[0].RouterPortName
                        }
-                       v3Server.ServerDetail = server.ServerDetail
+                       v3Server.ServerDetail, err = 
dbhelpers.GetServerDetailFromV4(server, inf.Tx.Tx)
+                       if err != nil {
+                               api.HandleErr(w, r, inf.Tx.Tx, 
http.StatusInternalServerError, nil, fmt.Errorf("failed to 
GetServerDetailFromV4: %w", err))
+                               return
+                       }
                        v3Server.RouterHostName = &routerHostName
                        v3Server.RouterPortName = &routerPortName
                        v3Interfaces, err := 
tc.V4InterfaceInfoToV3Interfaces(interfaces)
@@ -209,8 +217,7 @@ server.mgmt_ip_gateway,
 server.mgmt_ip_netmask,
 server.offline_reason,
 pl.name as phys_location,
-p.name as profile,
-p.description as profile_desc,
+(SELECT ARRAY_AGG(profile_name) FROM server_profile WHERE 
server_profile.server=server.id) AS profile_name,
 server.rack,
 st.name as status,
 server.tcp_port,
@@ -275,7 +282,38 @@ JOIN type t ON server.type = t.id
 
        for rows.Next() {
                s := tc.ServerDetailV40{}
-               if err := rows.Scan(&s.ID, &s.CacheGroup, &s.CDNName, 
pq.Array(&s.DeliveryServiceIDs), &s.DomainName, &s.GUID, &s.HostName, 
&s.HTTPSPort, &s.ILOIPAddress, &s.ILOIPGateway, &s.ILOIPNetmask, 
&s.ILOPassword, &s.ILOUsername, &serviceAddress, &service6Address, 
&serviceGateway, &service6Gateway, &serviceNetmask, &serviceInterface, 
&serviceMtu, &s.MgmtIPAddress, &s.MgmtIPGateway, &s.MgmtIPNetmask, 
&s.OfflineReason, &s.PhysLocation, &s.Profile, &s.ProfileDesc, &s.Rack, 
&s.Status, &s.TCPPort, & [...]
+               if err := rows.Scan(&s.ID,
+                       &s.CacheGroup,
+                       &s.CDNName,
+                       pq.Array(&s.DeliveryServiceIDs),
+                       &s.DomainName,
+                       &s.GUID,
+                       &s.HostName,
+                       &s.HTTPSPort,
+                       &s.ILOIPAddress,
+                       &s.ILOIPGateway,
+                       &s.ILOIPNetmask,
+                       &s.ILOPassword,
+                       &s.ILOUsername,
+                       &serviceAddress,
+                       &service6Address,
+                       &serviceGateway,
+                       &service6Gateway,
+                       &serviceNetmask,
+                       &serviceInterface,
+                       &serviceMtu,
+                       &s.MgmtIPAddress,
+                       &s.MgmtIPGateway,
+                       &s.MgmtIPNetmask,
+                       &s.OfflineReason,
+                       &s.PhysLocation,
+                       pq.Array(&s.ProfileNames),
+                       &s.Rack,
+                       &s.Status,
+                       &s.TCPPort,
+                       &s.Type,
+                       &s.XMPPID,
+                       &s.XMPPPasswd); err != nil {
                        return nil, errors.New("Error scanning detail server: " 
+ err.Error())
                }
                s.ServerInterfaces = []tc.ServerInterfaceInfoV40{}
diff --git a/traffic_ops/traffic_ops_golang/server/detail_test.go 
b/traffic_ops/traffic_ops_golang/server/detail_test.go
index 9ad95ac564..bbcfa53788 100644
--- a/traffic_ops/traffic_ops_golang/server/detail_test.go
+++ b/traffic_ops/traffic_ops_golang/server/detail_test.go
@@ -13,7 +13,9 @@ package server
 */
 
 import (
+       "fmt"
        "strconv"
+       "strings"
        "testing"
 
        "github.com/apache/trafficcontrol/lib/go-tc"
@@ -75,8 +77,7 @@ func TestGetDetailServers(t *testing.T) {
                "mgmt_ip_netmask",
                "offline_reason",
                "phys_location",
-               "profile",
-               "profile_desc",
+               "profile_name",
                "rack",
                "status",
                "tcp_port",
@@ -93,8 +94,6 @@ func TestGetDetailServers(t *testing.T) {
        serviceNetmask := util.StrPtr("")
        serviceInterface := util.StrPtr("")
        serviceMtu := util.StrPtr("")
-       //routerHostName := util.StrPtr("")
-       //routerPort := util.StrPtr("")
 
        for _, sd := range testServerDetails {
                detailRows = detailRows.AddRow(
@@ -123,8 +122,7 @@ func TestGetDetailServers(t *testing.T) {
                        sd.MgmtIPNetmask,
                        sd.OfflineReason,
                        sd.PhysLocation,
-                       sd.Profile,
-                       sd.ProfileDesc,
+                       fmt.Sprintf("{%s}", strings.Join(sd.ProfileNames, ",")),
                        sd.Rack,
                        sd.Status,
                        sd.TCPPort,
@@ -143,7 +141,7 @@ func TestGetDetailServers(t *testing.T) {
        mock.ExpectQuery("SELECT serverid").WillReturnRows(hwInfoRows)
        mock.ExpectCommit()
 
-       actualSrvs, err := getDetailServers(db.MustBegin().Tx, 
&auth.CurrentUser{PrivLevel: 30}, "test", 1, "id", 10, api.Version{Major: 3})
+       actualSrvs, err := getDetailServers(db.MustBegin().Tx, 
&auth.CurrentUser{PrivLevel: 30}, "test", 1, "id", 10, api.Version{Major: 4})
        if err != nil {
                t.Fatalf("an error '%s' occurred during read", err)
        }
@@ -170,9 +168,7 @@ func TestGetDetailServers(t *testing.T) {
 
 func getMockServerDetails() []tc.ServerDetailV40 {
        srvData := tc.ServerDetailV40{
-               ServerDetail: tc.ServerDetail{
-                       ID: util.IntPtr(1),
-               },
+               ID:               util.IntPtr(1),
                ServerInterfaces: []tc.ServerInterfaceInfoV40{}, // left empty 
because it must be written as json above since sqlmock does not support nested 
arrays
        }
        return []tc.ServerDetailV40{srvData}

Reply via email to