@Override
  public void deleteOrRemoveVolumesFromExportMask(
      URI arrayURI,
      URI exportGroupURI,
      URI exportMaskURI,
      List<URI> volumes,
      List<URI> initiatorURIs,
      TaskCompleter completer,
      String stepId) {
    try {
      StorageSystem array = _dbClient.queryObject(StorageSystem.class, arrayURI);
      BlockStorageDevice device = _blockController.getDevice(array.getSystemType());
      ExportMask exportMask = _dbClient.queryObject(ExportMask.class, exportMaskURI);
      WorkflowStepCompleter.stepExecuting(stepId);

      // If the exportMask isn't found, or has been deleted, nothing to do.
      if (exportMask == null || exportMask.getInactive()) {
        _log.info(String.format("ExportMask %s inactive, returning success", exportMaskURI));
        WorkflowStepCompleter.stepSucceded(stepId);
        return;
      }

      // Protect concurrent operations by locking {host, array} dupple.
      // Lock will be released when workflow step completes.
      List<String> lockKeys =
          ControllerLockingUtil.getHostStorageLockKeys(
              _dbClient,
              ExportGroupType.Host,
              StringSetUtil.stringSetToUriList(exportMask.getInitiators()),
              arrayURI);
      getWorkflowService()
          .acquireWorkflowStepLocks(
              stepId, lockKeys, LockTimeoutValue.get(LockType.VPLEX_BACKEND_EXPORT));

      // Make sure the completer will complete the workflow. This happens on rollback case.
      if (!completer.getOpId().equals(stepId)) {
        completer.setOpId(stepId);
      }

      // Refresh the ExportMask
      exportMask = refreshExportMask(array, device, exportMask);

      // Determine if we're deleting the last volume in the mask.
      StringMap maskVolumesMap = exportMask.getVolumes();
      Set<String> remainingVolumes = new HashSet<String>();
      List<URI> passedVolumesInMask = new ArrayList<>(volumes);
      if (maskVolumesMap != null) {
        remainingVolumes.addAll(maskVolumesMap.keySet());
      }
      for (URI volume : volumes) {
        remainingVolumes.remove(volume.toString());

        // Remove any volumes from the volume list that are no longer
        // in the export mask. When a failure occurs removing a backend
        // volume from a mask, the rollback method will try and remove it
        // again. However, in the case of a distributed volume, one side
        // may have succeeded, so we will try and remove it again. Previously,
        // this was not a problem. However, new validation exists at the
        // block level that checks to make sure the volume to remove is
        // actually in the mask, which now causes a failure when you remove
        // it a second time. So, we check here and remove any volumes that
        // are not in the mask to handle this condition.
        if ((maskVolumesMap != null) && (!maskVolumesMap.keySet().contains(volume.toString()))) {
          passedVolumesInMask.remove(volume);
        }
      }

      // None of the volumes is in the export mask, so we are done.
      if (passedVolumesInMask.isEmpty()) {
        _log.info(
            "None of these volumes {} are in export mask {}", volumes, exportMask.forDisplay());
        WorkflowStepCompleter.stepSucceded(stepId);
        return;
      }

      // If it is last volume and there are no existing volumes, delete the ExportMask.
      if (remainingVolumes.isEmpty() && !exportMask.hasAnyExistingVolumes()) {
        device.doExportDelete(array, exportMask, passedVolumesInMask, initiatorURIs, completer);
      } else {
        List<Initiator> initiators = null;
        if (initiatorURIs != null && !initiatorURIs.isEmpty()) {
          initiators = _dbClient.queryObject(Initiator.class, initiatorURIs);
        }
        device.doExportRemoveVolumes(array, exportMask, passedVolumesInMask, initiators, completer);
      }
    } catch (Exception ex) {
      _log.error("Failed to delete or remove volumes to export mask for vnx: ", ex);
      VPlexApiException vplexex =
          DeviceControllerExceptions.vplex.addStepsForCreateVolumesFailed(ex);
      WorkflowStepCompleter.stepFailed(stepId, vplexex);
    }
  }
  /** {@inheritDoc} */
  @Override
  public void updateStatus(JobContext jobContext) throws Exception {
    JobStatus jobStatus = getJobStatus();
    CloseableIterator<CIMObjectPath> volumeIter = null;
    try {
      DbClient dbClient = jobContext.getDbClient();
      TaskCompleter completer = getTaskCompleter();
      BlockSnapshot snapshot = dbClient.queryObject(BlockSnapshot.class, _snapshotURI);
      if (jobStatus == JobStatus.IN_PROGRESS) {
        return;
      }
      if (jobStatus == JobStatus.SUCCESS) {
        s_logger.info(
            "Post-processing successful link snapshot session target {} for task {}",
            snapshot.getId(),
            completer.getOpId());
        // Get the snapshot session to which the target is being linked.
        BlockSnapshotSession snapSession =
            dbClient.queryObject(BlockSnapshotSession.class, completer.getId());

        // Get the snapshot device ID and set it against the BlockSnapshot object.
        BlockObject sourceObj = BlockObject.fetch(dbClient, snapshot.getParent().getURI());
        CIMConnectionFactory cimConnectionFactory = jobContext.getCimConnectionFactory();
        WBEMClient client = getWBEMClient(dbClient, cimConnectionFactory);
        volumeIter =
            client.associatorNames(getCimJob(), null, SmisConstants.CIM_STORAGE_VOLUME, null, null);
        while (volumeIter.hasNext()) {
          // Get the sync volume native device id
          CIMObjectPath volumePath = volumeIter.next();
          s_logger.info("volumePath: {}", volumePath.toString());
          CIMInstance volume = client.getInstance(volumePath, false, false, null);
          String volumeDeviceId =
              volumePath.getKey(SmisConstants.CP_DEVICE_ID).getValue().toString();
          s_logger.info("volumeDeviceId: {}", volumeDeviceId);
          if (volumeDeviceId.equals(sourceObj.getNativeId())) {
            // Don't want the source, we want the linked target.
            continue;
          }
          String volumeElementName =
              CIMPropertyFactory.getPropertyValue(volume, SmisConstants.CP_ELEMENT_NAME);
          s_logger.info("volumeElementName: {}", volumeElementName);
          String volumeWWN = CIMPropertyFactory.getPropertyValue(volume, SmisConstants.CP_WWN_NAME);
          s_logger.info("volumeWWN: {}", volumeWWN);
          String volumeAltName = CIMPropertyFactory.getPropertyValue(volume, SmisConstants.CP_NAME);
          s_logger.info("volumeAltName: {}", volumeAltName);
          StorageSystem system = dbClient.queryObject(StorageSystem.class, getStorageSystemURI());
          snapshot.setNativeId(volumeDeviceId);
          snapshot.setNativeGuid(NativeGUIDGenerator.generateNativeGuid(system, snapshot));
          snapshot.setDeviceLabel(volumeElementName);
          snapshot.setInactive(false);
          snapshot.setIsSyncActive(Boolean.TRUE);
          snapshot.setCreationTime(Calendar.getInstance());
          snapshot.setWWN(volumeWWN.toUpperCase());
          snapshot.setAlternateName(volumeAltName);
          snapshot.setSettingsInstance(snapSession.getSessionInstance());
          commonSnapshotUpdate(
              snapshot,
              volume,
              client,
              system,
              sourceObj.getNativeId(),
              volumeDeviceId,
              false,
              dbClient);
          s_logger.info(
              String.format(
                  "For target volume path %1$s, going to set blocksnapshot %2$s nativeId to %3$s (%4$s). Associated volume is %5$s (%6$s)",
                  volumePath.toString(),
                  snapshot.getId().toString(),
                  volumeDeviceId,
                  volumeElementName,
                  sourceObj.getNativeId(),
                  sourceObj.getDeviceLabel()));
          dbClient.updateObject(snapshot);
        }
      } else if (jobStatus == JobStatus.FAILED || jobStatus == JobStatus.FATAL_ERROR) {
        s_logger.info(
            "Failed to link snapshot session target {} for task {}",
            snapshot.getId(),
            completer.getOpId());
        snapshot.setInactive(true);
        dbClient.updateObject(snapshot);
      }
    } catch (Exception e) {
      setPostProcessingErrorStatus(
          "Encountered an internal error in link snapshot session target job status processing: "
              + e.getMessage());
      s_logger.error(
          "Encountered an internal error in link snapshot session target job status processing", e);
    } finally {
      if (volumeIter != null) {
        volumeIter.close();
      }
      super.updateStatus(jobContext);
    }
  }
  @Override
  public void deleteOrRemoveVolumesFromExportMask(
      URI arrayURI,
      URI exportGroupURI,
      URI exportMaskURI,
      List<URI> volumes,
      TaskCompleter completer,
      String stepId) {
    try {
      WorkflowStepCompleter.stepExecuting(stepId);
      StorageSystem array = _dbClient.queryObject(StorageSystem.class, arrayURI);
      BlockStorageDevice device = _blockController.getDevice(array.getSystemType());
      ExportMask exportMask = _dbClient.queryObject(ExportMask.class, exportMaskURI);

      // If the exportMask isn't found, or has been deleted, nothing to do.
      if (exportMask == null || exportMask.getInactive()) {
        _log.info(String.format("ExportMask %s inactive, returning success", exportMaskURI));
        WorkflowStepCompleter.stepSucceded(stepId);
        return;
      }

      // Protect concurrent operations by locking {host, array} dupple.
      // Lock will be released when work flow step completes.
      List<String> lockKeys =
          ControllerLockingUtil.getHostStorageLockKeys(
              _dbClient,
              ExportGroupType.Host,
              StringSetUtil.stringSetToUriList(exportMask.getInitiators()),
              arrayURI);
      getWorkflowService()
          .acquireWorkflowStepLocks(
              stepId, lockKeys, LockTimeoutValue.get(LockType.VPLEX_BACKEND_EXPORT));

      // Make sure the completer will complete the work flow. This happens on roll back case.
      if (!completer.getOpId().equals(stepId)) {
        completer.setOpId(stepId);
      }

      // Refresh the ExportMask
      exportMask = refreshExportMask(array, device, exportMask);

      // Determine if we're deleting the last volume.
      Set<String> remainingVolumes = new HashSet<String>();
      if (exportMask.getVolumes() != null) {
        remainingVolumes.addAll(exportMask.getVolumes().keySet());
      }

      for (URI volume : volumes) {
        remainingVolumes.remove(volume.toString());
      }

      // If it is last volume, delete the ExportMask.
      if (remainingVolumes.isEmpty()
          && (exportMask.getExistingVolumes() == null
              || exportMask.getExistingVolumes().isEmpty())) {
        _log.debug(
            String.format(
                "Calling doExportGroupDelete on the device %s", array.getId().toString()));
        device.doExportGroupDelete(array, exportMask, completer);
      } else {
        _log.debug(
            String.format(
                "Calling doExportRemoveVolumes on the device %s", array.getId().toString()));
        device.doExportRemoveVolumes(array, exportMask, volumes, completer);
      }
    } catch (Exception ex) {
      _log.error("Failed to delete or remove volumes to export mask for cinder: ", ex);
      VPlexApiException vplexex =
          DeviceControllerExceptions.vplex.addStepsForDeleteVolumesFailed(ex);
      WorkflowStepCompleter.stepFailed(stepId, vplexex);
    }
  }