diff --git a/README.md b/README.md index 2f71c26..2a7eb07 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ lhp compile --parallel --host my-host-1 --host my-host-2 > /tmp/lhp-report.json Alternatively, you can use a configuration file which has additional benefit of attaching static information to your hosts such as external -documentation URL and/or tags. The configuration file looks like as -follows: +documentation URL and/or tags, and using SSH configuration instead of +plain host name. The configuration file looks like as follows: ```yaml ## config.yaml @@ -102,6 +102,12 @@ knownSshKeys: hosts: - ## Name of the host (required) name: somehost + ## SSH configuration (optional) + ssh: + ## SSH destination (required) + destination: root@10.10.10.10 + ## SSH options (optional) + options: ["-i", "/keys/hebele.pri"] ## External URL for the host (optional) url: https://internal.documentation/hosts/somehost ## List of tags for the host (optional) diff --git a/config.yaml b/config.yaml index f371173..1f400bb 100644 --- a/config.yaml +++ b/config.yaml @@ -9,6 +9,12 @@ knownSshKeys: hosts: - ## Name of the host (required) name: somehost + ## SSH configuration (optional) + ssh: + ## SSH destination (required) + destination: root@10.10.10.10 + ## SSH options (optional) + options: ["-i", "/keys/hebele.pri"] ## External URL for the host (optional) url: https://internal.documentation/hosts/somehost ## List of tags for the host (optional) diff --git a/src/Lhp/Cli.hs b/src/Lhp/Cli.hs index 7e715fa..1874bda 100644 --- a/src/Lhp/Cli.hs +++ b/src/Lhp/Cli.hs @@ -81,7 +81,7 @@ doCompile cpath dests par = do Left err -> BLC.hPutStrLn stderr (Aeson.encode err) >> pure (ExitFailure 1) Right sr -> BLC.putStrLn (Aeson.encode sr) >> pure ExitSuccess where - _mkHost d = Types.Host {Types._hostName = d, Types._hostUrl = Nothing, Types._hostTags = []} + _mkHost d = Types.Host {Types._hostName = d, Types._hostSsh = Nothing, Types._hostUrl = Nothing, Types._hostTags = []} -- ** schema diff --git a/src/Lhp/Remote.hs b/src/Lhp/Remote.hs index 5db4a44..7d17d5d 100644 --- a/src/Lhp/Remote.hs +++ b/src/Lhp/Remote.hs @@ -64,7 +64,7 @@ compileHostReport => Types.Host -> m Types.HostReport compileHostReport h@Types.Host {..} = do - kvs <- (++) <$> _fetchHostInfo _hostName <*> _fetchHostCloudInfo _hostName + kvs <- (++) <$> _fetchHostInfo h <*> _fetchHostCloudInfo h let _hostReportHost = h _hostReportHostname <- _toParseError _hostName $ _getParse pure "LHP_GENERAL_HOSTNAME" kvs _hostReportTimezone <- _toParseError _hostName $ _getParse pure "LHP_GENERAL_TIMEZONE" kvs @@ -72,10 +72,10 @@ compileHostReport h@Types.Host {..} = do _hostReportHardware <- _mkHardware _hostName kvs _hostReportKernel <- _mkKernel _hostName kvs _hostReportDistribution <- _mkDistribution _hostName kvs - _hostReportDockerContainers <- _fetchHostDockerContainers _hostName - _hostReportAuthorizedSshKeys <- _fetchHostAuthorizedSshKeys _hostName >>= mapM parseSshPublicKey - _hostReportSystemdServices <- _fetchHostSystemdServices _hostName - _hostReportSystemdTimers <- _fetchHostSystemdTimers _hostName + _hostReportDockerContainers <- _fetchHostDockerContainers h + _hostReportAuthorizedSshKeys <- _fetchHostAuthorizedSshKeys h >>= mapM parseSshPublicKey + _hostReportSystemdServices <- _fetchHostSystemdServices h + _hostReportSystemdTimers <- _fetchHostSystemdTimers h pure Types.HostReport {..} @@ -100,15 +100,20 @@ instance Aeson.ToJSON LhpError where -- * Internal +getHostSshConfig :: Types.Host -> Z.Ssh.SshConfig +getHostSshConfig Types.Host {..} = + fromMaybe Z.Ssh.SshConfig {_sshConfigDestination = _hostName, _sshConfigOptions = []} _hostSsh + + -- | Attempts to retrieve remote host information and return it as a -- list of key/value tuples. _fetchHostInfo :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m [(T.Text, T.Text)] -_fetchHostInfo h = - parseKVs <$> _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/info.sh") ["bash"]) +_fetchHostInfo h@Types.Host {..} = + parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/info.sh") ["bash"]) -- | Attempts to retrieve remote host cloud information and return it @@ -116,10 +121,10 @@ _fetchHostInfo h = _fetchHostCloudInfo :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m [(T.Text, T.Text)] -_fetchHostCloudInfo h = - parseKVs <$> _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/cloud.sh") ["bash"]) +_fetchHostCloudInfo h@Types.Host {..} = + parseKVs <$> _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/cloud.sh") ["bash"]) -- | Attempts to retrieve remote host docker containers information and return it. @@ -129,15 +134,15 @@ _fetchHostCloudInfo h = _fetchHostDockerContainers :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m (Maybe [Types.DockerContainer]) -_fetchHostDockerContainers h = +_fetchHostDockerContainers h@Types.Host {..} = (Just <$> (prog >>= _parseDockerContainers)) `catchError` const (pure Nothing) where - prog = _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/docker-containers.sh") ["bash"]) + prog = _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/docker-containers.sh") ["bash"]) _parseDockerContainers b = case ACD.eitherDecode (ACD.list _jsonDecoderDockerContainer) b of - Left err -> throwError (LhpErrorParse h ("Error while parsing containers information: " <> T.pack err)) + Left err -> throwError (LhpErrorParse _hostName ("Error while parsing containers information: " <> T.pack err)) Right sv -> pure sv @@ -146,12 +151,12 @@ _fetchHostDockerContainers h = _fetchHostAuthorizedSshKeys :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m [T.Text] -_fetchHostAuthorizedSshKeys h = +_fetchHostAuthorizedSshKeys h@Types.Host {..} = filter (not . T.null) . fmap T.strip . T.lines . Z.Text.unsafeTextFromBL <$> prog where - prog = _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/ssh-keys.sh") ["bash"]) + prog = _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/ssh-keys.sh") ["bash"]) -- | Attempts to find and return all systemd services on the remote @@ -159,12 +164,12 @@ _fetchHostAuthorizedSshKeys h = _fetchHostSystemdServices :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m [T.Text] -_fetchHostSystemdServices h = +_fetchHostSystemdServices h@Types.Host {..} = filter (not . T.null) . fmap T.strip . T.lines . Z.Text.unsafeTextFromBL <$> prog where - prog = _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/systemd-services.sh") ["bash"]) + prog = _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/systemd-services.sh") ["bash"]) -- | Attempts to find and return all systemd timers on the remote @@ -172,12 +177,12 @@ _fetchHostSystemdServices h = _fetchHostSystemdTimers :: MonadIO m => MonadError LhpError m - => Z.Ssh.Destination + => Types.Host -> m [T.Text] -_fetchHostSystemdTimers h = +_fetchHostSystemdTimers h@Types.Host {..} = filter (not . T.null) . fmap T.strip . T.lines . Z.Text.unsafeTextFromBL <$> prog where - prog = _toSshError h (Z.Ssh.runScript h $(embedStringFile "src/scripts/systemd-timers.sh") ["bash"]) + prog = _toSshError _hostName (Z.Ssh.runScript (getHostSshConfig h) $(embedStringFile "src/scripts/systemd-timers.sh") ["bash"]) -- | Smart constructor for remote host cloud information. diff --git a/src/Lhp/Types.hs b/src/Lhp/Types.hs index 28cb26e..9927342 100644 --- a/src/Lhp/Types.hs +++ b/src/Lhp/Types.hs @@ -13,6 +13,7 @@ import Data.Scientific (Scientific) import qualified Data.Text as T import qualified Data.Time as Time import GHC.Generics (Generic) +import Zamazingo.Ssh (SshConfig) -- * Report @@ -44,6 +45,7 @@ instance ADC.HasCodec Report where -- | Data definition for host descriptor. data Host = Host { _hostName :: !T.Text + , _hostSsh :: !(Maybe SshConfig) , _hostUrl :: !(Maybe T.Text) , _hostTags :: ![T.Text] } @@ -59,6 +61,7 @@ instance ADC.HasCodec Host where ADC.object "Host" $ Host <$> ADC.requiredField "name" "Name of the host." ADC..= _hostName + <*> ADC.optionalField "ssh" "SSH configuration." ADC..= _hostSsh <*> ADC.optionalField "url" "URL to external host information." ADC..= _hostUrl <*> ADC.optionalFieldWithDefault "tags" [] "Arbitrary tags for the host." ADC..= _hostTags diff --git a/src/Zamazingo/Ssh.hs b/src/Zamazingo/Ssh.hs index 8a40d72..722a1e2 100644 --- a/src/Zamazingo/Ssh.hs +++ b/src/Zamazingo/Ssh.hs @@ -1,10 +1,14 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingVia #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} -- | This module provides definitions to work with remote hosts over -- SSH. module Zamazingo.Ssh where +import qualified Autodocodec as ADC import Control.Monad (unless) import Control.Monad.Except (MonadError (throwError)) import Control.Monad.IO.Class (MonadIO) @@ -12,6 +16,7 @@ import Data.Aeson ((.=)) import qualified Data.Aeson as Aeson import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T +import GHC.Generics (Generic) import qualified Path as P import System.Exit (ExitCode (..)) import qualified System.Process.Typed as TP @@ -22,13 +27,33 @@ import qualified Zamazingo.Text as Z.Text type Destination = T.Text +-- | Data definition for SSH configuration. +data SshConfig = SshConfig + { _sshConfigDestination :: !Destination + , _sshConfigOptions :: ![T.Text] + } + deriving (Eq, Generic, Show) + deriving (Aeson.FromJSON, Aeson.ToJSON) via (ADC.Autodocodec SshConfig) + + +instance ADC.HasCodec SshConfig where + codec = + _codec ADC. "SSH Configuration" + where + _codec = + ADC.object "SshConfig" $ + SshConfig + <$> ADC.requiredField "destination" "SSH destination." ADC..= _sshConfigDestination + <*> ADC.optionalFieldWithDefault "options" [] "SSH options." ADC..= _sshConfigOptions + + -- | Data definition for errors this module can throw. data SshError - = SshErrorConnection Destination T.Text - | SshErrorCommandTimeout Destination [T.Text] - | SshErrorCommand Destination [T.Text] - | SshErrorFileRead Destination (P.Path P.Abs P.File) - | SshErrorMissingFile Destination (P.Path P.Abs P.File) + = SshErrorConnection SshConfig T.Text + | SshErrorCommandTimeout SshConfig [T.Text] + | SshErrorCommand SshConfig [T.Text] + | SshErrorFileRead SshConfig (P.Path P.Abs P.File) + | SshErrorMissingFile SshConfig (P.Path P.Abs P.File) deriving (Eq, Show) @@ -49,13 +74,13 @@ instance Aeson.ToJSON SshError where runCommand :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> [T.Text] -> m BL.ByteString -runCommand h cmd = do - (ec, out, _err) <- _runCommand h Nothing _sshConfig cmd +runCommand cfg cmd = do + (ec, out, _err) <- _runCommand cfg Nothing _sshConfig cmd case ec of - ExitFailure _ -> throwError (SshErrorCommand h cmd) + ExitFailure _ -> throwError (SshErrorCommand cfg cmd) ExitSuccess -> pure out @@ -63,14 +88,14 @@ runCommand h cmd = do runScript :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> BL.ByteString -> [T.Text] -> m BL.ByteString -runScript h scr cmd = do - (ec, out, _err) <- _runCommand h (Just scr) _sshConfig cmd +runScript cfg scr cmd = do + (ec, out, _err) <- _runCommand cfg (Just scr) _sshConfig cmd case ec of - ExitFailure _ -> throwError (SshErrorCommand h cmd) + ExitFailure _ -> throwError (SshErrorCommand cfg cmd) ExitSuccess -> pure out @@ -78,15 +103,15 @@ runScript h scr cmd = do readFile :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> P.Path P.Abs P.File -> m BL.ByteString -readFile h p = do - exists <- doesFileExist h p - unless exists (throwError (SshErrorMissingFile h p)) - (ec, out, _err) <- _runCommand h Nothing _sshConfig _cmd +readFile cfg p = do + exists <- doesFileExist cfg p + unless exists (throwError (SshErrorMissingFile cfg p)) + (ec, out, _err) <- _runCommand cfg Nothing _sshConfig _cmd case ec of - ExitFailure _ -> throwError (SshErrorFileRead h p) + ExitFailure _ -> throwError (SshErrorFileRead cfg p) ExitSuccess -> pure out where _cmd = ["cat", T.pack (P.toFilePath p)] @@ -96,14 +121,14 @@ readFile h p = do hasCommand :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> T.Text -> m Bool -hasCommand h cmd = do - (ec, _out, _err) <- _runCommand h Nothing _sshConfig _cmd +hasCommand cfg cmd = do + (ec, _out, _err) <- _runCommand cfg Nothing _sshConfig _cmd case ec of ExitFailure 1 -> pure False - ExitFailure _ -> throwError (SshErrorCommand h _cmd) + ExitFailure _ -> throwError (SshErrorCommand cfg _cmd) ExitSuccess -> pure True where _cmd = ["which", cmd] @@ -113,14 +138,14 @@ hasCommand h cmd = do doesFileExist :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> P.Path P.Abs P.File -> m Bool -doesFileExist h p = do - (ec, _out, _err) <- _runCommand h Nothing _sshConfig _cmd +doesFileExist cfg p = do + (ec, _out, _err) <- _runCommand cfg Nothing _sshConfig _cmd case ec of ExitFailure 2 -> pure False - ExitFailure _ -> throwError (SshErrorCommand h _cmd) + ExitFailure _ -> throwError (SshErrorCommand cfg _cmd) ExitSuccess -> pure True where _cmd = ["[ -f \"" <> T.pack (P.toFilePath p) <> "\" ] && exit 0 || exit 2"] @@ -136,21 +161,21 @@ doesFileExist h p = do _runCommand :: MonadIO m => MonadError SshError m - => Destination + => SshConfig -> Maybe BL.ByteString -> [T.Text] -> [T.Text] -> m (ExitCode, BL.ByteString, BL.ByteString) -_runCommand h mi cfg cmd = do +_runCommand cfg@SshConfig {..} mi opts cmd = do let stdin = maybe TP.nullStream TP.byteStringInput mi (ec, out, err) <- TP.readProcess (TP.setStdin stdin $ TP.proc "timeout" ("10" : "ssh" : _args)) case ec of - ExitFailure 124 -> throwError (SshErrorCommandTimeout h cmd) - ExitFailure 255 -> throwError (SshErrorConnection h ("Connection failed: " <> Z.Text.unsafeTextFromBL err)) + ExitFailure 124 -> throwError (SshErrorCommandTimeout cfg cmd) + ExitFailure 255 -> throwError (SshErrorConnection cfg ("Connection failed: " <> Z.Text.unsafeTextFromBL err)) ExitFailure _ -> pure (ec, out, err) ExitSuccess -> pure (ec, out, err) where - _args = fmap T.unpack (cfg <> [h] <> cmd) + _args = fmap T.unpack (opts <> _sshConfigOptions <> [_sshConfigDestination] <> cmd) -- | SSH config to pass on all our SSH connections. diff --git a/website/src/lib/data.ts b/website/src/lib/data.ts index e8b07b7..1738e73 100644 --- a/website/src/lib/data.ts +++ b/website/src/lib/data.ts @@ -138,6 +138,15 @@ export const LHP_PATROL_REPORT_SCHEMA = { $comment: 'Host descriptor.\nHost Descriptor\nHost', properties: { name: { $comment: 'Name of the host.', type: 'string' }, + ssh: { + $comment: 'SSH configuration.\nSSH Configuration\nSshConfig', + properties: { + destination: { $comment: 'SSH destination.', type: 'string' }, + options: { $comment: 'SSH options.', items: { type: 'string' }, type: 'array' }, + }, + required: ['destination'], + type: 'object', + }, tags: { $comment: 'Arbitrary tags for the host.', items: { type: 'string' }, type: 'array' }, url: { $comment: 'URL to external host information.', type: 'string' }, },