[VOL-5479] Implement IP Change RPC at Core and Trigger Towards OLT Adapter

Change-Id: I6fd056b127abe0c956c8e08a13829a23891af12a
Signed-off-by: mgouda <madhumati.gouda@radisys.com>
Signed-off-by: madhumatigouda <madhumati.gouda@radisys.com>
diff --git a/rw_core/core/device/agent.go b/rw_core/core/device/agent.go
index ac9b2fa..9631fee 100755
--- a/rw_core/core/device/agent.go
+++ b/rw_core/core/device/agent.go
@@ -55,6 +55,9 @@
 var errReconcileAborted = errors.New("reconcile aborted")
 var errContextExpired = errors.New("context expired")
 var errNoConnection = errors.New("no connection")
+var errNoAddrChange = errors.New("no change in device address")
+var errAddrDuplicate = errors.New("ip address already in use")
+var errInvalidAddr = errors.New("invalid address type")
 
 // Agent represents device agent attributes
 type Agent struct {
@@ -2205,3 +2208,67 @@
 	}
 	return s
 }
+
+// UpdateDevice updates the configuration of a device, such as changing the IP address of an OLT device.
+func (agent *Agent) updateDevice(ctx context.Context, config *voltha.UpdateDevice) error {
+	var desc string
+	var err error
+	requestStatus := &common.OperationResp{Code: common.OperationResp_OPERATION_FAILURE}
+	defer func() { agent.logDeviceUpdate(ctx, nil, nil, requestStatus, err, desc) }()
+
+	clonedDevice := agent.cloneDeviceWithoutLock()
+	if err = agent.requestQueue.WaitForGreenLight(ctx); err != nil {
+		return err
+	}
+
+	agent.UpdateAddress(ctx, clonedDevice, config)
+
+	logger.Infow(ctx, "update-device-ip-address", log.Fields{
+		"device-id": agent.deviceID,
+		"config":    config,
+	})
+
+	if !agent.proceedWithRequest(clonedDevice) && clonedDevice.OperStatus != voltha.OperStatus_RECONCILING {
+		agent.requestQueue.RequestComplete()
+		err = status.Errorf(codes.FailedPrecondition, "cannot complete operation as device deletion is in progress/failed: %s", agent.deviceID)
+		return err
+	}
+
+	client, err := agent.adapterMgr.GetAdapterClient(ctx, agent.adapterEndpoint)
+	if err != nil {
+		agent.requestQueue.RequestComplete()
+		logger.Errorw(ctx, "grpc-client-nil",
+			log.Fields{
+				"error":            err,
+				"device-id":        agent.deviceID,
+				"device-type":      agent.deviceType,
+				"adapter-endpoint": clonedDevice.AdapterEndpoint,
+			})
+		return err
+	}
+
+	subCtx, cancel := context.WithTimeout(coreutils.WithAllMetadataFromContext(ctx), agent.rpcTimeout)
+	defer cancel()
+	requestStatus.Code = common.OperationResp_OPERATION_IN_PROGRESS
+	logger.Infow(ctx, "sending-update-to-adapter", log.Fields{"device-id": agent.deviceID, "adapter-endpoint": clonedDevice.AdapterEndpoint, "config": config})
+	if _, err = client.UpdateDevice(subCtx, config); err == nil {
+		agent.onSuccess(subCtx, nil, nil, true)
+	} else {
+		logger.Errorw(ctx, "update-device-ip-failed", log.Fields{"device-id": agent.deviceID, "error": err})
+		agent.onFailure(subCtx, err, nil, nil, true)
+		agent.requestQueue.RequestComplete()
+		return err
+	}
+	return agent.updateDeviceAndReleaseLock(ctx, clonedDevice)
+}
+
+func (agent *Agent) UpdateAddress(ctx context.Context, device *voltha.Device, deviceConfig *voltha.UpdateDevice) {
+	switch addr := deviceConfig.Address.(type) {
+	case *voltha.UpdateDevice_Ipv4Address:
+		device.Address = &voltha.Device_Ipv4Address{Ipv4Address: addr.Ipv4Address}
+	case *voltha.UpdateDevice_Ipv6Address:
+		device.Address = &voltha.Device_Ipv6Address{Ipv6Address: addr.Ipv6Address}
+	case *voltha.UpdateDevice_HostAndPort:
+		device.Address = &voltha.Device_HostAndPort{HostAndPort: addr.HostAndPort}
+	}
+}
diff --git a/rw_core/core/device/manager.go b/rw_core/core/device/manager.go
index e39d822..6466744 100755
--- a/rw_core/core/device/manager.go
+++ b/rw_core/core/device/manager.go
@@ -898,3 +898,39 @@
 	}
 	return "", fmt.Errorf("ONU with serial %s not found under OLT %s", device.GetSerialNumber(), device.GetOltDeviceId().Id)
 }
+
+func (dMgr *Manager) checkIPExists(ctx context.Context, config *voltha.UpdateDevice) error {
+
+	newType, newIP := utils.GetAddrTypeAndValue(config.Address)
+	if newType == "" {
+		return errInvalidAddr
+	}
+
+	for id := range dMgr.rootDevices {
+
+		agent := dMgr.getDeviceAgent(ctx, id)
+		if agent == nil || agent.device == nil {
+			continue
+		}
+
+		existingType, existingIP := utils.GetAddrTypeAndValue(agent.device.Address)
+		if existingType == "" || existingType != newType {
+			continue
+		}
+
+		if existingIP == newIP {
+
+			// Same device → no change
+			if agent.device.Id == config.Id {
+				logger.Debugw(ctx, "no-change-in-device-address", log.Fields{"device-id": agent.device.Id})
+				return errNoAddrChange
+			}
+
+			// Another root device → duplicate
+			logger.Debugw(ctx, "ip-address-already-used-by-another-device", log.Fields{"device-id": agent.device.Id, "type": newType, "address": newIP})
+			return errAddrDuplicate
+		}
+	}
+
+	return nil
+}
diff --git a/rw_core/core/device/manager_nbi.go b/rw_core/core/device/manager_nbi.go
index 2a8aeb8..866d6dc 100644
--- a/rw_core/core/device/manager_nbi.go
+++ b/rw_core/core/device/manager_nbi.go
@@ -1032,3 +1032,43 @@
 
 	return &empty.Empty{}, agent.enableOnuSerialNumber(ctx, device, oltAgent.adapterEndpoint)
 }
+
+// UpdateDevice updates the configuration of a device, such as changing the IP address of an OLT device.
+func (dMgr *Manager) UpdateDevice(ctx context.Context, config *voltha.UpdateDevice) (*empty.Empty, error) {
+	ctx = utils.WithRPCMetadataContext(ctx, "UpdateDevice")
+	log.EnrichSpan(ctx, log.Fields{"device-id": config.Id})
+
+	logger.Infow(ctx, "update-ip-for-the-device", log.Fields{"device-id": config.Id, "config": config})
+
+	// Validate input
+	if config.Id == "" || config.Address == nil {
+		return nil, status.Error(codes.InvalidArgument, "missing device id or address")
+	}
+
+	// check if ip already exists
+	if err := dMgr.checkIPExists(ctx, config); err != nil {
+		switch {
+		case errors.Is(err, errNoAddrChange):
+			return nil, status.Error(codes.InvalidArgument, err.Error())
+		case errors.Is(err, errAddrDuplicate):
+			return nil, status.Error(codes.AlreadyExists, err.Error())
+		case errors.Is(err, errInvalidAddr):
+			return nil, status.Error(codes.InvalidArgument, err.Error())
+		default:
+			return nil, status.Error(codes.Internal, err.Error())
+		}
+	}
+
+	// Get the device agent
+	agent := dMgr.getDeviceAgent(ctx, config.Id)
+	if agent == nil {
+		return nil, status.Errorf(codes.NotFound, "device-%s", config.Id)
+	}
+
+	// Validate that this is a root device (typically OLT)
+	if !agent.isRootDevice {
+		return nil, status.Error(codes.InvalidArgument, "device-update-only-supported-for-olt-devices")
+	}
+
+	return &empty.Empty{}, agent.updateDevice(ctx, config)
+}
diff --git a/rw_core/mocks/adapter_olt.go b/rw_core/mocks/adapter_olt.go
index 9d4a4a6..eddaaf7 100644
--- a/rw_core/mocks/adapter_olt.go
+++ b/rw_core/mocks/adapter_olt.go
@@ -505,3 +505,9 @@
 func (onuA *OLTAdapter) EnableOnuDevice(ctx context.Context, device *voltha.Device) (*empty.Empty, error) {
 	return &empty.Empty{}, nil
 }
+
+func (onuA *OLTAdapter) UpdateDevice(ctx context.Context, req *voltha.UpdateDevice) (*empty.Empty, error) {
+	logger.Debugw(ctx, "UpdateDevice called", log.Fields{"device-config": req})
+	// You can add logic here to update the device in your mock if needed.
+	return &empty.Empty{}, nil
+}
diff --git a/rw_core/mocks/adapter_onu.go b/rw_core/mocks/adapter_onu.go
index c2e5a8f..610355e 100644
--- a/rw_core/mocks/adapter_onu.go
+++ b/rw_core/mocks/adapter_onu.go
@@ -354,3 +354,9 @@
 func (onuA *ONUAdapter) EnableOnuDevice(ctx context.Context, device *voltha.Device) (*empty.Empty, error) {
 	return &empty.Empty{}, nil
 }
+
+func (onuA *ONUAdapter) UpdateDevice(ctx context.Context, req *voltha.UpdateDevice) (*empty.Empty, error) {
+	logger.Debugw(ctx, "UpdateDevice called", log.Fields{"device-config": req})
+	// You can add logic here to update the device in your mock if needed.
+	return &empty.Empty{}, nil
+}
diff --git a/rw_core/utils/core_utils.go b/rw_core/utils/core_utils.go
index ce9fefa..24c380d 100644
--- a/rw_core/utils/core_utils.go
+++ b/rw_core/utils/core_utils.go
@@ -24,6 +24,7 @@
 	"time"
 
 	"github.com/opencord/voltha-lib-go/v7/pkg/log"
+	"github.com/opencord/voltha-protos/v5/go/voltha"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/peer"
 	"google.golang.org/grpc/status"
@@ -253,3 +254,24 @@
 		return 1
 	}
 }
+
+func GetAddrTypeAndValue(addr interface{}) (string, string) {
+	switch a := addr.(type) {
+
+	case *voltha.UpdateDevice_Ipv4Address:
+		return "ipv4", a.Ipv4Address
+	case *voltha.UpdateDevice_Ipv6Address:
+		return "ipv6", a.Ipv6Address
+	case *voltha.UpdateDevice_HostAndPort:
+		return "hostport", a.HostAndPort
+
+	case *voltha.Device_Ipv4Address:
+		return "ipv4", a.Ipv4Address
+	case *voltha.Device_Ipv6Address:
+		return "ipv6", a.Ipv6Address
+	case *voltha.Device_HostAndPort:
+		return "hostport", a.HostAndPort
+	default:
+		return "", ""
+	}
+}