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)
76
service('disable', service_name)
77
elif os.path.exists(upstart_file):
78
override_path = os.path.join(
79
init_dir, '{}.override'.format(service_name))
80
with open(override_path, 'w') as fh:
82
elif os.path.exists(sysv_file):
83
subprocess.check_call(["update-rc.d", service_name, "disable"])
86
"Unable to detect {0} as SystemD, Upstart {1} or"
88
service_name, upstart_file, sysv_file))
92
def service_resume(service_name, init_dir="/etc/init",
93
initd_dir="/etc/init.d"):
94
"""Resume a system service.
96
Reenable starting again at boot. Start the service"""
97
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
98
sysv_file = os.path.join(initd_dir, service_name)
100
service('enable', service_name)
101
elif os.path.exists(upstart_file):
102
override_path = os.path.join(
103
init_dir, '{}.override'.format(service_name))
104
if os.path.exists(override_path):
105
os.unlink(override_path)
106
elif os.path.exists(sysv_file):
107
subprocess.check_call(["update-rc.d", service_name, "enable"])
110
"Unable to detect {0} as SystemD, Upstart {1} or"
112
service_name, upstart_file, sysv_file))
114
started = service_running(service_name)
116
started = service_start(service_name)
120
def service(action, service_name):
121
"""Control a system service"""
122
if init_is_systemd():
123
cmd = ['systemctl', action, service_name]
125
cmd = ['service', service_name, action]
126
return subprocess.call(cmd) == 0
129
def service_running(service_name):
130
"""Determine whether a system service is running"""
131
if init_is_systemd():
132
return service('is-active', service_name)
135
output = subprocess.check_output(
136
['service', service_name, 'status'],
137
stderr=subprocess.STDOUT).decode('UTF-8')
138
except subprocess.CalledProcessError:
141
if ("start/running" in output or "is running" in output):
147
def service_available(service_name):
148
"""Determine whether a system service is available"""
150
subprocess.check_output(
151
['service', service_name, 'status'],
152
stderr=subprocess.STDOUT).decode('UTF-8')
153
except subprocess.CalledProcessError as e:
154
return b'unrecognized service' not in e.output
159
SYSTEMD_SYSTEM = '/run/systemd/system'
162
def init_is_systemd():
163
return os.path.isdir(SYSTEMD_SYSTEM)
166
def adduser(username, password=None, shell='/bin/bash', system_user=False,
167
primary_group=None, secondary_groups=None):
169
Add a user to the system.
171
Will log but otherwise succeed if the user already exists.
173
:param str username: Username to create
174
:param str password: Password for user; if ``None``, create a system user
175
:param str shell: The default shell for the user
176
:param bool system_user: Whether to create a login or system user
177
:param str primary_group: Primary group for user; defaults to their username
178
:param list secondary_groups: Optional list of additional groups
180
:returns: The password database entry struct, as returned by `pwd.getpwnam`
183
user_info = pwd.getpwnam(username)
184
log('user {0} already exists!'.format(username))
186
log('creating user {0}'.format(username))
188
if system_user or password is None:
189
cmd.append('--system')
194
'--password', password,
196
if not primary_group:
198
grp.getgrnam(username)
199
primary_group = username # avoid "group exists" error
203
cmd.extend(['-g', primary_group])
205
cmd.extend(['-G', ','.join(secondary_groups)])
207
subprocess.check_call(cmd)
208
user_info = pwd.getpwnam(username)
212
def user_exists(username):
213
"""Check if a user exists"""
215
pwd.getpwnam(username)
222
def add_group(group_name, system_group=False):
223
"""Add a group to the system"""
225
group_info = grp.getgrnam(group_name)
226
log('group {0} already exists!'.format(group_name))
228
log('creating group {0}'.format(group_name))
231
cmd.append('--system')
236
cmd.append(group_name)
237
subprocess.check_call(cmd)
238
group_info = grp.getgrnam(group_name)
242
def add_user_to_group(username, group):
243
"""Add a user to a group"""
244
cmd = ['gpasswd', '-a', username, group]
245
log("Adding user {} to group {}".format(username, group))
246
subprocess.check_call(cmd)
249
def rsync(from_path, to_path, flags='-r', options=None):
250
"""Replicate the contents of a path"""
251
options = options or ['--delete', '--executability']
252
cmd = ['/usr/bin/rsync', flags]
254
cmd.append(from_path)
257
return subprocess.check_output(cmd).decode('UTF-8').strip()
260
def symlink(source, destination):
261
"""Create a symbolic link"""
262
log("Symlinking {} as {}".format(source, destination))
269
subprocess.check_call(cmd)
272
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
273
"""Create a directory"""
274
log("Making dir {} {}:{} {:o}".format(path, owner, group,
276
uid = pwd.getpwnam(owner).pw_uid
277
gid = grp.getgrnam(group).gr_gid
278
realpath = os.path.abspath(path)
279
path_exists = os.path.exists(realpath)
280
if path_exists and force:
281
if not os.path.isdir(realpath):
282
log("Removing non-directory file {} prior to mkdir()".format(path))
284
os.makedirs(realpath, perms)
285
elif not path_exists:
286
os.makedirs(realpath, perms)
287
os.chown(realpath, uid, gid)
288
os.chmod(realpath, perms)
291
def write_file(path, content, owner='root', group='root', perms=0o444):
292
"""Create or overwrite a file with the contents of a byte string."""
293
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
294
uid = pwd.getpwnam(owner).pw_uid
295
gid = grp.getgrnam(group).gr_gid
296
with open(path, 'wb') as target:
297
os.fchown(target.fileno(), uid, gid)
298
os.fchmod(target.fileno(), perms)
299
target.write(content)
302
def fstab_remove(mp):
303
"""Remove the given mountpoint entry from /etc/fstab
305
return Fstab.remove_by_mountpoint(mp)
308
def fstab_add(dev, mp, fs, options=None):
309
"""Adds the given device entry to the /etc/fstab file
311
return Fstab.add(dev, mp, fs, options=options)
314
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
315
"""Mount a filesystem at a particular mountpoint"""
317
if options is not None:
318
cmd_args.extend(['-o', options])
319
cmd_args.extend([device, mountpoint])
321
subprocess.check_output(cmd_args)
322
except subprocess.CalledProcessError as e:
323
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
327
return fstab_add(device, mountpoint, filesystem, options=options)
331
def umount(mountpoint, persist=False):
332
"""Unmount a filesystem"""
333
cmd_args = ['umount', mountpoint]
335
subprocess.check_output(cmd_args)
336
except subprocess.CalledProcessError as e:
337
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
341
return fstab_remove(mountpoint)
346
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
347
with open('/proc/mounts') as f:
348
# [['/mount/point','/dev/path'],[...]]
349
system_mounts = [m[1::-1] for m in [l.strip().split()
350
for l in f.readlines()]]
354
def fstab_mount(mountpoint):
355
"""Mount filesystem using fstab"""
356
cmd_args = ['mount', mountpoint]
358
subprocess.check_output(cmd_args)
359
except subprocess.CalledProcessError as e:
360
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
365
def file_hash(path, hash_type='md5'):
367
Generate a hash checksum of the contents of 'path' or None if not found.
369
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
370
such as md5, sha1, sha256, sha512, etc.
372
if os.path.exists(path):
373
h = getattr(hashlib, hash_type)()
374
with open(path, 'rb') as source:
375
h.update(source.read())
383
Generate a hash checksum of all files matching 'path'. Standard wildcards
384
like '*' and '?' are supported, see documentation for the 'glob' module for
387
:return: dict: A { filename: hash } dictionary for all matched files.
391
filename: file_hash(filename)
392
for filename in glob.iglob(path)
396
def check_hash(path, checksum, hash_type='md5'):
398
Validate a file using a cryptographic checksum.
400
:param str checksum: Value of the checksum used to validate the file.
401
:param str hash_type: Hash algorithm used to generate `checksum`.
402
Can be any hash alrgorithm supported by :mod:`hashlib`,
403
such as md5, sha1, sha256, sha512, etc.
404
:raises ChecksumError: If the file fails the checksum
407
actual_checksum = file_hash(path, hash_type)
408
if checksum != actual_checksum:
409
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
412
class ChecksumError(ValueError):
416
def restart_on_change(restart_map, stopstart=False):
417
"""Restart services based on configuration files changing
419
This function is used a decorator, for example::
422
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
423
'/etc/apache/sites-enabled/*': [ 'apache2' ]
425
def config_changed():
426
pass # your code here
428
In this example, the cinder-api and cinder-volume services
429
would be restarted if /etc/ceph/ceph.conf is changed by the
430
ceph_client_changed function. The apache2 service would be
431
restarted if any file matching the pattern got changed, created
432
or removed. Standard wildcards are supported, see documentation
433
for the 'glob' module for more information.
436
def wrapped_f(*args, **kwargs):
437
checksums = {path: path_hash(path) for path in restart_map}
440
for path in restart_map:
441
if path_hash(path) != checksums[path]:
442
restarts += restart_map[path]
443
services_list = list(OrderedDict.fromkeys(restarts))
445
for service_name in services_list:
446
service('restart', service_name)
448
for action in ['stop', 'start']:
449
for service_name in services_list:
450
service(action, service_name)
456
"""Return /etc/lsb-release in a dict"""
458
with open('/etc/lsb-release', 'r') as lsb:
461
d[k.strip()] = v.strip()
465
def pwgen(length=None):
466
"""Generate a random pasword."""
468
# A random length is ok to use a weak PRNG
469
length = random.choice(range(35, 45))
470
alphanumeric_chars = [
471
l for l in (string.ascii_letters + string.digits)
472
if l not in 'l0QD1vAEIOUaeiou']
473
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
475
random_generator = random.SystemRandom()
477
random_generator.choice(alphanumeric_chars) for _ in range(length)]
478
return(''.join(random_chars))
481
def is_phy_iface(interface):
482
"""Returns True if interface is not virtual, otherwise False."""
484
sys_net = '/sys/class/net'
485
if os.path.isdir(sys_net):
486
for iface in glob.glob(os.path.join(sys_net, '*')):
487
if '/virtual/' in os.path.realpath(iface):
490
if interface == os.path.basename(iface):
496
def get_bond_master(interface):
497
"""Returns bond master if interface is bond slave otherwise None.
499
NOTE: the provided interface is expected to be physical
502
iface_path = '/sys/class/net/%s' % (interface)
503
if os.path.exists(iface_path):
504
if '/virtual/' in os.path.realpath(iface_path):
507
master = os.path.join(iface_path, 'master')
508
if os.path.exists(master):
509
master = os.path.realpath(master)
510
# make sure it is a bond master
511
if os.path.exists(os.path.join(master, 'bonding')):
512
return os.path.basename(master)
517
def list_nics(nic_type=None):
518
'''Return a list of nics of given type(s)'''
519
if isinstance(nic_type, six.string_types):
520
int_types = [nic_type]
526
for int_type in int_types:
527
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
528
ip_output = subprocess.check_output(cmd).decode('UTF-8')
529
ip_output = ip_output.split('\n')
530
ip_output = (line for line in ip_output if line)
531
for line in ip_output:
532
if line.split()[1].startswith(int_type):
533
matched = re.search('.*: (' + int_type +
534
r'[0-9]+\.[0-9]+)@.*', line)
536
iface = matched.groups()[0]
538
iface = line.split()[1].replace(":", "")
540
if iface not in interfaces:
541
interfaces.append(iface)
544
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
545
ip_output = (line.strip() for line in ip_output if line)
547
key = re.compile('^[0-9]+:\s+(.+):')
548
for line in ip_output:
549
matched = re.search(key, line)
551
iface = matched.group(1)
552
iface = iface.partition("@")[0]
553
if iface not in interfaces:
554
interfaces.append(iface)
559
def set_nic_mtu(nic, mtu):
560
'''Set MTU on a network interface'''
561
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
562
subprocess.check_call(cmd)
565
def get_nic_mtu(nic):
566
cmd = ['ip', 'addr', 'show', nic]
567
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
569
for line in ip_output:
572
mtu = words[words.index("mtu") + 1]
576
def get_nic_hwaddr(nic):
577
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
578
ip_output = subprocess.check_output(cmd).decode('UTF-8')
580
words = ip_output.split()
581
if 'link/ether' in words:
582
hwaddr = words[words.index('link/ether') + 1]
586
def cmp_pkgrevno(package, revno, pkgcache=None):
587
'''Compare supplied revno with the revno of the installed package
589
* 1 => Installed revno is greater than supplied arg
590
* 0 => Installed revno is the same as supplied arg
591
* -1 => Installed revno is less than supplied arg
593
This function imports apt_cache function from charmhelpers.fetch if
594
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
595
you call this function, or pass an apt_pkg.Cache() instance.
599
from charmhelpers.fetch import apt_cache
600
pkgcache = apt_cache()
601
pkg = pkgcache[package]
602
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
614
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
616
Recursively change user and group ownership of files and directories
617
in given path. Doesn't chown path itself by default, only its children.
619
:param bool follow_links: Also Chown links if True
620
:param bool chowntopdir: Also chown path itself if True
622
uid = pwd.getpwnam(owner).pw_uid
623
gid = grp.getgrnam(group).gr_gid
630
broken_symlink = os.path.lexists(path) and not os.path.exists(path)
631
if not broken_symlink:
632
chown(path, uid, gid)
633
for root, dirs, files in os.walk(path):
634
for name in dirs + files:
635
full = os.path.join(root, name)
636
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
637
if not broken_symlink:
638
chown(full, uid, gid)
641
def lchownr(path, owner, group):
642
chownr(path, owner, group, follow_links=False)
646
'''The total amount of system RAM in bytes.
648
This is what is reported by the OS, and may be overcommitted when
649
there are multiple containers hosted on the same machine.
651
with open('/proc/meminfo', 'r') as f:
652
for line in f.readlines():
654
key, value, unit = line.split()
655
if key == 'MemTotal:':
656
assert unit == 'kB', 'Unknown unit'
657
return int(value) * 1024 # Classic, not KiB.
658
raise NotImplementedError()