# Copyright 2019 VMware, Inc.  All rights reserved. -- VMware Confidential
#
'''
This module provides the base transport functionality needed to perform the base
operations on target machine.
'''
#pylint: disable=W0613
import sys
import logging
import tempfile
import os
import signal
import platform

PY2 = sys.version_info[0] == 2

logger = logging.getLogger(__name__)

class TransportException(Exception):
    '''The base exception of all transport exceptions. All other transport
    exception should inherit this class.
    '''

class FileException(TransportException):
    '''Thrown in case FileManager cannot do the requested file operation.
    '''

class ErrorCode(object):
    '''Error code definition. The code definition is used when want to raise
    Execution exception.
    '''
    # The request is invalid
    INVALID_REQUEST = 1
    # The arguments type is invalid or the argument value is incorrect
    INVALID_ARGUMENTS = 2

    # There is no enough storage to perform the operation
    INSUFFICIENT_STORAGE = 30

    # The operation is not supported on that platform
    INVALID_PLATFORM = 40

    # Internal error occurred
    UNKNOWN = 999

class ExecutionException(TransportException):
    '''Thrown in case the requested execution cannot be performed.
    '''
    def __init__(self, msg, error_code=ErrorCode.UNKNOWN):
        super(ExecutionException, self).__init__(msg, error_code)
        self.msg = msg
        self.error_code = error_code

class CommunicationException(TransportException):
    '''Thrown in case communication to target machine is down.
    '''

class NotSupportedException(Exception):
    '''Thrown in case the method is not supported for given implementation.
        '''

class AuthenticationException(Exception):
    '''Thrown in in case login to target machine failed.
        '''

def remove(fileManager, targetPath):
    '''Remove the specified file path regardless regular file or directory.

    @param fileManager: File manager instance
    @type fileManager: transport.FileManager

    @param targetPath: Directory on target machine
    @type targetPath: str

    @raise FileException: If
      * the target path exists, but cannot be removed.
    @raise CommunicationException: If target machine is unreachable
    '''
    if fileManager.pathExists(targetPath):
        if fileManager.isDirectory(targetPath):
            fileManager.removeDirectory(targetPath)
        elif fileManager.isFile(targetPath):
            fileManager.removeFile(targetPath)

def executeCommand(processManager, commandArgs, cwd=None, localStdinFile=None,
                   env=None, keepAsBytes=PY2):
    '''Executes a command on target host and return the result kept in the
      memory. This routine waits for the process to finish.

    @param processManager: Process manager instance
    @type processManager: transport.ProcessManager

    @param commandArgs: List command and arguments
    @type commandArgs: List, e.g. ['ls', '-l']

    @param cwd: The CWD of started process
    @type cwd: str

    @param localStdinFile: local file to be used as stdin, None for no stdin
    @param type: str

    @param env: extension of local environment. Pay attention that
      duplicated variables would be replaced rather than extending their
      value, i.e. if you want to extend that PATH variable you need to
      retrieve the local PATH value, extend its value and pass the extended
      environment variable.
    @type env: Dictionary where key points to the env variable name and
      value to the env value. Both keys and values must be strings.

    @param keepAsBytes: Indicates if the return type of the stdout and stderr
        should be unicode or ascii. Should be noted that those are different
        types on python2 and python3 and thus it has a default of True for
        Py2 and False for Py3 thus it by default returns the str type on both
        pythons. On Py2 with False will return unicode. On Py3 with True will
        return bytes
    @type keepAsBytes: bool

    @return: The process result
    @rtype: ProcessResult

    @raise ExecutionException: If the command cannot be executed
    @raise CommunicationException: If target machine is unreachable
    '''
    processUid = processManager.startProcess(commandArgs, cwd, localStdinFile, env=env)
    return processManager.pollProcess(processUid, True, keepAsBytes=keepAsBytes)

def getCommandExitCode(processManager, commandArgs, cwd=None, localStdinFile=None,
                localStdoutFile=None, localStderrFile=None, env=None, supressLogging=False):
    '''Executes a command on target host, capture the stdout and stderr in
      files and return the process exit code. This routine waits for the
      process to finish before return the result.

    @param processManager: Process manager instance
    @type processManager: transport.ProcessManager

    @param commandArgs: List command and arguments
    @type commandArgs: List, e.g. ['ls', '-l']

    @param cwd: The CWD of started process
    @type cwd: str

    @param localStdin: Local file to be used as stdin, None for no stdin
    @type localStdinFile: str

    @param localStdoutFile: Local file to capture stdout to, None for no
      stdout capture
    @type localStdoutFile: str

    @param localStderrFile: Local file to capture stderr to, None for no
      stderr capture
    @type localStdErr: str

    @param env: extension of local environment. Pay attention that
      duplicated variables would be replaced rather than extending their
      value, i.e. if you want to extend that PATH variable you need to
      retrieve the local PATH value, extend its value and pass the extended
      environment variable.
    @type env: Dictionary where key points to the env variable name and
      value to the env value. Both keys and values must be strings.


    @param supressLogging: Suppress command stdout and stderr logging in case of error
    @type supressLogging: boolean

    @return: The exit code of finished process
    @rtype: int

    @raise ExecutionException: If the command cannot be executed
    @raise CommunicationException: If target machine is unreachable
    '''
    processUuid = processManager.startProcess(commandArgs, cwd, localStdinFile,
                                    localStdoutFile, localStderrFile, env)
    result = processManager.pollProcess(processUuid, True)

    if result.exitCode and not supressLogging:
        logger.error('Command %s exit-code=%s, stdout=%s, stderr=%s',
                     commandArgs, result.exitCode, result.stdout.strip(), result.stderr.strip())

    return result.exitCode

def getCommandOutput(processManager, commandArgs, supressLogging=False,
                     keepAsBytes=PY2):
    '''Executes a command on target host and return the stdout as a string.
      This routine waits for the process to finish before return the result.

    @param processManager: Process manager instance
    @type processManager: transport.ProcessManager

    @param commandArgs: List command and arguments
    @type commandArgs: List, e.g. ['ls', '-l']

    @param supressLogging: Suppress command stdout and stderr logging in case of error
    @type supressLogging: boolean

    @param keepAsBytes: Indicates if the return type of the stdout and stderr
        should be unicode or ascii. Should be noted that those are different
        types on python2 and python3 and thus it has a default of True for
        Py2 and False for Py3 thus it by default returns the str type on both
        pythons. On Py2 with False will return unicode. On Py3 with True will
        return bytes
    @type keepAsBytes: bool

    @return: The stdout of executed command.
    @rtype: str

    @raise ExecutionException: If the command cannot be executed
    '''
    result = executeCommand(processManager, commandArgs, keepAsBytes=keepAsBytes)

    if result.exitCode:
        if not supressLogging:
            logger.error('Command %s exit-code=%s, stdout=%s, stderr=%s',
                         commandArgs, result.exitCode, result.stdout.strip(), result.stderr.strip())

        raise ExecutionException('Command failed with exit-code "%s"' % result.exitCode)

    return result.stdout

def joinPath(targetOS, *p):
    if targetOS.lower() == 'windows':
        import ntpath
        return ntpath.join(*p) #pylint: disable=E1120
    elif targetOS.lower() == 'linux':
        import posixpath
        return posixpath.join(*p) #pylint: disable=E1120
    else:
        raise ValueError('Unsupported target OS -- %s.' % targetOS)

def dirname(targetOS, path):
    if targetOS.lower() == 'windows':
        import ntpath
        return ntpath.dirname(path)
    elif targetOS.lower() == 'linux':
        import posixpath
        return posixpath.dirname(path)
    else:
        raise ValueError('Unsupported target OS -- %s.' % targetOS)

def basename(targetOS, path):
    if targetOS.lower() == 'windows':
        import ntpath
        return ntpath.basename(path)
    elif targetOS.lower() == 'linux':
        import posixpath
        return posixpath.basename(path)
    else:
        raise ValueError('Unsupported target OS -- %s.' % targetOS)

def normpath(targetOS, path):
    if targetOS.lower() == 'windows':
        import ntpath
        return ntpath.normpath(path)
    else:
        import posixpath
        return posixpath.normpath(path)

def isabs(targetOS, path):
    if targetOS.lower() == 'windows':
        import ntpath
        return ntpath.isabs(path)
    else:
        import posixpath
        return posixpath.isabs(path)

def abspath(targetOS, baseDir, path):
    ''' Return path joined with baseDir if path is a relative pathname.'''
    if not path:
        return path

    path = normpath(targetOS, path)

    if targetOS.lower() == 'windows':
        import ntpath
        if not ntpath.isabs(path):
            return ntpath.join(baseDir, path)
    else:
        import posixpath
        if not posixpath.isabs(path):
            return posixpath.join(baseDir, path)

    return path

class FileFilter(object):
    '''Class responsible to decide if the file needs to be migrated or not.
    '''
    def filter(self, sourceFilePath, sourceOpManager):
        '''Tests if the file has to be filtered and stop being copied over the
        destination.

        @param sourceFilePath: Path to the file on source host
        @type sourceFilePath: str

        @param sourceOpManager: Source operation manager
        @type sourceOpManager: OperationsManager
        '''
        return False

class FileCopier(object):
    '''Class responsible to move regular file from one machine to another one.
    '''
    def copy(self, srcPath, sourceOpManager, destPath, destOpManager):
        '''Copy regular file from srcPath on source host over the destination on
        destPath.

        @param srcPath: Path to the file which content should be downloaded
        @type srcPath: str

        @param sourceOpManager: Source operation manager
        @type sourceOpManager: OperationsManager

        @param srcPath: Path to the file which content should be overwritten
        @type srcPath: str

        @param destOpManager: Destination operation manager
        @type destOpManager: OperationsManager

        @raise FileException: If destination path exists and is not a regular
          file
        @raise CommunicationException: If target machine is unreachable
        '''
        if destOpManager.pathExists(destPath):
            if destOpManager.isDirectory(destPath):
                error = "Cannot upload regular file to directory '%s'" % destPath
                logger.error(error)
                raise FileException(error)
            else:
                logger.info("File %s content will be overwritten", destPath)
                destOpManager.removeFile(destPath)

        with tempfile.NamedTemporaryFile(prefix='tmp_download', delete=False) as tmp:
            localTmpPath = os.path.abspath(tmp.name)

        try:
            sourceOpManager.downloadFile(srcPath, localTmpPath)
            destOpManager.uploadFile(localTmpPath, destPath)
        except: #pylint: disable=W0702
            os.remove(localTmpPath)
            raise

def move(sourceDirPath, sourceOpManager, destDirPath, destOpManager,
         fileCopier=FileCopier(),
         fileFilter=FileFilter()):
    '''Moves the content of source dir and merge it with the content of
    destination directory. If a file with same relative path already exists in
    destination directory, then its content will be overwritten.

    @param sourceDirPath: Path to the directory which content should be downloaded
    @type sourceDirPath: str

    @param sourceOpManager: Source operation manager
    @type sourceOpManager: OperationsManager

    @param destDirPath: Path to the directory which content should be overwritten
    @type destDirPath: str

    @param destOpManager: Destination operation manager
    @type destOpManager: OperationsManager

    @param fileCopier: File copier manager
    @type fileCopier: FileCopier

    @param fileFilter: Manager which decides if the file has to be copied or not
    @type fileFilter: FileFilter

    @raise FileException: If
      * if source and destination directories have children with same name where
      one of it is a file, but the other one is directory
      * sourceDirPath does not exist.
      * sourceDirPath exists but it is not a valid regular directory.
    @raise CommunicationException: If target machine is unreachable
    '''
    if sourceOpManager == destOpManager and sourceDirPath == destDirPath:
        # No need to move anything on local setup with same directories.
        return

    if not destOpManager.pathExists(destDirPath):
        # Create destination directory
        destOpManager.createDirectory(destDirPath)
    elif not destOpManager.isDirectory(destDirPath):
        error = "Destination dir '%s' is not a valid directory" % destDirPath
        logger.error(error)
        raise FileException(error)

    if not sourceOpManager.isDirectory(sourceDirPath):
        error = "Source dir '%s' does not exist" % sourceDirPath
        logger.error(error)
        raise FileException(error)

    for srcPath in sourceOpManager.listFiles(sourceDirPath):
        fileName = basename(sourceOpManager.getPlatform(), srcPath)
        destPath = joinPath(destOpManager.getPlatform(), destDirPath, fileName)

        # TODO iradev: This might be automatically filtered by opManager
        if fileName.startswith('.'):
            # Exclude copying private files
            continue

        if fileFilter.filter(srcPath, sourceOpManager=sourceOpManager):
            logger.info('Skip downloading %s', srcPath)
            continue

        if sourceOpManager.isDirectory(srcPath):
            if destOpManager.isFile(destPath):
                error = "Cannot upload directory to regular file '%s'" % destPath
                logger.error(error)
                raise FileException(error)

            move(srcPath, sourceOpManager, destPath, destOpManager)
        else:
            fileCopier.copy(srcPath, sourceOpManager, destPath, destOpManager)

###################################################################
##################### FILE MANAGER API ############################
###################################################################

class FileManager(object):
    '''Manager responsible to perform file operations.
    '''
    def createTempFile(self, prefix='tmp', suffix=''):
        '''
        Creates temporary file on target machine.

        @param prefix: The prefix to be given to the new temporary file.
        @type prefix: str

        @param suffix: The prefix to be given to the new temporary file.
        @type suffix: str

        @return: The absolute path of the temporary file that is created.
        @rtype: str

        @raise FileException: If the file cannot be created.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def createTempDirectory(self, prefix='tmp', suffix = ''):
        '''
        Creates temporary directory on target machine.

        @param prefix: The prefix to be given to the new temporary directory.
        @type prefix: str

        @param prefix: The prefix to be given to the new temporary directory.
        @type prefix: str

        @return: The absolute path of the temporary directory that is created.
        @rtype: str

        @raise FileException: If the directory cannot be created.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def downloadFileContent(self, targetPath, keepAsBytes=PY2):
        '''Retrieves file content from a file located on target machine.

        @param targetPath: File path on target machine
        @type targetPath: str

        @param keepAsBytes: Indicates if the return type should be unicode or
            ascii. Should be noted that those are different types on python2
            and python3 and thus it has a default of True for Py2 and False
            for Py3 thus it by default returns the str type on both pythons. On
            Py2 with False will return unicode. On Py3 with True will
            return bytes
        @type keepAsBytes: bool

        @return: File content
        @rtype: str

        @raise FileException: If the target path cannot be accessed or the file
          is not a regular file.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def downloadFile(self, targetPath, localPath, fileMode=None,
                     createLocalPath=True):
        '''
        Downloads target file to local machine.

        @param targetPath: File path on target machine
        @type targetPath: str

        @param localPath: Local file path
        @type localPath: str

        @param fileMode: Optional file permissions. Default permission would be
          the file permissions on the target file.
        @type fileMode: int. The number should be Oct number
          Example: 0640

        @param createLocalPath: Whether to create the path to local file in case
          it does not exist.
        @type createLocalPath: boolean

        @raise FileException: If
          * the target path cannot be accessed or target file is not a regular file
          * localPath exists but it is not a valid regular file.
          * localParentDir does not exist and createLocalPath is set to False
          * localParentDir is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def createDirectory(self, targetPath):
        '''
        Creates directory on target machine.
        If directory already exists does not report error

        @param targetPath: Directory on target machine to be created.
        @type targetPath: str

        @raise FileException: If
            * the directory cannot be created.
            * targetPath is a regular file
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def downloadDirectory(self, targetPath, localPath, createLocalPath=True):
        '''
        Downloads whole target directory to local machine. All directories and
        files in target directory will also be downloaded.

        @param targetPath: Directory on target machine which will be downloaded
        @type targetPath: str

        @param localPath: Local directory path
        @type localPath: str

        @param createLocalPath: Whether to create the path to local directory
          in case it does not exist.
        @type createLocalPath: boolean

        @raise FileException: If
          * target directory is not valid directory or cannot be traversed
          * localPath does not exist and createLocalPath is set to False
          * localPath is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def uploadFileContent(self, fileContent, targetPath, fileMode=None,
                          createTargetPath=True):
        '''
        Uploads file content to a file located on target machine.

        @param fileContent: File content.
        @type fileContent: str

        @param targetPath: File path on target machine
        @type targetPath: str

        @param fileMode: Optional file permissions. Default permission would be
          0444 r--r--r--.
        @type fileMode: int. The number should be Oct number
          Example: 0640

        @param createTargetPath: Whether to create the path to target file in
          case it does not exist.
        @type createTargetPath: boolean

        @raise FileException: If
          * cannot write to the target file or target file is not a valid
        regular file.
          * targetPath parent directory does not exist and createTargetPath is
        set to False
          * targetPath is not a valid regular file.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def uploadFile(self, localPath, targetPath, fileMode=None,
                   createTargetPath=True):
        '''
        Uploads local file to a file on target machine.

        @param localPath: Local file path
        @type localPath: str

        @param targetPath: File path on target machine
        @type targetPath: str

        @param fileMode: Optional file permissions. Default permission would be
          the permission of local path file.
        @type fileMode: int. The number should be Oct number
          Example: 0640

        @param createTargetPath: Whether to create the path to target directory in
          case it does not exist..
        @type createTargetPath: boolean

        @raise FileException: If
          * the local path cannot be accessed or local file is not a regular file.
          * targetPath exists but it is not a valid regular directory.
          * targetParentDir does not exist and createTargetPath is set to False
          * targetParentDir is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def uploadDirectory(self, localPath, targetPath, createTargetPath=True):
        '''
        Uploads local directory to a directory on target machine.

        @param localPath: Local directory
        @type localPath: str

        @param targetPath: Directory on target machine
        @type targetPath: str

        @param createTargetPath: Whether to create the path to target directory
          if it does not exist.
        @type createTargetPath: boolean

        @raise FileException: If
          * local directory is not valid directory or cannot be traversed.
          * targetParentDir does not exist and createTargetPath is set to False
          * targetParentDir is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def pathExists(self, targetPath):
        '''
        Check whether the path on target machine exists.

        @param targetPath: File or directory path on target machine
        @type targetPath: str

        @returns: True if the path exists, otherwise False.
        @rtype : boolean

        @raise FileException: If cannot check for path existence.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def isFile(self, targetPath):
        '''
        Checks if the target path exists and is a regular file.

        @param targetPath: File path on target machine
        @type targetPath: str

        @return: True if target path exists and is regular file, otherwise False
        @rtype: boolean

        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def isDirectory(self, targetPath):
        '''
        Checks if the target path exists and is a valid directory.

        @param targetPath: File path on target machine
        @type targetPath: str

        @return: True if target path exists and is regular dir, otherwise False
        @rtype: boolean

        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def removeFile(self, targetPath):
        '''
        Removes file located on target machine. If the file does not exist, the
          method should complete successfully.

        @param targetPath: File path on target machine
        @type targetPath: str

        @raise FileException: If
          * the target file exists but cannot be removed.
          * the target file is not a valid regular file.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def removeDirectory(self, targetPath):
        '''
        Removes directory located on target machine. All files and directory
          under that directory will be also removed. If the directory does not
          exist, the method should complete successfully.

        @param targetPath: Directory on target machine
        @type targetPath: str

        @raise FileException: If
          * the target directory exists, but cannot be removed.
          * the target path is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def listFiles(self, targetPath):
        '''
        Gets all files under directory located on target machine.

        @param targetPath: Directory on target machine
        @type targetPath: str

        @return: Path to the files inside that directory. If target path does not
          exist or is a regular file the method should return empty array.
        @rtype: Array of strings

        @raise FileException: If the target directory cannot be listed.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def zipFile(self, targetPath, localZipFilePath, createLocalPath=True):
        '''
        Zips remote file and stores it in a local archive.

        @param targetPath: Remote regular file path.
        @type targetPath: str

        @param localZipFilePath: Local archive path
        @type localZipFilePath: str

        @param createLocalPath: Whether to create the path to local zip file in
          case it does not exist.
        @type createLocalPath: boolean

        @raise FileException: If
          * the target path cannot be accessed or is not regular file
          * localZipFilePath exists but it is not a valid regular file.
          * local parent dir does not exist and createLocalPath is set to False
          * local parent dir is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def zipDirectory(self, targetPath, localZipFilePath, createLocalPath=True):
        '''
        Zips remote directory and stores it in a local archive.

        @param targetPath: Remote directory or regular file path.
        @type targetPath: str

        @param localZipFilePath: Local archive path
        @type localZipFilePath: str

        @param createLocalPath: Whether to create the path to local zip file in
          case it does not exist.
        @type createLocalPath: boolean

        @raise FileException: If
          * the target path cannot be accessed or is not valid directory
          * localZipFilePath exists but it is not a valid regular file.
          * local parent dir does not exist and createLocalPath is set to False
          * local parent dir is not a valid directory.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def absPath(self, targetPath):
        '''
        Return absolute path on target machine.

        @param targetPath: Remote file path.
        @type targetPath: str

        @returns: The absolute path.
        @rtype : string

        @raise FileException: If cannot check for path existence.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

###################################################################
##################### PROCESS MANAGER API #########################
###################################################################

class ProcessResult():
    '''The result of executed process.
    '''
    NOT_COMPLETED = None

    def __init__(self, pid, startTime, exitCode=NOT_COMPLETED, finishTime=None,
                 stdout=None, stderr=None):
        '''Instantiates process result.

        @param pid: PID of started process
        @type pid: int

        @param startTime: The time when the process has been started
        @type startTime: floating point number

        @param exitCode: Exit code of the program. Should be set only if program
          completed
        @type exitCode: int

        @param finishTime: The time when the process has been finished. Should
          be set only if program completed.
        @type finishTime: floating point number

        @param stdout: Stdout of started process.
        @type stdout: str

        @param stderr: Stderr of started process.
        @type stderr: str
        '''
        self.pid = pid
        self.startTime = startTime
        self.exitCode = exitCode
        self.finishTime = finishTime
        self.stdout = stdout
        self.stderr = stderr

    def isCompleted(self):
        return self.exitCode != ProcessResult.NOT_COMPLETED

class ProcessManager(object):
    '''Manager responsible to execute commands on target machine.
    '''
    def getEnvironmentVariable(self, name, defaultValue=None):
        '''Retrieves the value of environment variable defined on target machine.

        @param name: Environment variable name
        @type name: str

        @param defaultValue: The value which will be returned if the target
          environment variable is not defined.
        @type defaultValue: str

        @return: The environment variable value if the environment variable
          exists, otherwise provided default value.
        @rtype: str

        @raise ExecutionException: If the command cannot be executed
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def getPlatform(self):
        '''Gets the platform name on the target machine.

        @return: The platform name: e.g. Linux, Windows, etc
        @rtype: str

        @raise ExecutionException: If the command cannot be executed
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def getAddress(self):
        '''Gets the address of the target machine.

        @return: Address of the target machine
        @rtype: str

        @raise ExecutionException: If the command cannot be executed
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def startProcess(self, commandArgs, cwd=None, localStdinFile=None,
                    localStdoutFile=None, localStderrFile=None, env=None):
        '''Starts a command on target host and return the process uuid which
        could be polled later.

        @param commandArgs: List command and arguments
        @type commandArgs: List, e.g. ['ls', '-l']

        @param cwd: The CWD of started process
        @type cwd: str

        @param localStdin: Local file to be used as stdin, None for no stdin
        @type localStdinFile: str

        @param localStdoutFile: Local file to capture stdout to, None for no
          stdout capture
        @type localStdoutFile: str

        @param localStderrFile: Local file to capture stderr to, None for no
          stderr capture
        @type localStdErr: str

        @param env: extension of local environment. Pay attention that
          duplicated variables would be replaced rather than extending their
          value, i.e. if you want to extend that PATH variable you need to
          retrieve the local PATH value, extend its value and pass the extended
          environment variable.
        @type env: Dictionary where key points to the env variable name and
          value to the env value. Both keys and values must be strings.

        @return: The process uuid. The process can be polled by pollProcess.
        @rtype: str

        @raise ExecutionException: If the command cannot be executed
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

    def pollProcess(self, processUuid, waitForCompletion=False,
                    keepAsBytes=PY2):
        '''Polls the process to get its intermediate result.

        @param processUuid: The process Uuid returned by startProcess.
        @type processUuid: str

        @param waitForCompletion: Whether to return immediately or wait the
          process to complete.
        @type waitForCompletion: boolean

        @param keepAsBytes: Indicates if the return type should be unicode or
            ascii. Should be noted that those are different types on python2
            and python3 and thus it has a default of True for Py2 and False
            for Py3 thus it by default returns the str type on both pythons. On
            Py2 with False will return unicode. On Py3 with True will
            return bytes
        @type keepAsBytes: bool

        @return: The process result if the process is completed, otherwise None
        @rtype: ProcessResult

        @raise ExecutionException: If
          * there is no such process started
          * the process has been already polled(after completed) and information
        about it has been removed.
        @raise CommunicationException: If target machine is unreachable
        '''
        raise NotSupportedException()

###################################################################
##################### OPERATIONS MANAGER API ######################
###################################################################

class OperationsManager(FileManager, ProcessManager):
    '''Cumulative manager responsible to do various arsenal of operations on
    the target machine.
    '''
    def __init__(self, target):
        ''' Creates an instance of OperationsManager.
        @param target: A terget or address of the host managed by this ops-manager.
        @type target: str
        '''
        self.target = target

    def __eq__(self, other):
        if not isinstance(other, OperationsManager):
            return False
        return self.target == other.target

    def __ne__(self, other):
        res = self.__eq__(other)
        return not res

if platform.system().lower() != 'windows':
    kill = os.kill
    class SerializablePopenMixin(object): pass
    CREATE_NEW_PROCESS_GROUP = 0
    CTRL_BREAK_SIGNAL = signal.SIGINT
    LISTEN_SHUTDOWN_SIGNAL = signal.SIGINT
else:
    raise Exception("Unsupported windows platform")