/
runner.py
394 lines (334 loc) · 13 KB
/
runner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#Copyright 2010-2012 Miquel Torres <tobami@googlemail.com>
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.
#
"""LittleChef: Configuration Management using Chef Solo"""
import ConfigParser
import os
import sys
import simplejson as json
from fabric.api import *
from fabric.contrib.files import append, exists
from fabric.contrib.console import confirm
from ssh.config import SSHConfig as _SSHConfig
from littlechef import solo
from littlechef import lib
from littlechef import chef
from littlechef.settings import cookbook_paths
# Fabric settings
import fabric
fabric.state.output['running'] = False
env.loglevel = "info"
env.output_prefix = False
__testing__ = False
@hosts('setup')
def new_kitchen():
"""Create LittleChef directory structure (Kitchen)"""
def _mkdir(d):
if not os.path.exists(d):
os.mkdir(d)
# Add an empty README so that it can be added to version control
readme_path = os.path.join(d, 'README')
if not os.path.exists(readme_path):
with open(readme_path, "w") as readme:
print >> readme, ""
print "{0}/ directory created...".format(d)
_mkdir("nodes")
_mkdir("roles")
_mkdir("data_bags")
for cookbook_path in cookbook_paths:
_mkdir(cookbook_path)
# Add skeleton auth.cfg
if not os.path.exists("auth.cfg"):
with open("auth.cfg", "w") as authfh:
print >> authfh, "[userinfo]"
print >> authfh, "user = "
print >> authfh, "password = "
print >> authfh, "keypair-file = "
print >> authfh, "ssh-config = "
print "auth.cfg file created..."
@hosts('setup')
def nodes_with_role(rolename):
"""Sets a list of nodes that have the given role
in their run list and calls node()
"""
nodes_in_env = []
nodes = lib.get_nodes_with_role(rolename)
if env.chef_environment is None:
# Pass all nodes
nodes_in_env = [n['name'] for n in nodes]
else:
# Only nodes in environment
nodes_in_env = [n['name'] for n in nodes \
if n.get('chef_environment') == env.chef_environment]
if not len(nodes_in_env):
print("No nodes found with role '{0}'".format(rolename))
sys.exit(0)
return node(*nodes_in_env)
@hosts('setup')
def node(*nodes):
"""Selects and configures a list of nodes. 'all' configures all nodes"""
if not len(nodes) or nodes[0] == '':
abort('No node was given')
elif nodes[0] == 'all':
# Fetch all nodes and add them to env.hosts
for node in lib.get_nodes(env.chef_environment):
env.hosts.append(node['name'])
if not len(env.hosts):
abort('No nodes found in /nodes/')
message = "Are you sure you want to configure all nodes ({0})".format(
len(env.hosts))
if env.chef_environment:
message += " in the {0} environment".format(env.chef_environment)
message += "?"
if not __testing__:
if not confirm(message):
abort('Aborted by user')
else:
# A list of nodes was given
env.hosts = list(nodes)
env.all_hosts = list(env.hosts) # Shouldn't be needed
if len(env.hosts) > 1:
print "Configuring nodes: {0}...".format(", ".join(env.hosts))
# Check whether another command was given in addition to "node:"
execute = True
if not(littlechef.__cooking__ and
'node:' not in sys.argv[-1] and
'nodes_with_role:' not in sys.argv[-1]):
# If user didn't type recipe:X, role:Y or deploy_chef,
# configure the nodes
for hostname in env.hosts:
env.host = hostname
env.host_string = hostname
node = lib.get_node(env.host)
lib.print_header("Configuring {0}".format(env.host))
if __testing__:
print "TEST: would now configure {0}".format(env.host)
else:
chef.sync_node(node)
def deploy_chef(gems="no", ask="yes", version="0.10",
distro_type=None, distro=None, stop_client='yes'):
"""Install chef-solo on a node"""
if not env.host_string:
abort('no node specified\nUsage: fix node:MYNODES deploy_chef')
chef_versions = ["0.9", "0.10"]
if version not in chef_versions:
abort('Wrong Chef version specified. Valid versions are {0}'.format(
", ".join(chef_versions)))
if distro_type is None and distro is None:
distro_type, distro = solo.check_distro()
elif distro_type is None or distro is None:
abort('Must specify both or neither of distro_type and distro')
if ask == "yes":
message = '\nAre you sure you want to install Chef {0}'.format(version)
message += ' at the node {0}'.format(env.host_string)
if gems == "yes":
message += ', using gems for "{0}"?'.format(distro)
else:
message += ', using "{0}" packages?'.format(distro)
if not confirm(message):
abort('Aborted by user')
else:
if gems == "yes":
method = 'using gems for "{0}"'.format(distro)
else:
method = '{0} using "{1}" packages'.format(version, distro)
print("Deploying Chef {0}...".format(method))
if not __testing__:
solo.install(distro_type, distro, gems, version, stop_client)
solo.configure()
def recipe(recipe):
"""Apply the given recipe to a node
Sets the run_list to the given recipe
If no nodes/hostname.json file exists, it creates one
"""
# Check that a node has been selected
if not env.host_string:
abort('no node specified\nUsage: fix node:MYNODES recipe:MYRECIPE')
lib.print_header(
"Applying recipe '{0}' on node {1}".format(recipe, env.host_string))
# Now create configuration and sync node
data = lib.get_node(env.host_string)
data["run_list"] = ["recipe[{0}]".format(recipe)]
if not __testing__:
chef.sync_node(data)
def role(role):
"""Apply the given role to a node
Sets the run_list to the given role
If no nodes/hostname.json file exists, it creates one
"""
# Check that a node has been selected
if not env.host_string:
abort('no node specified\nUsage: fix node:MYNODES role:MYROLE')
lib.print_header(
"Applying role '{0}' to {1}".format(role, env.host_string))
# Now create configuration and sync node
data = lib.get_node(env.host_string)
data["run_list"] = ["role[{0}]".format(role)]
if not __testing__:
chef.sync_node(data)
def ssh(name):
"""Executes the given command"""
if not env.host_string:
abort('no node specified\nUsage: fix node:MYNODES ssh:COMMAND')
print("\nExecuting the command '{0}' on the node {1}...".format(
name, env.host_string))
# Execute remotely using either the sudo or the run fabric functions
with settings(hide("warnings"), warn_only=True):
if name.startswith("sudo "):
with lib.credentials():
sudo(name[5:])
else:
with lib.credentials():
run(name)
def plugin(name):
"""Executes the selected plugin
Plugins are expected to be found in the kitchen's 'plugins' directory
"""
if not env.host_string:
abort('No node specified\nUsage: fix node:MYNODES plugin:MYPLUGIN')
plug = lib.import_plugin(name)
print("Executing plugin '{0}' on {1}".format(name, env.host_string))
node = lib.get_node(env.host_string)
if node == {'run_list': []}:
node['name'] = env.host_string
plug.execute(node)
print("Finished executing plugin")
@hosts('api')
def list_nodes():
"""List all configured nodes"""
lib.print_nodes(lib.get_nodes(env.chef_environment))
@hosts('api')
def list_nodes_detailed():
"""Show a detailed list of all nodes"""
lib.print_nodes(lib.get_nodes(env.chef_environment), detailed=True)
@hosts('api')
def list_nodes_with_recipe(recipe):
"""Show all nodes which have asigned a given recipe"""
lib.print_nodes(lib.get_nodes_with_recipe(recipe, env.chef_environment))
@hosts('api')
def list_nodes_with_role(role):
"""Show all nodes which have asigned a given role"""
lib.print_nodes(lib.get_nodes_with_role(role, env.chef_environment))
@hosts('api')
def list_recipes():
"""Show a list of all available recipes"""
for recipe in lib.get_recipes():
margin_left = lib.get_margin(len(recipe['name']))
print("{0}{1}{2}".format(
recipe['name'], margin_left, recipe['description']))
@hosts('api')
def list_recipes_detailed():
"""Show detailed information for all recipes"""
for recipe in lib.get_recipes():
lib.print_recipe(recipe)
@hosts('api')
def list_roles():
"""Show a list of all available roles"""
for role in lib.get_roles():
margin_left = lib.get_margin(len(role['fullname']))
print("{0}{1}{2}".format(
role['fullname'], margin_left,
role.get('description', '(no description)')))
@hosts('api')
def list_roles_detailed():
"""Show detailed information for all roles"""
for role in lib.get_roles():
lib.print_role(role)
@hosts('api')
def list_plugins():
"""Show all available plugins"""
lib.print_plugin_list()
# Check that user is cooking inside a kitchen and configure authentication #
def _check_appliances():
"""Look around and return True or False based on whether we are in a
kitchen
"""
names = os.listdir(os.getcwd())
missing = []
for dirname in ['nodes', 'roles', 'cookbooks', 'data_bags']:
if (dirname not in names) or (not os.path.isdir(dirname)):
missing.append(dirname)
if 'auth.cfg' not in names:
missing.append('auth.cfg')
return (not bool(missing)), missing
def _readconfig():
"""Configure environment"""
# Check that all dirs and files are present
in_a_kitchen, missing = _check_appliances()
missing_str = lambda m: ' and '.join(', '.join(m).rsplit(', ', 1))
if not in_a_kitchen:
msg = "Couldn't find {0}. ".format(missing_str(missing))
msg += "Are you are executing 'fix' outside of a kitchen?\n"\
"To create a new kitchen in the current directory "\
" type 'fix new_kitchen'"
abort(msg)
config = ConfigParser.ConfigParser()
config.read("auth.cfg")
# We expect an ssh_config file here,
# and/or a user, (password/keyfile) pair
env.ssh_config = None
try:
ssh_config = config.get('userinfo', 'ssh-config')
except ConfigParser.NoSectionError:
msg = 'You need to define a "userinfo" section'
msg += ' in auth.cfg. Refer to the README for help'
msg += ' (http://github.com/tobami/littlechef)'
abort(msg)
except ConfigParser.NoOptionError:
ssh_config = None
if ssh_config:
env.ssh_config = _SSHConfig()
try:
env.ssh_config.parse(open(os.path.expanduser(ssh_config)))
except IOError:
msg = "Couldn't open the ssh-config file '{0}'".format(ssh_config)
abort(msg)
except Exception:
msg = "Couldn't parse the ssh-config file '{0}'".format(ssh_config)
abort(msg)
try:
env.user = config.get('userinfo', 'user')
user_specified = True
except ConfigParser.NoOptionError:
if not ssh_config:
msg = 'You need to define a user in the "userinfo" section'
msg += ' of auth.cfg. Refer to the README for help'
msg += ' (http://github.com/tobami/littlechef)'
abort(msg)
user_specified = False
try:
env.password = config.get('userinfo', 'password') or None
except ConfigParser.NoOptionError:
pass
try:
#If keypair-file is empty, assign None or fabric will try to read key "
env.key_filename = config.get('userinfo', 'keypair-file') or None
except ConfigParser.NoOptionError:
pass
if user_specified and not env.password and not env.ssh_config:
abort('You need to define a password or a ssh-config file in auth.cfg')
# Only read config if fix is being used and we are not creating a new kitchen
import littlechef
env.chef_environment = littlechef.chef_environment
env.loglevel = littlechef.loglevel
env.verbose = littlechef.verbose
if littlechef.__cooking__:
# Called from command line
if env.chef_environment:
print("\nEnvironment: {0}".format(env.chef_environment))
if 'new_kitchen' not in sys.argv:
_readconfig()
else:
# runner module has been imported
env.ssh_config = None