Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cardano-node-chairman/test/Spec/Chairman/Cardano.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ hprop_chairman :: H.Property
hprop_chairman = integrationRetryWorkspace 2 "cardano-chairman" $ \tempAbsPath' -> H.runWithDefaultWatchdog_ $ do
conf <- mkConf tempAbsPath'

let creationOptions = def{ creationNodes = cardanoDefaultTestnetNodeOptions }
let creationOptions = def{ creationNodes = cardanoDefaultTestnetNodesWithOptions }
allNodes <- testnetNodes <$> createAndRunTestnet creationOptions def conf

chairmanOver 120 50 conf allNodes
5 changes: 5 additions & 0 deletions cardano-node/src/Cardano/Node/Testnet/Paths.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Cardano.Node.Testnet.Paths
, defaultSocketPath
, defaultConfigFile
, defaultPortFile
, defaultNodeEnvFile
) where

import System.FilePath ((</>))
Expand Down Expand Up @@ -62,3 +63,7 @@ defaultConfigFile = "configuration.yaml"
-- | Relative path to a node's port file: @defaultNodeDataDir n </> "port"@
defaultPortFile :: Int -> FilePath
defaultPortFile n = defaultNodeDataDir n </> "port"

-- | Relative path to a node's env file: @defaultNodeDataDir n </> "env"@
defaultNodeEnvFile :: Int -> FilePath
defaultNodeEnvFile n = defaultNodeDataDir n </> "env"
1 change: 1 addition & 0 deletions cardano-testnet/cardano-testnet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ library
, network
, network-mux
, optparse-applicative-fork
, parsec
, ouroboros-network:{api, framework, ouroboros-network} ^>= 1.1
, cardano-diffusion:{api, cardano-diffusion} ^>= 1.0
, prettyprinter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Added `--nodes` flag to specify node roles (SPO/relay) and custom `cardano-node` binaries per node.
Example: `--nodes spo,spo:node-bin=/path/to/bin,relay,relay`.
6 changes: 3 additions & 3 deletions cardano-testnet/src/Cardano/Testnet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ module Cardano.Testnet (
TestnetRuntimeOptions(..),
TestnetEnvOptions(..),
RpcSupport(..),
TestnetNodeOptions(..),
NodeOptions(..),
cardanoDefaultTestnetNodeOptions,
TestnetNodesWithOptions(..),
NodeWithOptions(..),
cardanoDefaultTestnetNodesWithOptions,
getDefaultAlonzoGenesis,
getDefaultShelleyGenesis,

Expand Down
92 changes: 77 additions & 15 deletions cardano-testnet/src/Parsers/Cardano.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module Parsers.Cardano
( cmdCardano
, cmdCreateEnv
, parseNodeSpecs
) where

import Cardano.Api (AnyShelleyBasedEra (..))
Expand All @@ -13,6 +14,7 @@ import Cardano.Prelude (readMaybe)
import Prelude

import Control.Applicative (optional, (<|>))
import Control.Monad (unless)
import Data.Default.Class (def)
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty ((:|)))
Expand All @@ -21,6 +23,9 @@ import Data.Word (Word64)
import Options.Applicative (CommandFields, Mod, Parser)
import qualified Options.Applicative as OA
import Options.Applicative.Types (readerAsk)
import Text.Parsec (char, many1, noneOf,
sepBy1, string, try, (<?>), parse, eof, notFollowedBy)
import qualified Text.Parsec as Parsec

import Testnet.Defaults (defaultEra)
import Testnet.Start.Cardano
Expand Down Expand Up @@ -55,7 +60,7 @@ pFromEnv = TestnetEnvOptions

pCreationOptions :: Parser TestnetCreationOptions
pCreationOptions = TestnetCreationOptions
<$> pTestnetNodeOptions
<$> pTestnetNodesWithOptions
<*> pure (AnyShelleyBasedEra defaultEra)
<*> pMaxLovelaceSupply
<*> pNumDReps
Expand Down Expand Up @@ -105,28 +110,85 @@ pKesSource = OA.flag UseKesKeyFile UseKesSocket
<> OA.showDefault
)

pTestnetNodeOptions :: Parser TestnetNodeOptions
pTestnetNodeOptions =
fmap (maybe cardanoDefaultTestnetNodeOptions mkPoolNodes) <$>
optional $ OA.option ensureAtLeastOne
( OA.long "num-pool-nodes"
<> OA.help "Number of pool nodes. Note this uses a default node configuration for all nodes."
<> OA.metavar "COUNT"
)
pTestnetNodesWithOptions :: Parser TestnetNodesWithOptions
pTestnetNodesWithOptions =
pNodes <|> pNumPoolNodes <|> pure cardanoDefaultTestnetNodesWithOptions
where
defaultSpoOption = NodeOptions []

mkPoolNodes num = TestnetNodeOptions
{ optSpoNodes = defaultSpoOption :| L.replicate (num - 1) defaultSpoOption
, optRelayNodes = []
}
pNumPoolNodes :: Parser TestnetNodesWithOptions
pNumPoolNodes =
(\num -> TestnetNodesWithOptions { optSpoNodes = defaultSpoOption :| L.replicate (num - 1) defaultSpoOption, optRelayNodes = [] }) <$>
OA.option ensureAtLeastOne
( OA.long "num-pool-nodes"
<> OA.help "Number of pool nodes. Note this uses a default node configuration for all nodes."
<> OA.metavar "COUNT"
)
defaultSpoOption = NodeWithOptions Nothing []

ensureAtLeastOne :: OA.ReadM Int
ensureAtLeastOne = readerAsk >>= \arg ->
case readMaybe arg of
Just n | n >= 1 -> pure n
_ -> fail "Need at least one SPO node to produce blocks, but got none."

pNodes :: Parser TestnetNodesWithOptions
pNodes = OA.option readNodeSpecs
( OA.long "nodes"
<> OA.help "Comma-separated node specifications. SPO nodes must come before relay nodes. \
\Each spec is a role (spo or relay) optionally followed by :node-bin=<path>. \
\If the path contains commas, colons, double quotes, or backslashes, wrap it \
\in double quotes and escape any literal double quotes as \\\" and backslashes \
\as \\\\ within. To prevent bash from consuming the double quotes, enclose the \
\whole argument in single quotes. \
\Examples: --nodes spo,spo:node-bin=/path/to/bin,relay,relay | \
\--nodes 'spo:node-bin=\"/path,with:commas\",relay'"
<> OA.metavar "SPEC[,SPEC...]"
)

readNodeSpecs :: OA.ReadM TestnetNodesWithOptions
readNodeSpecs = readerAsk >>= either (fail . show) pure . parseNodeSpecs

-- | Parse a @--nodes@ argument string into 'TestnetNodesWithOptions'.
parseNodeSpecs :: String -> Either Parsec.ParseError TestnetNodesWithOptions
parseNodeSpecs = parse (nodeSpecsParser <* eof) "Error parsing node specifications"
where
nodeSpecsParser :: Parsec.Parsec String () TestnetNodesWithOptions
nodeSpecsParser = do
specs <- nodeSpec `sepBy1` char ','
let (spos, relays) = span (\(role, _) -> role == Spo) specs
unless (all (\(role, _) -> role == Relay) relays) $
fail "SPO nodes must come before relay nodes. Example: --nodes spo,spo,relay,relay"
case map snd spos of
[] -> fail "Need at least one SPO node to produce blocks."
(s:ss) -> pure $ TestnetNodesWithOptions
{ optSpoNodes = s :| ss
, optRelayNodes = map snd relays
}

nodeSpec :: Parsec.Parsec String () (NodeRole, NodeWithOptions)
nodeSpec = do
role <- nodeRole
bin <- optional $ char ':' *> nodeBinKV
pure (role, NodeWithOptions bin [])

nodeRole :: Parsec.Parsec String () NodeRole
nodeRole =
Spo <$ try (string "spo" <* notFollowedBy (noneOf ",:\"\\"))
<|> Relay <$ try (string "relay" <* notFollowedBy (noneOf ",:\"\\"))
<?> "node role (\"spo\" or \"relay\")"

nodeBinKV :: Parsec.Parsec String () FilePath
nodeBinKV = string "node-bin=" *> (quotedPath <|> unquotedPath) <?> "\"node-bin=<path>\", where <path> is the path to the node binary, optionally quoted if it contains special characters"

quotedPath :: Parsec.Parsec String () FilePath
quotedPath = char '"' *> Parsec.many quotedChar <* char '"'
where
quotedChar = try (char '\\' *> (char '"' <|> char '\\')) <|> noneOf "\""

unquotedPath :: Parsec.Parsec String () FilePath
unquotedPath = many1 (noneOf ",:\"\\")

data NodeRole = Spo | Relay deriving Eq

pOnChainParams :: Parser TestnetOnChainParams
pOnChainParams = fmap (fromMaybe DefaultParams) <$> optional $
pCustomParamsFile <|> pMainnetParams
Expand Down
2 changes: 1 addition & 1 deletion cardano-testnet/src/Parsers/Run.hs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ runCardanoOptions = \case
let dirName = envPath fromEnvOptions
unlessM (doesDirectoryExist dirName) $ error $ "The provided path does not exist or is not a directory: " <> dirName
conf <- mkConfigAbs dirName
nodes <- readNodeOptionsFromEnv (unTmpAbsPath (tempAbsPath conf))
nodes <- readNodesWithOptionsFromEnv (unTmpAbsPath (tempAbsPath conf))
runSimpleApp . runResourceT $ do
logInfo $ "Starting testnet in environment: " <> display (tempAbsPath conf)
void $ cardanoTestnet nodes fromEnvRuntimeOptions
Expand Down
30 changes: 27 additions & 3 deletions cardano-testnet/src/Testnet/Process/RunIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Testnet.Process.RunIO
, procNode
, procKesAgent
, execKesAgentControl_
, procCustom
, procFlex
, liftIOAnnotated
) where
Expand Down Expand Up @@ -144,8 +145,7 @@ execFlexAny' execConfig pkgBin envBin arguments = GHC.withFrozenCallStack $ do
cp <- procFlex' execConfig pkgBin envBin arguments
liftIOAnnotated $ IO.readCreateProcessWithExitCode cp ""



-- | Like 'procFlex', but takes an explicit 'ExecConfig' instead of using 'defaultExecConfig'.
procFlex'
:: HasCallStack
=> MonadIO m
Expand All @@ -160,7 +160,20 @@ procFlex'
-- ^ Captured stdout
procFlex' execConfig pkg binaryEnv arguments = GHC.withFrozenCallStack $ do
bin <- binFlex pkg binaryEnv
return (IO.proc bin arguments)
procCustom' execConfig bin arguments

-- | Build a 'CreateProcess' from an already-resolved binary path, arguments, and 'ExecConfig'.
procCustom'
:: (HasCallStack)
=> MonadIO m
=> ExecConfig
-> FilePath
-- ^ Path to the binary
-> [String]
-- ^ Arguments to the CLI command
-> m CreateProcess
procCustom' execConfig bin arguments = GHC.withFrozenCallStack $
pure (IO.proc bin arguments)
{ IO.env = getLast $ execConfigEnv execConfig
, IO.cwd = getLast $ execConfigCwd execConfig
-- this allows sending signals to the created processes, without killing the test-suite process
Expand Down Expand Up @@ -311,6 +324,17 @@ procFlex
-- ^ Captured stdout
procFlex = procFlex' defaultExecConfig

-- | Like 'procFlex', but takes an explicit binary path instead of resolving
-- via package name and environment variable.
procCustom
:: (HasCallStack)
=> FilePath
-- ^ Path to the binary
-> [String]
-- ^ Arguments to the CLI command
-> RIO env CreateProcess
procCustom = procCustom' defaultExecConfig

-- This will also catch async exceptions as well.
liftIOAnnotated :: (HasCallStack, MonadIO m) => IO a -> m a
liftIOAnnotated action = GHC.withFrozenCallStack $
Expand Down
11 changes: 8 additions & 3 deletions cardano-testnet/src/Testnet/Runtime.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import Cardano.Node.Testnet.Paths (defaultSocketName)
import qualified Testnet.Ping as Ping
import Testnet.Process.Run (ProcessError (..), initiateProcess)
import Testnet.Process.RunIO (execCli_, execKesAgentControl_, liftIOAnnotated,
procKesAgent, procNode)
procCustom, procKesAgent, procNode)
import Testnet.Types (TestnetKesAgent (..), TestnetNode (..),
TestnetRuntime (configurationFile), showIpv4Address, testnetSprockets)

Expand Down Expand Up @@ -121,11 +121,13 @@ startNode
-- ^ Node port
-> Int
-- ^ Testnet magic
-> Maybe FilePath
-- ^ Optional custom node binary. 'Nothing' uses the default resolution.
-> [String]
-- ^ The command to execute to start the node.
-- @--socket-path@, @--port@, and @--host-addr@ gets added automatically.
-> ExceptT NodeStartFailure m TestnetNode
startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do
startNode tp node ipv4 port _testnetMagic mNodeBin nodeCmd = GHC.withFrozenCallStack $ do
let tempBaseAbsPath = makeTmpBaseAbsPath tp
socketDir = makeSocketDir tp
logDir = makeLogDir tp
Expand Down Expand Up @@ -156,7 +158,10 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do
, "--port", show port
, "--host-addr", showIpv4Address ipv4
]
nodeProcess <- newExceptT . fmap (first ExecutableRelatedFailure) . try $ runRIO () $ procNode completeNodeCmd
nodeProcess <- newExceptT . fmap (first ExecutableRelatedFailure) . try $ runRIO () $
case mNodeBin of
Nothing -> procNode completeNodeCmd
Just bin -> procCustom bin completeNodeCmd

-- The port number if it is obtained using 'H.randomPort', it is firstly bound to and then closed. The closing
-- and release in the operating system is done asynchronously and can be slow. Here we wait until the port
Expand Down
Loading
Loading