packages feed

ec2-unikernel-0.9.8: src/CommandLine.hs

module CommandLine(getOptions)
 where

import Control.Exception(SomeException,catch)
import Control.Lens(ASetter, view, set, elemOf, folded)
import Control.Monad(forM_, unless, when)
import Data.Char(isAlphaNum, toLower)
import Data.Either(isLeft)
import Data.String(IsString, fromString)
import Data.Time.Clock(UTCTime, getCurrentTime)
import Data.Time.Format(formatTime, defaultTimeLocale)
import Network.AWS(Region(..), Credentials(..), Env,
                   runAWS, runResourceT, newEnv, send, envRegion)
import Network.AWS.Data(toText)
import Network.AWS.EC2.DescribeAvailabilityZones(describeAvailabilityZones,
                                                 dazrsAvailabilityZones,
                                                 dazZoneNames)
import Network.AWS.EC2.Types(azZoneName)
import Options
import System.Console.GetOpt(ArgDescr(..), OptDescr(..), ArgOrder(..))
import System.Console.GetOpt(getOpt, usageInfo)
import System.Directory(doesFileExist)
import System.Environment(lookupEnv)
import System.Exit(ExitCode(ExitFailure), exitWith)
import System.FilePath(takeFileName)

type OptOrErr = Either [String] Options

addError :: OptOrErr -> String -> OptOrErr
addError (Left errs) err = Left (errs ++ [err])
addError (Right _)   err = Left [err]

addOpt :: OptOrErr -> (Options -> Options) -> OptOrErr
addOpt (Left errs) _ = Left errs
addOpt (Right o) f   = Right (f o)

validateAccessKey :: String -> OptOrErr -> OptOrErr
validateAccessKey ak opts
  | length ak /= 20           = addError opts "Access key doesn't look right."
  | any (not . isAlphaNum) ak = addError opts "Access key has weird characters."
  | otherwise                 = addOpt opts (set optAwsAccessKey (fromString ak))

validateSecretKey :: String -> OptOrErr -> OptOrErr
validateSecretKey sk opts
  | length sk /= 40           = addError opts "Secret key doesn't look right."
  | any (not . isSecKeyCh) sk = addError opts "Secret key has weird characters."
  | otherwise                 = addOpt opts (set optAwsSecretKey (fromString sk))
 where isSecKeyCh c = isAlphaNum c || (c == '/') || (c == '+')

validateS3Bucket :: String -> OptOrErr -> OptOrErr
validateS3Bucket b opts
  | any (not . isBuckCh) b = addError opts "S3 bucket has weird characters."
  | otherwise              = addOpt opts (set optS3Bucket (fromString b))
 where isBuckCh c = isAlphaNum c || (c == '-')

validateZone :: String -> OptOrErr -> OptOrErr
validateZone z opts
  | z `elem` availabilityZones = addOpt   opts (set optS3Zone (fromString z))
  | otherwise      = addError opts "Unknown S3 zone."

validateRegion :: String -> OptOrErr -> OptOrErr
validateRegion r opts =
  case lookup (map toLower r) regions of
    Nothing -> addError opts "Unknown AWS region."
    Just v  -> addOpt   opts (set optAwsRegion v)

regions :: [(String, Region)]
regions =
  [ ("ireland", Ireland), ("eu-west-1", Ireland)
  , ("frankfurt", Frankfurt), ("eu-central-1", Frankfurt)
  , ("tokyo", Tokyo), ("ap-northeast-1", Tokyo)
  , ("singapore", Singapore), ("ap-southeast-1", Singapore)
  , ("sydney", Sydney), ("ap-southeast-2", Sydney)
  , ("beijing", Beijing), ("cn-north-1", Beijing)
  , ("northvirginia", NorthVirginia), ("us-east-1", NorthVirginia)
  , ("northcalifornia", NorthCalifornia), ("us-west-1", NorthCalifornia)
  , ("oregon", Oregon), ("us-west-2", Oregon)
  , ("govcloud", GovCloud), ("us-gov-west-1", GovCloud)
  , ("govcloudfips", GovCloudFIPS), ("fips-us-gov-west-1", GovCloudFIPS)
  , ("saopaulo", SaoPaulo), ("sa-east-1", SaoPaulo)
  , ("london", London), ("eu-west-2", London)
  ]

availabilityZones :: [String]
availabilityZones =
  [ "us-west-2a"
  , "us-west-2b"
  , "us-west-2c"
  , "eu-west-1a"
  , "eu-west-1b"
  , "eu-west-1c"
  , "eu-west-2a"
  , "eu-west-2b"
  , "eu-west-2c"
  , "eu-central-1a"
  , "eu-central-1b"
  , "eu-central-1c"
  ]

options :: [OptDescr (OptOrErr -> OptOrErr)]
options =
  [ Option ['o'] ["aws-access-key"] (ReqArg validateAccessKey "KEY")
           "AWS access key to use"
  , Option ['w'] ["aws-secret-key"] (ReqArg validateSecretKey "VALUE")
           "AWS secret key to use"
  , Option ['b'] ["s3-bucket"] (ReqArg validateS3Bucket "BUCKET")
           "S3 bucket to upload to, temporarily."
  , Option ['z'] ["zone"] (ReqArg validateZone "ZONE")
           "S3 zone in which that bucket livees."
  , Option ['r'] ["region"] (ReqArg validateRegion "REGION")
           "S3 region to upload to."
  , Option ['a'] ["kernel-args"]
           (ReqArg (\a opts -> addOpt opts (set optKernelArgs a)) "STRING")
           "Kernel arguments to pass to the unikernel."
  ]

maybeSet :: IsString b => ASetter s s a b -> Maybe String -> s -> s
maybeSet _     Nothing  x = x
maybeSet field (Just v) x = set field (fromString v) x

getOptions :: [String] -> IO (Options, Env)
getOptions argv =
  do maccess <- lookupEnv "AWS_ACCESS_KEY"
     msecret <- lookupEnv "AWS_SECRET_KEY"
     let defaultOptions'  = maybeSet optAwsAccessKey maccess defaultOptions
         defaultOptions'' = maybeSet optAwsSecretKey msecret defaultOptions'
         (res, xs, errs)  = getOpt RequireOrder options argv
         doneOpts         = foldl (flip id) (Right defaultOptions'') res
         optErrors        = either id (const []) doneOpts
         kernelErrs       = if null xs then ["No unikernel specified!"] else []
         Right baseOpts   = doneOpts
     now                 <- getCurrentTime
     let opts             = adjustImageName
                               $ adjustTargetName now
                               $ set optKernel   (head xs)
                               $ set optRamdisks (tail xs) baseOpts
     when (isLeft doneOpts || null xs || not (null errs)) $
       fail' (optErrors ++ kernelErrs ++ errs)
     kernelOk   <- doesFileExist (view optKernel opts)
     ramdisksOk <- mapM doesFileExist (view optRamdisks opts)
     unless kernelOk $ fail' ["Unikernel not found"]
     unless (and ramdisksOk) $
       do let disks   = zip (view optRamdisks opts) ramdisksOk
              disks'  = filter (not . snd) disks
              disks'' = map fst disks'
          fail' (map (\s -> "Ramdisk "++s++" not found.") disks'')
     let akey  = view optAwsAccessKey opts
         skey  = view optAwsSecretKey opts
         creds = FromKeys akey skey
     e <- set envRegion (view optAwsRegion opts) `fmap` newEnv creds
     let region = toText (view optS3Zone opts)
         zoneRequest = set dazZoneNames [region] describeAvailabilityZones
     r <- catch ((runResourceT . runAWS e) (send zoneRequest))
            (\ se ->
               do printInitialServiceError se
                  exitWith (ExitFailure 1))
     let z = Just (view optS3Zone opts)
     unless (elemOf (dazrsAvailabilityZones . folded . azZoneName) z r) $
       fail' ["Invalid availability zone for region."]
     return (opts, e)

printInitialServiceError :: SomeException -> IO ()
printInitialServiceError se =
  do putStrLn "ERROR: Failed to get list of zones from Amazon. This typically"
     putStrLn "is caused by one of two problems:"
     putStrLn ""
     putStrLn "  #1: There's something wrong with your keys. Check your"
     putStrLn "      arguments, or make sure AWS_ACCESS_KEY and AWS_SECRET_KEY"
     putStrLn "      are what you want them to be."
     putStrLn "  #2: There's something wrong with your computer's clock. Run"
     putStrLn "      whatever software you have to synchronize your clock, and"
     putStrLn "      try again."
     putStrLn ""
     putStrLn "Just in case it's useful, here's the raw error:"
     putStrLn (show se)

adjustTargetName :: UTCTime -> Options -> Options
adjustTargetName now opts
  | view optTargetKey opts == view optTargetKey defaultOptions =
      let baseName = takeFileName (view optKernel opts)
          formStr  = baseName ++ "-%0C%0y%m%d-%H%M%S.raw"
          keyStr   = formatTime defaultTimeLocale formStr now
      in set optTargetKey (fromString keyStr) opts
  | otherwise = opts

adjustImageName :: Options -> Options
adjustImageName opts
  | view optImageName opts == view optImageName defaultOptions =
      set optImageName (toText (view optTargetKey opts)) opts
  | otherwise = opts

fail' :: [String] -> IO a
fail' errs =
  do forM_ errs $ \ e -> putStrLn ("ERROR: " ++ e)
     putStrLn ("\n" ++ usageInfo hdr options)
     exitWith (ExitFailure 1)
 where hdr = "Usage: ec2-unikernel [OPTION...] KERNEL [RAMDISK ...]"