@@ -30,6 +30,21 @@ const (
3030 filterTypeXPath = "xpath"
3131)
3232
33+ // NETCONF operation names
34+ const (
35+ opGet = "get"
36+ opGetConfig = "get-config"
37+ opEditConfig = "edit-config"
38+ opCopyConfig = "copy-config"
39+ opDeleteConfig = "delete-config"
40+ opLock = "lock"
41+ opUnlock = "unlock"
42+ opCommit = "commit"
43+ opDiscard = "discard"
44+ opValidate = "validate"
45+ opRPC = "rpc"
46+ )
47+
3348// Default client configuration values
3449const (
3550 DefaultPort = 830
@@ -198,6 +213,7 @@ func NewClient(host string, opts ...func(*Client)) (*Client, error) {
198213 options .WithAuthPassword (client .password ),
199214 options .WithPort (client .Port ),
200215 options .WithTimeoutSocket (client .ConnectTimeout ),
216+ options .WithTimeoutOps (client .OperationTimeout ),
201217 }
202218
203219 // Only disable host key verification if explicitly requested
@@ -1094,6 +1110,7 @@ func (c *Client) reconnect() error {
10941110 options .WithAuthPassword (c .password ),
10951111 options .WithPort (c .Port ),
10961112 options .WithTimeoutSocket (c .ConnectTimeout ),
1113+ options .WithTimeoutOps (c .OperationTimeout ),
10971114 }
10981115
10991116 if c .InsecureSkipVerify {
@@ -1280,12 +1297,21 @@ func (c *Client) sendRPC(ctx context.Context, req *Req) (Res, error) {
12801297
12811298 // Not transient or max retries reached
12821299 if ! isTransient || attempt >= c .MaxRetries {
1283- // Log operation failure
1284- c .logger .Error (ctx , "NETCONF operation failed" ,
1285- "operation" , req .Operation ,
1286- "retries" , attempt ,
1287- "transient" , isTransient ,
1288- "errorCount" , len (res .Errors ))
1300+ // Log operation failure with error details
1301+ if err != nil {
1302+ c .logger .Error (ctx , "NETCONF operation failed" ,
1303+ "operation" , req .Operation ,
1304+ "retries" , attempt ,
1305+ "transient" , isTransient ,
1306+ "errorCount" , len (res .Errors ),
1307+ "error" , err .Error ())
1308+ } else {
1309+ c .logger .Error (ctx , "NETCONF operation failed" ,
1310+ "operation" , req .Operation ,
1311+ "retries" , attempt ,
1312+ "transient" , isTransient ,
1313+ "errorCount" , len (res .Errors ))
1314+ }
12891315
12901316 // Log each RPC error
12911317 for i , rpcErr := range res .Errors {
@@ -1361,171 +1387,187 @@ func (c *Client) sendRPC(ctx context.Context, req *Req) (Res, error) {
13611387func (c * Client ) executeRPC (ctx context.Context , req * Req ) (Res , error ) {
13621388 _ = ctx // Accepted for future compatibility, timeout enforced by caller
13631389
1364- // Delegate to scrapligo driver based on operation
1365- var scrapligoRes * response.NetconfResponse
1366- var err error
1367-
13681390 // Check for nil driver before operation
13691391 if c .driver == nil {
13701392 return Res {}, fmt .Errorf ("operation %s failed: driver is nil (connection closed)" , req .Operation )
13711393 }
13721394
1395+ // Log request XML before sending
1396+ c .logRequestXML (ctx , req )
1397+
1398+ // Dispatch operation to scrapligo driver
1399+ scrapligoRes , err := c .dispatchOperation (req )
1400+
1401+ // Handle operation error
1402+ if err != nil {
1403+ return Res {}, c .formatOperationError (req , err )
1404+ }
1405+
1406+ // Check for nil response
1407+ if scrapligoRes == nil {
1408+ return Res {}, fmt .Errorf ("operation %s: received nil response from driver" , req .Operation )
1409+ }
1410+
1411+ // Log response XML
1412+ c .logResponseXML (ctx , req .Operation , scrapligoRes .Result )
1413+
1414+ // Parse response XML
1415+ return c .parseResponse (scrapligoRes )
1416+ }
1417+
1418+ // getRequestXMLForLogging determines what XML content to log based on operation type.
1419+ func (c * Client ) getRequestXMLForLogging (req * Req ) string {
13731420 switch req .Operation {
1374- case "get" :
1375- // Get method signature: Get(filter string, opts ...util.Option)
1376- // The filter string is the XML subtree for subtree filters, or empty string
1377- // For XPath filters, the XPath expression is passed as the filter string
1378- // Note: scrapligo driver/netconf may require filter type to be indicated via options
1379- // for XPath filters (this is validated but needs integration testing)
1380- filterStr := req .Filter .Content
1381- scrapligoRes , err = c .driver .Get (filterStr )
1382-
1383- case "get-config" :
1384- // GetConfig method signature: GetConfig(source string, opts ...util.Option)
1385- //
1386- // Filter options are provided via driver/opoptions package:
1387- // - opoptions.WithFilter(content) sets the filter XML or XPath expression
1388- // - opoptions.WithFilterType(type) sets "subtree" or "xpath" (default is "subtree")
1389- var opts []util.Option
1390- if req .Filter .Content != "" {
1391- // Add filter content
1392- opts = append (opts , opoptions .WithFilter (req .Filter .Content ))
1393-
1394- // Set filter type if XPath (subtree is default)
1395- if req .Filter .Type == filterTypeXPath {
1396- opts = append (opts , opoptions .WithFilterType (filterTypeXPath ))
1397- }
1398- }
1399- scrapligoRes , err = c .driver .GetConfig (req .Target , opts ... )
1400-
1401- case "edit-config" :
1402- // EditConfig method signature: EditConfig(target, config string)
1403- // If advanced edit-config options are set, build custom XML
1404- if req .DefaultOperation != "" || req .TestOption != "" || req .ErrorOption != "" {
1405- rpcXML := c .buildEditConfigXML (req )
1406- scrapligoRes , err = c .driver .RPC (opoptions .WithFilter (rpcXML ))
1407- } else {
1408- // For standard edit-config, ensure config has <config> wrapper
1409- // scrapligo's EditConfig expects the caller to provide the config element
1410- configContent := req .Config
1411- result := xmldot .Get (req .Config , "config" )
1412- if ! result .Exists () {
1413- // Config doesn't have <config> wrapper, add it
1414- configContent = "<config>" + req .Config + "</config>"
1415- }
1416- scrapligoRes , err = c .driver .EditConfig (req .Target , configContent )
1421+ case opEditConfig :
1422+ // For edit-config, ensure <config> wrapper is present (matching what will be sent)
1423+ if ! xmldot .Get (req .Config , "config" ).Exists () {
1424+ return "<config>" + req .Config + "</config>"
14171425 }
1426+ return req .Config
1427+ case opGetConfig , opGet :
1428+ return req .Filter .Content
1429+ case opLock , opUnlock , opCommit , opDiscard , opValidate :
1430+ return "" // Simple operations have no XML content
1431+ default :
1432+ return req .Config // For other operations (rpc, copy-config, etc.)
1433+ }
1434+ }
14181435
1419- case "copy-config" :
1420- // CopyConfig method signature: CopyConfig(source, target string)
1421- scrapligoRes , err = c .driver .CopyConfig (req .Config , req .Target )
1422-
1423- case "delete-config" :
1424- // DeleteConfig method signature: DeleteConfig(target string)
1425- scrapligoRes , err = c .driver .DeleteConfig (req .Target )
1426-
1427- case "lock" :
1428- // Lock method signature: Lock(target string)
1429- scrapligoRes , err = c .driver .Lock (req .Target )
1430-
1431- case "unlock" :
1432- // Unlock method signature: Unlock(target string)
1433- scrapligoRes , err = c .driver .Unlock (req .Target )
1434-
1435- case "commit" :
1436- // Commit method signature: Commit(opts ...util.Option)
1437- // Support confirmed commit parameters via scrapligo options
1438- var opts []util.Option
1439- if req .ConfirmTimeout > 0 {
1440- // Confirmed commit with timeout
1441- opts = append (opts , opoptions .WithCommitConfirmed ())
1442- opts = append (opts , opoptions .WithCommitConfirmTimeout (uint (req .ConfirmTimeout )))
1443- }
1444- if req .PersistID != "" {
1445- // Persist ID for commit operations
1446- opts = append (opts , opoptions .WithCommitConfirmedPersistID (req .PersistID ))
1447- }
1448- scrapligoRes , err = c .driver .Commit (opts ... )
1436+ // logRequestXML logs the request XML content if present.
1437+ func (c * Client ) logRequestXML (ctx context.Context , req * Req ) {
1438+ xmlToLog := c .getRequestXMLForLogging (req )
1439+ if xmlToLog == "" {
1440+ return
1441+ }
14491442
1450- case "discard" :
1451- // Discard method signature: Discard()
1452- scrapligoRes , err = c .driver .Discard ()
1443+ if len (xmlToLog ) > MaxXMLSizeForLogging {
1444+ c .logger .Debug (ctx , "NETCONF RPC request XML (truncated)" ,
1445+ "operation" , req .Operation ,
1446+ "size" , len (xmlToLog ),
1447+ "limit" , MaxXMLSizeForLogging ,
1448+ "xml" , "[XML TOO LARGE FOR LOGGING]" )
1449+ return
1450+ }
14531451
1454- case "validate" :
1455- // Validate method signature: Validate(target string)
1456- scrapligoRes , err = c .driver .Validate (req .Target )
1452+ if ! utf8 .ValidString (xmlToLog ) {
1453+ c .logger .Warn (ctx , "Invalid UTF-8 in NETCONF request XML" ,
1454+ "operation" , req .Operation ,
1455+ "size" , len (xmlToLog ))
1456+ return
1457+ }
14571458
1458- case "rpc" :
1459- // RPC method signature: RPC(opts ...util.Option)
1460- // Pass the RPC XML content via WithFilter option
1461- scrapligoRes , err = c .driver .RPC (opoptions .WithFilter (req .Config ))
1459+ requestXML := c .prepareXMLForLogging (xmlToLog )
1460+ c .logger .Debug (ctx , "NETCONF RPC request XML" ,
1461+ "operation" , req .Operation ,
1462+ "xml" , requestXML )
1463+ }
14621464
1463- default :
1464- return Res {}, fmt .Errorf ("unsupported operation: %s" , req .Operation )
1465+ // logResponseXML logs the response XML content.
1466+ func (c * Client ) logResponseXML (ctx context.Context , operation string , result string ) {
1467+ if result == "" {
1468+ return
14651469 }
14661470
1467- if err != nil {
1468- return Res {}, fmt .Errorf ("operation %s failed: %w" , req .Operation , err )
1471+ if len (result ) > MaxXMLSizeForLogging {
1472+ c .logger .Debug (ctx , "NETCONF RPC response XML (truncated)" ,
1473+ "operation" , operation ,
1474+ "size" , len (result ),
1475+ "limit" , MaxXMLSizeForLogging ,
1476+ "xml" , "[XML TOO LARGE FOR LOGGING]" )
1477+ return
14691478 }
14701479
1471- // Check for nil response
1472- if scrapligoRes == nil {
1473- return Res {}, fmt .Errorf ("operation %s: received nil response from driver" , req .Operation )
1480+ if ! utf8 .ValidString (result ) {
1481+ c .logger .Warn (ctx , "Invalid UTF-8 in NETCONF response XML" ,
1482+ "operation" , operation ,
1483+ "size" , len (result ))
1484+ return
14741485 }
14751486
1476- // Log request XML content (Debug level only)
1477- // Pre-check size and level to avoid expensive processing when not needed
1478- if len (scrapligoRes .Input ) > 0 {
1479- // Pre-check size limit before string conversion (avoid allocation)
1480- if len (scrapligoRes .Input ) <= MaxXMLSizeForLogging {
1481- // Validate UTF-8 encoding
1482- if ! utf8 .Valid (scrapligoRes .Input ) {
1483- c .logger .Warn (ctx , "Invalid UTF-8 in NETCONF request XML" ,
1484- "operation" , req .Operation ,
1485- "size" , len (scrapligoRes .Input ))
1486- } else {
1487- requestXML := c .prepareXMLForLogging (string (scrapligoRes .Input ))
1488- c .logger .Debug (ctx , "NETCONF RPC request XML" ,
1489- "operation" , req .Operation ,
1490- "xml" , requestXML )
1491- }
1492- } else {
1493- // Log truncation message only (cheap operation)
1494- c .logger .Debug (ctx , "NETCONF RPC request XML (truncated)" ,
1495- "operation" , req .Operation ,
1496- "size" , len (scrapligoRes .Input ),
1497- "limit" , MaxXMLSizeForLogging ,
1498- "xml" , "[XML TOO LARGE FOR LOGGING]" )
1499- }
1487+ responseXML := c .prepareXMLForLogging (result )
1488+ c .logger .Debug (ctx , "NETCONF RPC response XML" ,
1489+ "operation" , operation ,
1490+ "xml" , responseXML )
1491+ }
1492+
1493+ // formatOperationError formats an operation error with context.
1494+ func (c * Client ) formatOperationError (req * Req , err error ) error {
1495+ if req .Operation == opEditConfig {
1496+ return fmt .Errorf ("operation %s failed (target=%s, configSize=%d): %w" ,
1497+ req .Operation , req .Target , len (req .Config ), err )
15001498 }
1499+ return fmt .Errorf ("operation %s failed (target=%s): %w" , req .Operation , req .Target , err )
1500+ }
15011501
1502- // Log response XML content (Debug level only)
1503- if scrapligoRes .Result != "" {
1504- // Pre-check size limit before processing
1505- if len (scrapligoRes .Result ) <= MaxXMLSizeForLogging {
1506- // Validate UTF-8 encoding
1507- if ! utf8 .ValidString (scrapligoRes .Result ) {
1508- c .logger .Warn (ctx , "Invalid UTF-8 in NETCONF response XML" ,
1509- "operation" , req .Operation ,
1510- "size" , len (scrapligoRes .Result ))
1511- } else {
1512- responseXML := c .prepareXMLForLogging (scrapligoRes .Result )
1513- c .logger .Debug (ctx , "NETCONF RPC response XML" ,
1514- "operation" , req .Operation ,
1515- "xml" , responseXML )
1516- }
1517- } else {
1518- // Log truncation message only (cheap operation)
1519- c .logger .Debug (ctx , "NETCONF RPC response XML (truncated)" ,
1520- "operation" , req .Operation ,
1521- "size" , len (scrapligoRes .Result ),
1522- "limit" , MaxXMLSizeForLogging ,
1523- "xml" , "[XML TOO LARGE FOR LOGGING]" )
1502+ // dispatchOperation dispatches the operation to the appropriate scrapligo driver method.
1503+ func (c * Client ) dispatchOperation (req * Req ) (* response.NetconfResponse , error ) {
1504+ switch req .Operation {
1505+ case opGet :
1506+ return c .driver .Get (req .Filter .Content )
1507+ case opGetConfig :
1508+ return c .executeGetConfig (req )
1509+ case opEditConfig :
1510+ return c .executeEditConfig (req )
1511+ case opCopyConfig :
1512+ return c .driver .CopyConfig (req .Config , req .Target )
1513+ case opDeleteConfig :
1514+ return c .driver .DeleteConfig (req .Target )
1515+ case opLock :
1516+ return c .driver .Lock (req .Target )
1517+ case opUnlock :
1518+ return c .driver .Unlock (req .Target )
1519+ case opCommit :
1520+ return c .executeCommit (req )
1521+ case opDiscard :
1522+ return c .driver .Discard ()
1523+ case opValidate :
1524+ return c .driver .Validate (req .Target )
1525+ case opRPC :
1526+ return c .driver .RPC (opoptions .WithFilter (req .Config ))
1527+ default :
1528+ return nil , fmt .Errorf ("unsupported operation: %s" , req .Operation )
1529+ }
1530+ }
1531+
1532+ // executeGetConfig executes a get-config operation with filter options.
1533+ func (c * Client ) executeGetConfig (req * Req ) (* response.NetconfResponse , error ) {
1534+ var opts []util.Option
1535+ if req .Filter .Content != "" {
1536+ opts = append (opts , opoptions .WithFilter (req .Filter .Content ))
1537+ if req .Filter .Type == filterTypeXPath {
1538+ opts = append (opts , opoptions .WithFilterType (filterTypeXPath ))
15241539 }
15251540 }
1541+ return c .driver .GetConfig (req .Target , opts ... )
1542+ }
15261543
1527- // Parse response XML
1528- return c .parseResponse (scrapligoRes )
1544+ // executeEditConfig executes an edit-config operation.
1545+ func (c * Client ) executeEditConfig (req * Req ) (* response.NetconfResponse , error ) {
1546+ // If advanced edit-config options are set, build custom XML
1547+ if req .DefaultOperation != "" || req .TestOption != "" || req .ErrorOption != "" {
1548+ rpcXML := c .buildEditConfigXML (req )
1549+ return c .driver .RPC (opoptions .WithFilter (rpcXML ))
1550+ }
1551+
1552+ // For standard edit-config, ensure config has <config> wrapper
1553+ configContent := req .Config
1554+ if ! xmldot .Get (req .Config , "config" ).Exists () {
1555+ configContent = "<config>" + req .Config + "</config>"
1556+ }
1557+ return c .driver .EditConfig (req .Target , configContent )
1558+ }
1559+
1560+ // executeCommit executes a commit operation with optional confirmed commit.
1561+ func (c * Client ) executeCommit (req * Req ) (* response.NetconfResponse , error ) {
1562+ var opts []util.Option
1563+ if req .ConfirmTimeout > 0 {
1564+ opts = append (opts , opoptions .WithCommitConfirmed ())
1565+ opts = append (opts , opoptions .WithCommitConfirmTimeout (uint (req .ConfirmTimeout )))
1566+ }
1567+ if req .PersistID != "" {
1568+ opts = append (opts , opoptions .WithCommitConfirmedPersistID (req .PersistID ))
1569+ }
1570+ return c .driver .Commit (opts ... )
15291571}
15301572
15311573// parseResponse parses a NETCONF response from scrapligo
0 commit comments