Coverage for lib/lib_sysconfig.py : 97%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""SysConfig helper module.
3Manage grub, systemd, coufrequtils and kernel version configuration.
4"""
6import os
7import subprocess
8from datetime import datetime, timedelta, timezone
10from charmhelpers.contrib.openstack.utils import config_flags_parser
11from charmhelpers.core import hookenv, host, unitdata
12from charmhelpers.core.templating import render
13from charmhelpers.fetch import apt_install, apt_update
15GRUB_DEFAULT = 'Advanced options for Ubuntu>Ubuntu, with Linux {}'
16CPUFREQUTILS_TMPL = 'cpufrequtils.j2'
17GRUB_CONF_TMPL = 'grub.j2'
18SYSTEMD_SYSTEM_TMPL = 'etc.systemd.system.conf.j2'
20CPUFREQUTILS = '/etc/default/cpufrequtils'
21GRUB_CONF = '/etc/default/grub.d/90-sysconfig.cfg'
22SYSTEMD_SYSTEM = '/etc/systemd/system.conf'
23KERNEL = 'kernel'
26def parse_config_flags(config_flags):
27 """Parse config flags into a dict.
29 :param config_flags: key pairs list. Format: key1=value1,key2=value2
30 :return dict: format {'key1': 'value1', 'key2': 'value2'}
31 """
32 key_value_pairs = config_flags.split(",")
33 parsed_config_flags = {}
34 for index, pair in enumerate(key_value_pairs):
35 if '=' in pair:
36 key, value = map(str.strip, pair.split('=', 1))
37 # Note(peppepetra): if value contains a comma that is also used as
38 # delimiter, we need to reconstruct the value
39 i = index + 1
40 while i < len(key_value_pairs):
41 if '=' in key_value_pairs[i]:
42 break
43 value += ',' + key_value_pairs[i]
44 i += 1
45 parsed_config_flags[key] = value
46 return parsed_config_flags
49def running_kernel():
50 """Return kernel version running in the principal unit."""
51 return os.uname().release
54def boot_time():
55 """Return timestamp of last boot."""
56 with open('/proc/uptime', 'r') as f:
57 uptime_seconds = float(f.readline().split()[0])
58 boot_time = datetime.now(timezone.utc) - timedelta(seconds=uptime_seconds)
59 return boot_time
62class BootResourceState:
63 """A class to track resources changed since last reboot."""
65 def __init__(self, db=None):
66 """Initialize empty db used to track resources updates."""
67 if db is None:
68 db = unitdata.kv()
69 self.db = db
71 def key_for(self, resource_name):
72 """Return db key for a given resource."""
73 return "sysconfig.boot_resource.{}".format(resource_name)
75 def set_resource(self, resource_name):
76 """Update db entry for the resource_name with time.now."""
77 timestamp = datetime.now(timezone.utc)
78 self.db.set(self.key_for(resource_name), timestamp.timestamp())
80 def get_resource_changed_timestamp(self, resource_name):
81 """Retrieve timestamp of last resource change recorded.
83 :param resource_name: resource to check
84 :return: datetime of resource change, or datetime.min if resource not registered
85 """
86 tfloat = self.db.get(self.key_for(resource_name))
87 if tfloat is not None:
88 return datetime.fromtimestamp(tfloat, timezone.utc)
89 return datetime.min.replace(tzinfo=timezone.utc) # We don't have a ts -> changed at dawn of time
91 def resources_changed_since_boot(self, resource_names):
92 """Given a list of resource names return those that have changed since boot.
94 :param resource_names: list of names
95 :return: list of names
96 """
97 boot_ts = boot_time()
98 changed = [name for name in resource_names if boot_ts < self.get_resource_changed_timestamp(name)]
99 return changed
102class SysConfigHelper:
103 """Update sysconfig, grub, kernel and cpufrequtils config."""
105 boot_resources = BootResourceState()
107 def __init__(self):
108 """Retrieve charm configuration."""
109 self.charm_config = hookenv.config()
111 @property
112 def enable_container(self):
113 """Return enable-container config."""
114 return self.charm_config['enable-container']
116 @property
117 def reservation(self):
118 """Return reservation config."""
119 return self.charm_config['reservation']
121 @property
122 def cpu_range(self):
123 """Return cpu-range config."""
124 return self.charm_config['cpu-range']
126 @property
127 def hugepages(self):
128 """Return hugepages config."""
129 return self.charm_config['hugepages']
131 @property
132 def hugepagesz(self):
133 """Return hugepagesz config."""
134 return self.charm_config['hugepagesz']
136 @property
137 def raid_autodetection(self):
138 """Return raid-autodetection config option."""
139 return self.charm_config['raid-autodetection']
141 @property
142 def enable_pti(self):
143 """Return raid-autodetection config option."""
144 return self.charm_config['enable-pti']
146 @property
147 def enable_iommu(self):
148 """Return enable-iommu config option."""
149 return self.charm_config['enable-iommu']
151 @property
152 def grub_config_flags(self):
153 """Return grub-config-flags config option."""
154 return parse_config_flags(self.charm_config['grub-config-flags'])
156 @property
157 def systemd_config_flags(self):
158 """Return grub-config-flags config option."""
159 return parse_config_flags(self.charm_config['systemd-config-flags'])
161 @property
162 def kernel_version(self):
163 """Return grub-config-flags config option."""
164 return self.charm_config['kernel-version']
166 @property
167 def update_grub(self):
168 """Return grub-config-flags config option."""
169 return self.charm_config['update-grub']
171 @property
172 def governor(self):
173 """Return grub-config-flags config option."""
174 return self.charm_config['governor']
176 @property
177 def config_flags(self):
178 """Return parsed config-flags into dict.
180 [DEPRECATED]: this option should no longer be used.
181 Instead grub-config-flags and systemd-config-flags should be used.
182 """
183 if not self.charm_config.get('config-flags'):
184 return {}
185 flags = config_flags_parser(self.charm_config['config-flags'])
186 return flags
188 def _render_boot_resource(self, source, target, context):
189 """Render the template and set the resource as changed."""
190 render(source=source, templates_dir='templates', target=target, context=context)
191 self.boot_resources.set_resource(target)
193 def _is_kernel_already_running(self):
194 """Check if the kernel version required by charm config is equal to kernel running."""
195 configured = self.kernel_version
196 if configured == running_kernel():
197 hookenv.log("Already running kernel: {}".format(configured), hookenv.DEBUG)
198 return True
199 return False
201 def _update_grub(self):
202 """Call update-grub when update-grub config param is set to True."""
203 if self.update_grub and not host.is_container():
204 subprocess.check_call(['/usr/sbin/update-grub'])
205 hookenv.log('Running update-grub to apply grub conf updates', hookenv.DEBUG)
207 def is_config_valid(self):
208 """Validate config parameters."""
209 valid = True
211 if self.reservation not in ['off', 'isolcpus', 'affinity']:
212 hookenv.log('reservation not valid. Possible values: ["off", "isolcpus", "affinity"]', hookenv.DEBUG)
213 valid = False
215 if self.raid_autodetection not in ['', 'noautodetect', 'partitionable']:
216 hookenv.log('raid-autodetection not valid. '
217 'Possible values: ["off", "noautodetect", "partitionable"]', hookenv.DEBUG)
218 valid = False
220 if self.governor not in ['', 'powersave', 'performance']:
221 hookenv.log('governor not valid. Possible values: ["", "powersave", "performance"]', hookenv.DEBUG)
222 valid = False
224 return valid
226 def update_grub_file(self):
227 """Update /etc/default/grub.d/90-sysconfig.cfg according to charm configuration.
229 Will call update-grub if update-grub config is set to True.
230 """
231 context = {}
232 if self.reservation == 'isolcpus':
233 context['cpu_range'] = self.cpu_range
234 if self.hugepages:
235 context['hugepages'] = self.hugepages
236 if self.hugepagesz:
237 context['hugepagesz'] = self.hugepagesz
238 if self.raid_autodetection:
239 context['raid'] = self.raid_autodetection
240 if not self.enable_pti:
241 context['pti_off'] = True
242 if self.enable_iommu:
243 context['iommu'] = True
245 # Note(peppepetra): First check if new grub-config-flags is used
246 # if not try to fallback into legacy config-flags
247 if self.grub_config_flags:
248 context['grub_config_flags'] = self.grub_config_flags
249 else:
250 context['grub_config_flags'] = parse_config_flags(self.config_flags.get('grub', ''))
252 if self.kernel_version and not self._is_kernel_already_running():
253 context['grub_default'] = GRUB_DEFAULT.format(self.kernel_version)
255 self._render_boot_resource(GRUB_CONF_TMPL, GRUB_CONF, context)
256 hookenv.log('grub configuration updated')
257 self._update_grub()
259 def update_systemd_system_file(self):
260 """Update /etc/systemd/system.conf according to charm configuration."""
261 context = {}
262 if self.reservation == 'affinity':
263 context['cpu_range'] = self.cpu_range
265 # Note(peppepetra): First check if new systemd-config-flags is used
266 # if not try to fallback into legacy config-flags
267 if self.systemd_config_flags:
268 context['systemd_config_flags'] = self.systemd_config_flags
269 else:
270 context['systemd_config_flags'] = parse_config_flags(self.config_flags.get('systemd', ''))
272 self._render_boot_resource(SYSTEMD_SYSTEM_TMPL, SYSTEMD_SYSTEM, context)
273 hookenv.log('systemd configuration updated')
275 def install_configured_kernel(self):
276 """Install kernel as given by the kernel-version config option.
278 Will install kernel and matching modules-extra package
279 """
280 if not self.kernel_version or self._is_kernel_already_running():
281 hookenv.log('kernel running already to the reuired version', hookenv.DEBUG)
282 return
284 configured = self.kernel_version
285 pkgs = [tmpl.format(configured) for tmpl in ["linux-image-{}", "linux-modules-extra-{}"]]
286 apt_update()
287 apt_install(pkgs)
288 hookenv.log("installing: {}".format(pkgs))
289 self.boot_resources.set_resource(KERNEL)
291 def update_cpufreq(self):
292 """Update /etc/default/cpufrequtils and restart cpufrequtils service."""
293 if self.governor not in ('', 'performance', 'powersave'):
294 return
295 context = {'governor': self.governor}
296 self._render_boot_resource(CPUFREQUTILS_TMPL, CPUFREQUTILS, context)
297 # Ensure the ondemand initscript is disabled if governor is set, lp#1822774 and lp#740127
298 # Ondemand init script is not updated during test if host is container.
299 if host.get_distrib_codename() == 'xenial' and not host.is_container():
300 hookenv.log('disabling the ondemand initscript for lp#1822774'
301 ' and lp#740127 if a governor is specified', hookenv.DEBUG)
302 if self.governor:
303 subprocess.call(
304 ['/usr/sbin/update-rc.d', '-f', 'ondemand', 'remove'],
305 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
306 )
307 else:
308 # Renable ondemand when governor is unset.
309 subprocess.call(
310 ['/usr/sbin/update-rc.d', '-f', 'ondemand', 'defaults'],
311 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
312 )
314 host.service_restart('cpufrequtils')
316 def remove_grub_configuration(self):
317 """Remove /etc/default/grub.d/90-sysconfig.cfg if exists.
319 Will call update-grub if update-grub config is set to True.
320 """
321 grub_configuration_path = GRUB_CONF
322 if not os.path.exists(grub_configuration_path):
323 return
324 os.remove(grub_configuration_path)
325 hookenv.log(
326 'deleted grub configuration at '.format(grub_configuration_path),
327 hookenv.DEBUG
328 )
329 self._update_grub()
330 self.boot_resources.set_resource(GRUB_CONF)
332 def remove_systemd_configuration(self):
333 """Remove systemd configuration.
335 Will render systemd config with empty context.
336 """
337 context = {}
338 self._render_boot_resource(SYSTEMD_SYSTEM_TMPL, SYSTEMD_SYSTEM, context)
339 hookenv.log(
340 'deleted systemd configuration at '.format(SYSTEMD_SYSTEM),
341 hookenv.DEBUG
342 )
344 def remove_cpufreq_configuration(self):
345 """Remove cpufrequtils configuration.
347 Will render cpufrequtils config with empty context.
348 """
349 context = {}
350 if host.get_distrib_codename() == 'xenial' and not host.is_container():
351 hookenv.log('Enabling the ondemand initscript for lp#1822774'
352 ' and lp#740127', 'DEBUG')
353 subprocess.call(
354 ['/usr/sbin/update-rc.d', '-f', 'ondemand', 'defaults'],
355 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
356 )
358 self._render_boot_resource(CPUFREQUTILS_TMPL, CPUFREQUTILS, context)
359 hookenv.log(
360 'deleted cpufreq configuration at '.format(CPUFREQUTILS),
361 hookenv.DEBUG
362 )
363 host.service_restart('cpufrequtils')