module Network.Minio.AdminAPI
(
DriveInfo(..)
, ErasureInfo(..)
, Backend(..)
, ConnStats(..)
, HttpStats(..)
, ServerProps(..)
, CountNAvgTime(..)
, StorageClass(..)
, StorageInfo(..)
, SIData(..)
, ServerInfo(..)
, getServerInfo
, HealOpts(..)
, HealResultItem(..)
, HealStatus(..)
, HealStartResp(..)
, startHeal
, forceStartHeal
, getHealStatus
, SetConfigResult(..)
, NodeSummary(..)
, setConfig
, getConfig
, ServerVersion(..)
, ServiceStatus(..)
, serviceStatus
, ServiceAction(..)
, serviceSendAction
) where
import Data.Aeson (FromJSON, ToJSON, Value (Object),
eitherDecode, object, pairs,
parseJSON, toEncoding, toJSON,
withObject, withText, (.:), (.:?),
(.=))
import qualified Data.Aeson as A
import Data.Aeson.Types (typeMismatch)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as T
import Data.Time (NominalDiffTime, getCurrentTime)
import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT
import Network.HTTP.Types.Header (hHost)
import Lib.Prelude
import Network.Minio.APICommon
import Network.Minio.Data
import Network.Minio.Errors
import Network.Minio.Sign.V4
import Network.Minio.Utils
data DriveInfo = DriveInfo
{ diUuid :: Text
, diEndpoint :: Text
, diState :: Text
} deriving (Eq, Show)
instance FromJSON DriveInfo where
parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
<$> v .: "uuid"
<*> v .: "endpoint"
<*> v .: "state"
data StorageClass = StorageClass
{ scParity :: Int
, scData :: Int
} deriving (Eq, Show)
data ErasureInfo = ErasureInfo
{ eiOnlineDisks :: Int
, eiOfflineDisks :: Int
, eiStandard :: StorageClass
, eiReducedRedundancy :: StorageClass
, eiSets :: [[DriveInfo]]
} deriving (Eq, Show)
instance FromJSON ErasureInfo where
parseJSON = withObject "ErasureInfo" $ \v -> do
onlineDisks <- v .: "OnlineDisks"
offlineDisks <- v .: "OfflineDisks"
stdClass <- StorageClass
<$> v .: "StandardSCData"
<*> v .: "StandardSCParity"
rrClass <- StorageClass
<$> v .: "RRSCData"
<*> v .: "RRSCParity"
sets <- v .: "Sets"
return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
data Backend = BackendFS
| BackendErasure ErasureInfo
deriving (Eq, Show)
instance FromJSON Backend where
parseJSON = withObject "Backend" $ \v -> do
typ <- v .: "Type"
case typ :: Int of
1 -> return BackendFS
2 -> BackendErasure <$> parseJSON (Object v)
_ -> typeMismatch "BackendType" (Object v)
data ConnStats = ConnStats
{ csTransferred :: Int64
, csReceived :: Int64
} deriving (Eq, Show)
instance FromJSON ConnStats where
parseJSON = withObject "ConnStats" $ \v -> ConnStats
<$> v .: "transferred"
<*> v .: "received"
data ServerProps = ServerProps
{ spUptime :: NominalDiffTime
, spVersion :: Text
, spCommitId :: Text
, spRegion :: Text
, spSqsArns :: [Text]
} deriving (Eq, Show)
instance FromJSON ServerProps where
parseJSON = withObject "SIServer" $ \v -> do
uptimeNs <- v .: "uptime"
let uptime = uptimeNs / 1e9
ver <- v .: "version"
commitId <- v .: "commitID"
region <- v .: "region"
arn <- v .: "sqsARN"
return $ ServerProps uptime ver commitId region arn
data StorageInfo = StorageInfo
{ siUsed :: Int64
, siBackend :: Backend
} deriving (Eq, Show)
instance FromJSON StorageInfo where
parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
<$> v .: "Used"
<*> v .: "Backend"
data CountNAvgTime = CountNAvgTime
{ caCount :: Int64
, caAvgDuration :: Text
} deriving (Eq, Show)
instance FromJSON CountNAvgTime where
parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
<$> v .: "count"
<*> v .: "avgDuration"
data HttpStats = HttpStats
{ hsTotalHeads :: CountNAvgTime
, hsSuccessHeads :: CountNAvgTime
, hsTotalGets :: CountNAvgTime
, hsSuccessGets :: CountNAvgTime
, hsTotalPuts :: CountNAvgTime
, hsSuccessPuts :: CountNAvgTime
, hsTotalPosts :: CountNAvgTime
, hsSuccessPosts :: CountNAvgTime
, hsTotalDeletes :: CountNAvgTime
, hsSuccessDeletes :: CountNAvgTime
} deriving (Eq, Show)
instance FromJSON HttpStats where
parseJSON = withObject "HttpStats" $ \v -> HttpStats
<$> v .: "totalHEADs"
<*> v .: "successHEADs"
<*> v .: "totalGETs"
<*> v .: "successGETs"
<*> v .: "totalPUTs"
<*> v .: "successPUTs"
<*> v .: "totalPOSTs"
<*> v .: "successPOSTs"
<*> v .: "totalDELETEs"
<*> v .: "successDELETEs"
data SIData = SIData
{ sdStorage :: StorageInfo
, sdConnStats :: ConnStats
, sdHttpStats :: HttpStats
, sdProps :: ServerProps
} deriving (Eq, Show)
instance FromJSON SIData where
parseJSON = withObject "SIData" $ \v -> SIData
<$> v .: "storage"
<*> v .: "network"
<*> v .: "http"
<*> v .: "server"
data ServerInfo = ServerInfo
{ siError :: Text
, siAddr :: Text
, siData :: SIData
} deriving (Eq, Show)
instance FromJSON ServerInfo where
parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
<$> v .: "error"
<*> v .: "addr"
<*> v .: "data"
data ServerVersion = ServerVersion
{ svVersion :: Text
, svCommitId :: Text
} deriving (Eq, Show)
instance FromJSON ServerVersion where
parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
<$> v .: "version"
<*> v .: "commitID"
data ServiceStatus = ServiceStatus
{ ssVersion :: ServerVersion
, ssUptime :: NominalDiffTime
} deriving (Eq, Show)
instance FromJSON ServiceStatus where
parseJSON = withObject "ServiceStatus" $ \v -> do
serverVersion <- v .: "serverVersion"
uptimeNs <- v .: "uptime"
let uptime = uptimeNs / 1e9
return $ ServiceStatus serverVersion uptime
data ServiceAction = ServiceActionRestart
| ServiceActionStop
deriving (Eq, Show)
instance ToJSON ServiceAction where
toJSON a = object [ "action" .= serviceActionToText a ]
serviceActionToText :: ServiceAction -> Text
serviceActionToText a = case a of
ServiceActionRestart -> "restart"
ServiceActionStop -> "stop"
adminPath :: ByteString
adminPath = "/minio/admin"
data HealStartResp = HealStartResp
{ hsrClientToken :: Text
, hsrClientAddr :: Text
, hsrStartTime :: UTCTime
} deriving (Eq, Show)
instance FromJSON HealStartResp where
parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
<$> v .: "clientToken"
<*> v .: "clientAddress"
<*> v .: "startTime"
data HealOpts = HealOpts
{ hoRecursive :: Bool
, hoDryRun :: Bool
} deriving (Eq, Show)
instance ToJSON HealOpts where
toJSON (HealOpts r d) =
object ["recursive" .= r, "dryRun" .= d]
toEncoding (HealOpts r d) =
pairs ("recursive" .= r <> "dryRun" .= d)
instance FromJSON HealOpts where
parseJSON = withObject "HealOpts" $ \v -> HealOpts
<$> v .: "recursive"
<*> v .: "dryRun"
data HealItemType = HealItemMetadata
| HealItemBucket
| HealItemBucketMetadata
| HealItemObject
deriving (Eq, Show)
instance FromJSON HealItemType where
parseJSON = withText "HealItemType" $ \v -> case v of
"metadata" -> return HealItemMetadata
"bucket" -> return HealItemBucket
"object" -> return HealItemObject
"bucket-metadata" -> return HealItemBucketMetadata
_ -> typeMismatch "HealItemType" (A.String v)
data NodeSummary = NodeSummary
{ nsName :: Text
, nsErrSet :: Bool
, nsErrMessage :: Text
} deriving (Eq, Show)
instance FromJSON NodeSummary where
parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
<$> v .: "name"
<*> v .: "errSet"
<*> v .: "errMsg"
data SetConfigResult = SetConfigResult
{ scrStatus :: Bool
, scrNodeSummary :: [NodeSummary]
} deriving (Eq, Show)
instance FromJSON SetConfigResult where
parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
<$> v .: "status"
<*> v .: "nodeResults"
data HealResultItem = HealResultItem
{ hriResultIdx :: Int
, hriType :: HealItemType
, hriBucket :: Bucket
, hriObject :: Object
, hriDetail :: Text
, hriParityBlocks :: Maybe Int
, hriDataBlocks :: Maybe Int
, hriDiskCount :: Int
, hriSetCount :: Int
, hriObjectSize :: Int
, hriBefore :: [DriveInfo]
, hriAfter :: [DriveInfo]
} deriving (Eq, Show)
instance FromJSON HealResultItem where
parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
<$> v .: "resultId"
<*> v .: "type"
<*> v .: "bucket"
<*> v .: "object"
<*> v .: "detail"
<*> v .:? "parityBlocks"
<*> v .:? "dataBlocks"
<*> v .: "diskCount"
<*> v .: "setCount"
<*> v .: "objectSize"
<*> (do before <- v .: "before"
before .: "drives")
<*> (do after <- v .: "after"
after .: "drives")
data HealStatus = HealStatus
{ hsSummary :: Text
, hsStartTime :: UTCTime
, hsSettings :: HealOpts
, hsNumDisks :: Int
, hsFailureDetail :: Maybe Text
, hsItems :: Maybe [HealResultItem]
} deriving (Eq, Show)
instance FromJSON HealStatus where
parseJSON = withObject "HealStatus" $ \v -> HealStatus
<$> v .: "Summary"
<*> v .: "StartTime"
<*> v .: "Settings"
<*> v .: "NumDisks"
<*> v .:? "Detail"
<*> v .: "Items"
healPath :: Maybe Bucket -> Maybe Text -> ByteString
healPath bucket prefix = do
if (isJust bucket)
then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
<> fromMaybe "" prefix
else encodeUtf8 $ "v1/heal/"
serviceStatus :: Minio ServiceStatus
serviceStatus = do
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
, ariPayload = PayloadBS B.empty
, ariPayloadHash = Nothing
, ariPath = "v1/service"
, ariHeaders = []
, ariQueryParams = []
}
let rspBS = NC.responseBody rsp
case eitherDecode rspBS of
Right ss -> return ss
Left err -> throwIO $ MErrVJsonParse $ T.pack err
serviceSendAction :: ServiceAction -> Minio ()
serviceSendAction action = do
let payload = PayloadBS $ LBS.toStrict $ A.encode action
void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
, ariPayload = payload
, ariPayloadHash = Nothing
, ariPath = "v1/service"
, ariHeaders = []
, ariQueryParams = []
}
getConfig :: Minio ByteString
getConfig = do
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
, ariPayload = PayloadBS B.empty
, ariPayloadHash = Nothing
, ariPath = "v1/config"
, ariHeaders = []
, ariQueryParams = []
}
return $ LBS.toStrict $ NC.responseBody rsp
setConfig :: ByteString -> Minio SetConfigResult
setConfig config = do
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
, ariPayload = PayloadBS config
, ariPayloadHash = Nothing
, ariPath = "v1/config"
, ariHeaders = []
, ariQueryParams = []
}
let rspBS = NC.responseBody rsp
case eitherDecode rspBS of
Right scr -> return scr
Left err -> throwIO $ MErrVJsonParse $ T.pack err
getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
getHealStatus bucket prefix token = do
when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
let qparams = HT.queryTextToQuery [("clientToken", Just token)]
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
, ariPayload = PayloadBS B.empty
, ariPayloadHash = Nothing
, ariPath = healPath bucket prefix
, ariHeaders = []
, ariQueryParams = qparams
}
let rspBS = NC.responseBody rsp
case eitherDecode rspBS of
Right hs -> return hs
Left err -> throwIO $ MErrVJsonParse $ T.pack err
doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
doHeal bucket prefix opts forceStart = do
when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
let payload = PayloadBS $ LBS.toStrict $ A.encode opts
let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
forceStart
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
, ariPayload = payload
, ariPayloadHash = Nothing
, ariPath = healPath bucket prefix
, ariHeaders = []
, ariQueryParams = qparams
}
let rspBS = NC.responseBody rsp
case eitherDecode rspBS of
Right hsr -> return hsr
Left err -> throwIO $ MErrVJsonParse $ T.pack err
startHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
startHeal bucket prefix opts = doHeal bucket prefix opts False
forceStartHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Minio HealStartResp
forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
getServerInfo :: Minio [ServerInfo]
getServerInfo = do
rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
, ariPayload = PayloadBS B.empty
, ariPayloadHash = Nothing
, ariPath = "v1/info"
, ariHeaders = []
, ariQueryParams = []
}
let rspBS = NC.responseBody rsp
case eitherDecode rspBS of
Right si -> return si
Left err -> throwIO $ MErrVJsonParse $ T.pack err
executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
executeAdminRequest ari = do
req <- buildAdminRequest ari
mgr <- asks mcConnManager
httpLbs req mgr
buildAdminRequest :: AdminReqInfo -> Minio NC.Request
buildAdminRequest areq = do
ci <- asks mcConnInfo
sha256Hash <- if | connectIsSecure ci ->
return "UNSIGNED-PAYLOAD"
| otherwise -> getPayloadSHA256Hash (ariPayload areq)
timeStamp <- liftIO getCurrentTime
let hostHeader = (hHost, getHostAddr ci)
newAreq = areq { ariPayloadHash = Just sha256Hash
, ariHeaders = hostHeader
: sha256Header sha256Hash
: ariHeaders areq
}
signReq = toRequest ci newAreq
sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
timeStamp Nothing Nothing (ariPayloadHash newAreq)
signHeaders = signV4 sp signReq
return signReq {
NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
}
where
toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
toRequest ci aReq = NC.defaultRequest
{ NC.method = ariMethod aReq
, NC.secure = connectIsSecure ci
, NC.host = encodeUtf8 $ connectHost ci
, NC.port = connectPort ci
, NC.path = B.intercalate "/" [adminPath, ariPath aReq]
, NC.requestHeaders = ariHeaders aReq
, NC.queryString = HT.renderQuery False $ ariQueryParams aReq
, NC.requestBody = getRequestBody (ariPayload aReq)
}