packages feed

bein-0.1: Bein/BeinCtl.hs

-- This file is part of Bein.
--
-- Bein is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--    
-- Bein is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with Bein.  If not, see <http://www.gnu.org/licenses/>.


module Main where

import Data.Maybe (fromJust)
import System.FilePath
import System.Exit ( exitSuccess, exitFailure )
import System.IO ( hPutStrLn, stderr )
import System.Directory
import System.Console.GetOpt
import Database.HDBC
import Database.HDBC.PostgreSQL
import System.Environment (getArgs, getProgName)
import Bein.ShellScripting
import System.Posix.Files
import Bein.Configuration

data State = State { sqlDir :: Maybe FilePath,
                     outputFile :: Maybe FilePath,
                     verbose :: Bool } deriving (Eq,Show,Read)

defaultState :: State
defaultState = State { sqlDir = Nothing,
                       outputFile = Nothing,
                       verbose = False }

main :: IO ()
main = do (cmd,st) <- getState
          cmd st
            
getState :: IO (State -> IO (), State)
getState = do
  args <- getArgs
  case getOpt RequireOrder options args of
    (actions, [commandTxt], []) -> do opts <- foldl (>>=) (return defaultState) actions
                                      let command = parseCmd commandTxt
                                      case () of
                                        _ | (commandTxt `elem` ["init","cold-init","reload","deep-reload"]) && sqlDir opts == Nothing ->
                                          printHelpAndDie (Just "Must give an SQL source directory to init database.")
                                        _ | (commandTxt == "dump" && outputFile opts == Nothing) ->
                                          printHelpAndDie (Just "Must give a file to dump database to with -o option.")
                                        _ -> return (command,opts)
    (_, _, []) -> printHelpAndDie (Just "beinctl takes exactly one command.")
    (_, _, errs) -> printHelpAndDie (Just $ concat errs)

options :: [OptDescr (State -> IO State)]
options = [
  Option "h" ["help"]
         (NoArg (\_ -> printHelpAndDie Nothing))
         "Show help",
  Option "v" ["verbose"]
         (NoArg (\opt -> return opt { verbose = True }))
         "Be verbose.",
  Option "o" ["output"]
         (ReqArg (\arg opt -> return opt { outputFile = Just arg }) "OUTPUTFILE")
         "File to dump database to.",
  Option "d" ["sqldir"]
         (ReqArg (\arg opt -> do b <- doesDirectoryExist arg
                                 if b then return opt { sqlDir = Just arg }
                                   else printHelpAndDie (Just $ "Invalid directory: " ++ arg)) "SQLDIRECTORY")
         "Directory containing SQL source."
  ]

printHelpAndDie :: Maybe String -> IO a
printHelpAndDie msg = do
    case msg of
      Nothing -> help >> exitSuccess
      Just m -> hPutStrLn stderr m >> help >> exitFailure
    where help = do 
            programName <- getProgName
            hPutStrLn stderr (usageInfo (programName ++ " [-hvd] command") options)
            printCommands

parseCmd :: String -> (State -> IO ())
parseCmd "cold-init" = coldInitialize
parseCmd "init" = initialize
parseCmd "teardown" = teardown
parseCmd "full-teardown" = fullTeardown
parseCmd "dump" = dump
parseCmd "reload" = reload
parseCmd "deep-reload" = deepReload
parseCmd "show-config" = showConfig
parseCmd cmd = \_ -> printHelpAndDie (Just ("Unknown command: " ++ cmd))

printCommands :: IO ()
printCommands = do
  mapM_ putStrLn 
    ["  where command is one of:",
     "    init            - create Bein database ready for loading",
     "    cold-init       - create Bein database from scratch, including users",
     "    teardown        - delete the Bein database",
     "    full-teardown   - delete the Bein database and all users, leaving system pristine",
     "    dump            - dump data in the database to tar archive file", 
     "    reload          - same as teardown followed by init",
     "    deep-reload     - same as full-teardown followed by cold-init",
     "    show-config     - print the system configuration from the database"]
    
coldInitialize :: State -> IO ()
coldInitialize st = do
  nologin <- takeFirstM fileExist ["/sbin/nologin", "/usr/sbin/nologin"]
  systemM_ $ "useradd --system -M --user-group --shell " ++ nologin ++ " bein"
  systemM_ $ "sudo -u postgres createuser --no-superuser --login bein --no-createdb --no-createrole --no-superuser"
  initialize st
  
initialize :: State -> IO ()
initialize State { sqlDir = Nothing } = 
  printHelpAndDie (Just "Cannot initialize database without SQL directory.  Set -d option.")
initialize State { sqlDir = Just d, verbose = v } = do
  let inPath p = joinPath [d,p]
      unlessVerbose p = if v then "" else p
  systemM $ "sudo -u postgres createdb --owner bein bein"
  systemM $ "sudo -u postgres createlang plpgsql bein"
  systemM $ "sudo -u postgres createlang plpythonu bein"
  systemM $ "sudo -u bein psql " ++ unlessVerbose "--quiet" ++ " --dbname bein --file " ++ inPath "core_tables.sql"
  systemM $ "sudo -u postgres psql " ++ unlessVerbose "--quiet" ++ " --dbname bein --file "++ inPath "untrusted_functions.sql"
  systemM $ "sudo -u bein psql " ++ unlessVerbose "--quiet" ++ " --dbname bein --file " ++ inPath "triggers.sql"
  systemM $ "sudo -u bein psql " ++ unlessVerbose "--quiet" ++ " --dbname bein --file " ++ inPath "utility_functions.sql"

fullTeardown :: State -> IO ()
fullTeardown st = do
  teardown st
  systemM_ $ "userdel bein"
  systemM_ $ "groupdel bein"
  systemM_ $ "sudo -u postgres dropuser bein"

teardown :: State -> IO ()
teardown _ = do
  systemM_ $ "sudo -u postgres dropdb bein"

dump :: State -> IO ()
dump st = do
  systemM_ $ "sudo -u bein pg_dump --format=t -f " ++ fromJust (outputFile st) ++ " bein"

reload :: State -> IO ()
reload st = teardown st >> initialize st

deepReload :: State -> IO ()
deepReload st = fullTeardown st >> coldInitialize st

dbCommand :: (Connection -> IO a) -> IO ()
dbCommand cmd = do conn <- connectPostgreSQL "dbname=bein"
                   _ <- cmd conn
                   commit conn

showConfig :: State -> IO ()
showConfig _ = dbCommand (\conn -> readConfiguration conn >>= putStrLn . show)

takeFirstM :: Monad m => (a -> m Bool) -> [a] -> m a
takeFirstM _ [] = fail $ "Nothing in list succeeds."
takeFirstM f (c:cs) = f c >>= \r ->
  if r then return c else takeFirstM f cs