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>
32
from contextlib import contextmanager
33
from collections import OrderedDict
37
from .hookenv import log
38
from .fstab import Fstab
41
def service_start(service_name):
42
"""Start a system service"""
43
return service('start', service_name)
46
def service_stop(service_name):
47
"""Stop a system service"""
48
return service('stop', service_name)
51
def service_restart(service_name):
52
"""Restart a system service"""
53
return service('restart', service_name)
56
def service_reload(service_name, restart_on_failure=False):
57
"""Reload a system service, optionally falling back to restart if
59
service_result = service('reload', service_name)
60
if not service_result and restart_on_failure:
61
service_result = service('restart', service_name)
65
def service(action, service_name):
66
"""Control a system service"""
67
cmd = ['service', service_name, action]
68
return subprocess.call(cmd) == 0
71
def service_running(service):
72
"""Determine whether a system service is running"""
74
output = subprocess.check_output(
75
['service', service, 'status'],
76
stderr=subprocess.STDOUT).decode('UTF-8')
77
except subprocess.CalledProcessError:
80
if ("start/running" in output or "is running" in output):
86
def service_available(service_name):
87
"""Determine whether a system service is available"""
89
subprocess.check_output(
90
['service', service_name, 'status'],
91
stderr=subprocess.STDOUT).decode('UTF-8')
92
except subprocess.CalledProcessError as e:
93
return 'unrecognized service' not in e.output
98
def adduser(username, password=None, shell='/bin/bash', system_user=False):
99
"""Add a user to the system"""
101
user_info = pwd.getpwnam(username)
102
log('user {0} already exists!'.format(username))
104
log('creating user {0}'.format(username))
106
if system_user or password is None:
107
cmd.append('--system')
112
'--password', password,
115
subprocess.check_call(cmd)
116
user_info = pwd.getpwnam(username)
120
def add_group(group_name, system_group=False):
121
"""Add a group to the system"""
123
group_info = grp.getgrnam(group_name)
124
log('group {0} already exists!'.format(group_name))
126
log('creating group {0}'.format(group_name))
129
cmd.append('--system')
134
cmd.append(group_name)
135
subprocess.check_call(cmd)
136
group_info = grp.getgrnam(group_name)
140
def add_user_to_group(username, group):
141
"""Add a user to a group"""
147
log("Adding user {} to group {}".format(username, group))
148
subprocess.check_call(cmd)
151
def rsync(from_path, to_path, flags='-r', options=None):
152
"""Replicate the contents of a path"""
153
options = options or ['--delete', '--executability']
154
cmd = ['/usr/bin/rsync', flags]
156
cmd.append(from_path)
159
return subprocess.check_output(cmd).decode('UTF-8').strip()
162
def symlink(source, destination):
163
"""Create a symbolic link"""
164
log("Symlinking {} as {}".format(source, destination))
171
subprocess.check_call(cmd)
174
def mkdir(path, owner='root', group='root', perms=0o555, force=False):
175
"""Create a directory"""
176
log("Making dir {} {}:{} {:o}".format(path, owner, group,
178
uid = pwd.getpwnam(owner).pw_uid
179
gid = grp.getgrnam(group).gr_gid
180
realpath = os.path.abspath(path)
181
path_exists = os.path.exists(realpath)
182
if path_exists and force:
183
if not os.path.isdir(realpath):
184
log("Removing non-directory file {} prior to mkdir()".format(path))
186
os.makedirs(realpath, perms)
187
elif not path_exists:
188
os.makedirs(realpath, perms)
189
os.chown(realpath, uid, gid)
190
os.chmod(realpath, perms)
193
def write_file(path, content, owner='root', group='root', perms=0o444):
194
"""Create or overwrite a file with the contents of a string"""
195
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
196
uid = pwd.getpwnam(owner).pw_uid
197
gid = grp.getgrnam(group).gr_gid
198
with open(path, 'w') as target:
199
os.fchown(target.fileno(), uid, gid)
200
os.fchmod(target.fileno(), perms)
201
target.write(content)
204
def fstab_remove(mp):
205
"""Remove the given mountpoint entry from /etc/fstab
207
return Fstab.remove_by_mountpoint(mp)
210
def fstab_add(dev, mp, fs, options=None):
211
"""Adds the given device entry to the /etc/fstab file
213
return Fstab.add(dev, mp, fs, options=options)
216
def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
217
"""Mount a filesystem at a particular mountpoint"""
219
if options is not None:
220
cmd_args.extend(['-o', options])
221
cmd_args.extend([device, mountpoint])
223
subprocess.check_output(cmd_args)
224
except subprocess.CalledProcessError as e:
225
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
229
return fstab_add(device, mountpoint, filesystem, options=options)
233
def umount(mountpoint, persist=False):
234
"""Unmount a filesystem"""
235
cmd_args = ['umount', mountpoint]
237
subprocess.check_output(cmd_args)
238
except subprocess.CalledProcessError as e:
239
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
243
return fstab_remove(mountpoint)
248
"""Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
249
with open('/proc/mounts') as f:
250
# [['/mount/point','/dev/path'],[...]]
251
system_mounts = [m[1::-1] for m in [l.strip().split()
252
for l in f.readlines()]]
256
def file_hash(path, hash_type='md5'):
258
Generate a hash checksum of the contents of 'path' or None if not found.
260
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
261
such as md5, sha1, sha256, sha512, etc.
263
if os.path.exists(path):
264
h = getattr(hashlib, hash_type)()
265
with open(path, 'rb') as source:
266
h.update(source.read())
272
def check_hash(path, checksum, hash_type='md5'):
274
Validate a file using a cryptographic checksum.
276
:param str checksum: Value of the checksum used to validate the file.
277
:param str hash_type: Hash algorithm used to generate `checksum`.
278
Can be any hash alrgorithm supported by :mod:`hashlib`,
279
such as md5, sha1, sha256, sha512, etc.
280
:raises ChecksumError: If the file fails the checksum
283
actual_checksum = file_hash(path, hash_type)
284
if checksum != actual_checksum:
285
raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
288
class ChecksumError(ValueError):
292
def restart_on_change(restart_map, stopstart=False):
293
"""Restart services based on configuration files changing
295
This function is used a decorator, for example::
298
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
300
def ceph_client_changed():
301
pass # your code here
303
In this example, the cinder-api and cinder-volume services
304
would be restarted if /etc/ceph/ceph.conf is changed by the
305
ceph_client_changed function.
308
def wrapped_f(*args):
310
for path in restart_map:
311
checksums[path] = file_hash(path)
314
for path in restart_map:
315
if checksums[path] != file_hash(path):
316
restarts += restart_map[path]
317
services_list = list(OrderedDict.fromkeys(restarts))
319
for service_name in services_list:
320
service('restart', service_name)
322
for action in ['stop', 'start']:
323
for service_name in services_list:
324
service(action, service_name)
330
"""Return /etc/lsb-release in a dict"""
332
with open('/etc/lsb-release', 'r') as lsb:
335
d[k.strip()] = v.strip()
339
def pwgen(length=None):
340
"""Generate a random pasword."""
342
length = random.choice(range(35, 45))
343
alphanumeric_chars = [
344
l for l in (string.ascii_letters + string.digits)
345
if l not in 'l0QD1vAEIOUaeiou']
347
random.choice(alphanumeric_chars) for _ in range(length)]
348
return(''.join(random_chars))
351
def list_nics(nic_type):
352
'''Return a list of nics of given type(s)'''
353
if isinstance(nic_type, six.string_types):
354
int_types = [nic_type]
358
for int_type in int_types:
359
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
360
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
361
ip_output = (line for line in ip_output if line)
362
for line in ip_output:
363
if line.split()[1].startswith(int_type):
364
matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
366
interface = matched.groups()[0]
368
interface = line.split()[1].replace(":", "")
369
interfaces.append(interface)
374
def set_nic_mtu(nic, mtu):
375
'''Set MTU on a network interface'''
376
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
377
subprocess.check_call(cmd)
380
def get_nic_mtu(nic):
381
cmd = ['ip', 'addr', 'show', nic]
382
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
384
for line in ip_output:
387
mtu = words[words.index("mtu") + 1]
391
def get_nic_hwaddr(nic):
392
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
393
ip_output = subprocess.check_output(cmd).decode('UTF-8')
395
words = ip_output.split()
396
if 'link/ether' in words:
397
hwaddr = words[words.index('link/ether') + 1]
401
def cmp_pkgrevno(package, revno, pkgcache=None):
402
'''Compare supplied revno with the revno of the installed package
404
* 1 => Installed revno is greater than supplied arg
405
* 0 => Installed revno is the same as supplied arg
406
* -1 => Installed revno is less than supplied arg
408
This function imports apt_cache function from charmhelpers.fetch if
409
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
410
you call this function, or pass an apt_pkg.Cache() instance.
414
from charmhelpers.fetch import apt_cache
415
pkgcache = apt_cache()
416
pkg = pkgcache[package]
417
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
429
def chownr(path, owner, group, follow_links=True):
430
uid = pwd.getpwnam(owner).pw_uid
431
gid = grp.getgrnam(group).gr_gid
437
for root, dirs, files in os.walk(path):
438
for name in dirs + files:
439
full = os.path.join(root, name)
440
broken_symlink = os.path.lexists(full) and not os.path.exists(full)
441
if not broken_symlink:
442
chown(full, uid, gid)
445
def lchownr(path, owner, group):
446
chownr(path, owner, group, follow_links=False)