1
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/>.
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
7
# http://www.apache.org/licenses/LICENSE-2.0
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
17
15
"""Tools for working with the host system"""
18
16
# Copyright 2012 Canonical Ltd.
62
63
return service_result
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)
65
120
def service(action, service_name):
66
121
"""Control a system service"""
67
cmd = ['service', service_name, action]
122
if init_is_systemd():
123
cmd = ['systemctl', action, service_name]
125
cmd = ['service', service_name, action]
68
126
return subprocess.call(cmd) == 0
71
def service_running(service):
129
_UPSTART_CONF = "/etc/init/{}.conf"
130
_INIT_D_CONF = "/etc/init.d/{}"
133
def service_running(service_name):
72
134
"""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:
135
if init_is_systemd():
136
return service('is-active', service_name)
138
if os.path.exists(_UPSTART_CONF.format(service_name)):
140
output = subprocess.check_output(
141
['status', service_name],
142
stderr=subprocess.STDOUT).decode('UTF-8')
143
except subprocess.CalledProcessError:
146
# This works for upstart scripts where the 'service' command
147
# returns a consistent string to represent running 'start/running'
148
if "start/running" in output:
150
elif os.path.exists(_INIT_D_CONF.format(service_name)):
151
# Check System V scripts init script return codes
152
return service('status', service_name)
80
if ("start/running" in output or "is running" in output):
86
156
def service_available(service_name):
90
160
['service', service_name, 'status'],
91
161
stderr=subprocess.STDOUT).decode('UTF-8')
92
162
except subprocess.CalledProcessError as e:
93
return 'unrecognized service' not in e.output
163
return b'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"""
168
SYSTEMD_SYSTEM = '/run/systemd/system'
171
def init_is_systemd():
172
"""Return True if the host system uses systemd, False otherwise."""
173
return os.path.isdir(SYSTEMD_SYSTEM)
176
def adduser(username, password=None, shell='/bin/bash', system_user=False,
177
primary_group=None, secondary_groups=None, uid=None, home_dir=None):
178
"""Add a user to the system.
180
Will log but otherwise succeed if the user already exists.
182
:param str username: Username to create
183
:param str password: Password for user; if ``None``, create a system user
184
:param str shell: The default shell for the user
185
:param bool system_user: Whether to create a login or system user
186
:param str primary_group: Primary group for user; defaults to username
187
:param list secondary_groups: Optional list of additional groups
188
:param int uid: UID for user being created
189
:param str home_dir: Home directory for user
191
:returns: The password database entry struct, as returned by `pwd.getpwnam`
101
194
user_info = pwd.getpwnam(username)
102
195
log('user {0} already exists!'.format(username))
197
user_info = pwd.getpwuid(int(uid))
198
log('user with uid {0} already exists!'.format(uid))
104
200
log('creating user {0}'.format(username))
105
201
cmd = ['useradd']
203
cmd.extend(['--uid', str(uid)])
205
cmd.extend(['--home', str(home_dir)])
106
206
if system_user or password is None:
107
207
cmd.append('--system')
111
211
'--shell', shell,
112
212
'--password', password,
214
if not primary_group:
216
grp.getgrnam(username)
217
primary_group = username # avoid "group exists" error
221
cmd.extend(['-g', primary_group])
223
cmd.extend(['-G', ','.join(secondary_groups)])
114
224
cmd.append(username)
115
225
subprocess.check_call(cmd)
116
226
user_info = pwd.getpwnam(username)
120
def add_group(group_name, system_group=False):
121
"""Add a group to the system"""
230
def user_exists(username):
231
"""Check if a user exists"""
233
pwd.getpwnam(username)
241
"""Check if a uid exists"""
250
def group_exists(groupname):
251
"""Check if a group exists"""
253
grp.getgrnam(groupname)
261
"""Check if a gid exists"""
270
def add_group(group_name, system_group=False, gid=None):
271
"""Add a group to the system
273
Will log but otherwise succeed if the group already exists.
275
:param str group_name: group to create
276
:param bool system_group: Create system group
277
:param int gid: GID for user being created
279
:returns: The password database entry struct, as returned by `grp.getgrnam`
123
282
group_info = grp.getgrnam(group_name)
124
283
log('group {0} already exists!'.format(group_name))
285
group_info = grp.getgrgid(gid)
286
log('group with gid {0} already exists!'.format(gid))
126
288
log('creating group {0}'.format(group_name))
127
289
cmd = ['addgroup']
291
cmd.extend(['--gid', str(gid)])
129
293
cmd.append('--system')
253
411
return system_mounts
414
def fstab_mount(mountpoint):
415
"""Mount filesystem using fstab"""
416
cmd_args = ['mount', mountpoint]
418
subprocess.check_output(cmd_args)
419
except subprocess.CalledProcessError as e:
420
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
256
425
def file_hash(path, hash_type='md5'):
258
Generate a hash checksum of the contents of 'path' or None if not found.
426
"""Generate a hash checksum of the contents of 'path' or None if not found.
260
428
:param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
261
429
such as md5, sha1, sha256, sha512, etc.
441
"""Generate a hash checksum of all files matching 'path'. Standard
442
wildcards like '*' and '?' are supported, see documentation for the 'glob'
443
module for more information.
445
:return: dict: A { filename: hash } dictionary for all matched files.
449
filename: file_hash(filename)
450
for filename in glob.iglob(path)
272
454
def check_hash(path, checksum, hash_type='md5'):
274
Validate a file using a cryptographic checksum.
455
"""Validate a file using a cryptographic checksum.
276
457
:param str checksum: Value of the checksum used to validate the file.
277
458
:param str hash_type: Hash algorithm used to generate `checksum`.
288
469
class ChecksumError(ValueError):
470
"""A class derived from Value error to indicate the checksum failed."""
292
def restart_on_change(restart_map, stopstart=False):
474
def restart_on_change(restart_map, stopstart=False, restart_functions=None):
293
475
"""Restart services based on configuration files changing
295
477
This function is used a decorator, for example::
297
479
@restart_on_change({
298
480
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
481
'/etc/apache/sites-enabled/*': [ 'apache2' ]
300
def ceph_client_changed():
483
def config_changed():
301
484
pass # your code here
303
486
In this example, the cinder-api and cinder-volume services
304
487
would be restarted if /etc/ceph/ceph.conf is changed by the
305
ceph_client_changed function.
488
ceph_client_changed function. The apache2 service would be
489
restarted if any file matching the pattern got changed, created
490
or removed. Standard wildcards are supported, see documentation
491
for the 'glob' module for more information.
493
@param restart_map: {path_file_name: [service_name, ...]
494
@param stopstart: DEFAULT false; whether to stop, start OR restart
495
@param restart_functions: nonstandard functions to use to restart services
497
@returns result from decorated function
308
501
def wrapped_f(*args, **kwargs):
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)
502
return restart_on_change_helper(
503
(lambda: f(*args, **kwargs)), restart_map, stopstart,
509
def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
510
restart_functions=None):
511
"""Helper function to perform the restart_on_change function.
513
This is provided for decorators to restart services if files described
514
in the restart_map have changed after an invocation of lambda_f().
516
@param lambda_f: function to call.
517
@param restart_map: {file: [service, ...]}
518
@param stopstart: whether to stop, start or restart a service
519
@param restart_functions: nonstandard functions to use to restart services
521
@returns result of lambda_f()
523
if restart_functions is None:
524
restart_functions = {}
525
checksums = {path: path_hash(path) for path in restart_map}
527
# create a list of lists of the services to restart
528
restarts = [restart_map[path]
529
for path in restart_map
530
if path_hash(path) != checksums[path]]
531
# create a flat list of ordered services without duplicates from lists
532
services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
534
actions = ('stop', 'start') if stopstart else ('restart',)
535
for service_name in services_list:
536
if service_name in restart_functions:
537
restart_functions[service_name](service_name)
539
for action in actions:
540
service(action, service_name)
329
544
def lsb_release():
330
545
"""Return /etc/lsb-release in a dict"""
352
567
return(''.join(random_chars))
355
def list_nics(nic_type):
356
'''Return a list of nics of given type(s)'''
570
def is_phy_iface(interface):
571
"""Returns True if interface is not virtual, otherwise False."""
573
sys_net = '/sys/class/net'
574
if os.path.isdir(sys_net):
575
for iface in glob.glob(os.path.join(sys_net, '*')):
576
if '/virtual/' in os.path.realpath(iface):
579
if interface == os.path.basename(iface):
585
def get_bond_master(interface):
586
"""Returns bond master if interface is bond slave otherwise None.
588
NOTE: the provided interface is expected to be physical
591
iface_path = '/sys/class/net/%s' % (interface)
592
if os.path.exists(iface_path):
593
if '/virtual/' in os.path.realpath(iface_path):
596
master = os.path.join(iface_path, 'master')
597
if os.path.exists(master):
598
master = os.path.realpath(master)
599
# make sure it is a bond master
600
if os.path.exists(os.path.join(master, 'bonding')):
601
return os.path.basename(master)
606
def list_nics(nic_type=None):
607
"""Return a list of nics of given type(s)"""
357
608
if isinstance(nic_type, six.string_types):
358
609
int_types = [nic_type]
360
611
int_types = nic_type
362
for int_type in int_types:
363
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
615
for int_type in int_types:
616
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
617
ip_output = subprocess.check_output(cmd).decode('UTF-8')
618
ip_output = ip_output.split('\n')
619
ip_output = (line for line in ip_output if line)
620
for line in ip_output:
621
if line.split()[1].startswith(int_type):
622
matched = re.search('.*: (' + int_type +
623
r'[0-9]+\.[0-9]+)@.*', line)
625
iface = matched.groups()[0]
627
iface = line.split()[1].replace(":", "")
629
if iface not in interfaces:
630
interfaces.append(iface)
364
633
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
365
ip_output = (line for line in ip_output if line)
634
ip_output = (line.strip() for line in ip_output if line)
636
key = re.compile('^[0-9]+:\s+(.+):')
366
637
for line in ip_output:
367
if line.split()[1].startswith(int_type):
368
matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
370
interface = matched.groups()[0]
372
interface = line.split()[1].replace(":", "")
373
interfaces.append(interface)
638
matched = re.search(key, line)
640
iface = matched.group(1)
641
iface = iface.partition("@")[0]
642
if iface not in interfaces:
643
interfaces.append(iface)
375
645
return interfaces
378
648
def set_nic_mtu(nic, mtu):
379
'''Set MTU on a network interface'''
649
"""Set the Maximum Transmission Unit (MTU) on a network interface."""
380
650
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
381
651
subprocess.check_call(cmd)
384
654
def get_nic_mtu(nic):
655
"""Return the Maximum Transmission Unit (MTU) for a network interface."""
385
656
cmd = ['ip', 'addr', 'show', nic]
386
657
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
697
def chdir(directory):
698
"""Change the current working directory to a different directory for a code
699
block and return the previous directory after the block exits. Useful to
700
run commands from a specificed directory.
702
:param str directory: The directory path to change to for this context.
426
704
cur = os.getcwd()
706
yield os.chdir(directory)
433
def chownr(path, owner, group, follow_links=True):
711
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
712
"""Recursively change user and group ownership of files and directories
713
in given path. Doesn't chown path itself by default, only its children.
715
:param str path: The string path to start changing ownership.
716
:param str owner: The owner string to use when looking up the uid.
717
:param str group: The group string to use when looking up the gid.
718
:param bool follow_links: Also Chown links if True
719
:param bool chowntopdir: Also chown path itself if True
434
721
uid = pwd.getpwnam(owner).pw_uid
435
722
gid = grp.getgrnam(group).gr_gid
449
740
def lchownr(path, owner, group):
741
"""Recursively change user and group ownership of files and directories
742
in a given path, not following symbolic links. See the documentation for
743
'os.lchown' for more information.
745
:param str path: The string path to start changing ownership.
746
:param str owner: The owner string to use when looking up the uid.
747
:param str group: The group string to use when looking up the gid.
450
749
chownr(path, owner, group, follow_links=False)
753
"""The total amount of system RAM in bytes.
755
This is what is reported by the OS, and may be overcommitted when
756
there are multiple containers hosted on the same machine.
758
with open('/proc/meminfo', 'r') as f:
759
for line in f.readlines():
761
key, value, unit = line.split()
762
if key == 'MemTotal:':
763
assert unit == 'kB', 'Unknown unit'
764
return int(value) * 1024 # Classic, not KiB.
765
raise NotImplementedError()