[VOL-4687] Add transceivers to DMI, add commands to bbsimctl to manage them

Change-Id: Id1fe29eb48a7abb3c86958827edce70b75707de9
diff --git a/internal/bbsim/dmiserver/dmi_api_server.go b/internal/bbsim/dmiserver/dmi_api_server.go
index fea52a8..9ad8f94 100755
--- a/internal/bbsim/dmiserver/dmi_api_server.go
+++ b/internal/bbsim/dmiserver/dmi_api_server.go
@@ -18,9 +18,11 @@
 
 import (
 	"context"
+	"fmt"
 	"net"
 
 	"github.com/opencord/bbsim/api/bbsim"
+	"github.com/opencord/bbsim/internal/bbsim/devices"
 	"github.com/opencord/bbsim/internal/common"
 	dmi "github.com/opencord/device-management-interface/go/dmi"
 	log "github.com/sirupsen/logrus"
@@ -35,19 +37,16 @@
 
 //DmiAPIServer has the attributes for the Server handling the Device Management Interface
 type DmiAPIServer struct {
-	deviceSerial            string
-	deviceName              string
-	ipAddress               string
-	uuid                    string
-	ponTransceiverUuids     []string
-	ponTransceiverCageUuids []string
-	root                    *dmi.Component
-	metricChannel           chan interface{}
-	eventChannel            chan interface{}
-	kafkaEndpoint           string
-	loggingEndpoint         string
-	loggingProtocol         string
-	mPublisherCancelFunc    context.CancelFunc
+	ipAddress            string
+	uuid                 *dmi.Uuid
+	root                 *dmi.Component
+	Transceivers         []*Transceiver
+	metricChannel        chan interface{}
+	eventChannel         chan interface{}
+	kafkaEndpoint        string
+	loggingEndpoint      string
+	loggingProtocol      string
+	mPublisherCancelFunc context.CancelFunc
 }
 
 var dmiServ DmiAPIServer
@@ -56,9 +55,27 @@
 func StartDmiAPIServer() (*grpc.Server, error) {
 	dmiServ = DmiAPIServer{}
 
+	// Create the mapping between transceivers and PONS
+	// TODO: at the moment we create one transceiver for each PON,
+	// but in the case of COMBO PON this will not always be the case.
+	// This should be expanded to cover that scenario
+	logger.Debug("Creating transceivers from DMI")
+	dmiServ.Transceivers = []*Transceiver{}
+	for _, pon := range devices.GetOLT().Pons {
+		trans := newTransceiver(pon.ID, []*devices.PonPort{pon})
+		dmiServ.Transceivers = append(dmiServ.Transceivers, trans)
+	}
+
 	return dmiServ.newDmiAPIServer()
 }
 
+func getDmiAPIServer() (*DmiAPIServer, error) {
+	if dmiServ.root == nil {
+		return nil, fmt.Errorf("Device management not started")
+	}
+	return &dmiServ, nil
+}
+
 // newDmiAPIServer launches a new grpc server for the Device Manager Interface
 func (dms *DmiAPIServer) newDmiAPIServer() (*grpc.Server, error) {
 	address := common.Config.BBSim.DmiServerAddress
diff --git a/internal/bbsim/dmiserver/dmi_event_generator.go b/internal/bbsim/dmiserver/dmi_event_generator.go
index 2a9073e..93aa28f 100644
--- a/internal/bbsim/dmiserver/dmi_event_generator.go
+++ b/internal/bbsim/dmiserver/dmi_event_generator.go
@@ -57,6 +57,14 @@
 		componentType: dmi.ComponentType_COMPONENT_TYPE_FAN,
 		genFunc:       noThresholdEventGenerationFunc,
 	}
+	eventGenMap[dmi.EventIds_EVENT_TRANSCEIVER_PLUG_IN] = eventGenerationUtil{
+		componentType: dmi.ComponentType_COMPONENT_TYPE_TRANSCEIVER,
+		genFunc:       noThresholdEventGenerationFunc,
+	}
+	eventGenMap[dmi.EventIds_EVENT_TRANSCEIVER_PLUG_OUT] = eventGenerationUtil{
+		componentType: dmi.ComponentType_COMPONENT_TYPE_TRANSCEIVER,
+		genFunc:       noThresholdEventGenerationFunc,
+	}
 
 	eventGenMap[dmi.EventIds_EVENT_PSU_FAILURE] = eventGenerationUtil{
 		componentType: dmi.ComponentType_COMPONENT_TYPE_POWER_SUPPLY,
@@ -113,6 +121,16 @@
 		EventId:      dmi.EventIds_EVENT_PSU_FAILURE,
 		IsConfigured: true,
 	}
+
+	// Add Transceiver Plug in and out event configuration
+	dmiEG.configuredEvents[dmi.EventIds_EVENT_TRANSCEIVER_PLUG_IN] = dmi.EventCfg{
+		EventId:      dmi.EventIds_EVENT_TRANSCEIVER_PLUG_IN,
+		IsConfigured: true,
+	}
+	dmiEG.configuredEvents[dmi.EventIds_EVENT_TRANSCEIVER_PLUG_OUT] = dmi.EventCfg{
+		EventId:      dmi.EventIds_EVENT_TRANSCEIVER_PLUG_OUT,
+		IsConfigured: true,
+	}
 }
 
 // get the events list
@@ -146,9 +164,7 @@
 // update Event MetaData
 func updateEventMetaData(c *dmi.Component, apiSrv *DmiAPIServer, evt *dmi.Event) *dmi.Event {
 	evt.EventMetadata = &dmi.EventMetaData{
-		DeviceUuid: &dmi.Uuid{
-			Uuid: apiSrv.uuid,
-		},
+		DeviceUuid:    apiSrv.uuid,
 		ComponentUuid: c.Uuid,
 		ComponentName: c.Name,
 	}
@@ -212,9 +228,9 @@
 }
 
 // CreateEvent creates and the passed event if it's valid and sends it to the msg bus
-func (dms *DmiAPIServer) CreateEvent(ctx context.Context, evt *bbsim.DmiEvent) (*bbsim.DmiCreateEventResponse, error) {
-	retFunc := func(code codes.Code, msg string) (*bbsim.DmiCreateEventResponse, error) {
-		res := &bbsim.DmiCreateEventResponse{}
+func (dms *DmiAPIServer) CreateEvent(ctx context.Context, evt *bbsim.DmiEvent) (*bbsim.DmiResponse, error) {
+	retFunc := func(code codes.Code, msg string) (*bbsim.DmiResponse, error) {
+		res := &bbsim.DmiResponse{}
 		res.StatusCode = int32(code)
 		res.Message = msg
 		return res, nil
@@ -222,7 +238,7 @@
 
 	if dmiEG.apiSrv == nil || dmiEG.apiSrv.root == nil || dmiEG.apiSrv.root.Children == nil {
 		// inventory might not yet be created
-		return retFunc(codes.Internal, "inventory do no exist")
+		return retFunc(codes.Internal, "Inventory does not exist")
 	}
 
 	eventID, exists := dmi.EventIds_value[evt.EventName]
diff --git a/internal/bbsim/dmiserver/dmi_hw_mgmt.go b/internal/bbsim/dmiserver/dmi_hw_mgmt.go
index 06c078f..00e4a92 100755
--- a/internal/bbsim/dmiserver/dmi_hw_mgmt.go
+++ b/internal/bbsim/dmiserver/dmi_hw_mgmt.go
@@ -21,6 +21,7 @@
 	"fmt"
 
 	"github.com/Shopify/sarama"
+	log "github.com/sirupsen/logrus"
 
 	"github.com/golang/protobuf/ptypes/empty"
 	"github.com/golang/protobuf/ptypes/timestamp"
@@ -41,6 +42,46 @@
 	return guuid.NewMD5(guuid.Nil, []byte(seed)).String()
 }
 
+func getOltName() string {
+	return fmt.Sprintf("%s-%s", common.Config.Olt.Vendor, devices.GetOLT().SerialNumber)
+}
+
+func getOltUUID() *dmi.Uuid {
+	return &dmi.Uuid{
+		Uuid: getUUID(devices.GetOLT().SerialNumber),
+	}
+}
+
+func getCageName(id uint32) string {
+	return fmt.Sprintf("sfp-plus-transceiver-cage-%d", id)
+}
+
+func getCageUUID(id uint32) *dmi.Uuid {
+	return &dmi.Uuid{
+		Uuid: getUUID(fmt.Sprintf("%s-%s", devices.GetOLT().SerialNumber, getCageName(id))),
+	}
+}
+
+func getTransceiverName(id uint32) string {
+	return fmt.Sprintf("sfp-plus-%d", id)
+}
+
+func getTransceiverUUID(id uint32) *dmi.Uuid {
+	return &dmi.Uuid{
+		Uuid: getUUID(fmt.Sprintf("%s-%s", devices.GetOLT().SerialNumber, getTransceiverName(id))),
+	}
+}
+
+func getPonName(id uint32) string {
+	return fmt.Sprintf("pon-%d", id)
+}
+
+func getPonUUID(id uint32) *dmi.Uuid {
+	return &dmi.Uuid{
+		Uuid: getUUID(fmt.Sprintf("%s-%s", devices.GetOLT().SerialNumber, getPonName(id))),
+	}
+}
+
 //StartManagingDevice establishes connection with the device and does checks to ascertain if the device with passed identity can be managed
 func (dms *DmiAPIServer) StartManagingDevice(req *dmi.ModifiableComponent, stream dmi.NativeHWManagementService_StartManagingDeviceServer) error {
 	//Get serial number and generate the UUID based on this serial number. Store this UUID in local cache
@@ -57,13 +98,9 @@
 
 	// Uri is the IP address
 	dms.ipAddress = req.GetUri().GetUri()
-	dms.deviceSerial = olt.SerialNumber
-	dms.deviceName = fmt.Sprintf("%s-%s", common.Config.Olt.Vendor, dms.deviceSerial)
 
-	dms.uuid = getUUID(dms.deviceSerial)
-
-	dms.ponTransceiverUuids = make([]string, olt.NumPon)
-	dms.ponTransceiverCageUuids = make([]string, olt.NumPon)
+	deviceName := getOltName()
+	dms.uuid = getOltUUID()
 
 	// Start device metrics generator
 	dms.metricChannel = make(chan interface{}, kafkaChannelSize)
@@ -76,57 +113,27 @@
 	var components []*dmi.Component
 
 	// Create and store the component for transceivers and transceiver cages
-	for i, pon := range olt.Pons {
-		label := fmt.Sprintf("pon-%d", pon.ID)
-		dms.ponTransceiverUuids[i] = getUUID(dms.deviceSerial + label)
-		dms.ponTransceiverCageUuids[i] = getUUID(dms.deviceSerial + "cage" + label)
-
-		transName := fmt.Sprintf("sfp-%d", i)
-		cageName := fmt.Sprintf("sfp-plus-transceiver-cage-pon-%d", i)
-
-		var transType dmi.TransceiverType
-		var rxWavelength, txWavelength []uint32
-		switch pon.Technology {
-		case common.GPON:
-			transType = dmi.TransceiverType_GPON
-			rxWavelength = []uint32{1490} // nanometers
-			txWavelength = []uint32{1550} // nanometers
-		case common.XGSPON:
-			transType = dmi.TransceiverType_XGSPON
-			rxWavelength = []uint32{1270} // nanometers
-			txWavelength = []uint32{1577} // nanometers
-		}
-
-		trans := dmi.Component{
-			Name:        transName,
-			Class:       dmi.ComponentType_COMPONENT_TYPE_TRANSCEIVER,
-			Description: pon.Technology.String(),
-			Uuid: &dmi.Uuid{
-				Uuid: dms.ponTransceiverUuids[i],
-			},
-			Parent: cageName,
-			Specific: &dmi.Component_TransceiverAttr{
-				TransceiverAttr: &dmi.TransceiverComponentsAttributes{
-					FormFactor:       dmi.TransceiverComponentsAttributes_SFP_PLUS,
-					TransType:        transType,
-					MaxDistance:      10, // kilometers (see scale below)
-					MaxDistanceScale: dmi.ValueScale_VALUE_SCALE_KILO,
-					RxWavelength:     rxWavelength,
-					TxWavelength:     txWavelength,
-					WavelengthScale:  dmi.ValueScale_VALUE_SCALE_NANO,
-				},
-			},
-		}
+	for _, trans := range dms.Transceivers {
+		//Make one cage for each of the transceivers
+		cageName := getCageName(trans.ID)
 
 		cage := dmi.Component{
 			Name:        cageName,
 			Class:       dmi.ComponentType_COMPONENT_TYPE_CONTAINER,
 			Description: "cage",
-			Uuid: &dmi.Uuid{
-				Uuid: dms.ponTransceiverCageUuids[i],
-			},
-			Parent:   dms.deviceName,
-			Children: []*dmi.Component{&trans},
+			Uuid:        getCageUUID(trans.ID),
+			Parent:      deviceName,
+			Children:    []*dmi.Component{},
+		}
+
+		//If the transceiver is not plugged in, only the empty cage is created
+		if trans.PluggedIn {
+			transComponent, err := createTransceiverComponent(trans, cageName)
+			if err != nil {
+				logger.Error(err)
+				continue
+			}
+			cage.Children = append(cage.Children, transComponent)
 		}
 
 		components = append(components, &cage)
@@ -150,30 +157,26 @@
 
 	// create the root component
 	dms.root = &dmi.Component{
-		Name:         dms.deviceName,
+		Name:         deviceName,
 		Class:        0,
 		Description:  "",
 		Parent:       "",
 		ParentRelPos: 0,
 		Children:     components,
-		SerialNum:    dms.deviceSerial,
+		SerialNum:    olt.SerialNumber,
 		MfgName:      common.Config.Olt.Vendor,
 		IsFru:        false,
 		Uri: &dmi.Uri{
 			Uri: dms.ipAddress,
 		},
-		Uuid: &dmi.Uuid{
-			Uuid: dms.uuid,
-		},
+		Uuid:  dms.uuid,
 		State: &dmi.ComponentState{},
 	}
 
-	logger.Debugf("Generated UUID for the uri %s is %s", dms.ipAddress, dms.uuid)
+	logger.Debugf("Generated UUID for the uri %s is %s", dms.ipAddress, dms.uuid.Uuid)
 	response := &dmi.StartManagingDeviceResponse{
-		Status: dmi.Status_OK_STATUS,
-		DeviceUuid: &dmi.Uuid{
-			Uuid: dms.uuid,
-		},
+		Status:     dmi.Status_OK_STATUS,
+		DeviceUuid: dms.uuid,
 	}
 
 	err := stream.Send(response)
@@ -185,6 +188,90 @@
 	return nil
 }
 
+func createTransceiverComponent(trans *Transceiver, cageName string) (*dmi.Component, error) {
+	portName := getPonName(trans.ID)
+
+	var rxWavelength, txWavelength []uint32
+
+	if len(trans.Pons) == 0 {
+		return nil, fmt.Errorf("No pons in list for transceiver %d", trans.ID)
+	} else if len(trans.Pons) <= 1 {
+		//Assuming a transceiver with only one PON
+		//has the technology of the PON
+
+		switch trans.Pons[0].Technology {
+		case common.GPON:
+			trans.Technology = dmi.TransceiverType_GPON
+			rxWavelength = []uint32{1490} // nanometers
+			txWavelength = []uint32{1550} // nanometers
+		case common.XGSPON:
+			trans.Technology = dmi.TransceiverType_XGSPON
+			rxWavelength = []uint32{1270} // nanometers
+			txWavelength = []uint32{1577} // nanometers
+		}
+	} else {
+		//Assuming more than one PON for the transceiver
+		//is COMBO PON
+
+		trans.Technology = dmi.TransceiverType_COMBO_GPON_XGSPON
+
+		rxWavelength = []uint32{1490, 1270} // nanometers
+		txWavelength = []uint32{1550, 1577} // nanometers
+	}
+
+	//Create all ports mapped to this transceiver
+	ports := []*dmi.Component{}
+	for _, pon := range trans.Pons {
+		var portProto dmi.PortComponentAttributes_Protocol
+
+		switch pon.Technology {
+		case common.GPON:
+			portProto = dmi.PortComponentAttributes_GPON
+		case common.XGSPON:
+			portProto = dmi.PortComponentAttributes_XGSPON
+		}
+
+		p := dmi.Component{
+			Name:        portName,
+			Class:       dmi.ComponentType_COMPONENT_TYPE_PORT,
+			Description: "bbsim-pon-port",
+			Uuid:        getPonUUID(pon.ID),
+			Parent:      trans.Name,
+			Specific: &dmi.Component_PortAttr{
+				PortAttr: &dmi.PortComponentAttributes{
+					Protocol: portProto,
+				},
+			},
+		}
+
+		ports = append(ports, &p)
+	}
+
+	transComponent := dmi.Component{
+		Name:        trans.Name,
+		Class:       dmi.ComponentType_COMPONENT_TYPE_TRANSCEIVER,
+		Description: "bbsim-transceiver",
+		Uuid: &dmi.Uuid{
+			Uuid: trans.Uuid,
+		},
+		Parent: cageName,
+		Specific: &dmi.Component_TransceiverAttr{
+			TransceiverAttr: &dmi.TransceiverComponentsAttributes{
+				FormFactor:       dmi.TransceiverComponentsAttributes_SFP_PLUS,
+				TransType:        trans.Technology,
+				MaxDistance:      10, // kilometers (see scale below)
+				MaxDistanceScale: dmi.ValueScale_VALUE_SCALE_KILO,
+				RxWavelength:     rxWavelength,
+				TxWavelength:     txWavelength,
+				WavelengthScale:  dmi.ValueScale_VALUE_SCALE_NANO,
+			},
+		},
+		Children: ports,
+	}
+
+	return &transComponent, nil
+}
+
 func createFanComponent(fanIdx int) *dmi.Component {
 	fanName := fmt.Sprintf("Thermal/Fans/System Fan/%d", fanIdx)
 	fanSerial := fmt.Sprintf("bbsim-fan-serial-%d", fanIdx)
@@ -299,6 +386,115 @@
 	}
 }
 
+func PlugoutTransceiverComponent(transId uint32, dms *DmiAPIServer) error {
+	if dms == nil {
+		return fmt.Errorf("Nil API server")
+	}
+
+	if dms.root == nil {
+		return fmt.Errorf("Device management not started")
+	}
+
+	trans, err := getTransceiverWithId(transId, dms)
+	if err != nil {
+		return err
+	}
+
+	if !trans.PluggedIn {
+		return fmt.Errorf("Cannot plug out transceiver with ID %d since it's not plugged in", transId)
+	}
+
+	//Find the transceiver node in the tree
+	targetUuid := getTransceiverUUID(transId)
+
+	var targetCage *dmi.Component
+	targetTransIndex := -1
+
+loop:
+	for _, rootChild := range dms.root.Children {
+		if rootChild.Uuid.Uuid == getCageUUID(transId).Uuid {
+			currentCage := rootChild
+
+			for j, cageChild := range currentCage.Children {
+				if cageChild.Uuid.Uuid == targetUuid.Uuid {
+					targetCage = currentCage
+					targetTransIndex = j
+					break loop
+				}
+			}
+		}
+	}
+
+	if targetCage == nil || targetTransIndex == -1 {
+		return fmt.Errorf("Cannot find transceiver with id %d", transId)
+	}
+
+	//Remove transceiver
+	targetCage.Children = append(targetCage.Children[:targetTransIndex], targetCage.Children[targetTransIndex+1:]...)
+	logger.WithFields(log.Fields{
+		"transId":      transId,
+		"cageName":     targetCage.Name,
+		"cageChildren": targetCage.Children,
+	}).Debug("Removed transceiver from DMI inventory")
+
+	//Change plugged-in state
+	trans.PluggedIn = false
+
+	return nil
+}
+
+func PluginTransceiverComponent(transId uint32, dms *DmiAPIServer) error {
+	if dms == nil {
+		return fmt.Errorf("Nil API server")
+	}
+
+	if dms.root == nil {
+		return fmt.Errorf("Device management not started")
+	}
+
+	trans, err := getTransceiverWithId(transId, dms)
+	if err != nil {
+		return err
+	}
+
+	if trans.PluggedIn {
+		return fmt.Errorf("Cannot plug in transceiver with ID %d since it's already plugged in", transId)
+	}
+
+	//Find transceiver node in the tree
+	var targetCage *dmi.Component
+
+	for _, rootChild := range dms.root.Children {
+		if rootChild.Uuid.Uuid == getCageUUID(transId).Uuid {
+			targetCage = rootChild
+			break
+		}
+	}
+
+	if targetCage == nil {
+		return fmt.Errorf("Cannot find cage for transceiver with id %d", transId)
+	}
+
+	//Add transceiver
+	transComponent, err := createTransceiverComponent(trans, targetCage.Name)
+	if err != nil {
+		return err
+	}
+
+	targetCage.Children = append(targetCage.Children, transComponent)
+
+	logger.WithFields(log.Fields{
+		"transId":      transId,
+		"cageName":     targetCage.Name,
+		"cageChildren": targetCage.Children,
+	}).Debug("Added transceiver to DMI inventory")
+
+	//Change plugged-in state
+	trans.PluggedIn = true
+
+	return nil
+}
+
 //StopManagingDevice stops management of a device and cleans up any context and caches for that device
 func (dms *DmiAPIServer) StopManagingDevice(ctx context.Context, req *dmi.StopManagingDeviceRequest) (*dmi.StopManagingDeviceResponse, error) {
 	logger.Debugf("StopManagingDevice API invoked")
@@ -318,16 +514,15 @@
 		dms.mPublisherCancelFunc()
 	}
 
-	dms.deviceName = ""
 	dms.kafkaEndpoint = ""
 	dms.ipAddress = ""
-	dms.deviceSerial = ""
-	dms.ponTransceiverUuids = nil
-	dms.ponTransceiverCageUuids = nil
-	dms.uuid = ""
+	dms.uuid = nil
 	dms.root = nil
 	dms.metricChannel = nil
 
+	//Don't clear the Transceivers, so that they will survive
+	//new StartManagingDevice calls
+
 	logger.Infof("Stopped managing the device")
 	return &dmi.StopManagingDeviceResponse{Status: dmi.Status_OK_STATUS}, nil
 }
@@ -348,8 +543,8 @@
 		return nil
 	}
 
-	if req.DeviceUuid.Uuid != dms.uuid {
-		logger.Errorf("Requested uuid =%s, uuid of existing device = %s", req.DeviceUuid.Uuid, dms.uuid)
+	if req.DeviceUuid.Uuid != dms.uuid.Uuid {
+		logger.Errorf("Requested uuid =%s, uuid of existing device = %s", req.DeviceUuid.Uuid, dms.uuid.Uuid)
 		// Wrong uuid, return error
 		errResponse := &dmi.PhysicalInventoryResponse{
 			Status:    dmi.Status_ERROR_STATUS,
@@ -490,7 +685,7 @@
 	if request.LoggingProtocol == "" {
 		return errRetFunc(dmi.Status_ERROR_STATUS, dmi.SetRemoteEndpointResponse_LOGGING_ENDPOINT_PROTOCOL_ERROR)
 	}
-	if request.DeviceUuid == nil || request.DeviceUuid.Uuid != dms.uuid {
+	if request.DeviceUuid == nil || request.DeviceUuid.Uuid != dms.uuid.Uuid {
 		return errRetFunc(dmi.Status_ERROR_STATUS, dmi.SetRemoteEndpointResponse_UNKNOWN_DEVICE)
 	}
 
@@ -511,7 +706,7 @@
 			Reason: dmi.GetLoggingEndpointResponse_UNKNOWN_DEVICE,
 		}, status.Errorf(codes.InvalidArgument, "invalid request")
 	}
-	if request.Uuid.Uuid != dms.uuid {
+	if request.Uuid.Uuid != dms.uuid.Uuid {
 		return &dmi.GetLoggingEndpointResponse{
 			Status: dmi.Status_ERROR_STATUS,
 			Reason: dmi.GetLoggingEndpointResponse_UNKNOWN_DEVICE,
@@ -578,17 +773,15 @@
 func (dms *DmiAPIServer) GetManagedDevices(context.Context, *empty.Empty) (*dmi.ManagedDevicesResponse, error) {
 	retResponse := dmi.ManagedDevicesResponse{}
 	//If our uuid is empty, we return empty list; else we fill details and return
-	if dms.uuid != "" {
+	if dms.root != nil {
 		root := dmi.ManagedDeviceInfo{
 			Info: &dmi.ModifiableComponent{
-				Name: dms.deviceName,
+				Name: getOltName(),
 				Uri: &dmi.Uri{
 					Uri: dms.ipAddress,
 				},
 			},
-			DeviceUuid: &dmi.Uuid{
-				Uuid: dms.uuid,
-			},
+			DeviceUuid: dms.uuid,
 		}
 
 		retResponse.Devices = append(retResponse.Devices, &root)
@@ -601,32 +794,26 @@
 //GetLogLevel Gets the configured log level for a certain entity on a certain device.
 func (dms *DmiAPIServer) GetLogLevel(context.Context, *dmi.GetLogLevelRequest) (*dmi.GetLogLevelResponse, error) {
 	return &dmi.GetLogLevelResponse{
-		Status: dmi.Status_OK_STATUS,
-		DeviceUuid: &dmi.Uuid{
-			Uuid: dms.uuid,
-		},
-		LogLevels: []*dmi.EntitiesLogLevel{},
+		Status:     dmi.Status_OK_STATUS,
+		DeviceUuid: dms.uuid,
+		LogLevels:  []*dmi.EntitiesLogLevel{},
 	}, nil
 }
 
 // SetLogLevel Sets the log level of the device, for each given entity to a certain level.
 func (dms *DmiAPIServer) SetLogLevel(context.Context, *dmi.SetLogLevelRequest) (*dmi.SetLogLevelResponse, error) {
 	return &dmi.SetLogLevelResponse{
-		Status: dmi.Status_OK_STATUS,
-		DeviceUuid: &dmi.Uuid{
-			Uuid: dms.uuid,
-		},
+		Status:     dmi.Status_OK_STATUS,
+		DeviceUuid: dms.uuid,
 	}, nil
 }
 
 // GetLoggableEntities Gets the entities of a device on which log can be configured.
 func (dms *DmiAPIServer) GetLoggableEntities(context.Context, *dmi.GetLoggableEntitiesRequest) (*dmi.GetLogLevelResponse, error) {
 	return &dmi.GetLogLevelResponse{
-		Status: dmi.Status_OK_STATUS,
-		DeviceUuid: &dmi.Uuid{
-			Uuid: dms.uuid,
-		},
-		LogLevels: []*dmi.EntitiesLogLevel{},
+		Status:     dmi.Status_OK_STATUS,
+		DeviceUuid: dms.uuid,
+		LogLevels:  []*dmi.EntitiesLogLevel{},
 	}, nil
 }
 
diff --git a/internal/bbsim/dmiserver/dmi_metrics_generator.go b/internal/bbsim/dmiserver/dmi_metrics_generator.go
index fdb5bb4..c76e940 100755
--- a/internal/bbsim/dmiserver/dmi_metrics_generator.go
+++ b/internal/bbsim/dmiserver/dmi_metrics_generator.go
@@ -238,9 +238,7 @@
 func updateMetricIDAndMetaData(id dmi.MetricNames, c *dmi.Component, apiSrv *DmiAPIServer, m *dmi.Metric) *dmi.Metric {
 	m.MetricId = id
 	m.MetricMetadata = &dmi.MetricMetaData{
-		DeviceUuid: &dmi.Uuid{
-			Uuid: apiSrv.uuid,
-		},
+		DeviceUuid:    apiSrv.uuid,
 		ComponentUuid: c.Uuid,
 		ComponentName: c.Name,
 	}
diff --git a/internal/bbsim/dmiserver/dmi_sw_mgmt.go b/internal/bbsim/dmiserver/dmi_sw_mgmt.go
index 0c89d42..934b41d 100755
--- a/internal/bbsim/dmiserver/dmi_sw_mgmt.go
+++ b/internal/bbsim/dmiserver/dmi_sw_mgmt.go
@@ -98,7 +98,7 @@
 		return status.Errorf(codes.InvalidArgument, "ConfigRequest is nil")
 	}
 
-	if request.DeviceUuid == nil || request.DeviceUuid.Uuid != dms.uuid {
+	if request.DeviceUuid == nil || request.DeviceUuid.Uuid != dms.uuid.Uuid {
 		if err := stream.Send(&dmi.ConfigResponse{
 			Status: dmi.Status_ERROR_STATUS,
 			Reason: dmi.ConfigResponse_UNKNOWN_DEVICE,
@@ -129,7 +129,7 @@
 		return nil, status.Errorf(codes.InvalidArgument, "DeviceUuid is nil")
 	}
 
-	if request.DeviceUuid.Uuid != dms.uuid {
+	if request.DeviceUuid.Uuid != dms.uuid.Uuid {
 		return &dmi.StartupConfigInfoResponse{
 			Status: dmi.Status_ERROR_STATUS,
 			Reason: dmi.StartupConfigInfoResponse_UNKNOWN_DEVICE,
diff --git a/internal/bbsim/dmiserver/dmi_transceiver.go b/internal/bbsim/dmiserver/dmi_transceiver.go
new file mode 100644
index 0000000..5dcc9b0
--- /dev/null
+++ b/internal/bbsim/dmiserver/dmi_transceiver.go
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dmiserver
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/opencord/bbsim/api/bbsim"
+	"github.com/opencord/bbsim/internal/bbsim/devices"
+	dmi "github.com/opencord/device-management-interface/go/dmi"
+	log "github.com/sirupsen/logrus"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+	ponInterfaceType = "pon"
+	alarmStatusRaise = "on"
+	alarmStatusClear = "off"
+)
+
+type Transceiver struct {
+	ID         uint32
+	Uuid       string
+	Name       string
+	Pons       []*devices.PonPort
+	Technology dmi.TransceiverType
+	//Setting this bool will prevent the transceiver
+	//from being plugged out while already out, and
+	//plugged in while already in, but won't prevent
+	//the associated PONs from being enabled in other
+	//ways while the transceiver is plugged out
+	PluggedIn bool
+}
+
+func newTransceiver(id uint32, pons []*devices.PonPort) *Transceiver {
+	return &Transceiver{
+		ID:         id,
+		Uuid:       getTransceiverUUID(id).Uuid,
+		Name:       getTransceiverName(id),
+		Pons:       pons,
+		Technology: dmi.TransceiverType_TYPE_UNDEFINED,
+		PluggedIn:  true,
+	}
+}
+
+func getTransceiverWithId(transId uint32, dms *DmiAPIServer) (*Transceiver, error) {
+	for _, t := range dms.Transceivers {
+		if t.ID == transId {
+			return t, nil
+		}
+	}
+
+	return nil, fmt.Errorf("Cannot find transceiver with ID %d", transId)
+}
+
+/////// Handler methods for grpc API
+
+func (s DmiAPIServer) GetTransceivers(ctx context.Context, req *bbsim.DmiEmpty) (*bbsim.Transceivers, error) {
+	res := &bbsim.Transceivers{
+		Items: []*bbsim.Transceiver{},
+	}
+
+	for _, t := range s.Transceivers {
+		item := bbsim.Transceiver{
+			ID:         t.ID,
+			UUID:       t.Uuid,
+			Name:       t.Name,
+			Technology: t.Technology.String(),
+			PluggedIn:  t.PluggedIn,
+			PonIds:     []uint32{},
+		}
+
+		for _, pon := range t.Pons {
+			item.PonIds = append(item.PonIds, pon.ID)
+		}
+
+		res.Items = append(res.Items, &item)
+	}
+
+	return res, nil
+}
+
+// PlugOutTransceiver plugs out the transceiver by its ID
+func (s DmiAPIServer) PlugOutTransceiver(ctx context.Context, req *bbsim.TransceiverRequest) (*bbsim.DmiResponse, error) {
+	logger.WithFields(log.Fields{
+		"IntfId": req.TransceiverId,
+	}).Infof("Received request to plug out PON transceiver")
+
+	res := &bbsim.DmiResponse{}
+	olt := devices.GetOLT()
+
+	//Generate DMI event
+	dmiServ, err := getDmiAPIServer()
+	if err != nil {
+		res.StatusCode = int32(codes.Unavailable)
+		res.Message = fmt.Sprintf("Cannot get DMI server instance: %v", err)
+		return res, nil
+	}
+
+	trans, err := getTransceiverWithId(req.TransceiverId, dmiServ)
+	if err != nil {
+		res.StatusCode = int32(codes.NotFound)
+		res.Message = fmt.Sprintf("Cannot find transceiver with ID %d: %v", req.TransceiverId, err)
+		return res, nil
+	}
+
+	if !trans.PluggedIn {
+		res.StatusCode = int32(codes.Aborted)
+		res.Message = fmt.Sprintf("Cannot plug out transceiver with ID %d since it's not plugged in", req.TransceiverId)
+		return res, nil
+	}
+
+	err = PlugoutTransceiverComponent(req.TransceiverId, dmiServ)
+	if err != nil {
+		res.StatusCode = int32(codes.NotFound)
+		res.Message = fmt.Sprintf("Cannot remove transceiver with ID %d: %v", req.TransceiverId, err)
+		return res, nil
+	}
+	logger.Debug("Removed transceiver from DMI inventory")
+
+	if olt.InternalState.Is(devices.OltInternalStateEnabled) {
+		logger.Debug("Sending alarms for transceiver plug out")
+		for _, pon := range trans.Pons {
+			if pon.InternalState.Is("enabled") {
+
+				if err = olt.SetAlarm(pon.ID, ponInterfaceType, alarmStatusRaise); err != nil {
+					logger.WithFields(log.Fields{
+						"ponId": pon.ID,
+						"err":   err,
+					}).Error("Cannot raise LOS alarm for PON")
+				}
+
+				if err = pon.InternalState.Event("disable"); err != nil {
+					logger.WithFields(log.Fields{
+						"ponId": pon.ID,
+						"err":   err,
+					}).Error("Cannot disable PON")
+					continue
+				}
+
+				for _, onu := range pon.Onus {
+					if err := onu.SetAlarm(bbsim.AlarmType_ONU_ALARM_LOS.String(), alarmStatusRaise); err != nil {
+						logger.WithFields(log.Fields{
+							"ponId": pon.ID,
+							"onuId": onu.ID,
+							"err":   err,
+						}).Error("Cannot raise LOS alarm for ONU")
+					}
+				}
+			}
+		}
+	} else {
+		logger.Debug("No operation on devices since the OLT is not enabled")
+	}
+
+	event := dmi.Event{
+		EventId: dmi.EventIds_EVENT_TRANSCEIVER_PLUG_OUT,
+		EventMetadata: &dmi.EventMetaData{
+			DeviceUuid: dmiServ.uuid,
+			ComponentUuid: &dmi.Uuid{
+				Uuid: trans.Uuid,
+			},
+			ComponentName: trans.Name,
+		},
+		RaisedTs: timestamppb.Now(),
+	}
+
+	sendOutEventOnKafka(event, dmiServ)
+	logger.Debug("Transceiver plug out event sent")
+
+	res.StatusCode = int32(codes.OK)
+	res.Message = fmt.Sprintf("Plugged out transceiver %d", req.TransceiverId)
+
+	return res, nil
+}
+
+// PlugInTransceiver plugs in the transceiver by its ID
+func (s DmiAPIServer) PlugInTransceiver(ctx context.Context, req *bbsim.TransceiverRequest) (*bbsim.DmiResponse, error) {
+	logger.WithFields(log.Fields{
+		"IntfId": req.TransceiverId,
+	}).Infof("Received request to plug in PON transceiver")
+
+	res := &bbsim.DmiResponse{}
+	olt := devices.GetOLT()
+
+	//Generate DMI event
+	dmiServ, err := getDmiAPIServer()
+	if err != nil {
+		res.StatusCode = int32(codes.Unavailable)
+		res.Message = fmt.Sprintf("Cannot get DMI server instance: %v", err)
+		return res, nil
+	}
+
+	trans, err := getTransceiverWithId(req.TransceiverId, dmiServ)
+	if err != nil {
+		res.StatusCode = int32(codes.NotFound)
+		res.Message = fmt.Sprintf("Cannot find transceiver with ID %d: %v", req.TransceiverId, err)
+		return res, nil
+	}
+
+	if trans.PluggedIn {
+		res.StatusCode = int32(codes.Aborted)
+		res.Message = fmt.Sprintf("Cannot plug in transceiver with ID %d since it's already plugged in", req.TransceiverId)
+		return res, nil
+	}
+
+	err = PluginTransceiverComponent(req.TransceiverId, dmiServ)
+	if err != nil {
+		res.StatusCode = int32(codes.NotFound)
+		res.Message = fmt.Sprintf("Cannot add transceiver with ID %d: %v", req.TransceiverId, err)
+		return res, nil
+	}
+	logger.Debug("Added transceiver to DMI inventory")
+
+	if olt.InternalState.Is(devices.OltInternalStateEnabled) {
+		logger.Debug("Sending alarms for transceiver plug in")
+		for _, pon := range trans.Pons {
+
+			if err = olt.SetAlarm(pon.ID, ponInterfaceType, alarmStatusClear); err != nil {
+				logger.WithFields(log.Fields{
+					"ponId": pon.ID,
+					"err":   err,
+				}).Error("Cannot clear LOS alarm for ONU")
+			}
+
+			if err = pon.InternalState.Event("enable"); err != nil {
+				logger.WithFields(log.Fields{
+					"ponId": pon.ID,
+					"err":   err,
+				}).Error("Cannot enable PON")
+				continue
+			}
+
+			for _, onu := range pon.Onus {
+				if err := onu.SetAlarm(bbsim.AlarmType_ONU_ALARM_LOS.String(), alarmStatusClear); err != nil {
+					logger.WithFields(log.Fields{
+						"ponId": pon.ID,
+						"onuId": onu.ID,
+						"err":   err,
+					}).Error("Cannot clear LOS alarm for ONU")
+				}
+			}
+		}
+	} else {
+		logger.Debug("No operation on devices since the OLT is not enabled")
+	}
+
+	event := dmi.Event{
+		EventId: dmi.EventIds_EVENT_TRANSCEIVER_PLUG_IN,
+		EventMetadata: &dmi.EventMetaData{
+			DeviceUuid: dmiServ.uuid,
+			ComponentUuid: &dmi.Uuid{
+				Uuid: trans.Uuid,
+			},
+			ComponentName: trans.Name,
+		},
+		RaisedTs: timestamppb.Now(),
+	}
+
+	sendOutEventOnKafka(event, dmiServ)
+	logger.Debug("Transceiver plug in event sent")
+
+	res.StatusCode = int32(codes.OK)
+	res.Message = fmt.Sprintf("Plugged in transceiver %d", req.TransceiverId)
+
+	return res, nil
+}
diff --git a/internal/bbsimctl/commands/dmi.go b/internal/bbsimctl/commands/dmi.go
new file mode 100644
index 0000000..526cbad
--- /dev/null
+++ b/internal/bbsimctl/commands/dmi.go
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package commands
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/jessevdk/go-flags"
+	"github.com/opencord/bbsim/api/bbsim"
+	pb "github.com/opencord/bbsim/api/bbsim"
+	"github.com/opencord/bbsim/internal/bbsimctl/config"
+	"github.com/opencord/cordctl/pkg/format"
+	log "github.com/sirupsen/logrus"
+	"google.golang.org/grpc"
+)
+
+const (
+	DEFAULT_TRANSCEIVER_HEADER_FORMAT = "table{{ .ID }}\t{{ .UUID }}\t{{ .Name }}\t{{ .Technology }}\t{{ .PluggedIn }}\t{{ .PonIds }}"
+)
+
+type DMIOptions struct {
+	Events      DmiEventOptions       `command:"events"`
+	Transceiver DmiTransceiverOptions `command:"transceiver"`
+}
+
+type DmiEventOptions struct {
+	Create DmiEventCreate `command:"create"`
+}
+
+type DmiEventCreate struct {
+	Args struct {
+		Name string
+	} `positional-args:"yes" required:"yes"`
+}
+
+type DmiTransceiverOptions struct {
+	PlugIn  DmiTransceiverPlugIn  `command:"plug_in"`
+	PlugOut DmiTransceiverPlugOut `command:"plug_out"`
+	List    DmiTransceiversList   `command:"list"`
+}
+
+type DmiTransceiversList struct {
+}
+
+type DmiTransceiverPlugIn struct {
+	Args struct {
+		TransceiverId uint32
+	} `positional-args:"yes" required:"yes"`
+}
+
+type DmiTransceiverPlugOut struct {
+	Args struct {
+		TransceiverId uint32
+	} `positional-args:"yes" required:"yes"`
+}
+
+func RegisterDMICommands(parser *flags.Parser) {
+	_, _ = parser.AddCommand("dmi", "DMI Commands", "Commands to create events", &DMIOptions{})
+}
+
+func dmiEventGrpcClient() (bbsim.BBsimDmiClient, *grpc.ClientConn) {
+	conn, err := grpc.Dial(config.DmiConfig.Server, grpc.WithInsecure())
+	if err != nil {
+		log.Errorf("BBsimDmiClient connection failed  : %v", err)
+		return nil, conn
+	}
+	return bbsim.NewBBsimDmiClient(conn), conn
+}
+
+// Execute create event
+func (o *DmiEventCreate) Execute(args []string) error {
+	client, conn := dmiEventGrpcClient()
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	req := bbsim.DmiEvent{EventName: o.Args.Name}
+	res, err := client.CreateEvent(ctx, &req)
+	if err != nil {
+		log.Errorf("Cannot create DMI event: %v", err)
+		return err
+	}
+
+	fmt.Println(fmt.Sprintf("[Status: %d] %s", res.StatusCode, res.Message))
+	return nil
+}
+
+//Print a list of the transceivers and their state
+func (pon *DmiTransceiversList) Execute(args []string) error {
+	client, conn := dmiEventGrpcClient()
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	transceivers, err := client.GetTransceivers(ctx, &pb.DmiEmpty{})
+	if err != nil {
+		log.Errorf("Cannot get transceivers list: %v", err)
+		return err
+	}
+
+	// print out
+	tableFormat := format.Format(DEFAULT_TRANSCEIVER_HEADER_FORMAT)
+
+	if err := tableFormat.Execute(os.Stdout, true, transceivers.Items); err != nil {
+		log.Fatalf("Error while formatting transceivers table: %s", err)
+	}
+
+	return nil
+}
+
+//Plug in the specified transceiver
+func (pon *DmiTransceiverPlugIn) Execute(args []string) error {
+	client, conn := dmiEventGrpcClient()
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	req := pb.TransceiverRequest{
+		TransceiverId: uint32(pon.Args.TransceiverId),
+	}
+
+	res, err := client.PlugInTransceiver(ctx, &req)
+	if err != nil {
+		log.Errorf("Cannot plug in PON transceiver: %v", err)
+		return err
+	}
+
+	fmt.Println(fmt.Sprintf("[Status: %d] %s", res.StatusCode, res.Message))
+	return nil
+}
+
+//Plug out the specified transceiver
+func (pon *DmiTransceiverPlugOut) Execute(args []string) error {
+	client, conn := dmiEventGrpcClient()
+	defer conn.Close()
+
+	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	req := pb.TransceiverRequest{
+		TransceiverId: uint32(pon.Args.TransceiverId),
+	}
+
+	res, err := client.PlugOutTransceiver(ctx, &req)
+	if err != nil {
+		log.Errorf("Cannot plug out PON transceiver: %v", err)
+		return err
+	}
+
+	fmt.Println(fmt.Sprintf("[Status: %d] %s", res.StatusCode, res.Message))
+	return nil
+}
diff --git a/internal/bbsimctl/commands/dmi_events.go b/internal/bbsimctl/commands/dmi_events.go
deleted file mode 100644
index 1608965..0000000
--- a/internal/bbsimctl/commands/dmi_events.go
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2018-present Open Networking Foundation
-
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
-
- * http://www.apache.org/licenses/LICENSE-2.0
-
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package commands
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/jessevdk/go-flags"
-	"github.com/opencord/bbsim/api/bbsim"
-	"github.com/opencord/bbsim/internal/bbsimctl/config"
-	log "github.com/sirupsen/logrus"
-	"google.golang.org/grpc"
-)
-
-type DMIOptions struct {
-	Events DmiEventOptions `command:"events"`
-}
-
-type DmiEventCreate struct {
-	Args struct {
-		Name string
-	} `positional-args:"yes" required:"yes"`
-}
-
-type DmiEventOptions struct {
-	Create DmiEventCreate `command:"create"`
-}
-
-func RegisterDMICommands(parser *flags.Parser) {
-	_, _ = parser.AddCommand("dmi", "DMI Commands", "Commands to create events", &DMIOptions{})
-}
-
-func dmiEventGrpcClient() (bbsim.BBsimDmiClient, *grpc.ClientConn) {
-	conn, err := grpc.Dial(config.DmiConfig.Server, grpc.WithInsecure())
-	if err != nil {
-		log.Errorf("BBsimDmiClient connection failed  : %v", err)
-		return nil, conn
-	}
-	return bbsim.NewBBsimDmiClient(conn), conn
-}
-
-// Execute create event
-func (o *DmiEventCreate) Execute(args []string) error {
-	client, conn := dmiEventGrpcClient()
-	defer conn.Close()
-
-	ctx, cancel := context.WithTimeout(context.Background(), config.GlobalConfig.Grpc.Timeout)
-	defer cancel()
-
-	req := bbsim.DmiEvent{EventName: o.Args.Name}
-	res, err := client.CreateEvent(ctx, &req)
-	if err != nil {
-		log.Errorf("Cannot create DMI event: %v", err)
-		return err
-	}
-
-	fmt.Println(fmt.Sprintf("[Status: %d] %s", res.StatusCode, res.Message))
-	return nil
-}