Now that CB 2.0 is capable of bootstrapping itself and of partially bootstrapping other nodes into Sledgehammer, I figured it would be a good time to provide a brain dump going over the main components of Crowbar and how they interact with each other. Feedback and questions welcome!
There are 3 basic objects that everything else in Crowbar 2.0 relies on -- node objects, roles, and noderole objects. Node objects encapsulate machine-specific state -- they have unique programmatically generated names (which must also be the machine's FQDN in DNS), track whether Crowbar is allowed to manage the machine, and track whether the machine is alive and reachable. In the CB 2.0 framework, nodes are things that a jig performs operations on as directed by a role through a noderole. Roles are the primary unit of functionality in CB 2.0 -- they provide the code that the jigs use to effect change on the nodes in accordance with the desired state stored in the noderole graph. Roles form a dependency graph of their own (roles must declare what other roles they depend on), each role must declare the jig that will be used to do things to a node, and roles can have flags that affect how the annealer will handle certian aspects of building the noderole graph and initial node bootstrap. Noderoles represent a binding of a role to a node. Each noderole tracks any state that needs to communicated from the user or the Crowbar framework to a node (and vice versa), and the overall noderole graph determines both the order in which roles are enacted on nodes and what attributes are visible from other noderoles when the noderole runs. On top of those 3 basic object types, we have 3 more that are used to help keep cluster administrators from dying of information overload when staring at a noderole graph with 10,000 edges. These are deployments, deployment roles, and snapshots. A deployment is an administratively convenient logical grouping of nodes along with a set of default role configurations (the deployment roles) relevant to whatever workload is being run in the deployment. Deployments all have a parent deployment except for the system deployment, which Crowbar manages and which is where all newly-discovered nodes wind up. A snapshot consists of a collection of node-role bindings created in a particular deployment. A deployment has a linear history of snapshots, and each snapshot can be proposed (in which case the user can edit noderole attributes and state, and the annealer will ignore them), committed (where user edits to the noderole are not allowed, and the annealer can use the noderoles), and archived (where the noderoles are not editable and are ignored by the annealer). Roles, in a little more detail: Roles have a lot to do. Their dependency graph is used a template to build the noderole DAG, they need to provide their jig with all the code and data it will need to effect the changes that the role wants on a node, they need to ensure that the noderole graph is build properly, and in some cases they need to track state that should not be represented directly in the noderole graph. Most roles should not need to have any state outside of the state stored in the noderole graph, but there are some (primarily those provided by the network, dns, and provisioner barclamps) that need to maintain a significant amout of state outside the noderole graph or that need to be able to react to noderole and node state transitions. To give them a formal method of doing so, you can override the base Role model with one that responds to several events that happen in the noderole and node lifecycles. The Two Rules for Events: 1: Events run synchronously, so they must be fast. If your event takes more than a few milliseconds to run, or you want to do something on a remote machine, you should make it a role of its own and bind it to that node as a noderole instead. 2: Events must be idempotent. If the work you were going to do has already been done, don't do it again. How To Respond to Events: The base Role model has a mechanism for letting Rails dynamically subclass it if there is an appropriatly named model in the Rails engine that the barclamp provides -- you can provide an override based on the name of the role by providing class BarclampFoo::RoleName < Role class, and you can provide a general Role override for your barclamp by providing class BarclampFoo::Role < Role Noderole Events: You can provide event hooks on your roles that work with noderoles at 6 points in their lifecycle: on_proposed, on_todo, on_blocked, on_transition, on_error, and on_active. Each method will be called with the noderole that just completed its state transition after the noderole has transitioned to the state, and all of its child noderoles have had their state updated accordingly -- for an example, the network barclamp provides an override to on_proposed that automatically allocates an IP address when a role for a network is bound to a node: https://github.com/crowbar/barclamp-network/blob/master/crowbar_engine/barclamp_network/app/models/barclamp_network/role.rb Node Events: You can provide event hooks on your roles that work with nodes at 2 points in their lifecycle for now: on_node_create will be called after the node is created and the default set of noderoles has been bound to it. on_node_delete will be called just before the node is destroyed. Right now, roles have 4 flags that the Crowbar framework knows how to handle: 1: Discovery, which means that this role will be automatically bound to all non-admin nodes when the node is freshly-created if the role's jig is active. 2: Bootstrap, which means that this role will be automatically bound to all freshly-created admin nodes. This flag is primarily used by the Crowbar framework to bootstrap the initial Crowbar admin node into existence. 3: Implicit, which signals that this role can be implicitly created and bound to a node as part of the dependency resolution process, and that it must be bound to the same node as the role that depends on it is being bound to. 4: Library, which is not used by anything right now and may be removed. Role dependency rules: Each role must declare what other roles it directly depends on, and those dependencies are not allowed to be cyclic -- a role cannot directly or indirectly depend on itself. Roles should not declare a dependency on a role it only indirectly depends on, as that makes the dependency graph needlessly more complicated. A role is dependent on another role if that other role must be deployed somewhere in the cluster before the current role. How the noderole graph is built: Right now, all nodes are ultimately added to the noderole graph via the add_to_node_in_snapshot function on role objects. You pass it a node and a snapshot, and it either creates a node role bound to an appropriate place in the graph or dies with an exception. In detail: 1: Verify that the jig that implements the role is active. 2: Create a deployment role for the deployment that the snapshot is a part of, or do nothing if one was already created. 3: Check to see if this role has already been bound to this node. If it has, return that noderole. 4: For each of our parent roles: 1: Check to see if there is a noderole binding for the parent role on the same node. If there is, save it on a list and move on to the next parent. 2: Check to see if there is a noderole binding anywhere for the parent role. If there is, save it on a list and move to the next parent. 3: Check to see if the parent is an implicit role. If it is, call add_to_node_in_snapshot on the parent role with our node and snapshot, save the noderole that returns on the list, and move to the next parent. 4: Throw an exception. 5: Create a new noderole binding this role to the requested node in the snapshot, and create parent/child relationships between the new noderole and the parents we found. The noderole will be created in the PROPOSED state. 6: Call the on_proposed event hook for this role with the new noderole. 7: Return the new noderole to the caller. This function will need to grow more ornate when we want to start supporting more than just the system deployment -- right now it will not respect deployment-level scoping. Adding it is a fairly straightforward extension to the tests in step 4. This function is also arguably one of the more critical pieces of code in the Crowbar framework -- it determines the shape and connectedness of the noderole graph, and hence it plays a large part in determining whether what we are deploying makes sense. What is in a noderole: 1: Pointers to its parents and children in the noderole graph. 2: The state of the noderole. 3: A blob of JSON that the user can edit. This blob is seeded from the deployment role data, which in turn is seeded from the role template 4: A blob of data that the Crowbar framework can edit. This is used by the roles to pass system-generated data to the jigs, and is usually seeded by one of the noderole events. 5: A blob of data that we get back at the end of a jig run. What happens in CB 2.0 to create the admin node: All nodes have roughly the same creation process: 1: an API request come in with the requested name of the new node, and a flag that indicates whether it is an admin node. 2: The requested name is checked to see it is a valid FQDN in the cluster's administrative DNS domain and that it is unique. If neither of those are true, the request fails, otherwise we create the node object. 3: We get all of the bootstrap roles, solve their dependencies to create a list of roles sorted in dependency order, and add them to the freshly-created node in the current committed snapshot of the system deployment. 4: Once all the node roles are added, the system will automatically recommit the snapshot. After that, the annealer takes over to bootstrap Crowbar. The NodeRole state machine, the framework-driven parts: All noderoles start in PROPOSED state, and they stay there until the snapshot they are a part of is committed. From PROPOSED, a noderole can go to TODO (if the noderole has no parents or all its parents are ACTIVE), or BLOCKED (if it has any non-ACTIVE parents). >From BLOCKED, a noderole can go to TODO when all of its parents are ACTIVE. The annealer looks for noderoles in TODO that meet the following conditions: 1: The jig that is associated with the noderole via the role half of the binding is active, 2: The snapshot that the noderole belongs to is COMMITTED, 3: The node that the noderole binds to is alive and available, 4: There is no noderole for that node that is in TRANSITION It takes all the noderoles that meet those conditions, sets them in TRANSITION, and kicks off a delayed job that will wind up setting the noderole either to ACTIVE or ERROR. When a noderole is set to ACTIVE, it sets all of its children in BLOCKED state to TODO if the rest of that child's parents are ACTIVE. When a noderole is set to ERROR, it transitions all of its children to BLOCKED if they were not already blocked. How we determine what information is visible to a node during a jig run: Right now, we use the dumbest method possible that still obeys scoping rules. We deep-merge all the JSON blobs from all the noderoles on this node that are ACTIVE, deep-merge that with all the JSON blobs from all the noderoles and deployment roles that are parents of mine, starting from the most distant set to the closest set, and then deep-merge that with the JSON blobs from the current noderole. That gets handed to the jig, which does its jiggy thing with it and whatever scripts/cookbooks/modules/whatever, and we get a blob of JSON back. We deep diff that blob with the blob we sent to the jig, and that is what winds up on the noderole's wall. -- Victor Lowther Dell CloudEdge Solutions Continuous Integration and Build Automation Czar
_______________________________________________ Crowbar mailing list Crowbar@dell.com https://lists.us.dell.com/mailman/listinfo/crowbar For more information: http://crowbar.github.com/