Skip to content

Commit

Permalink
Automatic attributes: added "domain", changed "hostname" to reflect t…
Browse files Browse the repository at this point in the history
…he Chef definition

Change name of the temporary node file from "tmp_node.json" to "tmp_<fqdn>.json"
  • Loading branch information
tobami committed Sep 20, 2011
1 parent 45e7b60 commit 6521b4b
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 24 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -38,7 +38,7 @@ Chef Solo does not support Environments, but as mentioned above, the cookbook li
Node search is achieved by creating a "node" data bag on the fly for every run,
with the data from each node defined in nodes/, but with the attribute values being the
result from merging cookbook, node and role attributes, following the standard
[Chef attribute preference rules][].
[Chef attribute preference rules][]. Some [automatic attributes][] are also added.

```ruby
munin_servers = search(:node, "role:#{node['munin']['server_role']} AND chef_environment:node.chef_environment']}")`
Expand Down Expand Up @@ -192,6 +192,7 @@ Happy cooking!
[Data Bags]: http://wiki.opscode.com/display/chef/Data+Bags
[cookbook library that implements search]: https://github.com/edelight/chef-solo-search
[Chef attribute preference rules]: http://wiki.opscode.com/display/chef/Attributes#Attributes-SettingAttributes
[automatic attributes]: http://wiki.opscode.com/display/chef/Recipes#Recipes-CommonAutomaticAttributes
[search wiki page]: http://wiki.opscode.com/display/chef/Search
[Opscode repository]: http://wiki.opscode.com/display/chef/Installation#Installation-InstallingChefClientandChefSolo
[Automated Deployments with LittleChef]: http://sysadvent.blogspot.com/2010/12/day-9-automated-deployments-with.html
Expand Down
31 changes: 23 additions & 8 deletions littlechef/chef.py
Expand Up @@ -39,15 +39,16 @@ def save_config(node, force=False):
it also saves to tmp_node.json
"""
filepath = os.path.join("nodes/", env.host_string + ".json")
files_to_create = ['tmp_node.json']
tmp_filename = 'tmp_{0}.json'.format(env.host_string)
files_to_create = [tmp_filename]
if not os.path.exists(filepath) or force:
# Only save to nodes/ if there is not already a file
print "Saving node configuration to {0}...".format(filepath)
files_to_create.append(filepath)
for node_file in files_to_create:
with open(node_file, 'w') as f:
f.write(json.dumps(node, indent=4))
return 'tmp_node.json'
return tmp_filename


def _get_ipaddress(node):
Expand Down Expand Up @@ -132,7 +133,17 @@ def update_dct(dic1, dic2):
dic1[key] = val


def _merge_attributes(node, all_recipes, all_roles):
def _add_automatic_attributes(node):
"""Adds some of Chef's automatic attributes:
http://wiki.opscode.com/display/chef/Recipes#Recipes-CommonAutomaticAttributes
"""
node['fqdn'] = node['name']
node['hostname'] = node['fqdn'].split('.')[0]
node['domain'] = ".".join(node['fqdn'].split('.')[1:])


def _add_merged_attributes(node, all_recipes, all_roles):
"""Merges attributes from cookbooks, node and roles
Chef Attribute precedence:
Expand All @@ -145,6 +156,7 @@ def _merge_attributes(node, all_recipes, all_roles):
NOTE: In order for cookbook attributes to be read, they need to be
correctly defined in its metadata.json
"""
# Get cookbooks from extended recipes
attributes = {}
Expand Down Expand Up @@ -181,7 +193,6 @@ def _merge_attributes(node, all_recipes, all_roles):
if role == r['name']:
update_dct(attributes, r['override_attributes'])
node.update(attributes)
return node


def _build_node_data_bag():
Expand All @@ -197,6 +208,7 @@ def _build_node_data_bag():
Returns the node object of the node which is about to be configured, or None
if this node object cannot be found.
"""
current_node = None
nodes = lib.get_nodes()
Expand All @@ -207,22 +219,25 @@ def _build_node_data_bag():
all_roles = lib.get_roles()
for node in nodes:
node['id'] = node['name'].split('.')[0]
node['fqdn'] = node['name']
node['hostname'] = node['name']

# Build extended role list
node['role'] = lib.get_roles_in_node(node)
node['roles'] = node['role'][:]
for role in node['role']:
node['roles'].extend(lib.get_roles_in_role(role))
node['roles'] = list(set(node['roles']))

# Build extended recipe list
node['recipes'] = lib.get_recipes_in_node(node)
# Add recipes found inside each roles in the extended role list
for role in node['roles']:
node['recipes'].extend(lib.get_recipes_in_role(role))
node['recipes'] = list(set(node['recipes']))
# Add attributes
node = _merge_attributes(node, all_recipes, all_roles)

# Add node attributes
_add_merged_attributes(node, all_recipes, all_roles)
_add_automatic_attributes(node)

# Save node data bag item
with open(os.path.join(
'data_bags', 'node', node['id'] + '.json'), 'w') as f:
Expand Down
44 changes: 29 additions & 15 deletions tests/test_lib.py
Expand Up @@ -30,16 +30,16 @@

class BaseTest(unittest.TestCase):
def tearDown(self):
if os.path.exists('tmp_node.json'):
os.remove('tmp_node.json')
for nodename in ['tmp_testnode1', 'tmp_testnode2', 'tmp_testnode4']:
if os.path.exists(nodename + '.json'):
os.remove(nodename + '.json')


class TestRunner(BaseTest):
def test_not_a_kitchen(self):
"""Should exit with error when not a kitchen directory"""
# Change to a directory which is not a kitchen
# NOTE: when used as a library chdir has no effect anyway
# We need absolute paths for the kitchen
# NOTE: We need absolute paths for the kitchen
os.chdir(littlechef_top)
self.assertRaises(SystemExit, runner._readconfig)

Expand Down Expand Up @@ -77,8 +77,13 @@ def test_list_recipes(self):


class TestChef(BaseTest):
def tearDown(self):
chef._remove_node_data_bag()
super(TestChef, self).tearDown()

def test_save_config(self):
"""Should create a tmp_node.json and a nodes/testnode4.json config file
"""Should create a tmp_testnode4.json and a nodes/testnode4.json config file
"""
# Save a new node
env.host_string = 'testnode4'
Expand Down Expand Up @@ -123,10 +128,7 @@ def test_build_node_data_bag(self):
self.assertTrue('recipes' in data)
self.assertEquals(data['role'], [u'all_you_can_eat'])
self.assertEquals(data['roles'], [u'base', u'all_you_can_eat'])
# Clean up
chef._remove_node_data_bag()
self.assertFalse(os.path.exists(item_path))


def test_build_node_data_bag_nonalphanumeric(self):
"""Should create a node data bag when node name contains non-alphanumerical
characters"""
Expand All @@ -137,11 +139,25 @@ def test_build_node_data_bag_nonalphanumeric(self):
data = json.loads(f.read())
self.assertTrue('id' in data and data['id'] == 'testnode3')
self.assertTrue('name' in data and data['name'] == 'testnode3.mydomain.com')

def test_automatic_attributes(self):
"""Should add Chef's automatic attributes"""
chef._build_node_data_bag()
# Check node with single word fqdn
testnode1_path = os.path.join('data_bags', 'node', 'testnode1.json')
with open(testnode1_path, 'r') as f:
data = json.loads(f.read())
self.assertTrue('fqdn' in data and data['fqdn'] == 'testnode1')
self.assertTrue('hostname' in data and data['hostname'] == 'testnode1')
self.assertTrue('domain' in data and data['domain'] == '')

# Check node with complex fqdn
testnode3_path = os.path.join('data_bags', 'node', 'testnode3.json')
with open(testnode3_path, 'r') as f:
data = json.loads(f.read())
self.assertTrue('fqdn' in data and data['fqdn'] == 'testnode3.mydomain.com')
self.assertTrue('hostname' in data and data['hostname'] == 'testnode3.mydomain.com')
# Clean up
chef._remove_node_data_bag()
self.assertFalse(os.path.exists(item_path))
self.assertTrue('hostname' in data and data['hostname'] == 'testnode3')
self.assertTrue('domain' in data and data['domain'] == 'mydomain.com')

def test_attribute_merge_cookbook_default(self):
"""Should have the value found in recipe/attributes/default.rb"""
Expand All @@ -164,7 +180,6 @@ def test_attribute_merge_site_cookbook_default(self):
self.assertTrue('subversion' in data)
self.assertTrue(data['subversion']['repo_dir'] == '/srv/svn2')


def test_attribute_merge_role_default(self):
"""Should have the value found in the roles default attributes"""
chef._build_node_data_bag()
Expand All @@ -176,7 +191,6 @@ def test_attribute_merge_role_default(self):
self.assertTrue('other_attr' in data)
self.assertTrue(data['other_attr']['other_key'] == 'nada')


def test_attribute_merge_node_normal(self):
"""Should have the value found in the node attributes"""
chef._build_node_data_bag()
Expand Down

0 comments on commit 6521b4b

Please sign in to comment.