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/>.
17
"""Tools for working with the host system"""
18
# Copyright 2012 Canonical Ltd.
21
# Nick Moffitt <nick.moffitt@canonical.com>
22
# Matthew Wedgwood <matthew.wedgwood@canonical.com>
33
from contextlib import contextmanager
34
from collections import OrderedDict
38
from .hookenv import log
39
from .fstab import Fstab
42
def service_start(service_name):
43
"""Start a system service"""
44
return service('start', service_name)
47
def service_stop(service_name):
48
"""Stop a system service"""
49
return service('stop', service_name)
52
def service_restart(service_name):
53
"""Restart a system service"""
54
return service('restart', service_name)
57
def service_reload(service_name, restart_on_failure=False):
58
"""Reload a system service, optionally falling back to restart if
60
service_result = service('reload', service_name)
61
if not service_result and restart_on_failure:
62
service_result = service('restart', service_name)
66
def service_pause(service_name, init_dir=None):
67
"""Pause a system service.
69
Stop it, and prevent it from starting again at boot."""
71
init_dir = "/etc/init"
72
stopped = service_stop(service_name)
73
# XXX: Support systemd too
74
override_path = os.path.join(
75
init_dir, '{}.override'.format(service_name))
76
with open(override_path, 'w') as fh:
81
def service_resume(service_name, init_dir=None):
82
"""Resume a system service.
84
Reenable starting again at boot. Start the service"""
85
# XXX: Support systemd too
87
init_dir = "/etc/init"
88
override_path = os.path.join(
89
init_dir, '{}.override'.format(service_name))
90
if os.path.exists(override_path):
91
os.unlink(override_path)
92
started = service_start(service_name)
96
def service(action, service_name):
97
"""Control a system service"""
98
cmd = ['service', service_name, action]
99
return subprocess.call(cmd) == 0
102
def service_running(service):
103
"""Determine whether a system service is running"""
105
output = subprocess.check_output(
106
['service', service, 'status'],
107
stderr=subprocess.STDOUT).decode('UTF-8')
108
except subprocess.CalledProcessError:
111
if ("start/running" in output or "is running" in output):
117
def service_available(service_name):
118
"""Determine whether a system service is available"""
120
subprocess.check_output(
121
['service', service_name, 'status'],
122
stderr=subprocess.STDOUT).decode('UTF-8')
123
except subprocess.CalledProcessError as e:
124
return b'unrecognized service' not in e.output
129
def adduser(username, password=None, shell='/bin/bash', system_user=False):
130
"""Add a user to the system"""
132
user_info = pwd.getpwnam(username)
133
log('user {0} already exists!'.format(username))
135
log('creating user {0}'.format(username))
137
if system_user or password is None:
138
cmd.append('--system')
143
'--password', password,
146
subprocess.check_call(cmd)
147
user_info = pwd.getpwnam(username)
151
def add_group(group_name, system_group=False):
152
"""Add a group to the system"""
154
group_info = grp.getgrnam(group_name)
155
log('group {0} already exists!'.format(group_name))
157
log('creating group {0}'.format(group_name))
160
cmd.append('--system')
165
cmd.append(group_name)
166
subprocess.check_call(cmd)
167
group_info = grp.getgrnam(group_name)
171
def add_user_to_group(username, group):
172
"""Add a user to a group"""
173
cmd = ['gpasswd', '-a', username, group]
174
log("Adding user {} to group {}".format(username, group))
175
subprocess.check_call(cmd)
178
def rsync(from_path, to_path, flags='-r', options=None):
179
"""Replicate the contents of a path"""
180
options = options or ['--delete', '--executability']
181
cmd = ['/usr/bin/rsync', flags]
183
cmd.append(from_path)
186
return subprocess.check_output(cmd).decode('UTF-8').strip()
189
def symlink(source, destination):
190
"""Create a symbolic link"""
191
log("Symlinking {} as {}".format(source, destination))
198
subprocess.check_call(cmd)
201
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
202
"""Create a directory"""
203
log("Making dir {} {}:{} {:o}".format(path, owner, group,
205
uid = pwd.getpwnam(owner).pw_uid
206
gid = grp.getgrnam(group).gr_gid
207
realpath = os.path.abspath(path)
208
path_exists = os.path.exists(realpath)
209
if path_exists and force:
210
if not os.path.isdir(realpath):
211
log("Removing non-directory file {} prior to mkdir()".format(path))
213
os.makedirs(realpath, perms)
214
elif not path_exists:
215
os.makedirs(realpath, perms)
216
os.chown(realpath, uid, gid)
217
os.chmod(realpath, perms)
220
def write_file(path, content, owner='root', group='root', perms=0o444):
221
"""Create or overwrite a file with the contents of a byte string."""
222
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
223
uid = pwd.getpwnam(owner).pw_uid
224
gid = grp.getgrnam(group).gr_gid
225
with open(path, 'wb') as target:
226
os.fchown(target.fileno(), uid, gid)
227
os.fchmod(target.fileno(), perms)
228
target.write(content)
231
def fstab_remove(mp):
232
"""Remove the given mountpoint entry from /etc/fstab
234
return Fstab.remove_by_mountpoint(mp)
237
def fstab_add(dev, mp, fs, options=None):
238
"""Adds the given device entry to the /etc/fstab file
240
return Fstab.add(dev, mp, fs, options=options)
243
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
244
"""Mount a filesystem at a particular mountpoint"""
246
if options is not None:
247
cmd_args.extend(['-o', options])
248
cmd_args.extend([device, mountpoint])
250
subprocess.check_output(cmd_args)
251
except subprocess.CalledProcessError as e:
252
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
256
return fstab_add(device, mountpoint, filesystem, options=options)
260
def umount(mountpoint, persist=False):
261
"""Unmount a filesystem"""
262
cmd_args = ['umount', mountpoint]
264
subprocess.check_output(cmd_args)
265
except subprocess.CalledProcessError as e:
266
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
270
return fstab_remove(mountpoint)
275
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
276
with open('/proc/mounts') as f:
277
# [['/mount/point','/dev/path'],[...]]
278
system_mounts = [m[1::-1] for m in [l.strip().split()
279
for l in f.readlines()]]
283
def file_hash(path, hash_type='md5'):
285
Generate a hash checksum of the contents of 'path' or None if not found.
287
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
288
such as md5, sha1, sha256, sha512, etc.
290
if os.path.exists(path):
291
h = getattr(hashlib, hash_type)()
292
with open(path, 'rb') as source:
293
h.update(source.read())
301
Generate a hash checksum of all files matching 'path'. Standard wildcards
302
like '*' and '?' are supported, see documentation for the 'glob' module for
305
:return: dict: A { filename: hash } dictionary for all matched files.
309
filename: file_hash(filename)
310
for filename in glob.iglob(path)
314
def check_hash(path, checksum, hash_type='md5'):
316
Validate a file using a cryptographic checksum.
318
:param str checksum: Value of the checksum used to validate the file.
319
:param str hash_type: Hash algorithm used to generate `checksum`.
320
Can be any hash alrgorithm supported by :mod:`hashlib`,
321
such as md5, sha1, sha256, sha512, etc.
322
:raises ChecksumError: If the file fails the checksum
325
actual_checksum = file_hash(path, hash_type)
326
if checksum != actual_checksum:
327
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
330
class ChecksumError(ValueError):
334
def restart_on_change(restart_map, stopstart=False):
335
"""Restart services based on configuration files changing
337
This function is used a decorator, for example::
340
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
341
'/etc/apache/sites-enabled/*': [ 'apache2' ]
343
def config_changed():
344
pass # your code here
346
In this example, the cinder-api and cinder-volume services
347
would be restarted if /etc/ceph/ceph.conf is changed by the
348
ceph_client_changed function. The apache2 service would be
349
restarted if any file matching the pattern got changed, created
350
or removed. Standard wildcards are supported, see documentation
351
for the 'glob' module for more information.
354
def wrapped_f(*args, **kwargs):
355
checksums = {path: path_hash(path) for path in restart_map}
358
for path in restart_map:
359
if path_hash(path) != checksums[path]:
360
restarts += restart_map[path]
361
services_list = list(OrderedDict.fromkeys(restarts))
363
for service_name in services_list:
364
service('restart', service_name)
366
for action in ['stop', 'start']:
367
for service_name in services_list:
368
service(action, service_name)
374
"""Return /etc/lsb-release in a dict"""
376
with open('/etc/lsb-release', 'r') as lsb:
379
d[k.strip()] = v.strip()
383
def pwgen(length=None):
384
"""Generate a random pasword."""
386
# A random length is ok to use a weak PRNG
387
length = random.choice(range(35, 45))
388
alphanumeric_chars = [
389
l for l in (string.ascii_letters + string.digits)
390
if l not in 'l0QD1vAEIOUaeiou']
391
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
393
random_generator = random.SystemRandom()
395
random_generator.choice(alphanumeric_chars) for _ in range(length)]
396
return(''.join(random_chars))
399
def list_nics(nic_type):
400
'''Return a list of nics of given type(s)'''
401
if isinstance(nic_type, six.string_types):
402
int_types = [nic_type]
406
for int_type in int_types:
407
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
408
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
409
ip_output = (line for line in ip_output if line)
410
for line in ip_output:
411
if line.split()[1].startswith(int_type):
412
matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
414
interface = matched.groups()[0]
416
interface = line.split()[1].replace(":", "")
417
interfaces.append(interface)
422
def set_nic_mtu(nic, mtu):
423
'''Set MTU on a network interface'''
424
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
425
subprocess.check_call(cmd)
428
def get_nic_mtu(nic):
429
cmd = ['ip', 'addr', 'show', nic]
430
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
432
for line in ip_output:
435
mtu = words[words.index("mtu") + 1]
439
def get_nic_hwaddr(nic):
440
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
441
ip_output = subprocess.check_output(cmd).decode('UTF-8')
443
words = ip_output.split()
444
if 'link/ether' in words:
445
hwaddr = words[words.index('link/ether') + 1]
449
def cmp_pkgrevno(package, revno, pkgcache=None):
450
'''Compare supplied revno with the revno of the installed package
452
* 1 => Installed revno is greater than supplied arg
453
* 0 => Installed revno is the same as supplied arg
454
* -1 => Installed revno is less than supplied arg
456
This function imports apt_cache function from charmhelpers.fetch if
457
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
458
you call this function, or pass an apt_pkg.Cache() instance.
462
from charmhelpers.fetch import apt_cache
463
pkgcache = apt_cache()
464
pkg = pkgcache[package]
465
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
477
def chownr(path, owner, group, follow_links=True):
478
uid = pwd.getpwnam(owner).pw_uid
479
gid = grp.getgrnam(group).gr_gid
485
for root, dirs, files in os.walk(path):
486
for name in dirs + files:
487
full = os.path.join(root, name)
488
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
489
if not broken_symlink:
490
chown(full, uid, gid)
493
def lchownr(path, owner, group):
494
chownr(path, owner, group, follow_links=False)