Skip to content

Commit 8a5f7bd

Browse files
authored
Merge pull request #83 from nadvornik/filter_iss
Use sync.hub.listPeripheralServers API call
2 parents 93cf095 + cd32c69 commit 8a5f7bd

4 files changed

Lines changed: 202 additions & 11 deletions

File tree

uyuni/client/xmlrpc_client.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,42 @@ package client
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"fmt"
58
"net"
69
"net/http"
10+
"sync"
711
"time"
812

913
xmlrpc "github.com/uyuni-project/xmlrpc-public-methods"
1014
)
1115

1216
type Client struct {
1317
connectTimeout, requestTimeout int
18+
rootCAs *x509.CertPool
19+
mu sync.RWMutex
1420
}
1521

1622
func NewClient(connectTimeout, requestTimeout int) *Client {
17-
return &Client{connectTimeout: connectTimeout, requestTimeout: requestTimeout}
23+
return &Client{
24+
connectTimeout: connectTimeout,
25+
requestTimeout: requestTimeout,
26+
rootCAs: x509.NewCertPool(),
27+
}
28+
}
29+
30+
func (c *Client) AddRootCA(pem []byte) error {
31+
c.mu.Lock()
32+
defer c.mu.Unlock()
33+
if !c.rootCAs.AppendCertsFromPEM(pem) {
34+
return fmt.Errorf("failed to parse root CA certificate")
35+
}
36+
return nil
1837
}
1938

2039
func (c *Client) ExecuteCall(endpoint string, call string, args []interface{}) (response interface{}, err error) {
21-
client, err := getClientWithTimeout(endpoint, c.connectTimeout, c.requestTimeout)
40+
client, err := c.getClientWithTimeout(endpoint, c.connectTimeout, c.requestTimeout)
2241
if err != nil {
2342
return nil, err
2443
}
@@ -38,9 +57,14 @@ func timeoutDialer(connectTimeout, requestTimeout time.Duration) func(ctx contex
3857
}
3958
}
4059

41-
func getClientWithTimeout(url string, connectTimeout, requestTimeout int) (*xmlrpc.Client, error) {
60+
func (c *Client) getClientWithTimeout(url string, connectTimeout, requestTimeout int) (*xmlrpc.Client, error) {
61+
c.mu.RLock()
62+
defer c.mu.RUnlock()
4263
transport := http.Transport{
4364
DialContext: timeoutDialer(time.Duration(connectTimeout)*time.Second, time.Duration(requestTimeout)*time.Second),
65+
TLSClientConfig: &tls.Config{
66+
RootCAs: c.rootCAs,
67+
},
4468
}
4569
return xmlrpc.NewClient(url, &transport)
4670
}

uyuni/uyuni_call_executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type uyuniCallExecutor struct {
66

77
type Client interface {
88
ExecuteCall(endpoint string, call string, args []interface{}) (response interface{}, err error)
9+
AddRootCA(pem []byte) error
910
}
1011

1112
func NewUyuniCallExecutor(client Client) *uyuniCallExecutor {

uyuni/uyuni_topology_info_retriever.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package uyuni
33
import (
44
"errors"
55
"log"
6+
"sync"
67

78
"github.com/uyuni-project/hub-xmlrpc-api/gateway"
89
)
@@ -12,17 +13,24 @@ const (
1213
listSystemsWithEntitlementPath = "system.listSystemsWithEntitlement"
1314
listSystemFQDNsPath = "system.listFqdns"
1415
listUserSystemsPath = "system.listUserSystems"
16+
listPeripheralServersPath = "sync.hub.listPeripheralServers"
1517
systemIDField = "id"
1618
peripheralServerEntitlement = "peripheral_server"
1719
)
1820

1921
type uyuniTopologyInfoRetriever struct {
2022
uyuniCallExecutor *uyuniCallExecutor
2123
useSSL bool
24+
serverEndpoints map[int64]string
25+
mu sync.RWMutex
2226
}
2327

2428
func NewUyuniTopologyInfoRetriever(uyuniCallExecutor *uyuniCallExecutor, useSSL bool) *uyuniTopologyInfoRetriever {
25-
return &uyuniTopologyInfoRetriever{uyuniCallExecutor, useSSL}
29+
return &uyuniTopologyInfoRetriever{
30+
uyuniCallExecutor: uyuniCallExecutor,
31+
useSSL: useSSL,
32+
serverEndpoints: make(map[int64]string),
33+
}
2634
}
2735

2836
func (h *uyuniTopologyInfoRetriever) RetrieveUserServerIDs(endpoint, sessionKey, username string) ([]int64, error) {
@@ -60,14 +68,41 @@ func contains(s []int64, v int64) bool {
6068
}
6169

6270
func (h *uyuniTopologyInfoRetriever) ListServerIDs(endpoint, sessionKey string) ([]int64, error) {
71+
peripheralServers, err := h.uyuniCallExecutor.ExecuteCall(endpoint, listPeripheralServersPath, []interface{}{sessionKey})
72+
if err != nil {
73+
log.Printf("Error occured while calling listPeripheralServersPath: %v", err)
74+
} else {
75+
serversSlice := peripheralServers.([]interface{})
76+
systemIDs := make([]int64, len(serversSlice))
77+
h.mu.Lock()
78+
defer h.mu.Unlock()
79+
for i, s := range serversSlice {
80+
server := s.(map[string]interface{})
81+
id := server["id"].(int64)
82+
fqdn := server["fqdn"].(string)
83+
rootCA := server["root_ca"].(string)
84+
systemIDs[i] = id
85+
86+
serverEndpoint := constructEndpoint(fqdn, h.useSSL)
87+
h.serverEndpoints[id] = serverEndpoint
88+
if rootCA != "" {
89+
err := h.uyuniCallExecutor.client.AddRootCA([]byte(rootCA))
90+
if err != nil {
91+
log.Printf("Error ocurred while adding root CA for serverID %v: %v", id, err)
92+
}
93+
}
94+
}
95+
return systemIDs, nil
96+
}
97+
6398
systemList, err := h.uyuniCallExecutor.ExecuteCall(endpoint, listSystemsWithEntitlementPath, []interface{}{sessionKey, peripheralServerEntitlement})
6499
if err != nil {
65100
log.Printf("Error occured while retrieving the list of serverIDs: %v", err)
66101
return nil, err
67102
}
68103
systemsSlice := systemList.([]interface{})
69104
if len(systemsSlice) == 0 {
70-
// No entitled servers - fallback to full list, for legacy HUB server
105+
// No entitled servers - fallback to full list, for legacy HUB server
71106
systemList, err = h.uyuniCallExecutor.ExecuteCall(endpoint, listSystemsPath, []interface{}{sessionKey})
72107
if err != nil {
73108
log.Printf("Error occured while retrieving the list of serverIDs: %v", err)
@@ -87,12 +122,21 @@ func (h *uyuniTopologyInfoRetriever) RetrieveServerAPIEndpoints(endpoint, sessio
87122
serverAPIEndpointByServer := make(map[int64]string)
88123
failedServers := make(map[int64]string)
89124
for _, serverID := range serverIDs {
90-
serverAPIEndpoint, err := h.retrieveServerAPIEndpoint(endpoint, sessionKey, serverID)
91-
if err != nil {
92-
failedServers[serverID] = err.Error()
93-
} else {
94-
serverAPIEndpointByServer[serverID] = serverAPIEndpoint
125+
h.mu.RLock()
126+
serverAPIEndpoint, ok := h.serverEndpoints[serverID]
127+
h.mu.RUnlock()
128+
if !ok {
129+
var err error
130+
serverAPIEndpoint, err = h.retrieveServerAPIEndpoint(endpoint, sessionKey, serverID)
131+
if err != nil {
132+
failedServers[serverID] = err.Error()
133+
continue
134+
}
135+
h.mu.Lock()
136+
h.serverEndpoints[serverID] = serverAPIEndpoint
137+
h.mu.Unlock()
95138
}
139+
serverAPIEndpointByServer[serverID] = serverAPIEndpoint
96140
}
97141
return &gateway.RetrieveServerAPIEndpointsResponse{serverAPIEndpointByServer, failedServers}, nil
98142
}
@@ -122,9 +166,13 @@ func parseFQDN(fqdnResponse interface{}, useSSL bool) (string, error) {
122166
log.Printf("Error ocurred when parsing the FQDNs of peripheral servers")
123167
return "", errors.New("Error ocurred when parsing the FQDNs of peripheral servers")
124168
}
169+
return constructEndpoint(firstFqdn, useSSL), nil
170+
}
171+
172+
func constructEndpoint(fqdn string, useSSL bool) string {
125173
protocol := "http://"
126174
if useSSL {
127175
protocol = "https://"
128176
}
129-
return protocol + firstFqdn + "/rpc/api", nil
177+
return protocol + fqdn + "/rpc/api"
130178
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package uyuni
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
type mockClient struct {
10+
mockExecuteCall func(endpoint string, call string, args []interface{}) (response interface{}, err error)
11+
mockAddRootCA func(pem []byte) error
12+
}
13+
14+
func (m *mockClient) ExecuteCall(endpoint string, call string, args []interface{}) (interface{}, error) {
15+
return m.mockExecuteCall(endpoint, call, args)
16+
}
17+
18+
func (m *mockClient) AddRootCA(pem []byte) error {
19+
if m.mockAddRootCA != nil {
20+
return m.mockAddRootCA(pem)
21+
}
22+
return nil
23+
}
24+
25+
func TestListServerIDs(t *testing.T) {
26+
client := &mockClient{}
27+
executor := NewUyuniCallExecutor(client)
28+
retriever := NewUyuniTopologyInfoRetriever(executor, true)
29+
30+
sessionKey := "test-session"
31+
endpoint := "http://hub/rpc/api"
32+
33+
// Mock sync.hub.listPeripheralServers
34+
client.mockExecuteCall = func(ep string, call string, args []interface{}) (interface{}, error) {
35+
if call == "sync.hub.listPeripheralServers" {
36+
return []interface{}{
37+
map[string]interface{}{
38+
"id": int64(1001),
39+
"fqdn": "peripheral1.example.com",
40+
"root_ca": "PEM1",
41+
},
42+
map[string]interface{}{
43+
"id": int64(1002),
44+
"fqdn": "peripheral2.example.com",
45+
"root_ca": "PEM2",
46+
},
47+
}, nil
48+
}
49+
return nil, nil
50+
}
51+
52+
rootCAsAdded := make([]string, 0)
53+
client.mockAddRootCA = func(pem []byte) error {
54+
rootCAsAdded = append(rootCAsAdded, string(pem))
55+
return nil
56+
}
57+
58+
ids, err := retriever.ListServerIDs(endpoint, sessionKey)
59+
if err != nil {
60+
t.Fatalf("unexpected error: %v", err)
61+
}
62+
63+
expectedIDs := []int64{1001, 1002}
64+
if !reflect.DeepEqual(ids, expectedIDs) {
65+
t.Errorf("expected IDs %v, got %v", expectedIDs, ids)
66+
}
67+
68+
// Verify cache
69+
retriever.mu.RLock()
70+
endpoint1, ok1 := retriever.serverEndpoints[1001]
71+
endpoint2, ok2 := retriever.serverEndpoints[1002]
72+
retriever.mu.RUnlock()
73+
74+
if !ok1 || endpoint1 != "https://peripheral1.example.com/rpc/api" {
75+
t.Errorf("unexpected endpoint for 1001: %v", endpoint1)
76+
}
77+
if !ok2 || endpoint2 != "https://peripheral2.example.com/rpc/api" {
78+
t.Errorf("unexpected endpoint for 1002: %v", endpoint2)
79+
}
80+
81+
// Verify root CAs
82+
expectedRootCAs := []string{"PEM1", "PEM2"}
83+
if !reflect.DeepEqual(rootCAsAdded, expectedRootCAs) {
84+
t.Errorf("expected root CAs %v, got %v", expectedRootCAs, rootCAsAdded)
85+
}
86+
}
87+
88+
func TestListServerIDsFallback(t *testing.T) {
89+
client := &mockClient{}
90+
executor := NewUyuniCallExecutor(client)
91+
retriever := NewUyuniTopologyInfoRetriever(executor, true)
92+
93+
sessionKey := "test-session"
94+
endpoint := "http://hub/rpc/api"
95+
96+
// Mock sync.hub.listPeripheralServers to fail
97+
client.mockExecuteCall = func(ep string, call string, args []interface{}) (interface{}, error) {
98+
if call == "sync.hub.listPeripheralServers" {
99+
return nil, errors.New("fail")
100+
}
101+
if call == listSystemsWithEntitlementPath {
102+
return []interface{}{
103+
map[string]interface{}{"id": int64(2001)},
104+
}, nil
105+
}
106+
return nil, nil
107+
}
108+
109+
ids, err := retriever.ListServerIDs(endpoint, sessionKey)
110+
if err != nil {
111+
t.Fatalf("unexpected error: %v", err)
112+
}
113+
114+
expectedIDs := []int64{2001}
115+
if !reflect.DeepEqual(ids, expectedIDs) {
116+
t.Errorf("expected IDs %v, got %v", expectedIDs, ids)
117+
}
118+
}

0 commit comments

Comments
 (0)