module Network.LXD.Client.Commands (
def
, module Network.LXD.Client.Remotes
, module Network.LXD.Client.Types
, Host(..)
, HasClient(..)
, defaultClientEnv
, WithLocalHost, runWithLocalHost
, WithRemoteHost, runWithRemoteHost
, lxcApi
, lxcList
, lxcCreate
, lxcDelete
, lxcInfo
, lxcStart
, lxcStop
, lxcRestart
, lxcFreeze
, lxcUnfreeze
, lxcExec
, lxcExecEnv
, lxcExecRaw
, lxcFileDelete
, lxcFilePull
, lxcFilePullRaw
, lxcFilePush
, lxcFilePushAttrs
, lxcFilePushRaw
, lxcFilePushRawAttrs
, lxcFileListDir
, lxcFileMkdir
, lxcFileMkdirTemplate
, lxcFileMkdirAttrs
, lxcFilePullRecursive
, lxcFilePushRecursive
, lxcFilePushRecursiveAttrs
, lxcImageList
, lxcImageAliases
, lxcImageInfo
, lxcImageAlias
, lxcImageCreate
, lxcImageDelete
, lxcNetworkList
, lxcNetworkCreate
, lxcNetworkInfo
, lxcNetworkConfig
, lxcNetworkDelete
, lxcProfileList
, lxcProfileCreate
, lxcProfileInfo
, lxcProfileConfig
, lxcProfileDelete
, lxcStorageList
, lxcStorageCreate
, lxcStorageInfo
, lxcStorageConfig
, lxcStorageDelete
, lxcVolumeList
, lxcVolumeCreate
, lxcVolumeInfo
, lxcVolumeConfig
, lxcVolumeDelete
) where
import Network.LXD.Client.Internal.Prelude
import Control.Concurrent.Async (Async, async, withAsync, wait, uninterruptibleCancel)
import Control.Concurrent.MVar
import Control.Monad ((>=>))
import Control.Monad.Catch (Exception, SomeException, MonadThrow, throwM, MonadCatch, catch, MonadMask, bracket)
import Control.Monad.State (StateT, evalStateT, gets, modify')
import Data.ByteString.Lazy (ByteString)
import Data.Default (def)
import Data.List (inits)
import Data.Map.Strict (Map)
import qualified Data.Aeson.Types as Aeson
import qualified Data.ByteString.Lazy as BL
import Numeric (readOct, showOct)
import qualified Network.WebSockets as WS
import Servant.Client (ClientM, ClientEnv, runClientM)
import System.Directory (createDirectory, doesDirectoryExist, listDirectory)
import System.FilePath ((</>), splitPath)
import System.Posix.Files (getFileStatus, fileMode, setFileMode)
import qualified System.IO as IO
import Network.LXD.Client hiding (Host)
import Network.LXD.Client.API
import Network.LXD.Client.Types
import Network.LXD.Client.Events
import Network.LXD.Client.Remotes
data Host = HLocalHost LocalHost
| HRemoteHost RemoteHost
class (MonadIO m, MonadMask m) => HasClient m where
askHost :: m Host
askClientEnv :: m ClientEnv
askClientEnv = defaultClientEnv
defaultClientEnv :: HasClient m => m ClientEnv
defaultClientEnv = do
host <- askHost
liftIO (runExceptT $ client host) >>= \case
Left err -> throwM $ ClientError err
Right env -> return env
where
client (HLocalHost host) = localHostClient host
client (HRemoteHost host) = remoteHostClient host
runWebSockets :: Host -> String -> WS.ClientApp a -> IO a
runWebSockets host' path app =
runExceptT runWS >>= \case
Left err -> throwM $ ClientError err
Right env -> return env
where
runWS | HLocalHost host <- host' = runWebSocketsLocal host path app
| HRemoteHost host <- host' = runWebSocketsRemote host path app
newtype WithLocalHost a = WithLocalHost (StateT (LocalHost, Maybe ClientEnv) IO a)
deriving (Applicative, Functor, Monad, MonadIO, MonadCatch, MonadThrow, MonadMask)
instance HasClient WithLocalHost where
askHost = WithLocalHost . gets $ HLocalHost . fst
askClientEnv = do
env <- WithLocalHost . gets $ snd
case env of
Just env' -> return env'
Nothing -> do
env' <- defaultClientEnv
WithLocalHost . modify' $ \(x, _) -> (x, Just env')
return env'
runWithLocalHost :: LocalHost -> WithLocalHost a -> IO a
runWithLocalHost host (WithLocalHost m) = evalStateT m (host, Nothing)
newtype WithRemoteHost a = WithRemoteHost (StateT (RemoteHost, Maybe ClientEnv) IO a)
deriving (Applicative, Functor, Monad, MonadIO, MonadCatch, MonadThrow, MonadMask)
instance HasClient WithRemoteHost where
askHost = WithRemoteHost . gets $ HRemoteHost . fst
askClientEnv = do
env <- WithRemoteHost . gets $ snd
case env of
Just env' -> return env'
Nothing -> do
env' <- defaultClientEnv
WithRemoteHost . modify' $ \(x, _) -> (x, Just env')
return env'
runWithRemoteHost :: RemoteHost -> WithRemoteHost a -> IO a
runWithRemoteHost host (WithRemoteHost m) = evalStateT m (host, Nothing)
lxcApi :: HasClient m => m ApiConfig
lxcApi = runClient $ apiConfig >>= checkResponseOK
lxcList :: HasClient m => m [ContainerName]
lxcList = runClient $ containerNames >>= checkResponseOK
lxcCreate :: HasClient m => ContainerCreateRequest -> m ()
lxcCreate req = runAndWait $ containerCreate req >>= checkResponseCreated
lxcDelete :: HasClient m => ContainerName -> m ()
lxcDelete n = runAndWait $ containerDelete n ContainerDeleteRequest
>>= checkResponseCreated
lxcInfo :: HasClient m => ContainerName -> m Container
lxcInfo = runClient . (container >=> checkResponseOK)
lxcStart :: HasClient m => ContainerName -> m ()
lxcStart name = runAndWait $ containerPutState name s >>= checkResponseCreated
where s = containerNewState Start False
lxcStop :: HasClient m => ContainerName -> Bool -> m ()
lxcStop name force = runAndWait $ containerPutState name s >>= checkResponseCreated
where s = containerNewState Stop force
lxcRestart :: HasClient m => ContainerName -> Bool -> m ()
lxcRestart name force = runAndWait $ containerPutState name s >>= checkResponseCreated
where s = containerNewState Restart force
lxcFreeze :: HasClient m => ContainerName -> m ()
lxcFreeze name = runAndWait $ containerPutState name s >>= checkResponseCreated
where s = containerNewState Freeze False
lxcUnfreeze :: HasClient m => ContainerName -> m ()
lxcUnfreeze name = runAndWait $ containerPutState name s >>= checkResponseCreated
where s = containerNewState Unfreeze False
lxcExec :: HasClient m
=> ContainerName
-> String
-> [String]
-> ByteString
-> m ByteString
lxcExec name cmd args = lxcExecEnv name cmd args mempty
lxcExecEnv :: HasClient m
=> ContainerName
-> String
-> [String]
-> Map String String
-> ByteString
-> m ByteString
lxcExecEnv name cmd args env stdin' = do
stdin <- liftIO newEmptyMVar
stdout <- liftIO newEmptyMVar
stderr <- liftIO newEmptyMVar
bs <- liftIO $ newMVar mempty
let printStderr :: IO ()
printStderr = takeMVar stderr
>>= BL.hPut IO.stdout
>> IO.hFlush IO.stdout
>> printStderr
let saveStdout :: IO ()
saveStdout = do bs' <- takeMVar stdout
modifyMVar_ bs $ return . (<> bs')
exec <- lxcExecRaw name cmd args env stdin stdout stderr
liftIO $
withAsync printStderr $ \_ ->
withAsync saveStdout $ \_ -> do
unless (BL.null stdin') $ putMVar stdin (Just stdin')
putMVar stdin Nothing
wait exec
liftIO $ takeMVar bs
lxcExecRaw :: HasClient m
=> ContainerName
-> String
-> [String]
-> Map String String
-> MVar (Maybe ByteString)
-> MVar ByteString
-> MVar ByteString
-> m (Async ())
lxcExecRaw name cmd args env stdin stdout stderr = do
resp <- runClient $ containerExecWebsocketNonInteractive name req
md <- checkResponseCreatedMetadata resp
let oid = responseOperation resp
fds = execResponseMetadataWebsocketFds md
stdout' = operationWebSocket oid (fdsAllStdout fds)
stderr' = operationWebSocket oid (fdsAllStderr fds)
stdin' = operationWebSocket oid (fdsAllStdin fds)
host <- askHost
let runWS = runWebSockets host
liftIO . async $
withAsync (runWS stdout' $ readAllWebSocket (putMVar stdout)) $ \stdoutThread ->
withAsync (runWS stderr' $ readAllWebSocket (putMVar stderr)) $ \stderrThread ->
withAsync (runWS stdin' $ writeAllWebSocket stdin) $ \stdinThread -> do
wait stdoutThread
wait stderrThread
wait stdinThread
where
req = def { execRequestCommand = cmd:args
, execRequestEnvironment = env }
lxcFileDelete :: HasClient m => ContainerName -> FilePath -> m ()
lxcFileDelete name fp = void . runClient $ containerDeletePath name fp
lxcFilePull :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> m ()
lxcFilePull name src dst = do
path <- runClient $ containerGetPath name src
case getFile path of
Directory _ -> throwM $ ClientError "expected a file, but got a directory"
File bs -> do
m' <- convFileMode' $ pathMode path
liftIO $ BL.writeFile dst bs
liftIO $ setFileMode dst m'
lxcFilePullRaw name src >>= liftIO . BL.writeFile dst
lxcFilePullRaw :: HasClient m => ContainerName -> FilePath -> m ByteString
lxcFilePullRaw name src = do
path <- runClient $ containerGetPath name src
case getFile path of
File bs -> return bs
Directory _ -> throwM $ ClientError "expected a file, but got a directory"
lxcFilePush :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> m ()
lxcFilePush name src dst = lxcFilePushAttrs name src dst Nothing Nothing
lxcFilePushAttrs :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> Maybe Uid
-> Maybe Gid
-> m ()
lxcFilePushAttrs name src dst uid gid = do
mode <- liftIO $ fileMode <$> getFileStatus src
bs <- liftIO $ BL.readFile src
lxcFilePushRawAttrs name dst
uid gid
(Just $ convFileMode mode)
"file"
Nothing
bs
lxcFilePushRaw :: HasClient m => ContainerName -> FilePath -> ByteString -> m ()
lxcFilePushRaw name src = lxcFilePushRawAttrs name src n n n "file" n where n = Nothing
lxcFilePushRawAttrs :: HasClient m
=> ContainerName
-> FilePath
-> Maybe Uid
-> Maybe Gid
-> Maybe FileMode
-> FileType
-> Maybe WriteMode
-> ByteString
-> m ()
lxcFilePushRawAttrs name src uid gid fm ft wm bs = void . runClient $
containerPostPath name src uid gid fm ft wm bs >>= checkResponseOK
lxcFileMkdirTemplate :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> m ()
lxcFileMkdirTemplate name src dst = do
mode <- liftIO $ fileMode <$> getFileStatus src
lxcFileMkdirAttrs name dst False Nothing Nothing (Just $ convFileMode mode)
lxcFileListDir :: HasClient m => ContainerName -> FilePath -> m [String]
lxcFileListDir name fp = do
path <- runClient $ containerGetPath name fp
case getFile path of
File _ -> throwM $ ClientError "expected a directory, but got a file"
Directory r -> checkResponseOK r
lxcFileMkdir :: HasClient m
=> ContainerName
-> String
-> Bool
-> m ()
lxcFileMkdir name dir p = lxcFileMkdirAttrs name dir p Nothing Nothing Nothing
lxcFileMkdirAttrs :: HasClient m
=> ContainerName
-> String
-> Bool
-> Maybe Uid
-> Maybe Gid
-> Maybe FileMode
-> m ()
lxcFileMkdirAttrs name dir False uid gid fm = void . runClient $
containerPostPath name dir uid gid fm "directory" Nothing mempty
lxcFileMkdirAttrs name dir True uid gid fm =
mapM_ mkdir $ inits (splitPath dir)
where
mkdir xs = case concat xs of
"/" -> return ()
path -> do
pathInfo <- runClient $ (Just <$> containerGetPath name path) `catch` ignoreExc
case getFile <$> pathInfo of
Just (File _) -> throwM $ ClientError $ "couldn't make dir " ++ dir ++ ": " ++ path ++ " is not a directory"
Just (Directory _) -> return ()
Nothing -> lxcFileMkdirAttrs name dir False uid gid fm
ignoreExc :: Monad m => SomeException -> m (Maybe a)
ignoreExc _ = return Nothing
lxcFilePullRecursive :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> m ()
lxcFilePullRecursive name src dst = do
path <- runClient $ containerGetPath name src
m' <- convFileMode' $ pathMode path
case getFile path of
File bs -> do
liftIO $ BL.writeFile dst bs
liftIO $ setFileMode dst m'
Directory resp -> do
contents <- checkResponseOK resp
liftIO $ createDirectory dst
mapM_ go contents
liftIO $ setFileMode dst m'
where
go file = lxcFilePullRecursive name src' dst'
where src' = src </> file
dst' = dst </> file
lxcFilePushRecursive :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> m ()
lxcFilePushRecursive name src dst = lxcFilePushRecursiveAttrs name src dst Nothing Nothing
lxcFilePushRecursiveAttrs :: HasClient m
=> ContainerName
-> FilePath
-> FilePath
-> Maybe Uid
-> Maybe Gid
-> m ()
lxcFilePushRecursiveAttrs name src dst uid gid = do
isDir <- liftIO $ doesDirectoryExist src
if not isDir
then lxcFilePushAttrs name src dst uid gid
else do
lxcFileMkdirTemplate name src dst
files <- liftIO $ listDirectory src
mapM_ go files
where
go file = lxcFilePushRecursiveAttrs name src' dst' uid gid
where src' = src </> file
dst' = dst </> file
lxcImageList :: HasClient m => m [ImageId]
lxcImageList = runClient $ imageIds >>= checkResponseOK
lxcImageAliases :: HasClient m => m [ImageAliasName]
lxcImageAliases = runClient $ imageAliases >>= checkResponseOK
lxcImageInfo :: HasClient m => ImageId -> m Image
lxcImageInfo = runClient . image >=> checkResponseOK
lxcImageAlias :: HasClient m => ImageAliasName -> m ImageAlias
lxcImageAlias = runClient . imageAlias >=> checkResponseOK
lxcImageCreate :: HasClient m => ImageCreateRequest -> m ()
lxcImageCreate req = runAndWait $ imageCreate req >>= checkResponseCreated
lxcImageDelete :: HasClient m => ImageId -> m ()
lxcImageDelete img = runAndWait $ imageDelete img def >>= checkResponseCreated
lxcNetworkList :: HasClient m => m [NetworkName]
lxcNetworkList = runClient $ networkList >>= checkResponseOK
lxcNetworkCreate :: HasClient m => NetworkCreateRequest -> m ()
lxcNetworkCreate n = void . runClient $ networkCreate n >>= checkResponseOK
lxcNetworkInfo :: HasClient m => NetworkName -> m Network
lxcNetworkInfo n = runClient $ network n >>= checkResponseOK
lxcNetworkConfig :: HasClient m => NetworkName -> NetworkConfigRequest -> m ()
lxcNetworkConfig n c = void . runClient $ networkPatch n c >>= checkResponseOK
lxcNetworkDelete :: HasClient m => NetworkName -> m ()
lxcNetworkDelete n = void . runClient $ networkDelete n >>= checkResponseOK
lxcProfileList :: HasClient m => m [ProfileName]
lxcProfileList = runClient $ profileList >>= checkResponseOK
lxcProfileCreate :: HasClient m => ProfileCreateRequest -> m ()
lxcProfileCreate n = void . runClient $ profileCreate n >>= checkResponseOK
lxcProfileInfo :: HasClient m => ProfileName -> m Profile
lxcProfileInfo n = runClient $ profile n >>= checkResponseOK
lxcProfileConfig :: HasClient m => ProfileName -> ProfileConfigRequest -> m ()
lxcProfileConfig n c = void . runClient $ profilePatch n c >>= checkResponseOK
lxcProfileDelete :: HasClient m => ProfileName -> m ()
lxcProfileDelete n = void . runClient $ profileDelete n >>= checkResponseOK
lxcStorageList :: HasClient m => m [PoolName]
lxcStorageList = runClient $ poolList >>= checkResponseOK
lxcStorageCreate :: HasClient m => PoolCreateRequest -> m ()
lxcStorageCreate n = void . runClient $ poolCreate n >>= checkResponseOK
lxcStorageInfo :: HasClient m => PoolName -> m Pool
lxcStorageInfo n = runClient $ pool n >>= checkResponseOK
lxcStorageConfig :: HasClient m => PoolName -> PoolConfigRequest -> m ()
lxcStorageConfig n c = void . runClient $ poolPatch n c >>= checkResponseOK
lxcStorageDelete :: HasClient m => PoolName -> m ()
lxcStorageDelete n = void . runClient $ poolDelete n >>= checkResponseOK
lxcVolumeList :: HasClient m => PoolName -> m [VolumeName]
lxcVolumeList p = runClient $ volumeList p >>= checkResponseOK
lxcVolumeCreate :: HasClient m => PoolName -> VolumeCreateRequest -> m ()
lxcVolumeCreate p r = void . runClient $ volumeCreate p r >>= checkResponseOK
lxcVolumeInfo :: HasClient m => PoolName -> VolumeName -> m Volume
lxcVolumeInfo p n = runClient $ volume p n >>= checkResponseOK
lxcVolumeConfig :: HasClient m => PoolName -> VolumeName -> VolumeConfigRequest -> m ()
lxcVolumeConfig p n c = void . runClient $ volumePatch p n c >>= checkResponseOK
lxcVolumeDelete :: HasClient m => PoolName -> VolumeName -> m ()
lxcVolumeDelete p n = void . runClient $ volumeDelete p n >>= checkResponseOK
runClient :: HasClient m => ClientM a -> m a
runClient action = do
clientEnv <- askClientEnv
liftIO (runClientM action clientEnv) >>= \case
Left err -> throwM $ ClientError (show err)
Right a -> return a
convFileMode :: (Integral a, Show a) => a -> FileMode
convFileMode m = FileMode . pad $ showOct m ""
where pad s | n <- length s, n < 4 = replicate (4 n) '0' ++ s
| otherwise = s
convFileMode' :: (MonadThrow m, Eq a, Num a) => FileMode -> m a
convFileMode' (FileMode fm) = case readOct fm of
[(m, "")] -> return m
_ -> throwM $ ClientError $ "received invalid file mode: " ++ show fm
newtype ClientError = ClientError String
instance Show ClientError where
show (ClientError err) = "Could not connect to the LXD host: " ++ err
instance Exception ClientError where
data StatusError = StatusError {
statusErrorExpected :: StatusCode
, statusErrorReceived :: StatusCode }
instance Show StatusError where
show StatusError{..} = "Unexpected response with code "
++ show statusErrorReceived ++ ", expected "
++ show statusErrorExpected
instance Exception StatusError where
checkResponseOK :: MonadThrow m => Response a -> m a
checkResponseOK Response{..}
| SSuccess <- statusCode = return metadata
| otherwise = throwM $ StatusError SSuccess statusCode
checkResponseCreated :: MonadThrow m => AsyncResponse a -> m OperationId
checkResponseCreated Response{..}
| SCreated <- statusCode = return responseOperation
| otherwise = throwM $ StatusError SCreated statusCode
checkResponseCreatedMetadata :: MonadThrow m => AsyncResponse a -> m a
checkResponseCreatedMetadata Response{..}
| SCreated <- statusCode = return $ backgroundOperationMetadata metadata
| otherwise = throwM $ StatusError SCreated statusCode
runAndWait :: HasClient m => ClientM OperationId -> m ()
runAndWait op = do
oid <- liftIO newEmptyMVar
ops <- liftIO newEmptyMVar
host <- askHost
let err = throwM . OperationError
let waitForDone = do op' <- liftIO $ takeMVar ops
case operationStatusCode op' of
SSuccess -> return ()
SStopped -> err "Operation unexpectedly stopped"
SCancelled -> err "Opeartion unexpectedly cancelled"
SFailure -> err $ "Operation failed: " ++ operationErr op'
SRunning -> printProgress op' >> waitForDone
_ -> waitForDone
bracket
(liftIO . async . runWebSockets host operationsPath $ listenForOperation oid ops)
(liftIO . uninterruptibleCancel) $ \_ -> do
oid' <- runClient op
liftIO $ putMVar oid oid'
waitForDone
where
printProgress op' = case Aeson.fromJSON (operationMetadata op') of
Aeson.Error _ -> return ()
Aeson.Success (OperationProgress p) -> liftIO $ do
IO.hPutStr IO.stderr $ "Progres: " ++ p ++ "\r"
IO.hFlush IO.stderr
newtype OperationError = OperationError String
instance Show OperationError where
show (OperationError err) = "Operation unsuccessful: " ++ err
instance Exception OperationError where