hal / dmr.repl   0.2.1

GitHub

Scala REPL shell for managing WildFly server instances

Scala versions: 2.10

Build Status

DMR.repl

A REPL shell for managing WildFly server instances. Build on a Scala wrapper around DMR, SBT and the Scala REPL at it's core.

Features:

  • DSL for creating DMR operations
  • Interact with model nodes in a more natural way

For an introduction to DMR please refer to the WildFly Wiki.

Working with model nodes

Please make sure to import the following packages in order to bring the necessary implicit conversions into scope and to save you some keystrokes.

import org.jboss.dmr.scala._
import org.jboss.dmr.scala.ModelNode

Creating model nodes

Use the factory methods in org.jboss.dmr.scala.ModelNode to create model nodes:

// creates an empty model node
val node = ModelNode()

// creates a new model node holding a simple value
val node = ModelNode(42)

// creates a new model node with structure
val node = ModelNode(
  "flag" -> true,
  "hello" -> "world",
  "answer" -> 42,
  "child" -> ModelNode(
    "inner-a" -> 123,
    "inner-b" -> "test",
    "deep-inside" -> ModelNode("foo" -> "bar"),
    "deep-list" -> List(
      ModelNode("one" -> 1),
      ModelNode("two" -> 2),
      ModelNode("three" -> 3)
    ),
    "value-list" -> List(
      ModelNode(1),
      ModelNode(2),
      ModelNode(3)
    )
  )
)

Addressing and operations

You can use a DSL like API to set the address and operation for a model node. To describe the "read-resource" operation on "/subsystem=datasources/data-source=ExampleDS" use the following code:

ModelNode() at ("subsystem" -> "datasources") / ("data-source" -> "ExampleDS") op 'read_resource(
  'include_runtime -> false,
  'recursive_depth -> 2
)

Addresses can be written down as (String, String) tuples separated by "/". Operations are specified using Symbols and an optional list of parameters. Each parameter is made up of another Symbol and a value. Using symbols makes the DSL both more readable and extentable.

However there's one drawback in using symbols: when using the short form 'someSymbol characters like "-" are not allowed. 'read-resource is therefore an illegal symbol. As most DMR operations and many parameters do contain "-", all underscores will be replaced with dashes:

ModelNode() op 'read_resource('include_runtime -> true)
// is exactly the same as
ModelNode() op Symbol("read-resource")(Symbol("include-runtime") -> true)

Here are some more examples using addresses and operations:

// root is a constant for an empty address
ModelNode() at root op 'read_resource
ModelNode() at ("subsystem" -> "datasources") / ("data-source" -> "ExampleDS") op 'disable

// parameters are specified as pairs (Symbol -> Any)
ModelNode() at ("core-service" -> "platform-mbean") / ("type" -> "runtime") op 'read_resource(
  'attributes_only -> true,
  'include_runtime -> false,
  'recursive_depth -> 3,
  'custom_parameter -> "custom-value"
)

// unsupported parameter types will throw an IllegalArgumentException
ModelNode() at root op 'read_resource('proxies -> Console.out)

Reading Nodes

Reading values from a model node follows the sementics of a Map[String, ModelNode], but instead of a string you have to provide a Path as key. Thanks to an implicit conversion expressions like "a" / "b" / "c" are automatically converted to a path.

val node = ModelNode(
  "flag" -> true,
  "hello" -> "world",
  "answer" -> 42,
  "level0" -> ModelNode(
    "level1" -> ModelNode(
      "level2" -> ModelNode(
        "level3" -> ModelNode(
          "level4" -> ModelNode("foo" -> "bar")
        )
      )
    )
  )
)

val flag = node("flag")
val boom = node("gag") // throws a NoSuchelementException
val hello = node.get("hello") // returns an Option[ModelNode]
val x = node.getOrElse("nope", ModelNode("y"))
val check = node.contains("level0" / "level1" / "level2" / "level3")
val level3 = node("level0" / "level1" / "level2" / "level3")
val level2 = for {
  l0 <- node.get("level0")
  l1 <- l0.get("level1")
  l2 <- l1.get("level2")
} yield l2

Since the result of node.get("a" / "b" / "c") is Option[ModelNode], reading nested model nodes is safe even if some children in the path do not exist. In this case None wil be returned:

val nope = node.get("level0" / "oops" / "level2" / "level3")

Reading Values

You can use the folowing methods to read values from model nodes:

  • ModelNode.asBoolean
  • ModelNode.asInt
  • ModelNode.asLong
  • ModelNode.asBigInt
  • ModelNode.asDouble
  • ModelNode.asString

These methods return Option instances of the relevant type. This is because not all conversions make sense on all kind of model nodes:

val node = ModelNode(
  "flag" -> true,
  "child" -> ModelNode(
    "size" -> 0
  )
)

val nonsense = node("child").asDouble // None

Writing

Simple values can be set using node("foo") = "bar". If "foo" doesn't exist it will be created and updated otherwise. As an alternative you can use the += operator, which comes in handy if you want to add multiple key / value pairs:

val node = ModelNode()

node("foo") = "bar"
node += ("foo" -> "bar")

node += (
  "flag" -> true,
  "hello" -> "world",
  "answer" -> 42,
  "child" -> ModelNode(
    "inner-a" -> 123,
    "inner-b" -> "test",
    "deep-inside" -> ModelNode(
      "foo" -> "bar"
    ),
    "deep-list" -> List(
      ModelNode("one" -> 1),
      ModelNode("two" -> 2),
      ModelNode("three" -> 3)
    )
  )
)

Reading and writing can also be combined in one call:

node("child" / "deep-inside") += ("foo" -> "xyz")

Collection Operations

Since ModelNode mixes in Traversable[(String, ModelNode)] you can use all those nifty collection methods like foreach, map or filter:

val node = ModelNode(
  "flag" -> true,
  "hello" -> "world",
  "answer" -> 42
)

// turn all keys to upper case
val shout = node.map(kv => kv._1.toUpperCase -> kv._2)

// filter for nodes
val aa = node.filter(_._1 contains "a")
val n42 = node.filter(_._2 == ModelNode(42))

// combine nodes
val node2 = node ++ ModelNode("abc" -> 1)

Please note that these kind of methods only traverse over the direct children of a model node. If you want to traverse over all children in a deeply nested model node use the inOrder method which gives you a list of (String, ModelNode) tuples:

val node = ModelNode(
  "flag" -> true,
  "answer" -> 42,
  "child" -> ModelNode(
    "inner-a" -> 123,
    "inner-b" -> "test",
    "deep-inside" -> ModelNode("foo" -> "bar"),
    "deep-list" -> List(
      ModelNode("one" -> 1),
      ModelNode("two" -> 2)
    ),
    "value-list" -> List(
      ModelNode(1),
      ModelNode(2)
    )
  )
)

node.inOrder map { tpl => tpl._1 }
// will result in List(flag, answer, child, inner-a, inner-b, deep-inside, foo, deep-list, one, two, value-list)

Pattern Matching / Extractor

Model nodes support pattern matching / extractor against their type:

val node = ModelNode(
  "flag" -> true,
  "hello" -> "world",
  "answer" -> 42,
  "one" -> 1,
  "two" -> 2
)

import org.jboss.dmr.ModelType._
for ((key, node) <- node) node match {
  case ModelNode(INT) => println(s"$key is an integer: $node")
  case ModelNode(t) => println(s"$key is not an integer, but $t")
}

val (key, firstNode) = node.head
val ModelNode(booleanType) = firstNode
// booleanType == org.jboss.dmr.ModelType.BOOLEAN

val integerNodes = for {
  (key, node) <- node
  ModelNode(t) = node
  if t == INT
} yield node
// results in List(42, 1, 2)

Composites

A composite operation is setup using the ModelNode.composite(n: ModelNode, xn: ModelNode*) factory method:

ModelNode.composite(
  ModelNode() at ("core-service" -> "management") / ("access" -> "authorization") op 'read_resource(
    'recursive_depth -> 2),
  ModelNode() at ("core-service" -> "management") / ("access" -> "authorization") op 'read_children_names(
    'name -> "role-mapping"),
  ModelNode() at ("subsystem" -> "mail") / ("mail-session" -> "*") op 'read_resource_description,
  ModelNode() at ("subsystem" -> "datasources") / ("data-source" -> "ExampleDS") op 'disable ,
  ModelNode() at ("core-service" -> "platform-mbean") / ("type" -> "runtime") op 'read_attribute(
    'name -> "start-time")
)

Execute an operation

To execute DMR operations against a running WildFly instance use DMR.repl. The Response object has an extractor and constants to parse the DMR response using pattern matching. The pattern matching variables result and failure are both model nodes containing the response payload or the wrapped error description:

val client = connect()
val node = ModelNode() at ("subsystem" -> "datasources") op 'read_resource

import org.jboss.dmr.repl.Response
import org.jboss.dmr.repl.Response.{Success, Failure}
(client ! node) map {
  case Response(Success, result) => ...
  case Response(Failure, failure) => ...
}

import scala.util.{Success, Failure}
(client ? node).onComplete {
  case Success(response) => ...
  case Failure(ex) => ...
}

REPL Shell

Connect

To make use of DMR.repl, first thing you'll need is a client for a WildFly server.

import org.jboss.dmr.repl.Client._

// Creates a client and connects to 127.0.0.1:9999
val client  = connect()

// Connects to specified host:port
val client = connect("homer", 9876)

Execute DMR Operations

To execute an operation you can choose between ! for synchronous and ? for asynchronous execution. The synchronous method returns with a Try[ModelNode], the asynchronous with a Future[ModelNode].

Synchronous

import org.jboss.dmr.repl.Response.{Success, Failure}
val node = ModelNode() at ("subsystem" -> "datasources") op 'read_resource

(client ! node) map {
  case Response(Success, result) => ...
  case Response(Failure, failure) => ...
}

Asynchronous

import scala.util.{Success, Failure}
val node = ModelNode() at ("subsystem" -> "datasources") op 'read_resource

(client ? node).onComplete {
  case Success(response) => ...
  case Failure(ex) => ...
}

When executing DMR operations, you can make use of some handy extractors which will return the actual payload of a DMR response / composite response. If you execute a DMR operation like this

val node = ModelNode() at ("subsystem" -> "datasources") op 'read_resource
val response = client ! node getOrElse ModelNode.Undefined

response will contain the full response:

{
    "outcome" => "success",
    "result" => {
        "data-source" => {"ExampleDS" => undefined},
        "jdbc-driver" => {"h2" => undefined},
        "xa-data-source" => undefined
    }
}

If you're just interested in the result, use

val Response(Success, result) = response

which will assign a model node to result containing just the payload:

{
    "data-source" => {"ExampleDS" => undefined},
    "jdbc-driver" => {"h2" => undefined},
    "xa-data-source" => undefined
}

The same applies to composite operations:

val d1 = ModelNode() at ("deployment" -> "foo.war") op 'read_resource
val d2 = ModelNode() at ("deployment" -> "bar.war") op 'read_resource
val comp = ModelNode.composite(d1, d2)
val response = client ! comp getOrElse ModelNode.Undefined

Again response will contain the full response with the all steps and nested responses:

{
    "outcome" => "success",
    "result" => {
        "step-1" => {
            "outcome" => "success",
            "result" => {
                "name" => "foo.war",
                ...
            }
        },
        "step-2" => {
            "outcome" => "success",
            "result" => {
                "name" => "bar.war",
                ...
            }
        }
    }
}

To extract just the nested result nodes, use the Composite extractor:

val Composite(Success, steps) = response

which will give you a List[ModelNode]:

List(
{
    "name" => "foo.war",
    ...
},
{
    "name" => "bar.war",
    ...
})

DMR Scripts

If you have a more advanced use cases or want to chain several operations, scripts are the way to go. To write a script create a subclass of Script and override the code method. Scripts carry a type parameter for the expected result. Furthermore they rely on an implicit client instance which is brought into scope by the clients companion object. So please make sure you import org.jboss.dmr.repl.Client._ when you run your script.

import scala.concurrent.duration._
import scala.util.Try
import org.jboss.dmr.scala._
import org.jboss.dmr.repl._
import org.jboss.dmr.repl.Response._

/** Returns the uptime for the standalone server */
class Uptime extends Script[Duration] {
  def code: Try[Duration] = {
    val node = ModelNode() at ("core-service" -> "platform-mbean") / ("type" -> "runtime") op 'read_attribute('name -> "uptime")
    client ! node map {
      case Response(Success, result) => result.asLong.get.millis
      case Response(Failure, failure) => throw new ScriptException(failure)
    }
  }
}

Execute a script using its run method:

import org.jboss.dmr.repl.Client._

connect()
val uptime = new Uptime().run()

Please see the samples package for more advanced samples.

Local Storage

You can save and load model nodes to and from the local file system. By default they are written as base64 to ~/.sbt/node_<counter>, but you can choose another folder and/or filename.

val node = ModelNode() at ("core-service" -> "platform-mbean") / ("type" -> "runtime") op 'read_resource(
  'attributes_only -> true,
  'include_runtime -> false,
  'recursive_depth -> 3,
  'custom_parameter -> "custom-value"
)

// Uses an implicit conversion and the default folder and filename
node.save()

// Uses a storage using the folder 'nodes' in the current folder and saves the node as 'mbean'
val storage = new Storage(new java.io.File("nodes"))
storage.save(node, "mbean")

// load by name
val copy = storage.load("mbean")

// get rid of the saved node
storage.remove("mbean")