Skip to content

Adds options to run littlechef outside of kitchen, fixes library usage #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.pyc
*.egg
.eggs/
MANIFEST
build
dist
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,15 @@ from littlechef import runner as lc
lc.env.user = 'MyUsername'
lc.env.password = 'MyPassword'
lc.env.host_string = 'MyHostnameOrIP'
lc.deploy_chef(gems='yes', ask='no')
lc.deploy_chef(ask='no')

lc.recipe('MYRECIPE') #Applies <MYRECIPE> to <MyHostnameOrIP>
lc.node('MyHostnameOrIP') #Applies the saved nodes/MyHostnameOrIP.json configuration
```

You will need to specify additional `lc.env` parameters per your requirements.
A simple [working example](https://gist.github.com/iashwash/4dd3e6c655c545cb272f).

### Performance Tips

You can greatly reduce the SSH connection setup time by reusing existing connections.
Expand Down
20 changes: 20 additions & 0 deletions fix
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,26 @@ def parse_arguments():
help=("When searching for nodes with tags also include virtualized"
"guests of matching hosts")
)
parser.add_argument(
"--kitchen-path", dest="kitchen_path", default=os.getcwd(),
help=("Path to the kitchen repository (containing cookbooks, databags,"
" nodes, etc.) - assumed to be the working directory if not provided.")
)
parser.add_argument(
"--no-color", dest="no_color", action="store_true",
default=False,
help=("Don't colorize the output")
)
parser.add_argument(
"--skip-node-data-bag", dest="skip_node_data_bag", action="store_true",
default=False,
help=("Skips creation of the node databag.")
)
parser.add_argument(
"--skip-node-json", dest="skip_node_json", action="store_true",
default=False,
help=("Skips creation of the node json file.")
)
return parser, vars(parser.parse_args())


Expand Down Expand Up @@ -158,7 +173,12 @@ if __name__ == '__main__':
if not commands or ":" in args['environment']:
parser.error("No value given for --env")
littlechef.chef_environment = args['environment']
if args['skip_node_data_bag']:
littlechef.skip_node_data_bag = True
if args['skip_node_json']:
littlechef.skip_node_json = True
littlechef.no_color = args['no_color']
littlechef.kitchen_path = args['kitchen_path']

# overwrite all commandline arguments and proxy
# execution to the fabric script
Expand Down
6 changes: 5 additions & 1 deletion littlechef/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
.. _Chef: http://wiki.opscode.com/display/chef/Home

"""
__version__ = "1.8.0"
import os
__version__ = "1.8.1"
__author__ = "Miquel Torres <[email protected]>"

__cooking__ = False

chef_environment = None
kitchen_path = os.getcwd()

loglevel = "info"
noninteractive = False
Expand All @@ -29,6 +31,8 @@
whyrun = False
concurrency = False
include_guests = False
skip_node_data_bag = False
skip_node_json = False
no_color = False

node_work_path = "/tmp/chef-solo"
Expand Down
41 changes: 27 additions & 14 deletions littlechef/chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import subprocess
from copy import deepcopy

from fabric.api import settings, hide, env, sudo, put
from fabric.api import settings, hide, env, sudo, put, run
from fabric.contrib.files import exists
from fabric.utils import abort
from fabric.contrib.project import rsync_project
Expand All @@ -39,13 +39,18 @@ def save_config(node, force=False):
it also saves to tmp_node.json

"""
filepath = os.path.join("nodes", env.host_string + ".json")
filepath = os.path.join(lib.kitchen_relative_path("nodes"), env.host_string + ".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)
# and --skip-node-json was not called
if env.skip_node_json and not force:
print "SKIPPING save of node configuration to {0}...".format(filepath)
else:
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, sort_keys=True))
Expand Down Expand Up @@ -144,13 +149,14 @@ def _synchronize_node(configfile, node):
mode=0600)
sudo('chown root:$(id -g -n root) /etc/chef/encrypted_data_bag_secret')

paths_to_sync = ['./data_bags', './roles', './environments']
paths_to_sync = ['data_bags', 'roles', 'environments']
paths_to_sync = [lib.kitchen_relative_path(path) for path in paths_to_sync]
for cookbook_path in cookbook_paths:
paths_to_sync.append('./{0}'.format(cookbook_path))
paths_to_sync.append(lib.kitchen_relative_path(cookbook_path))

# Add berksfile directory to sync_list
if env.berksfile:
paths_to_sync.append(env.berksfile_cookbooks_directory)
paths_to_sync.append(lib.kitchen_relative_path(env.berksfile_cookbooks_directory))

if env.loglevel is "debug":
extra_opts = ""
Expand All @@ -160,7 +166,10 @@ def _synchronize_node(configfile, node):
env.host_string)['identityfile']))
ssh_opts += " " + env.gateway + " ssh -o StrictHostKeyChecking=no -i "
ssh_opts += ssh_key_file



ssh_opts += " -o StrictHostKeyChecking=no "
print "Rsyncing {} paths to {}".format(' '.join(paths_to_sync), env.node_work_path)
rsync_project(
env.node_work_path,
' '.join(paths_to_sync),
Expand Down Expand Up @@ -365,8 +374,8 @@ def ensure_berksfile_cookbooks_are_installed():
print(msg.format(env.berksfile, env.berksfile_cookbooks_directory))

run_vendor = True
cookbooks_dir = env.berksfile_cookbooks_directory
berksfile_lock_path = cookbooks_dir+'/Berksfile.lock'
cookbooks_dir = lib.kitchen_relative_path(env.berksfile_cookbooks_directory)
berksfile_lock_path = os.path.join(cookbooks_dir,'Berksfile.lock')

berksfile_lock_exists = os.path.isfile(berksfile_lock_path)
cookbooks_dir_exists = os.path.isdir(cookbooks_dir)
Expand All @@ -378,9 +387,10 @@ def ensure_berksfile_cookbooks_are_installed():

if run_vendor:
if cookbooks_dir_exists:
shutil.rmtree(env.berksfile_cookbooks_directory)
shutil.rmtree(cookbooks_dir)

p = subprocess.Popen(['berks', 'vendor', env.berksfile_cookbooks_directory],
p = subprocess.Popen(['berks', 'vendor', cookbooks_dir],
cwd=lib.kitchen_relative_path('.'),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
Expand Down Expand Up @@ -438,6 +448,9 @@ def _configure_node():
# Backup last report
with settings(hide('stdout', 'warnings', 'running'), warn_only=True):
sudo("mv {0} {0}.1".format(LOGFILE))
logfile_directory = os.path.dirname(LOGFILE)
# make sure the current user can write to the log
sudo("chown {} {}".format(env.user, logfile_directory))
# Build chef-solo command
cmd = "RUBYOPT=-Ku chef-solo"
if whyrun:
Expand All @@ -448,8 +461,8 @@ def _configure_node():
if env.loglevel == "debug":
print("Executing Chef Solo with the following command:\n"
"{0}".format(cmd))
with settings(hide('warnings', 'running'), warn_only=True):
output = sudo(cmd)
with settings(hide('warnings', 'running'), warn_only=True, forward_agent=True):
output = run("sudo -E -s " + cmd)
if (output.failed or "FATAL: Stacktrace dumped" in output or
("Chef Run complete" not in output and
"Report handlers complete" not in output)):
Expand Down
33 changes: 21 additions & 12 deletions littlechef/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ def _resolve_hostname(name):
"""Returns resolved hostname using the ssh config"""
if env.ssh_config is None:
return name
elif not os.path.exists(os.path.join("nodes", name + ".json")):
elif not os.path.exists(os.path.join(kitchen_relative_path("nodes"), name + ".json")):
resolved_name = env.ssh_config.lookup(name)['hostname']
if os.path.exists(os.path.join("nodes", resolved_name + ".json")):
if os.path.exists(os.path.join(kitchen_relative_path("nodes"), resolved_name + ".json")):
name = resolved_name
return name

Expand Down Expand Up @@ -63,7 +63,7 @@ def get_environment(name):
"""Returns a JSON environment file as a dictionary"""
if name == "_default":
return env_from_template(name)
filename = os.path.join("environments", name + ".json")
filename = os.path.join(kitchen_relative_path("environments"), name + ".json")
try:
with open(filename) as f:
try:
Expand All @@ -79,21 +79,24 @@ def get_environment(name):
def get_environments():
"""Gets all environments found in the 'environments' directory"""
envs = []
for root, subfolders, files in os.walk('environments'):
for root, subfolders, files in os.walk(kitchen_relative_path('environments')):
for filename in files:
if filename.endswith(".json"):
path = os.path.join(
root[len('environments'):], filename[:-len('.json')])
root.split('environments')[1], filename[:-len('.json')])
envs.append(get_environment(path))
return sorted(envs, key=lambda x: x['name'])


def get_node(name, merged=False):
"""Returns a JSON node file as a dictionary"""
if merged:
node_path = os.path.join("data_bags", "node", name.replace('.', '_') + ".json")
node_path = os.path.join(kitchen_relative_path("data_bags"),
kitchen_relative_path("node"),
name.replace('.', '_') + ".json")
else:
node_path = os.path.join("nodes", name + ".json")
node_path = os.path.join(kitchen_relative_path("nodes"),
name + ".json")
if os.path.exists(node_path):
# Read node.json
with open(node_path, 'r') as f:
Expand All @@ -115,11 +118,11 @@ def get_node(name, merged=False):

def get_nodes(environment=None):
"""Gets all nodes found in the nodes/ directory"""
if not os.path.exists('nodes'):
if not os.path.exists(kitchen_relative_path('nodes')):
return []
nodes = []
for filename in sorted(
[f for f in os.listdir('nodes')
[f for f in os.listdir(kitchen_relative_path('nodes'))
if (not os.path.isdir(f)
and f.endswith(".json") and not f.startswith('.'))]):
fqdn = ".".join(filename.split('.')[:-1]) # Remove .json from name
Expand Down Expand Up @@ -278,7 +281,7 @@ def get_recipes_in_cookbook(name):
cookbook_exists = False
metadata_exists = False
for cookbook_path in cookbook_paths:
path = os.path.join(cookbook_path, name)
path = os.path.join(kitchen_relative_path(cookbook_path), name)
path_exists = os.path.exists(path)
# cookbook exists if present in any of the cookbook paths
cookbook_exists = cookbook_exists or path_exists
Expand Down Expand Up @@ -324,7 +327,7 @@ def get_recipes_in_cookbook(name):
# Add recipes found in the 'recipes' directory but not listed
# in the metadata
for cookbook_path in cookbook_paths:
recipes_dir = os.path.join(cookbook_path, name, 'recipes')
recipes_dir = os.path.join(kitchen_relative_path(cookbook_path), name, 'recipes')
if not os.path.isdir(recipes_dir):
continue
for basename in os.listdir(recipes_dir):
Expand Down Expand Up @@ -368,6 +371,7 @@ def get_recipes():
"""Gets all recipes found in the cookbook directories"""
dirnames = set()
for path in cookbook_paths:
path = kitchen_relative_path(path)
dirnames.update([d for d in os.listdir(path) if os.path.isdir(
os.path.join(path, d)) and not d.startswith('.')])
recipes = []
Expand Down Expand Up @@ -457,6 +461,11 @@ def print_role(role, detailed=True):
print("")


def kitchen_relative_path(path):
"""Returns path relative to chef kitchen path"""
return os.path.join(env.kitchen_path, path)


def print_plugin_list():
"""Prints a list of available plugins"""
print("List of available plugins:")
Expand Down Expand Up @@ -502,7 +511,7 @@ def import_plugin(name):
def get_cookbook_path(cookbook_name):
"""Returns path to the cookbook for the given cookbook name"""
for cookbook_path in cookbook_paths:
path = os.path.join(cookbook_path, cookbook_name)
path = os.path.join(kitchen_relative_path(cookbook_path), cookbook_name)
if os.path.exists(path):
return path
raise IOError('Can\'t find cookbook with name "{0}"'.format(cookbook_name))
Expand Down
Loading