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