1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1
17
"""Tools for working with the host system"""
2
18
# Copyright 2012 Canonical Ltd.
54
72
def service_running(service):
55
73
"""Determine whether a system service is running"""
57
output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
75
output = subprocess.check_output(
76
['service', service, 'status'],
77
stderr=subprocess.STDOUT).decode('UTF-8')
58
78
except subprocess.CalledProcessError:
67
87
def service_available(service_name):
68
88
"""Determine whether a system service is available"""
70
subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
71
except subprocess.CalledProcessError:
90
subprocess.check_output(
91
['service', service_name, 'status'],
92
stderr=subprocess.STDOUT).decode('UTF-8')
93
except subprocess.CalledProcessError as e:
94
return b'unrecognized service' not in e.output
121
def add_group(group_name, system_group=False):
122
"""Add a group to the system"""
124
group_info = grp.getgrnam(group_name)
125
log('group {0} already exists!'.format(group_name))
127
log('creating group {0}'.format(group_name))
130
cmd.append('--system')
135
cmd.append(group_name)
136
subprocess.check_call(cmd)
137
group_info = grp.getgrnam(group_name)
99
141
def add_user_to_group(username, group):
100
142
"""Add a user to a group"""
130
172
subprocess.check_call(cmd)
133
def mkdir(path, owner='root', group='root', perms=0555, force=False):
175
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
134
176
"""Create a directory"""
135
177
log("Making dir {} {}:{} {:o}".format(path, owner, group,
137
179
uid = pwd.getpwnam(owner).pw_uid
138
180
gid = grp.getgrnam(group).gr_gid
139
181
realpath = os.path.abspath(path)
140
if os.path.exists(realpath):
141
if force and not os.path.isdir(realpath):
182
path_exists = os.path.exists(realpath)
183
if path_exists and force:
184
if not os.path.isdir(realpath):
142
185
log("Removing non-directory file {} prior to mkdir()".format(path))
143
186
os.unlink(realpath)
187
os.makedirs(realpath, perms)
188
elif not path_exists:
145
189
os.makedirs(realpath, perms)
146
190
os.chown(realpath, uid, gid)
149
def write_file(path, content, owner='root', group='root', perms=0444):
150
"""Create or overwrite a file with the contents of a string"""
191
os.chmod(realpath, perms)
194
def write_file(path, content, owner='root', group='root', perms=0o444):
195
"""Create or overwrite a file with the contents of a byte string."""
151
196
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
152
197
uid = pwd.getpwnam(owner).pw_uid
153
198
gid = grp.getgrnam(group).gr_gid
154
with open(path, 'w') as target:
199
with open(path, 'wb') as target:
155
200
os.fchown(target.fileno(), uid, gid)
156
201
os.fchmod(target.fileno(), perms)
157
202
target.write(content)
177
222
cmd_args.extend([device, mountpoint])
179
224
subprocess.check_output(cmd_args)
180
except subprocess.CalledProcessError, e:
225
except subprocess.CalledProcessError as e:
181
226
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
191
236
cmd_args = ['umount', mountpoint]
193
238
subprocess.check_output(cmd_args)
194
except subprocess.CalledProcessError, e:
239
except subprocess.CalledProcessError as e:
195
240
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
209
254
return system_mounts
213
"""Generate a md5 hash of the contents of 'path' or None if not found """
257
def file_hash(path, hash_type='md5'):
259
Generate a hash checksum of the contents of 'path' or None if not found.
261
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
262
such as md5, sha1, sha256, sha512, etc.
214
264
if os.path.exists(path):
216
with open(path, 'r') as source:
217
h.update(source.read()) # IGNORE:E1101 - it does have update
265
h = getattr(hashlib, hash_type)()
266
with open(path, 'rb') as source:
267
h.update(source.read())
218
268
return h.hexdigest()
275
Generate a hash checksum of all files matching 'path'. Standard wildcards
276
like '*' and '?' are supported, see documentation for the 'glob' module for
279
:return: dict: A { filename: hash } dictionary for all matched files.
283
filename: file_hash(filename)
284
for filename in glob.iglob(path)
288
def check_hash(path, checksum, hash_type='md5'):
290
Validate a file using a cryptographic checksum.
292
:param str checksum: Value of the checksum used to validate the file.
293
:param str hash_type: Hash algorithm used to generate `checksum`.
294
Can be any hash alrgorithm supported by :mod:`hashlib`,
295
such as md5, sha1, sha256, sha512, etc.
296
:raises ChecksumError: If the file fails the checksum
299
actual_checksum = file_hash(path, hash_type)
300
if checksum != actual_checksum:
301
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
304
class ChecksumError(ValueError):
223
308
def restart_on_change(restart_map, stopstart=False):
224
309
"""Restart services based on configuration files changing
228
313
@restart_on_change({
229
314
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
315
'/etc/apache/sites-enabled/*': [ 'apache2' ]
231
def ceph_client_changed():
317
def config_changed():
232
318
pass # your code here
234
320
In this example, the cinder-api and cinder-volume services
235
321
would be restarted if /etc/ceph/ceph.conf is changed by the
236
ceph_client_changed function.
322
ceph_client_changed function. The apache2 service would be
323
restarted if any file matching the pattern got changed, created
324
or removed. Standard wildcards are supported, see documentation
325
for the 'glob' module for more information.
239
def wrapped_f(*args):
241
for path in restart_map:
242
checksums[path] = file_hash(path)
328
def wrapped_f(*args, **kwargs):
329
checksums = {path: path_hash(path) for path in restart_map}
245
332
for path in restart_map:
246
if checksums[path] != file_hash(path):
333
if path_hash(path) != checksums[path]:
247
334
restarts += restart_map[path]
248
335
services_list = list(OrderedDict.fromkeys(restarts))
249
336
if not stopstart:
270
357
def pwgen(length=None):
271
358
"""Generate a random pasword."""
272
359
if length is None:
360
# A random length is ok to use a weak PRNG
273
361
length = random.choice(range(35, 45))
274
362
alphanumeric_chars = [
275
l for l in (string.letters + string.digits)
363
l for l in (string.ascii_letters + string.digits)
276
364
if l not in 'l0QD1vAEIOUaeiou']
365
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
367
random_generator = random.SystemRandom()
278
random.choice(alphanumeric_chars) for _ in range(length)]
369
random_generator.choice(alphanumeric_chars) for _ in range(length)]
279
370
return(''.join(random_chars))
282
373
def list_nics(nic_type):
283
374
'''Return a list of nics of given type(s)'''
284
if isinstance(nic_type, basestring):
375
if isinstance(nic_type, six.string_types):
285
376
int_types = [nic_type]
287
378
int_types = nic_type
289
380
for int_type in int_types:
290
381
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
291
ip_output = subprocess.check_output(cmd).split('\n')
382
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
292
383
ip_output = (line for line in ip_output if line)
293
384
for line in ip_output:
294
385
if line.split()[1].startswith(int_type):
295
interfaces.append(line.split()[1].replace(":", ""))
386
matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
388
interface = matched.groups()[0]
390
interface = line.split()[1].replace(":", "")
391
interfaces.append(interface)
296
393
return interfaces
316
413
def get_nic_hwaddr(nic):
317
414
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
318
ip_output = subprocess.check_output(cmd)
415
ip_output = subprocess.check_output(cmd).decode('UTF-8')
320
417
words = ip_output.split()
321
418
if 'link/ether' in words:
330
427
* 0 => Installed revno is the same as supplied arg
331
428
* -1 => Installed revno is less than supplied arg
430
This function imports apt_cache function from charmhelpers.fetch if
431
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
432
you call this function, or pass an apt_pkg.Cache() instance.
335
from charmhelpers.fetch import apt_cache
436
from charmhelpers.fetch import apt_cache
337
437
pkgcache = apt_cache()
338
438
pkg = pkgcache[package]
339
439
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
351
def chownr(path, owner, group):
451
def chownr(path, owner, group, follow_links=True):
352
452
uid = pwd.getpwnam(owner).pw_uid
353
453
gid = grp.getgrnam(group).gr_gid
355
459
for root, dirs, files in os.walk(path):
356
460
for name in dirs + files:
357
461
full = os.path.join(root, name)
358
462
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
359
463
if not broken_symlink:
360
os.chown(full, uid, gid)
464
chown(full, uid, gid)
467
def lchownr(path, owner, group):
468
chownr(path, owner, group, follow_links=False)