Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added support for build tag add/removal.

Updated docs.
Added more unit tests.
  • Loading branch information...
commit 2b195623d998d37c4176b71b3e329f69133e330c 1 parent 93fa217
@ahonor ahonor authored
View
4 docs/en/guide/01-introduction.md
@@ -3,8 +3,8 @@
## What is Yana?
Yana is yet another node authority! What is a node authority?
-Yana is an information repository where you store information
-about hosts on your network.
+A node authority is an information repository where one
+stores information about hosts on the network.
Driving tools and other operations management software from
an up to date central repository about the nodes in your
View
6 docs/en/guide/02-getting-started.md
@@ -25,7 +25,7 @@ behavior driven from views of it.
* `Tag`: A symbolic label that can be associated with a Node
* Data: text
- For attributes that are shared between nodes, two other concepts are useful:
+ For attributes that are shared between nodes, two other concepts can be useful:
* `ExternalAttribute`: Like an `Attribute` but shareable between Nodes.
* Data: name, value, description
@@ -59,8 +59,8 @@ behavior driven from views of it.
: Yana is not a CMDB. Yana is a repository of data about your Nodes.
Other kinds of resources exist your environment like services,
- network topology, packaged artefacts. One or more other tools
- might already be providing this info in your environment now.
+ network topology, packaged artifacts. Other tools
+ may already be providing this info in your environment now.
## Installation
View
21 docs/en/manpages/man5/node-v10.md
@@ -1,4 +1,4 @@
-% NODE-V10(5) YANA User Manuals | Version 1.0
+% NODE-V10(5) YANA Reference
% Alex Honor
% August 03, 2011
@@ -10,7 +10,7 @@ The Node Model XML document declares a node model that can also be
uploaded to a Yana server. This document describes the
format and necessary elements.
-# Elements
+## node
The root (aka "top-level") element of the node file is called `node`.
@@ -18,7 +18,7 @@ The root (aka "top-level") element of the node file is called `node`.
name
-: The node name. This is a logical identifier from the node. (required)
+: The node name. This is a logical identifier from the node and must be unique. (required)
description
@@ -32,6 +32,11 @@ osName
: The operating system name such as Linux or Mac OS X. (optional)
+id
+
+: The database identifier. If left out, a new object will be created.
+
+
*Nested Elements*
[attributes](#attributes)
@@ -46,7 +51,7 @@ osName
: Collection of external defined attributes
-## attributes
+### attributes
Defines a collection of attribute elements for the node.
@@ -56,7 +61,7 @@ Defines a collection of attribute elements for the node.
: Further user defined attribute.
-## attribute
+### attribute
A single metadata attribute.
@@ -70,7 +75,7 @@ value
: The value of the attribute
-## tags
+### tags
Collection of symbolic names for the node
@@ -80,7 +85,7 @@ Collection of symbolic names for the node
: Single tag definition
-## tag
+### tag
A single tag definition
@@ -88,7 +93,7 @@ A single tag definition
## name
-: The name
+: The name. Must be unqiue.
View
12 grails-app/conf/UrlMappings.groovy
@@ -1,4 +1,5 @@
+
class UrlMappings {
static mappings = {
@@ -12,6 +13,7 @@ class UrlMappings {
"/login/$action?"(controller: "login")
"/logout/$action?"(controller: "logout")
+ // Nodes
"/api/nodes"(controller: "nodeRest", parseRequest:true) {
action = [ GET: "list" , POST: "add" ]
}
@@ -20,6 +22,16 @@ class UrlMappings {
action = [ GET: "show" , POST: "save" , DELETE: "delete" ]
}
+ "/api/nodes/$nodeId/tags"(controller: "nodeRest", parseRequest:true) {
+ action = [ GET: "listTags" ]
+ }
+
+ // Apply and list tags on sets of Nodes
+ "/api/nodes/tags/$name"(controller: "tagRest", parseRequest:true) {
+ action = [ GET: "show", POST: "save", DELETE: "remove" ]
+ }
+
+ // Attributes
"/api/attributes"(controller: "attributesRest") {
action = [ GET: "list" ]
}
View
21 grails-app/controllers/yana/node/NodeRestController.groovy
@@ -203,5 +203,26 @@ class NodeRestController {
}
}
+ //
+ // List the tags. Url: GET /api/nodes/{id}/tags
+ //
+ def listTags = {
+ Node obj = Node.get(params.id)
+ if (obj) {
+ def writer = new StringWriter()
+ def xml = new MarkupBuilder(writer)
+ def list = obj.tags
+ xml.tags() {
+ list.each{ Tag tagInstance->
+ tag(id:tagInstance.id) {
+ name(tagInstance.name)
+ }
+ }
+ }
+ render writer.toString()
+ } else {
+ response.sendError(404)
+ }
+ }
}
View
118 grails-app/controllers/yana/node/TagRestController.groovy
@@ -0,0 +1,118 @@
+package yana.node
+
+import groovy.xml.MarkupBuilder
+
+import grails.converters.XML
+import grails.converters.JSON
+
+import yana.node.Node
+
+class TagRestController {
+
+
+ //
+ // Get the tag. Url: GET /api/nodes/tags/$name
+ //
+ def show = {
+ println "DEBUG: inside show()..."
+ def nodes = []
+ if (params?.nodeName) {
+ println "DEBUG: params.nodeName="+params.nodeName
+ nodes = Node.findAllByLikeNameAndTagsByName(params.nodeName,
+ params.name)
+ } else {
+ nodes = Node.findAllTagsByName(params.name)
+ }
+
+ if (nodes.size() >0) {
+ println "DEBUG: there are some nodes"
+ render renderTag(params.name,nodes)
+ } else {
+ println "DEBUG: no nodes were found matching the tag"
+ response.sendError(404)
+ }
+ }
+
+ //
+ // Helper method to generate tag xml
+ //
+ static def renderTag(tagName, nodeList) {
+ def writer = new StringWriter()
+ def xml = new MarkupBuilder(writer)
+ xml.tag() {
+ name "${tagName}"
+ nodes(count: nodeList.size()) {
+ nodeList.each{ Node nodeInstance->
+ node(id: nodeInstance.id) {
+ name(nodeInstance.name)
+ }
+ }
+ }
+ }
+ writer.toString()
+ }
+
+ //
+ // Save a tag to matching Nodes
+ //
+ def save = {
+ println "DEBUG: inside save..."
+ println "DEBUG: looking for nodes like: "+params?.nodeName
+ def nodes = Node.findAllByNameLike(params?.nodeName)
+ if (nodes.size()>0) {
+ println "DEBUG: number nodes found: "+nodes.size()
+ nodes.each{Node nodeInstance->
+ println "DEBUG: addToTags for node: " +nodeInstance
+ nodeInstance.addToTags(new Tag(name: params.name))
+ nodeInstance.save()
+ }
+ render renderTag(params.name,nodes)
+ } else {
+ println "DEBUG: no nodes were found matching: ${params.nodeName}"
+ response.sendError(404)
+ }
+
+ }
+
+ //
+ // Remove a tag from matching Nodes
+ //
+ def remove = {
+ println "DEBUG: inside remove..."
+ println "DEBUG: looking for nodes like: "+params?.nodeName
+ // validate the parameter, nodeName
+ if (params?.nodeName) {
+
+ }
+ def foundNodes = Node.findAllByNameLikeAndTagsByName(params?.nodeName,
+ params.name)
+ println "DEBUG: foundNodes.size()=" + foundNodes.size()
+
+ def writer = new StringWriter()
+ def xml = new MarkupBuilder(writer)
+
+ xml.tag() {
+ name "${params.name}"
+ nodes(count: foundNodes.size()) {
+ foundNodes.each{Node nodeInstance->
+ def tags = nodeInstance.findTagByName(params.name)
+ tags.each {Tag tagInstance->
+ println "DEBUG: removing Tag: ${tagInstance} from node: " +nodeInstance+"..."
+ try {
+ nodeInstance.removeFromTags(tagInstance)
+ nodeInstance.save()
+ println "DEBUG: Tag removed from node: " + nodeInstance
+ node(id: nodeInstance.id) {
+ name "${nodeInstance.name}"
+ }
+ } catch (Exception e) {
+ println "DEBUB: Caught an error removing tag: "+ e
+ println "DEBUG: e instanceof "+ e.getClass().getName()
+ }
+ }
+ }
+ }
+ }
+ render writer.toString()
+ }
+}
View
45 grails-app/domain/yana/node/Node.groovy
@@ -57,12 +57,12 @@ class Node {
if (map.tags.tag instanceof List) {
map.tags.tag.each {
def tagInstance = Tag.fromMap(it)
- tagInstance.save(flush:true)
+ tagInstance.save(flush:false)
nodeInstance.addToTags(tagInstance)
}
} else if (map.tags.tag instanceof Map) {
def tagInstance = Tag.fromMap(map.tags.tag)
- tagInstance.save(flush:true)
+ tagInstance.save(flush:false)
nodeInstance.addToTags(tagInstance)
}
}
@@ -73,12 +73,12 @@ class Node {
if (map.attributes.attribute instanceof List) {
map.attributes.attribute.each {
def attrInstance = Attribute.fromMap(it)
- attrInstance.save(flush:true)
+ attrInstance.save(flush:false)
nodeInstance.addToAttributes(attrInstance)
}
} else if (map.attributes.attribute instanceof Map) {
def attrInstance = Attribute.fromMap(map.attributes.attribute)
- attrInstance.save(flush:true)
+ attrInstance.save(flush:false)
nodeInstance.addToAttributes(attrInstance)
}
@@ -90,12 +90,12 @@ class Node {
if (map.externalAttributes.attributes instanceof List) {
map.externalAttributes.attributes.each {
def attrsInstance = Attributes.fromMap(it)
- attrsInstance.save(flush:true)
+ attrsInstance.save(flush:false)
nodeInstance.addToExternalAttributes(attrsInstance)
}
} else if (map.externalAttributes.attributes instanceof Map) {
def attrsInstance = Attributes.fromMap(map.externalAttributes.attributes)
- attrsInstance.save(flush:true)
+ attrsInstance.save(flush:false)
nodeInstance.addToExternalAttributes(attrsInstance)
}
}
@@ -109,7 +109,38 @@ class Node {
this.tags.each {tag->
list << tag.name
}
- return list.join(delimiter)
+ return list.sort().join(delimiter)
+ }
+
+ // A dynamic (like?) find method
+ static Set<Node> findAllTagsByName(String name) {
+ println "DEBUG: inside findAllTagsByName. name="+name
+ return Node.withCriteria {
+ tags {
+ eq('name',name)
+ }
+ }
+ }
+
+ // A dynamic (like?) find method
+ static Set<Node> findAllByNameLikeAndTagsByName(String nameLike, String tagName) {
+ println "DEBUG: inside findAllByNameLikeAndTagsByName. name="+tagName
+ return Node.withCriteria {
+ ilike('name',nameLike)
+ tags {
+ eq('name',tagName)
+ }
+ }
+ }
+
+ def Set<Tag> findTagByName(String tagName) {
+ println "DEBUG: inside findTagByName. name="+tagName
+ return Tag.withCriteria {
+ eq('name',tagName)
+ node() {
+ eq('id',this.id)
+ }
+ }
}
}
View
18 grails-app/domain/yana/node/Tag.groovy
@@ -5,6 +5,7 @@ class Tag {
static constraints = {
name(blank:false)
+ node(nullable:true)
}
static belongsTo = [ node : Node ]
@@ -28,4 +29,21 @@ class Tag {
return tagInstance
}
+
+ // Mimics a "dynamic" find method
+ static Set<Tag> findByNameAndNodeId(tagName, nodeId) {
+ println "DEBUG: inside findByNameAndNodeId. name=${tagName} nodeId=${nodeId}"
+ def results = Tag.withCriteria {
+ eq('name',tagName)
+ node {
+ eq('id',nodeId)
+ }
+ }
+
+ println "DEBUG: results class of " + results.getClass().getName()
+ results.each {
+ println "DEBUG: result it classof " + it.getClass().getName()
+ }
+ return results
+ }
}
View
15 scripts/Docs.groovy
@@ -1,7 +1,11 @@
+//
+// Docs command
+//
+
includeTargets << grailsScript("Init")
-PANDOC = "pandoc" // The pandoc executable
-DIST = "target/docs" // Build target dir
+PANDOC = "pandoc" // The pandoc executable in your PATH
+DIST = "target/docs" // Target directory where docs are written
target(main: "Builds the Yana documentation") {
@@ -38,8 +42,7 @@ target(html: "Generates the HTML pages" ) {
depends( figures, userguide, apimanual, refpages )
ant.echo ( message: 'Generating the index ...')
ant.copy ( file: "docs/en/index.md.template",
- tofile: "${DIST}/html/index.md", filtering:true )
-
+ tofile: "${DIST}/html/index.md", filtering:true )
// Concat the index files and generate index.html
ant.exec ( executable: PANDOC ) {
arg ( value: "-s" )
@@ -52,9 +55,7 @@ target(html: "Generates the HTML pages" ) {
arg ( value: "-o" )
arg ( value: "${DIST}/html/index.html" )
}
- ant.echo ( message: "Completed: ${DIST}/html/index.html")
-
-
+ ant.echo ( message: "Completed: ${DIST}/html/index.html")
}
target(userguide: "Generates the HTML pages" ) {
View
51 test/integration/yana/NodeIntegrationTests.groovy
@@ -30,9 +30,58 @@ class NodeIntegrationTests extends GrailsUnitTestCase {
void testTagsString() {
def nodeInstance = new Node(name: 'node1', osFamily: 'unix')
- nodeInstance.addToTags(new Tag(name: 'web'))
+
nodeInstance.addToTags(new Tag(name: 'app'))
+ nodeInstance.addToTags(new Tag(name: 'web'))
assertEquals "app,web", nodeInstance.tagsString(",")
}
+
+ void testFindAllTagsByName() {
+ def node1 = new Node(name: 'node1', osFamily: 'unix')
+ def node2 = new Node(name: 'node2', osFamily: 'unix')
+ def node3 = new Node(name: 'node3', osFamily: 'unix')
+ node1.addToTags(new Tag(name: 'web'))
+ node2.addToTags(new Tag(name: 'web'))
+ node3.addToTags(new Tag(name: 'app'))
+ node1.save(); node2.save(); node3.save();
+
+ def list = Node.findAllTagsByName("web")
+ assertEquals "incorrect result size", 2, list.size()
+
+ }
+
+ void testFindAllByNameAndTagsByName() {
+ def web1 = new Node(name: 'web1', osFamily: 'unix')
+ def web2 = new Node(name: 'web2', osFamily: 'unix')
+ def app1 = new Node(name: 'app1', osFamily: 'unix')
+ web1.addToTags(new Tag(name: 'web'))
+ web2.addToTags(new Tag(name: 'web'))
+ app1.addToTags(new Tag(name: 'app'))
+ web1.save(); web2.save(); app1.save();
+
+ def list = Node.findAllByNameLikeAndTagsByName("web%","web")
+ println "TEST: list="+list
+ assertEquals "incorrect result size", 2, list.size()
+ assertTrue "tagged node not found: web1.", list.contains(web1)
+ assertTrue "tagged node not found: web2.", list.contains(web2)
+ }
+
+
+ void testFindTagByName() {
+ def web1 = new Node(name: 'web1', osFamily: 'unix')
+ def web2 = new Node(name: 'web2', osFamily: 'unix')
+ def app1 = new Node(name: 'app1', osFamily: 'unix')
+ web1.addToTags(new Tag(name: 'web'))
+ web1.addToTags(new Tag(name: 'app'))
+ web2.addToTags(new Tag(name: 'web'))
+ app1.addToTags(new Tag(name: 'app'))
+ web1.save(); web2.save(); app1.save();
+
+ def found = web1.findTagByName("web")
+ println "TEST: found="+found
+ assertEquals "incorrect result size", 1, found.size()
+ def Tag tag = found.asList()[0]
+ assertEquals "tag name did not match", "web", tag.name
+ }
}
View
106 test/integration/yana/node/TagRestControllerTests.groovy
@@ -0,0 +1,106 @@
+package yana.node
+
+import grails.test.*
+
+class TagRestControllerTests extends ControllerUnitTestCase {
+
+
+ protected void setUp() {
+ super.setUp()
+
+ }
+
+ protected void tearDown() {
+ super.tearDown()
+ }
+
+ void testRenderTag() {
+ Node node1 = new Node(name: 'node1', osFamily:'unix')
+ Node node3 = new Node(name: 'node3', osFamily:'unix')
+ node1.addToTags(new Tag(name: 'roleX'))
+ node3.addToTags(new Tag(name: 'roleX'))
+ def list = [node1,node3]
+
+ def output = this.controller.renderTag("roleX",list)
+ println "TEST: output="+output
+ def root = new XmlSlurper().parseText(output)
+ assertEquals("incorrect value for name.", "roleX", root.name.text())
+ assertEquals("incorrect count.", "2", root.nodes."@count".text())
+
+ def nodes = root.nodes.node.findAll{ it.name =~ 'node.*' }.collect{ it.text() }
+ assertTrue "tagged node not found.", nodes.contains("node1")
+ assertTrue "tagged node not found.", nodes.contains("node3")
+ }
+
+ void testShow() {
+ Node node1 = new Node(name: 'node1', osFamily:'unix')
+ Node node2 = new Node(name: 'node2', osFamily:'unix')
+ Node node3 = new Node(name: 'node3', osFamily:'unix')
+ node1.addToTags(new Tag(name: 'roleX'))
+ node2.addToTags(new Tag(name: 'roleA'))
+ node3.addToTags(new Tag(name: 'roleX'))
+ node1.save(); node2.save(); node3.save();
+
+ this.controller.params.name = "roleX"
+ def result = this.controller.show()
+ assertNotNull "result was null.", result
+
+ def root = new XmlSlurper().parseText(this.controller.response.contentAsString)
+ assertEquals("incorrect value for name.", "roleX", root.name.text())
+ assertEquals("incorrect count.", "2", root.nodes."@count".text())
+
+ def nodes = root.nodes.node.findAll{ it.name =~ 'node.*' }.collect{ it.text() }
+ assertTrue "tagged node not found.", nodes.contains("node1")
+ assertTrue "tagged node not found.", nodes.contains("node3")
+ assertTrue "untagged node was found.", !nodes.contains('node2')
+ }
+
+
+ void testSave() {
+ Node node1 = new Node(name: 'node1', osFamily:'unix')
+ Node node2 = new Node(name: 'other', osFamily:'unix')
+ Node node3 = new Node(name: 'node3', osFamily:'unix')
+ node2.addToTags(new Tag(name: 'roleA'))
+ node1.save(); node2.save(); node3.save();
+
+ this.controller.params.name = "roleX"
+ this.controller.params.nodeName = "node%"
+ def result = this.controller.save()
+ assertNotNull "result was null.", result
+
+ def root = new XmlSlurper().parseText(this.controller.response.contentAsString)
+ println "TEST: this.controller.response.contentAsString:"+this.controller.response.contentAsString
+ assertEquals("incorrect value for name.", "roleX", root.name.text())
+ assertEquals("incorrect count.", "2", root.nodes."@count".text())
+
+ def nodes = root.nodes.node.findAll{ it.name =~ 'node.*' }.collect{ it.text() }
+ assertTrue "tagged node not found.", nodes.contains("node1")
+ assertTrue "tagged node not found.", nodes.contains("node3")
+ assertTrue "untagged node was found.", !nodes.contains('node2')
+ }
+
+ void testRemove() {
+ Node node1 = new Node(name: 'node1', osFamily:'unix')
+ Node node2 = new Node(name: 'other', osFamily:'unix')
+ Node node3 = new Node(name: 'node3', osFamily:'unix')
+ node1.addToTags(new Tag(name: 'roleX'))
+ node2.addToTags(new Tag(name: 'roleA'))
+ node3.addToTags(new Tag(name: 'roleX'))
+ node1.save(); node2.save(); node3.save();
+
+ this.controller.params.name = "roleX"
+ this.controller.params.nodeName = "node%"
+ def result = this.controller.remove()
+ assertNotNull "result was null.", result
+
+ def root = new XmlSlurper().parseText(this.controller.response.contentAsString)
+ println "TEST: this.controller.response.contentAsString:"+this.controller.response.contentAsString
+ assertEquals("incorrect value for name.", "roleX", root.name.text())
+ assertEquals("incorrect count.", "2", root.nodes."@count".text())
+
+ def nodes = root.nodes.node.findAll{ it.name =~ 'node.*' }.collect{ it.text() }
+ assertTrue "tagged node not found.", nodes.contains("node1")
+ assertTrue "tagged node not found.", nodes.contains("node3")
+ assertTrue "untagged node was found.", !nodes.contains('node2')
+ }
+}
View
37 test/unit/yana/node/NodeRestControllerTests.groovy
@@ -16,8 +16,8 @@ class NodeRestControllerTests extends ControllerUnitTestCase {
mockDomain(Node, [
new Node(name: "node1", osFamily: "unix")
])
- def model = this.controller.list()
- assertNotNull("model was null", model)
+ def result = this.controller.list()
+ assertNotNull("result was null", result)
def list = new XmlSlurper().parseText(this.controller.response.contentAsString)
def allNodes = list.node
assertEquals "wrong node list size", 1, list.size()
@@ -34,8 +34,8 @@ class NodeRestControllerTests extends ControllerUnitTestCase {
new Node(name: "testShow", osFamily: "unix")
])
this.controller.params.id = 1
- def model = this.controller.show()
- assertNotNull "model was null", (model)
+ def result = this.controller.show()
+ assertNotNull "result was null", (result)
def node = new XmlSlurper().parseText(this.controller.response.contentAsString)
assertEquals("incorrect value for name", "testShow", node.name.text())
@@ -49,8 +49,8 @@ class NodeRestControllerTests extends ControllerUnitTestCase {
new Node(name: "testDelete", osFamily: "unix")
])
this.controller.params.id = 1
- def model = this.controller.delete()
- assertNotNull "model was null", (model)
+ def content = this.controller.delete()
+ assertNotNull "result was null", (content)
def results = new XmlSlurper().parseText(this.controller.response.contentAsString)
def result = results.result[0]
assertEquals("incorrect value for result",
@@ -58,6 +58,31 @@ class NodeRestControllerTests extends ControllerUnitTestCase {
}
+ void testListTags() {
+
+
+ Node node1 = new Node(name: 'node1', osFamily:'unix')
+ Tag tag1 = new Tag(name: 'roleA')
+
+ mockDomain(Node, [node1])
+ mockDomain(Tag, [tag1])
+ node1.addToTags(tag1)
+ node1.save()
+ assertNotNull "instance did not validate", node1.save()
+ this.controller.params.id = 1
+ def result = this.controller.listTags()
+ assertNotNull("result was null", result)
+ assertNotNull "Null content in response", this.controller.response.contentAsString
+ assertTrue "empty content in response", "" != this.controller.response.contentAsString
+ def root = new XmlSlurper().parseText(this.controller.response.contentAsString)
+
+
+ println "TEST: contentAsString:"+this.controller.response.contentAsString
+ def allTags = root.tag
+ assertEquals "wrong tag list size", 1, allTags.size()
+ def firstTag = root.tag[0]
+ assertEquals "incorrect value for name", "roleA", firstTag."name".text()
+ }
void testRundeckXml() {
View
10 web-app/css/yana.css
@@ -209,17 +209,17 @@ input:focus, select:focus, textarea:focus {
padding: 4px 6px;
}
.menuButton a.home {
- background: url(../images/skin/house.png) center left no-repeat;
+ background: url(../images/icons/icon-tiny-rarrow-sep.png) center left no-repeat;
color: #333;
padding-left: 25px;
}
.menuButton a.list {
- background: url(../images/skin/database_table.png) center left no-repeat;
+ background: url(../images/icons/list.png) center left no-repeat;
color: #333;
padding-left: 25px;
}
.menuButton a.create {
- background: url(../images/skin/database_add.png) center left no-repeat;
+ background: url(../images/icons/add.png) center left no-repeat;
color: #333;
padding-left: 25px;
}
@@ -380,11 +380,11 @@ th.desc a {
padding: 2px 6px;
}
.buttons input.delete {
- background: transparent url(../images/skin/database_delete.png) 5px 50% no-repeat;
+ background: transparent url(../images/icons/delete.png) 5px 50% no-repeat;
padding-left: 28px;
}
.buttons input.edit {
- background: transparent url(../images/skin/database_edit.png) 5px 50% no-repeat;
+ background: transparent url(../images/icons/edit.png) 5px 50% no-repeat;
padding-left: 28px;
}
.buttons input.save {
Please sign in to comment.
Something went wrong with that request. Please try again.