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.
63
79
return service_result
82
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
83
"""Pause a system service.
85
Stop it, and prevent it from starting again at boot."""
87
if service_running(service_name):
88
stopped = service_stop(service_name)
89
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
90
sysv_file = os.path.join(initd_dir, service_name)
92
service('disable', service_name)
93
elif os.path.exists(upstart_file):
94
override_path = os.path.join(
95
init_dir, '{}.override'.format(service_name))
96
with open(override_path, 'w') as fh:
98
elif os.path.exists(sysv_file):
99
subprocess.check_call(["update-rc.d", service_name, "disable"])
102
"Unable to detect {0} as SystemD, Upstart {1} or"
104
service_name, upstart_file, sysv_file))
108
def service_resume(service_name, init_dir="/etc/init",
109
initd_dir="/etc/init.d"):
110
"""Resume a system service.
112
Reenable starting again at boot. Start the service"""
113
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
114
sysv_file = os.path.join(initd_dir, service_name)
115
if init_is_systemd():
116
service('enable', service_name)
117
elif os.path.exists(upstart_file):
118
override_path = os.path.join(
119
init_dir, '{}.override'.format(service_name))
120
if os.path.exists(override_path):
121
os.unlink(override_path)
122
elif os.path.exists(sysv_file):
123
subprocess.check_call(["update-rc.d", service_name, "enable"])
126
"Unable to detect {0} as SystemD, Upstart {1} or"
128
service_name, upstart_file, sysv_file))
130
started = service_running(service_name)
132
started = service_start(service_name)
66
136
def service(action, service_name):
67
137
"""Control a system service"""
68
cmd = ['service', service_name, action]
138
if init_is_systemd():
139
cmd = ['systemctl', action, service_name]
141
cmd = ['service', service_name, action]
69
142
return subprocess.call(cmd) == 0
72
def service_running(service):
145
_UPSTART_CONF = "/etc/init/{}.conf"
146
_INIT_D_CONF = "/etc/init.d/{}"
149
def service_running(service_name):
73
150
"""Determine whether a system service is running"""
75
output = subprocess.check_output(
76
['service', service, 'status'],
77
stderr=subprocess.STDOUT).decode('UTF-8')
78
except subprocess.CalledProcessError:
151
if init_is_systemd():
152
return service('is-active', service_name)
154
if os.path.exists(_UPSTART_CONF.format(service_name)):
156
output = subprocess.check_output(
157
['status', service_name],
158
stderr=subprocess.STDOUT).decode('UTF-8')
159
except subprocess.CalledProcessError:
162
# This works for upstart scripts where the 'service' command
163
# returns a consistent string to represent running
165
if ("start/running" in output or
166
"is running" in output or
167
"up and running" in output):
169
elif os.path.exists(_INIT_D_CONF.format(service_name)):
170
# Check System V scripts init script return codes
171
return service('status', service_name)
81
if ("start/running" in output or "is running" in output):
87
def service_available(service_name):
88
"""Determine whether a system service is available"""
90
subprocess.check_output(
91
['service', service_name, 'status'],
92
stderr=subprocess.STDOUT).decode('UTF-8')
93
except subprocess.CalledProcessError as e:
94
return b'unrecognized service' not in e.output
99
def adduser(username, password=None, shell='/bin/bash', system_user=False):
100
"""Add a user to the system"""
175
SYSTEMD_SYSTEM = '/run/systemd/system'
178
def init_is_systemd():
179
"""Return True if the host system uses systemd, False otherwise."""
180
return os.path.isdir(SYSTEMD_SYSTEM)
183
def adduser(username, password=None, shell='/bin/bash',
184
system_user=False, primary_group=None,
185
secondary_groups=None, uid=None, home_dir=None):
186
"""Add a user to the system.
188
Will log but otherwise succeed if the user already exists.
190
:param str username: Username to create
191
:param str password: Password for user; if ``None``, create a system user
192
:param str shell: The default shell for the user
193
:param bool system_user: Whether to create a login or system user
194
:param str primary_group: Primary group for user; defaults to username
195
:param list secondary_groups: Optional list of additional groups
196
:param int uid: UID for user being created
197
:param str home_dir: Home directory for user
199
:returns: The password database entry struct, as returned by `pwd.getpwnam`
102
202
user_info = pwd.getpwnam(username)
103
203
log('user {0} already exists!'.format(username))
205
user_info = pwd.getpwuid(int(uid))
206
log('user with uid {0} already exists!'.format(uid))
105
208
log('creating user {0}'.format(username))
106
209
cmd = ['useradd']
211
cmd.extend(['--uid', str(uid)])
213
cmd.extend(['--home', str(home_dir)])
107
214
if system_user or password is None:
108
215
cmd.append('--system')
112
219
'--shell', shell,
113
220
'--password', password,
222
if not primary_group:
224
grp.getgrnam(username)
225
primary_group = username # avoid "group exists" error
229
cmd.extend(['-g', primary_group])
231
cmd.extend(['-G', ','.join(secondary_groups)])
115
232
cmd.append(username)
116
233
subprocess.check_call(cmd)
117
234
user_info = pwd.getpwnam(username)
121
def add_group(group_name, system_group=False):
122
"""Add a group to the system"""
238
def user_exists(username):
239
"""Check if a user exists"""
241
pwd.getpwnam(username)
249
"""Check if a uid exists"""
258
def group_exists(groupname):
259
"""Check if a group exists"""
261
grp.getgrnam(groupname)
269
"""Check if a gid exists"""
278
def add_group(group_name, system_group=False, gid=None):
279
"""Add a group to the system
281
Will log but otherwise succeed if the group already exists.
283
:param str group_name: group to create
284
:param bool system_group: Create system group
285
:param int gid: GID for user being created
287
:returns: The password database entry struct, as returned by `grp.getgrnam`
124
290
group_info = grp.getgrnam(group_name)
125
291
log('group {0} already exists!'.format(group_name))
293
group_info = grp.getgrgid(gid)
294
log('group with gid {0} already exists!'.format(gid))
127
296
log('creating group {0}'.format(group_name))
130
cmd.append('--system')
135
cmd.append(group_name)
136
subprocess.check_call(cmd)
297
add_new_group(group_name, system_group, gid)
137
298
group_info = grp.getgrnam(group_name)
138
299
return group_info
141
302
def add_user_to_group(username, group):
142
303
"""Add a user to a group"""
304
cmd = ['gpasswd', '-a', username, group]
148
305
log("Adding user {} to group {}".format(username, group))
149
306
subprocess.check_call(cmd)
323
487
restarted if any file matching the pattern got changed, created
324
488
or removed. Standard wildcards are supported, see documentation
325
489
for the 'glob' module for more information.
491
@param restart_map: {path_file_name: [service_name, ...]
492
@param stopstart: DEFAULT false; whether to stop, start OR restart
493
@param restart_functions: nonstandard functions to use to restart services
495
@returns result from decorated function
328
499
def wrapped_f(*args, **kwargs):
329
checksums = {path: path_hash(path) for path in restart_map}
332
for path in restart_map:
333
if path_hash(path) != checksums[path]:
334
restarts += restart_map[path]
335
services_list = list(OrderedDict.fromkeys(restarts))
337
for service_name in services_list:
338
service('restart', service_name)
340
for action in ['stop', 'start']:
341
for service_name in services_list:
342
service(action, service_name)
500
return restart_on_change_helper(
501
(lambda: f(*args, **kwargs)), restart_map, stopstart,
348
"""Return /etc/lsb-release in a dict"""
350
with open('/etc/lsb-release', 'r') as lsb:
353
d[k.strip()] = v.strip()
507
def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
508
restart_functions=None):
509
"""Helper function to perform the restart_on_change function.
511
This is provided for decorators to restart services if files described
512
in the restart_map have changed after an invocation of lambda_f().
514
@param lambda_f: function to call.
515
@param restart_map: {file: [service, ...]}
516
@param stopstart: whether to stop, start or restart a service
517
@param restart_functions: nonstandard functions to use to restart services
519
@returns result of lambda_f()
521
if restart_functions is None:
522
restart_functions = {}
523
checksums = {path: path_hash(path) for path in restart_map}
525
# create a list of lists of the services to restart
526
restarts = [restart_map[path]
527
for path in restart_map
528
if path_hash(path) != checksums[path]]
529
# create a flat list of ordered services without duplicates from lists
530
services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
532
actions = ('stop', 'start') if stopstart else ('restart',)
533
for service_name in services_list:
534
if service_name in restart_functions:
535
restart_functions[service_name](service_name)
537
for action in actions:
538
service(action, service_name)
357
542
def pwgen(length=None):
370
555
return(''.join(random_chars))
373
def list_nics(nic_type):
374
'''Return a list of nics of given type(s)'''
558
def is_phy_iface(interface):
559
"""Returns True if interface is not virtual, otherwise False."""
561
sys_net = '/sys/class/net'
562
if os.path.isdir(sys_net):
563
for iface in glob.glob(os.path.join(sys_net, '*')):
564
if '/virtual/' in os.path.realpath(iface):
567
if interface == os.path.basename(iface):
573
def get_bond_master(interface):
574
"""Returns bond master if interface is bond slave otherwise None.
576
NOTE: the provided interface is expected to be physical
579
iface_path = '/sys/class/net/%s' % (interface)
580
if os.path.exists(iface_path):
581
if '/virtual/' in os.path.realpath(iface_path):
584
master = os.path.join(iface_path, 'master')
585
if os.path.exists(master):
586
master = os.path.realpath(master)
587
# make sure it is a bond master
588
if os.path.exists(os.path.join(master, 'bonding')):
589
return os.path.basename(master)
594
def list_nics(nic_type=None):
595
"""Return a list of nics of given type(s)"""
375
596
if isinstance(nic_type, six.string_types):
376
597
int_types = [nic_type]
378
599
int_types = nic_type
380
for int_type in int_types:
381
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
603
for int_type in int_types:
604
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
605
ip_output = subprocess.check_output(cmd).decode('UTF-8')
606
ip_output = ip_output.split('\n')
607
ip_output = (line for line in ip_output if line)
608
for line in ip_output:
609
if line.split()[1].startswith(int_type):
610
matched = re.search('.*: (' + int_type +
611
r'[0-9]+\.[0-9]+)@.*', line)
613
iface = matched.groups()[0]
615
iface = line.split()[1].replace(":", "")
617
if iface not in interfaces:
618
interfaces.append(iface)
382
621
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
383
ip_output = (line for line in ip_output if line)
622
ip_output = (line.strip() for line in ip_output if line)
624
key = re.compile('^[0-9]+:\s+(.+):')
384
625
for line in ip_output:
385
if line.split()[1].startswith(int_type):
386
matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
388
interface = matched.groups()[0]
390
interface = line.split()[1].replace(":", "")
391
interfaces.append(interface)
626
matched = re.search(key, line)
628
iface = matched.group(1)
629
iface = iface.partition("@")[0]
630
if iface not in interfaces:
631
interfaces.append(iface)
393
633
return interfaces
396
636
def set_nic_mtu(nic, mtu):
397
'''Set MTU on a network interface'''
637
"""Set the Maximum Transmission Unit (MTU) on a network interface."""
398
638
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
399
639
subprocess.check_call(cmd)
402
642
def get_nic_mtu(nic):
643
"""Return the Maximum Transmission Unit (MTU) for a network interface."""
403
644
cmd = ['ip', 'addr', 'show', nic]
404
645
ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
423
def cmp_pkgrevno(package, revno, pkgcache=None):
424
'''Compare supplied revno with the revno of the installed package
426
* 1 => Installed revno is greater than supplied arg
427
* 0 => Installed revno is the same as supplied arg
428
* -1 => Installed revno is less than supplied arg
430
This function imports apt_cache function from charmhelpers.fetch if
431
the pkgcache argument is None. Be sure to add charmhelpers.fetch if
432
you call this function, or pass an apt_pkg.Cache() instance.
436
from charmhelpers.fetch import apt_cache
437
pkgcache = apt_cache()
438
pkg = pkgcache[package]
439
return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
666
def chdir(directory):
667
"""Change the current working directory to a different directory for a code
668
block and return the previous directory after the block exits. Useful to
669
run commands from a specificed directory.
671
:param str directory: The directory path to change to for this context.
444
673
cur = os.getcwd()
675
yield os.chdir(directory)
451
def chownr(path, owner, group, follow_links=True):
680
def chownr(path, owner, group, follow_links=True, chowntopdir=False):
681
"""Recursively change user and group ownership of files and directories
682
in given path. Doesn't chown path itself by default, only its children.
684
:param str path: The string path to start changing ownership.
685
:param str owner: The owner string to use when looking up the uid.
686
:param str group: The group string to use when looking up the gid.
687
:param bool follow_links: Also Chown links if True
688
:param bool chowntopdir: Also chown path itself if True
452
690
uid = pwd.getpwnam(owner).pw_uid
453
691
gid = grp.getgrnam(group).gr_gid
467
709
def lchownr(path, owner, group):
710
"""Recursively change user and group ownership of files and directories
711
in a given path, not following symbolic links. See the documentation for
712
'os.lchown' for more information.
714
:param str path: The string path to start changing ownership.
715
:param str owner: The owner string to use when looking up the uid.
716
:param str group: The group string to use when looking up the gid.
468
718
chownr(path, owner, group, follow_links=False)
722
"""The total amount of system RAM in bytes.
724
This is what is reported by the OS, and may be overcommitted when
725
there are multiple containers hosted on the same machine.
727
with open('/proc/meminfo', 'r') as f:
728
for line in f.readlines():
730
key, value, unit = line.split()
731
if key == 'MemTotal:':
732
assert unit == 'kB', 'Unknown unit'
733
return int(value) * 1024 # Classic, not KiB.
734
raise NotImplementedError()