Hide keyboard shortcuts

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. 

2 

3Manage grub, systemd, coufrequtils and kernel version configuration. 

4""" 

5 

6import os 

7import subprocess 

8from datetime import datetime, timedelta, timezone 

9 

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 

14 

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' 

19 

20CPUFREQUTILS = '/etc/default/cpufrequtils' 

21GRUB_CONF = '/etc/default/grub.d/90-sysconfig.cfg' 

22SYSTEMD_SYSTEM = '/etc/systemd/system.conf' 

23KERNEL = 'kernel' 

24 

25 

26def parse_config_flags(config_flags): 

27 """Parse config flags into a dict. 

28 

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 

47 

48 

49def running_kernel(): 

50 """Return kernel version running in the principal unit.""" 

51 return os.uname().release 

52 

53 

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 

60 

61 

62class BootResourceState: 

63 """A class to track resources changed since last reboot.""" 

64 

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 

70 

71 def key_for(self, resource_name): 

72 """Return db key for a given resource.""" 

73 return "sysconfig.boot_resource.{}".format(resource_name) 

74 

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()) 

79 

80 def get_resource_changed_timestamp(self, resource_name): 

81 """Retrieve timestamp of last resource change recorded. 

82 

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 

90 

91 def resources_changed_since_boot(self, resource_names): 

92 """Given a list of resource names return those that have changed since boot. 

93 

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 

100 

101 

102class SysConfigHelper: 

103 """Update sysconfig, grub, kernel and cpufrequtils config.""" 

104 

105 boot_resources = BootResourceState() 

106 

107 def __init__(self): 

108 """Retrieve charm configuration.""" 

109 self.charm_config = hookenv.config() 

110 

111 @property 

112 def enable_container(self): 

113 """Return enable-container config.""" 

114 return self.charm_config['enable-container'] 

115 

116 @property 

117 def reservation(self): 

118 """Return reservation config.""" 

119 return self.charm_config['reservation'] 

120 

121 @property 

122 def cpu_range(self): 

123 """Return cpu-range config.""" 

124 return self.charm_config['cpu-range'] 

125 

126 @property 

127 def hugepages(self): 

128 """Return hugepages config.""" 

129 return self.charm_config['hugepages'] 

130 

131 @property 

132 def hugepagesz(self): 

133 """Return hugepagesz config.""" 

134 return self.charm_config['hugepagesz'] 

135 

136 @property 

137 def raid_autodetection(self): 

138 """Return raid-autodetection config option.""" 

139 return self.charm_config['raid-autodetection'] 

140 

141 @property 

142 def enable_pti(self): 

143 """Return raid-autodetection config option.""" 

144 return self.charm_config['enable-pti'] 

145 

146 @property 

147 def enable_iommu(self): 

148 """Return enable-iommu config option.""" 

149 return self.charm_config['enable-iommu'] 

150 

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']) 

155 

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']) 

160 

161 @property 

162 def kernel_version(self): 

163 """Return grub-config-flags config option.""" 

164 return self.charm_config['kernel-version'] 

165 

166 @property 

167 def update_grub(self): 

168 """Return grub-config-flags config option.""" 

169 return self.charm_config['update-grub'] 

170 

171 @property 

172 def governor(self): 

173 """Return grub-config-flags config option.""" 

174 return self.charm_config['governor'] 

175 

176 @property 

177 def config_flags(self): 

178 """Return parsed config-flags into dict. 

179 

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 

187 

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) 

192 

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 

200 

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) 

206 

207 def is_config_valid(self): 

208 """Validate config parameters.""" 

209 valid = True 

210 

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 

214 

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 

219 

220 if self.governor not in ['', 'powersave', 'performance']: 

221 hookenv.log('governor not valid. Possible values: ["", "powersave", "performance"]', hookenv.DEBUG) 

222 valid = False 

223 

224 return valid 

225 

226 def update_grub_file(self): 

227 """Update /etc/default/grub.d/90-sysconfig.cfg according to charm configuration. 

228 

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 

244 

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', '')) 

251 

252 if self.kernel_version and not self._is_kernel_already_running(): 

253 context['grub_default'] = GRUB_DEFAULT.format(self.kernel_version) 

254 

255 self._render_boot_resource(GRUB_CONF_TMPL, GRUB_CONF, context) 

256 hookenv.log('grub configuration updated') 

257 self._update_grub() 

258 

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 

264 

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', '')) 

271 

272 self._render_boot_resource(SYSTEMD_SYSTEM_TMPL, SYSTEMD_SYSTEM, context) 

273 hookenv.log('systemd configuration updated') 

274 

275 def install_configured_kernel(self): 

276 """Install kernel as given by the kernel-version config option. 

277 

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 

283 

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) 

290 

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 ) 

313 

314 host.service_restart('cpufrequtils') 

315 

316 def remove_grub_configuration(self): 

317 """Remove /etc/default/grub.d/90-sysconfig.cfg if exists. 

318 

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) 

331 

332 def remove_systemd_configuration(self): 

333 """Remove systemd configuration. 

334 

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 ) 

343 

344 def remove_cpufreq_configuration(self): 

345 """Remove cpufrequtils configuration. 

346 

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 ) 

357 

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')