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."""
71
if service_running(service_name):
72
stopped = service_stop(service_name)
73
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74
sysv_file = os.path.join(initd_dir, service_name)
75
if os.path.exists(upstart_file):
76
override_path = os.path.join(
77
init_dir, '{}.override'.format(service_name))
78
with open(override_path, 'w') as fh:
80
elif os.path.exists(sysv_file):
81
subprocess.check_call(["update-rc.d", service_name, "disable"])
83
# XXX: Support SystemD too
85
"Unable to detect {0} as either Upstart {1} or SysV {2}".format(
86
service_name, upstart_file, sysv_file))
90
def service_resume(service_name, init_dir="/etc/init",
91
initd_dir="/etc/init.d"):
92
"""Resume a system service.
94
Reenable starting again at boot. Start the service"""
95
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
96
sysv_file = os.path.join(initd_dir, service_name)
97
if os.path.exists(upstart_file):
98
override_path = os.path.join(
99
init_dir, '{}.override'.format(service_name))
100
if os.path.exists(override_path):
101
os.unlink(override_path)
102
elif os.path.exists(sysv_file):
103
subprocess.check_call(["update-rc.d", service_name, "enable"])
105
# XXX: Support SystemD too
107
"Unable to detect {0} as either Upstart {1} or SysV {2}".format(
108
service_name, upstart_file, sysv_file))
110
started = service_running(service_name)
112
started = service_start(service_name)
116
def service(action, service_name):
117
"""Control a system service"""
118
cmd = ['service', service_name, action]
119
return subprocess.call(cmd) == 0
122
def service_running(service):
123
"""Determine whether a system service is running"""
125
output = subprocess.check_output(
126
['service', service, 'status'],
127
stderr=subprocess.STDOUT).decode('UTF-8')
128
except subprocess.CalledProcessError:
131
if ("start/running" in output or "is running" in output):
137
def service_available(service_name):
138
"""Determine whether a system service is available"""
140
subprocess.check_output(
141
['service', service_name, 'status'],
142
stderr=subprocess.STDOUT).decode('UTF-8')
143
except subprocess.CalledProcessError as e:
144
return b'unrecognized service' not in e.output
149
def adduser(username, password=None, shell='/bin/bash', system_user=False,
150
primary_group=None, secondary_groups=None):
152
Add a user to the system.
154
Will log but otherwise succeed if the user already exists.
156
:param str username: Username to create
157
:param str password: Password for user; if ``None``, create a system user
158
:param str shell: The default shell for the user
159
:param bool system_user: Whether to create a login or system user
160
:param str primary_group: Primary group for user; defaults to their username
161
:param list secondary_groups: Optional list of additional groups
163
:returns: The password database entry struct, as returned by `pwd.getpwnam`
166
user_info = pwd.getpwnam(username)
167
log('user {0} already exists!'.format(username))
169
log('creating user {0}'.format(username))
171
if system_user or password is None:
172
cmd.append('--system')
177
'--password', password,
179
if not primary_group:
181
grp.getgrnam(username)
182
primary_group = username # avoid "group exists" error
186
cmd.extend(['-g', primary_group])
188
cmd.extend(['-G', ','.join(secondary_groups)])
190
subprocess.check_call(cmd)
191
user_info = pwd.getpwnam(username)
195
def user_exists(username):
196
"""Check if a user exists"""
198
pwd.getpwnam(username)
205
def add_group(group_name, system_group=False):
206
"""Add a group to the system"""
208
group_info = grp.getgrnam(group_name)
209
log('group {0} already exists!'.format(group_name))
211
log('creating group {0}'.format(group_name))
214
cmd.append('--system')
219
cmd.append(group_name)
220
subprocess.check_call(cmd)
221
group_info = grp.getgrnam(group_name)
225
def add_user_to_group(username, group):
226
"""Add a user to a group"""
227
cmd = ['gpasswd', '-a', username, group]
228
log("Adding user {} to group {}".format(username, group))
229
subprocess.check_call(cmd)
232
def rsync(from_path, to_path, flags='-r', options=None):
233
"""Replicate the contents of a path"""
234
options = options or ['--delete', '--executability']
235
cmd = ['/usr/bin/rsync', flags]
237
cmd.append(from_path)
240
return subprocess.check_output(cmd).decode('UTF-8').strip()
243
def symlink(source, destination):
244
"""Create a symbolic link"""
245
log("Symlinking {} as {}".format(source, destination))
252
subprocess.check_call(cmd)
255
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
256
"""Create a directory"""
257
log("Making dir {} {}:{} {:o}".format(path, owner, group,
259
uid = pwd.getpwnam(owner).pw_uid
260
gid = grp.getgrnam(group).gr_gid
261
realpath = os.path.abspath(path)
262
path_exists = os.path.exists(realpath)
263
if path_exists and force:
264
if not os.path.isdir(realpath):
265
log("Removing non-directory file {} prior to mkdir()".format(path))
267
os.makedirs(realpath, perms)
268
elif not path_exists:
269
os.makedirs(realpath, perms)
270
os.chown(realpath, uid, gid)
271
os.chmod(realpath, perms)
274
def write_file(path, content, owner='root', group='root', perms=0o444):
275
"""Create or overwrite a file with the contents of a byte string."""
276
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
277
uid = pwd.getpwnam(owner).pw_uid
278
gid = grp.getgrnam(group).gr_gid
279
with open(path, 'wb') as target:
280
os.fchown(target.fileno(), uid, gid)
281
os.fchmod(target.fileno(), perms)
282
target.write(content)
285
def fstab_remove(mp):
286
"""Remove the given mountpoint entry from /etc/fstab
288
return Fstab.remove_by_mountpoint(mp)
291
def fstab_add(dev, mp, fs, options=None):
292
"""Adds the given device entry to the /etc/fstab file
294
return Fstab.add(dev, mp, fs, options=options)
297
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
298
"""Mount a filesystem at a particular mountpoint"""
300
if options is not None:
301
cmd_args.extend(['-o', options])
302
cmd_args.extend([device, mountpoint])
304
subprocess.check_output(cmd_args)
305
except subprocess.CalledProcessError as e:
306
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
310
return fstab_add(device, mountpoint, filesystem, options=options)
314
def umount(mountpoint, persist=False):
315
"""Unmount a filesystem"""
316
cmd_args = ['umount', mountpoint]
318
subprocess.check_output(cmd_args)
319
except subprocess.CalledProcessError as e:
320
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
324
return fstab_remove(mountpoint)
329
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
330
with open('/proc/mounts') as f:
331
# [['/mount/point','/dev/path'],[...]]
332
system_mounts = [m[1::-1] for m in [l.strip().split()
333
for l in f.readlines()]]
337
def fstab_mount(mountpoint):
338
"""Mount filesystem using fstab"""
339
cmd_args = ['mount', mountpoint]
341
subprocess.check_output(cmd_args)
342
except subprocess.CalledProcessError as e:
343
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
348
def file_hash(path, hash_type='md5'):
350
Generate a hash checksum of the contents of 'path' or None if not found.
352
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
353
such as md5, sha1, sha256, sha512, etc.
355
if os.path.exists(path):
356
h = getattr(hashlib, hash_type)()
357
with open(path, 'rb') as source:
358
h.update(source.read())
366
Generate a hash checksum of all files matching 'path'. Standard wildcards
367
like '*' and '?' are supported, see documentation for the 'glob' module for
370
:return: dict: A { filename: hash } dictionary for all matched files.
374
filename: file_hash(filename)
375
for filename in glob.iglob(path)
379
def check_hash(path, checksum, hash_type='md5'):
381
Validate a file using a cryptographic checksum.
383
:param str checksum: Value of the checksum used to validate the file.
384
:param str hash_type: Hash algorithm used to generate `checksum`.
385
Can be any hash alrgorithm supported by :mod:`hashlib`,
386
such as md5, sha1, sha256, sha512, etc.
387
:raises ChecksumError: If the file fails the checksum
390
actual_checksum = file_hash(path, hash_type)
391
if checksum != actual_checksum:
392
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
395
class ChecksumError(ValueError):
399
def restart_on_change(restart_map, stopstart=False):
400
"""Restart services based on configuration files changing
402
This function is used a decorator, for example::
405
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
406
'/etc/apache/sites-enabled/*': [ 'apache2' ]
408
def config_changed():
409
pass # your code here
411
In this example, the cinder-api and cinder-volume services
412
would be restarted if /etc/ceph/ceph.conf is changed by the
413
ceph_client_changed function. The apache2 service would be
414
restarted if any file matching the pattern got changed, created
415
or removed. Standard wildcards are supported, see documentation
416
for the 'glob' module for more information.
419
def wrapped_f(*args, **kwargs):
420
checksums = {path: path_hash(path) for path in restart_map}
423
for path in restart_map:
424
if path_hash(path) != checksums[path]:
425
restarts += restart_map[path]
426
services_list = list(OrderedDict.fromkeys(restarts))
428
for service_name in services_list:
429
service('restart', service_name)
431
for action in ['stop', 'start']:
432
for service_name in services_list:
433
service(action, service_name)
439
"""Return /etc/lsb-release in a dict"""
441
with open('/etc/lsb-release', 'r') as lsb:
444
d[k.strip()] = v.strip()
448
def pwgen(length=None):
449
"""Generate a random pasword."""
451
# A random length is ok to use a weak PRNG
452
length = random.choice(range(35, 45))
453
alphanumeric_chars = [
454
l for l in (string.ascii_letters + string.digits)
455
if l not in 'l0QD1vAEIOUaeiou']
456
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
458
random_generator = random.SystemRandom()
460
random_generator.choice(alphanumeric_chars) for _ in range(length)]
461
return(''.join(random_chars))
464
def is_phy_iface(interface):
465
"""Returns True if interface is not virtual, otherwise False."""
467
sys_net = '/sys/class/net'
468
if os.path.isdir(sys_net):
469
for iface in glob.glob(os.path.join(sys_net, '*')):
470
if '/virtual/' in os.path.realpath(iface):
473
if interface == os.path.basename(iface):
479
def get_bond_master(interface):
480
"""Returns bond master if interface is bond slave otherwise None.
482
NOTE: the provided interface is expected to be physical
485
iface_path = '/sys/class/net/%s' % (interface)
486
if os.path.exists(iface_path):
487
if '/virtual/' in os.path.realpath(iface_path):
490
master = os.path.join(iface_path, 'master')
491
if os.path.exists(master):
492
master = os.path.realpath(master)
493
# make sure it is a bond master
494
if os.path.exists(os.path.join(master, 'bonding')):
495
return os.path.basename(master)
500
def list_nics(nic_type=None):
501
'''Return a list of nics of given type(s)'''
502
if isinstance(nic_type, six.string_types):
503
int_types = [nic_type]
509
for int_type in int_types:
510
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
511
ip_output = subprocess.check_output(cmd).decode('UTF-8')
512
ip_output = ip_output.split('\n')
513
ip_output = (line for line in ip_output if line)
514
for line in ip_output:
515
if line.split()[1].startswith(int_type):
516
matched = re.search('.*: (' + int_type +
517
r'[0-9]+\.[0-9]+)@.*', line)
519
iface = matched.groups()[0]
521
iface = line.split()[1].replace(":", "")
523
if iface not in interfaces:
524
interfaces.append(iface)
527
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
528
ip_output = (line.strip() for line in ip_output if line)
530
key = re.compile('^[0-9]+:\s+(.+):')
531
for line in ip_output:
532
matched = re.search(key, line)
534
iface = matched.group(1)
535
iface = iface.partition("@")[0]
536
if iface not in interfaces:
537
interfaces.append(iface)
542
def set_nic_mtu(nic, mtu):
543
'''Set MTU on a network interface'''
544
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
545
subprocess.check_call(cmd)
548
def get_nic_mtu(nic):
549
cmd = ['ip', 'addr', 'show', nic]
550
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
552
for line in ip_output:
555
mtu = words[words.index("mtu") + 1]
559
def get_nic_hwaddr(nic):
560
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
561
ip_output = subprocess.check_output(cmd).decode('UTF-8')
563
words = ip_output.split()
564
if 'link/ether' in words:
565
hwaddr = words[words.index('link/ether') + 1]
569
def cmp_pkgrevno(package, revno, pkgcache=None):
570
'''Compare supplied revno with the revno of the installed package
572
* 1 => Installed revno is greater than supplied arg
573
* 0 => Installed revno is the same as supplied arg
574
* -1 => Installed revno is less than supplied arg
576
This function imports apt_cache function from charmhelpers.fetch if
577
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
578
you call this function, or pass an apt_pkg.Cache() instance.
582
from charmhelpers.fetch import apt_cache
583
pkgcache = apt_cache()
584
pkg = pkgcache[package]
585
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
597
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
599
Recursively change user and group ownership of files and directories
600
in given path. Doesn't chown path itself by default, only its children.
602
:param bool follow_links: Also Chown links if True
603
:param bool chowntopdir: Also chown path itself if True
605
uid = pwd.getpwnam(owner).pw_uid
606
gid = grp.getgrnam(group).gr_gid
613
broken_symlink = os.path.lexists(path) and not os.path.exists(path)
614
if not broken_symlink:
615
chown(path, uid, gid)
616
for root, dirs, files in os.walk(path):
617
for name in dirs + files:
618
full = os.path.join(root, name)
619
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
620
if not broken_symlink:
621
chown(full, uid, gid)
624
def lchownr(path, owner, group):
625
chownr(path, owner, group, follow_links=False)
629
'''The total amount of system RAM in bytes.
631
This is what is reported by the OS, and may be overcommitted when
632
there are multiple containers hosted on the same machine.
634
with open('/proc/meminfo', 'r') as f:
635
for line in f.readlines():
637
key, value, unit = line.split()
638
if key == 'MemTotal:':
639
assert unit == 'kB', 'Unknown unit'
640
return int(value) * 1024 # Classic, not KiB.
641
raise NotImplementedError()