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="/etc/init", initd_dir="/etc/init.d"):
67
"""Pause a system service.
69
Stop it, and prevent it from starting again at boot."""
70
stopped = service_stop(service_name)
71
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
72
sysv_file = os.path.join(initd_dir, service_name)
73
if os.path.exists(upstart_file):
74
override_path = os.path.join(
75
init_dir, '{}.override'.format(service_name))
76
with open(override_path, 'w') as fh:
78
elif os.path.exists(sysv_file):
79
subprocess.check_call(["update-rc.d", service_name, "disable"])
81
# XXX: Support SystemD too
83
"Unable to detect {0} as either Upstart {1} or SysV {2}".format(
84
service_name, upstart_file, sysv_file))
88
def service_resume(service_name, init_dir="/etc/init",
89
initd_dir="/etc/init.d"):
90
"""Resume a system service.
92
Reenable starting again at boot. Start the service"""
93
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
94
sysv_file = os.path.join(initd_dir, service_name)
95
if os.path.exists(upstart_file):
96
override_path = os.path.join(
97
init_dir, '{}.override'.format(service_name))
98
if os.path.exists(override_path):
99
os.unlink(override_path)
100
elif os.path.exists(sysv_file):
101
subprocess.check_call(["update-rc.d", service_name, "enable"])
103
# XXX: Support SystemD too
105
"Unable to detect {0} as either Upstart {1} or SysV {2}".format(
106
service_name, upstart_file, sysv_file))
108
started = service_start(service_name)
112
def service(action, service_name):
113
"""Control a system service"""
114
cmd = ['service', service_name, action]
115
return subprocess.call(cmd) == 0
118
def service_running(service):
119
"""Determine whether a system service is running"""
121
output = subprocess.check_output(
122
['service', service, 'status'],
123
stderr=subprocess.STDOUT).decode('UTF-8')
124
except subprocess.CalledProcessError:
127
if ("start/running" in output or "is running" in output):
133
def service_available(service_name):
134
"""Determine whether a system service is available"""
136
subprocess.check_output(
137
['service', service_name, 'status'],
138
stderr=subprocess.STDOUT).decode('UTF-8')
139
except subprocess.CalledProcessError as e:
140
return b'unrecognized service' not in e.output
145
def adduser(username, password=None, shell='/bin/bash', system_user=False):
146
"""Add a user to the system"""
148
user_info = pwd.getpwnam(username)
149
log('user {0} already exists!'.format(username))
151
log('creating user {0}'.format(username))
153
if system_user or password is None:
154
cmd.append('--system')
159
'--password', password,
162
subprocess.check_call(cmd)
163
user_info = pwd.getpwnam(username)
167
def user_exists(username):
168
"""Check if a user exists"""
170
pwd.getpwnam(username)
177
def add_group(group_name, system_group=False):
178
"""Add a group to the system"""
180
group_info = grp.getgrnam(group_name)
181
log('group {0} already exists!'.format(group_name))
183
log('creating group {0}'.format(group_name))
186
cmd.append('--system')
191
cmd.append(group_name)
192
subprocess.check_call(cmd)
193
group_info = grp.getgrnam(group_name)
197
def add_user_to_group(username, group):
198
"""Add a user to a group"""
199
cmd = ['gpasswd', '-a', username, group]
200
log("Adding user {} to group {}".format(username, group))
201
subprocess.check_call(cmd)
204
def rsync(from_path, to_path, flags='-r', options=None):
205
"""Replicate the contents of a path"""
206
options = options or ['--delete', '--executability']
207
cmd = ['/usr/bin/rsync', flags]
209
cmd.append(from_path)
212
return subprocess.check_output(cmd).decode('UTF-8').strip()
215
def symlink(source, destination):
216
"""Create a symbolic link"""
217
log("Symlinking {} as {}".format(source, destination))
224
subprocess.check_call(cmd)
227
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
228
"""Create a directory"""
229
log("Making dir {} {}:{} {:o}".format(path, owner, group,
231
uid = pwd.getpwnam(owner).pw_uid
232
gid = grp.getgrnam(group).gr_gid
233
realpath = os.path.abspath(path)
234
path_exists = os.path.exists(realpath)
235
if path_exists and force:
236
if not os.path.isdir(realpath):
237
log("Removing non-directory file {} prior to mkdir()".format(path))
239
os.makedirs(realpath, perms)
240
elif not path_exists:
241
os.makedirs(realpath, perms)
242
os.chown(realpath, uid, gid)
243
os.chmod(realpath, perms)
246
def write_file(path, content, owner='root', group='root', perms=0o444):
247
"""Create or overwrite a file with the contents of a byte string."""
248
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
249
uid = pwd.getpwnam(owner).pw_uid
250
gid = grp.getgrnam(group).gr_gid
251
with open(path, 'wb') as target:
252
os.fchown(target.fileno(), uid, gid)
253
os.fchmod(target.fileno(), perms)
254
target.write(content)
257
def fstab_remove(mp):
258
"""Remove the given mountpoint entry from /etc/fstab
260
return Fstab.remove_by_mountpoint(mp)
263
def fstab_add(dev, mp, fs, options=None):
264
"""Adds the given device entry to the /etc/fstab file
266
return Fstab.add(dev, mp, fs, options=options)
269
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
270
"""Mount a filesystem at a particular mountpoint"""
272
if options is not None:
273
cmd_args.extend(['-o', options])
274
cmd_args.extend([device, mountpoint])
276
subprocess.check_output(cmd_args)
277
except subprocess.CalledProcessError as e:
278
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
282
return fstab_add(device, mountpoint, filesystem, options=options)
286
def umount(mountpoint, persist=False):
287
"""Unmount a filesystem"""
288
cmd_args = ['umount', mountpoint]
290
subprocess.check_output(cmd_args)
291
except subprocess.CalledProcessError as e:
292
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
296
return fstab_remove(mountpoint)
301
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
302
with open('/proc/mounts') as f:
303
# [['/mount/point','/dev/path'],[...]]
304
system_mounts = [m[1::-1] for m in [l.strip().split()
305
for l in f.readlines()]]
309
def fstab_mount(mountpoint):
310
"""Mount filesystem using fstab"""
311
cmd_args = ['mount', mountpoint]
313
subprocess.check_output(cmd_args)
314
except subprocess.CalledProcessError as e:
315
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
320
def file_hash(path, hash_type='md5'):
322
Generate a hash checksum of the contents of 'path' or None if not found.
324
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
325
such as md5, sha1, sha256, sha512, etc.
327
if os.path.exists(path):
328
h = getattr(hashlib, hash_type)()
329
with open(path, 'rb') as source:
330
h.update(source.read())
338
Generate a hash checksum of all files matching 'path'. Standard wildcards
339
like '*' and '?' are supported, see documentation for the 'glob' module for
342
:return: dict: A { filename: hash } dictionary for all matched files.
346
filename: file_hash(filename)
347
for filename in glob.iglob(path)
351
def check_hash(path, checksum, hash_type='md5'):
353
Validate a file using a cryptographic checksum.
355
:param str checksum: Value of the checksum used to validate the file.
356
:param str hash_type: Hash algorithm used to generate `checksum`.
357
Can be any hash alrgorithm supported by :mod:`hashlib`,
358
such as md5, sha1, sha256, sha512, etc.
359
:raises ChecksumError: If the file fails the checksum
362
actual_checksum = file_hash(path, hash_type)
363
if checksum != actual_checksum:
364
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
367
class ChecksumError(ValueError):
371
def restart_on_change(restart_map, stopstart=False):
372
"""Restart services based on configuration files changing
374
This function is used a decorator, for example::
377
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
378
'/etc/apache/sites-enabled/*': [ 'apache2' ]
380
def config_changed():
381
pass # your code here
383
In this example, the cinder-api and cinder-volume services
384
would be restarted if /etc/ceph/ceph.conf is changed by the
385
ceph_client_changed function. The apache2 service would be
386
restarted if any file matching the pattern got changed, created
387
or removed. Standard wildcards are supported, see documentation
388
for the 'glob' module for more information.
391
def wrapped_f(*args, **kwargs):
392
checksums = {path: path_hash(path) for path in restart_map}
395
for path in restart_map:
396
if path_hash(path) != checksums[path]:
397
restarts += restart_map[path]
398
services_list = list(OrderedDict.fromkeys(restarts))
400
for service_name in services_list:
401
service('restart', service_name)
403
for action in ['stop', 'start']:
404
for service_name in services_list:
405
service(action, service_name)
411
"""Return /etc/lsb-release in a dict"""
413
with open('/etc/lsb-release', 'r') as lsb:
416
d[k.strip()] = v.strip()
420
def pwgen(length=None):
421
"""Generate a random pasword."""
423
# A random length is ok to use a weak PRNG
424
length = random.choice(range(35, 45))
425
alphanumeric_chars = [
426
l for l in (string.ascii_letters + string.digits)
427
if l not in 'l0QD1vAEIOUaeiou']
428
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
430
random_generator = random.SystemRandom()
432
random_generator.choice(alphanumeric_chars) for _ in range(length)]
433
return(''.join(random_chars))
436
def is_phy_iface(interface):
437
"""Returns True if interface is not virtual, otherwise False."""
439
sys_net = '/sys/class/net'
440
if os.path.isdir(sys_net):
441
for iface in glob.glob(os.path.join(sys_net, '*')):
442
if '/virtual/' in os.path.realpath(iface):
445
if interface == os.path.basename(iface):
451
def get_bond_master(interface):
452
"""Returns bond master if interface is bond slave otherwise None.
454
NOTE: the provided interface is expected to be physical
457
iface_path = '/sys/class/net/%s' % (interface)
458
if os.path.exists(iface_path):
459
if '/virtual/' in os.path.realpath(iface_path):
462
master = os.path.join(iface_path, 'master')
463
if os.path.exists(master):
464
master = os.path.realpath(master)
465
# make sure it is a bond master
466
if os.path.exists(os.path.join(master, 'bonding')):
467
return os.path.basename(master)
472
def list_nics(nic_type=None):
473
'''Return a list of nics of given type(s)'''
474
if isinstance(nic_type, six.string_types):
475
int_types = [nic_type]
481
for int_type in int_types:
482
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
483
ip_output = subprocess.check_output(cmd).decode('UTF-8')
484
ip_output = ip_output.split('\n')
485
ip_output = (line for line in ip_output if line)
486
for line in ip_output:
487
if line.split()[1].startswith(int_type):
488
matched = re.search('.*: (' + int_type +
489
r'[0-9]+\.[0-9]+)@.*', line)
491
iface = matched.groups()[0]
493
iface = line.split()[1].replace(":", "")
495
if iface not in interfaces:
496
interfaces.append(iface)
499
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
500
ip_output = (line.strip() for line in ip_output if line)
502
key = re.compile('^[0-9]+:\s+(.+):')
503
for line in ip_output:
504
matched = re.search(key, line)
506
iface = matched.group(1)
507
iface = iface.partition("@")[0]
508
if iface not in interfaces:
509
interfaces.append(iface)
514
def set_nic_mtu(nic, mtu):
515
'''Set MTU on a network interface'''
516
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
517
subprocess.check_call(cmd)
520
def get_nic_mtu(nic):
521
cmd = ['ip', 'addr', 'show', nic]
522
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
524
for line in ip_output:
527
mtu = words[words.index("mtu") + 1]
531
def get_nic_hwaddr(nic):
532
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
533
ip_output = subprocess.check_output(cmd).decode('UTF-8')
535
words = ip_output.split()
536
if 'link/ether' in words:
537
hwaddr = words[words.index('link/ether') + 1]
541
def cmp_pkgrevno(package, revno, pkgcache=None):
542
'''Compare supplied revno with the revno of the installed package
544
* 1 => Installed revno is greater than supplied arg
545
* 0 => Installed revno is the same as supplied arg
546
* -1 => Installed revno is less than supplied arg
548
This function imports apt_cache function from charmhelpers.fetch if
549
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
550
you call this function, or pass an apt_pkg.Cache() instance.
554
from charmhelpers.fetch import apt_cache
555
pkgcache = apt_cache()
556
pkg = pkgcache[package]
557
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
569
def chownr(path, owner, group, follow_links=True):
570
uid = pwd.getpwnam(owner).pw_uid
571
gid = grp.getgrnam(group).gr_gid
577
for root, dirs, files in os.walk(path):
578
for name in dirs + files:
579
full = os.path.join(root, name)
580
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
581
if not broken_symlink:
582
chown(full, uid, gid)
585
def lchownr(path, owner, group):
586
chownr(path, owner, group, follow_links=False)