But that said....
The other day, I got thinking about Scala traits and the potential they offer for creating composable, refinable components that express configuration information and configuration logic. Other Scala features such as its type-safe nature and its package system could enable an elegant and simple way to represent configuration data and logic.
Let's start with a simple example that we will gradually extend. Let's assume I want to setup a cluster of nodes where each node is configured with an admin user account.
I start of with modelling a user with a User trait, a collection of properties such as the user's name, uid, etc.. The trait also contains (mock in this case) logic that can be executed to add or remove users on a computer. Notice how the user trait is itself composed from other traits.
trait Deployable{
def deploy
def undeploy
}
trait Named{
var name : String = _
}
trait User extends Named with Deployable{
var uid : Short = _
override def deploy = println ("useradd -u " + uid + " " + name)
override def undeploy = println ("userdel -r " + name)
}
Users are to be deployed on nodes that I've modelled with a node trait (and once again that trait itself is composed from others). A node has an IP address and provides a convenience method to add things (such as a user) to it. Things added to the node will be deployed as the node is deployed by the hypothetical deployment runtime.
trait WithChildren{
var includes : List[Any] = List()
def contains (a : Any*) = a.foreach ( item => includes = item :: includes)
}
trait Node extends WithChildren with Deployable{
var ip : String = _
def deploy = println ("Actual logic to deploy children goes here.")
def undeploy = println ("Actual logic to undeploy children goes here.")
}
trait Config extends WithChildren with Deployable{
def deploy = println ("Actual logic to deploy nodes goes here.")
def undeploy = println ("Actual logic to undeploy nodes goes here.")
}
The config trait models a collection of nodes. Again, in a real system, it would be the thing I actually pass to a deployment runtime for enaction.
With all the pieces in place, let's see a simple configuration:
object config1 extends Config{
contains{
new Node{
ip = "192.168.1.1"
contains{
new User{
name = "admin"
uid = 102
}
}
}
}
}
In the configuration above, I deploy the admin user on a single node. I can refine this a little bit by subclassing the node trait to create an AdminNode trait which includes the admin user by default.
object adminUser extends User{
name ="admin"
uid = 102
}
trait AdminNode extends Node{
contains{
adminUser
}
}
In the snippet above, I've created an object adminUser which is a trait which has been instantiated. The adminUser can no longer be refined (subclassed) as Scala (unlike SmartFrog) is not a prototype based language. However the object can still be reused and composed into other traits. The other important thing to note is that, in Scala, when you create a trait, the logic that is executed when invoking its constructor is the entire body of the trait. So thanks to this feature, I can define new variables, change existing ones or invoke method calls within the curly braces without having to define an explicit constructor method as I would have to if using Java or Groovy.
object config2 extends Config{
contains{
new AdminNode{ ip = "192.168.1.2" }
}
}
If you want to modify a particular instance of AdminNode in place let's say to add another user account, it is also easily done.
object config3 extends Config{
contains{
new AdminNode{
ip = "192.168.1.3"
contains{
new User{
name = "demo"
uid = 102
}
}
}
}
}
One of the benefits of using Scala directly to write the configuration is that I can use constructs such as loops to create or modify the data. For instance, let's create a set of admin nodes from a list of IP addresses.
object config4 extends Config{
List("192.168.1.2","192.168.1.3","192.168.1.4").foreach{ addr=>
contains{
new AdminNode{ ip = addr}
}
}
}
Just as I modelled users, I can also model applications running on nodes (again by composing traits). In the example below I model generic applications installed through packages (via a package manager a la apt-get) and controlled via Linux services.
trait Package extends Deployable{
var packages : List[String] = List()
override def deploy : Unit = println (packages.foreach(s => "apt-get install " + s))
override def undeploy : Unit = println (packages.foreach(s => "apt-get remove " + s))
}
trait Services extends Runnable{
var services : List[String] = List()
override def start : Unit = println (services.foreach(s => "/etc/init.d/" + s + " start"))
override def stop : Unit = println (services.foreach(s => "/etc/init.d/" + s + " stop"))
}
Using those traits, I can then model an application such as an Apache Web Server, or refine an Apache Web server into a Django application server running as an apache module.
trait WebServer{
var port = 8080
}
trait Apache2 extends Services with Package with WebServer{
packages += "apache2"
services += "apache2"
port = 80
}
trait Django extends Apache2{
packages = "libapache2-mod-python" :: "python-django" :: packages
}
I can then include instances of Apache2 or Django in my hypothetical cluster.
object config5 extends Config{
contains(
new AdminNode{
ip = "192.168.1.3"
contains{
new Apache2{ port = 8182 }
}
},
new AdminNode{
ip = "192.168.1.4"
contains{
new Django{ port = 87 }
}
}
)
}
As the configuration information is written directly in Scala, I automatically gain access to interesting features:
- the compiler highlights syntax errors in the description
- I can use IDE for syntax highlighting, auto completion and re-factoring
- descriptions can be organised into packages and imported as required.
Obviously, as a thought experiment, this ought to be taken with a pinch of salt. I've only touched on some of the language features that are appropriate for expressing composable and reusable models of configuration data. I have not tried (and most likely wont try) to implement a distributed deployment engine that could deploy such configuration descriptions.
No comments:
Post a Comment