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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

# Copyright (c) 2017 Ansible Project 

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 

from __future__ import (absolute_import, division, print_function) 

__metaclass__ = type 

 

DOCUMENTATION = ''' 

inventory: ini 

version_added: "2.4" 

short_description: Uses an Ansible INI file as inventory source. 

description: 

- INI file based inventory, sections are groups or group related with special `:modifiers`. 

- Entries in sections C([group_1]) are hosts, members of the group. 

- Hosts can have variables defined inline as key/value pairs separated by C(=). 

- The C(children) modifier indicates that the section contains groups. 

- The C(vars) modifier indicates that the section contains variables assigned to members of the group. 

- Anything found outside a section is considered an 'ungrouped' host. 

- Values passed in using the C(key=value) syntax are interpreted as Python literal structure (strings, numbers, tuples, lists, dicts, 

booleans, None), alternatively as string. For example C(var=FALSE) would create a string equal to 'FALSE'. Do not rely on types set 

during definition, always make sure you specify type with a filter when needed when consuming the variable. 

notes: 

- It takes the place of the previously hardcoded INI inventory. 

- To function it requires being whitelisted in configuration. 

- Variable values are processed by Python's ast.literal_eval function (U(https://docs.python.org/2/library/ast.html#ast.literal_eval)) 

which could cause the value to change in some cases. See the Examples for proper quoting to prevent changes. Another option would be 

to use the yaml format for inventory source which processes the values correctly. 

''' 

 

EXAMPLES = ''' 

example1: | 

# example cfg file 

[web] 

host1 

host2 ansible_port=222 

 

[web:vars] 

http_port=8080 # all members of 'web' will inherit these 

myvar=23 

 

[web:children] # child groups will automatically add their hosts to partent group 

apache 

nginx 

 

[apache] 

tomcat1 

tomcat2 myvar=34 # host specific vars override group vars 

tomcat3 mysecret="'03#pa33w0rd'" # proper quoting to prevent value changes 

 

[nginx] 

jenkins1 

 

[nginx:vars] 

has_java = True # vars in child groups override same in parent 

 

[all:vars] 

has_java = False # 'all' is 'top' parent 

 

example2: | 

# other example config 

host1 # this is 'ungrouped' 

 

# both hosts have same IP but diff ports, also 'ungrouped' 

host2 ansible_host=127.0.0.1 ansible_port=44 

host3 ansible_host=127.0.0.1 ansible_port=45 

 

[g1] 

host4 

 

[g2] 

host4 # same host as above, but member of 2 groups, will inherit vars from both 

# inventory hostnames are unique 

''' 

 

import ast 

import re 

 

from ansible.plugins.inventory import BaseFileInventoryPlugin, detect_range, expand_hostname_range 

from ansible.parsing.utils.addresses import parse_address 

 

from ansible.errors import AnsibleError, AnsibleParserError 

from ansible.module_utils._text import to_bytes, to_text 

from ansible.utils.shlex import shlex_split 

 

 

class InventoryModule(BaseFileInventoryPlugin): 

""" 

Takes an INI-format inventory file and builds a list of groups and subgroups 

with their associated hosts and variable settings. 

""" 

NAME = 'ini' 

_COMMENT_MARKERS = frozenset((u';', u'#')) 

b_COMMENT_MARKERS = frozenset((b';', b'#')) 

 

def __init__(self): 

 

super(InventoryModule, self).__init__() 

 

self.patterns = {} 

self._filename = None 

 

def parse(self, inventory, loader, path, cache=True): 

 

super(InventoryModule, self).parse(inventory, loader, path) 

 

self._filename = path 

 

try: 

# Read in the hosts, groups, and variables defined in the 

# inventory file. 

109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false if self.loader: 

(b_data, private) = self.loader._get_file_contents(path) 

else: 

b_path = to_bytes(path, errors='surrogate_or_strict') 

with open(b_path, 'rb') as fh: 

b_data = fh.read() 

 

try: 

# Faster to do to_text once on a long string than many 

# times on smaller strings 

data = to_text(b_data, errors='surrogate_or_strict').splitlines() 

except UnicodeError: 

# Handle non-utf8 in comment lines: https://github.com/ansible/ansible/issues/17593 

data = [] 

for line in b_data.splitlines(): 

if line and line[0] in self.b_COMMENT_MARKERS: 

# Replace is okay for comment lines 

# data.append(to_text(line, errors='surrogate_then_replace')) 

# Currently we only need these lines for accurate lineno in errors 

data.append(u'') 

else: 

# Non-comment lines still have to be valid uf-8 

data.append(to_text(line, errors='surrogate_or_strict')) 

 

self._parse(path, data) 

except Exception as e: 

raise AnsibleParserError(e) 

 

def _raise_error(self, message): 

raise AnsibleError("%s:%d: " % (self._filename, self.lineno) + message) 

 

def _parse(self, path, lines): 

''' 

Populates self.groups from the given array of lines. Raises an error on 

any parse failure. 

''' 

 

self._compile_patterns() 

 

# We behave as though the first line of the inventory is '[ungrouped]', 

# and begin to look for host definitions. We make a single pass through 

# each line of the inventory, building up self.groups and adding hosts, 

# subgroups, and setting variables as we go. 

 

pending_declarations = {} 

groupname = 'ungrouped' 

state = 'hosts' 

self.lineno = 0 

for line in lines: 

self.lineno += 1 

 

line = line.strip() 

# Skip empty lines and comments 

if not line or line[0] in self._COMMENT_MARKERS: 

continue 

 

# Is this a [section] header? That tells us what group we're parsing 

# definitions for, and what kind of definitions to expect. 

 

m = self.patterns['section'].match(line) 

if m: 

(groupname, state) = m.groups() 

 

state = state or 'hosts' 

173 ↛ 174line 173 didn't jump to line 174, because the condition on line 173 was never true if state not in ['hosts', 'children', 'vars']: 

title = ":".join(m.groups()) 

self._raise_error("Section [%s] has unknown type: %s" % (title, state)) 

 

# If we haven't seen this group before, we add a new Group. 

if groupname not in self.inventory.groups: 

# Either [groupname] or [groupname:children] is sufficient to declare a group, 

# but [groupname:vars] is allowed only if the # group is declared elsewhere. 

# We add the group anyway, but make a note in pending_declarations to check at the end. 

# 

# It's possible that a group is previously pending due to being defined as a child 

# group, in that case we simply pass so that the logic below to process pending 

# declarations will take the appropriate action for a pending child group instead of 

# incorrectly handling it as a var state pending declaration 

187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true if state == 'vars' and groupname not in pending_declarations: 

pending_declarations[groupname] = dict(line=self.lineno, state=state, name=groupname) 

 

self.inventory.add_group(groupname) 

 

# When we see a declaration that we've been waiting for, we process and delete. 

193 ↛ 194,   193 ↛ 1992 missed branches: 1) line 193 didn't jump to line 194, because the condition on line 193 was never true, 2) line 193 didn't jump to line 199, because the condition on line 193 was never false if groupname in pending_declarations and state != 'vars': 

if pending_declarations[groupname]['state'] == 'children': 

self._add_pending_children(groupname, pending_declarations) 

elif pending_declarations[groupname]['state'] == 'vars': 

del pending_declarations[groupname] 

 

continue 

200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true elif line.startswith('[') and line.endswith(']'): 

self._raise_error("Invalid section entry: '%s'. Please make sure that there are no spaces" % line + 

"in the section entry, and that there are no other invalid characters") 

 

# It's not a section, so the current state tells us what kind of 

# definition it must be. The individual parsers will raise an 

# error if we feed them something they can't digest. 

 

# [groupname] contains host definitions that must be added to 

# the current group. 

if state == 'hosts': 

hosts, port, variables = self._parse_host_definition(line) 

self._populate_host_vars(hosts, variables, groupname, port) 

 

# [groupname:vars] contains variable definitions that must be 

# applied to the current group. 

216 ↛ 224line 216 didn't jump to line 224, because the condition on line 216 was never false elif state == 'vars': 

(k, v) = self._parse_variable_definition(line) 

self.inventory.set_variable(groupname, k, v) 

 

# [groupname:children] contains subgroup names that must be 

# added as children of the current group. The subgroup names 

# must themselves be declared as groups, but as before, they 

# may only be declared later. 

elif state == 'children': 

child = self._parse_group_name(line) 

if child not in self.inventory.groups: 

if child not in pending_declarations: 

pending_declarations[child] = dict(line=self.lineno, state=state, name=child, parents=[groupname]) 

else: 

pending_declarations[child]['parents'].append(groupname) 

else: 

self.inventory.add_child(groupname, child) 

else: 

# This can happen only if the state checker accepts a state that isn't handled above. 

self._raise_error("Entered unhandled state: %s" % (state)) 

 

# Any entries in pending_declarations not removed by a group declaration above mean that there was an unresolved reference. 

# We report only the first such error here. 

239 ↛ 240line 239 didn't jump to line 240, because the loop on line 239 never started for g in pending_declarations: 

decl = pending_declarations[g] 

if decl['state'] == 'vars': 

raise AnsibleError("%s:%d: Section [%s:vars] not valid for undefined group: %s" % (path, decl['line'], decl['name'], decl['name'])) 

elif decl['state'] == 'children': 

raise AnsibleError("%s:%d: Section [%s:children] includes undefined group: %s" % (path, decl['line'], decl['parents'].pop(), decl['name'])) 

 

def _add_pending_children(self, group, pending): 

for parent in pending[group]['parents']: 

self.inventory.add_child(parent, group) 

if parent in pending and pending[parent]['state'] == 'children': 

self._add_pending_children(parent, pending) 

del pending[group] 

 

def _parse_group_name(self, line): 

''' 

Takes a single line and tries to parse it as a group name. Returns the 

group name if successful, or raises an error. 

''' 

 

m = self.patterns['groupname'].match(line) 

if m: 

return m.group(1) 

 

self._raise_error("Expected group name, got: %s" % (line)) 

 

def _parse_variable_definition(self, line): 

''' 

Takes a string and tries to parse it as a variable definition. Returns 

the key and value if successful, or raises an error. 

''' 

 

# TODO: We parse variable assignments as a key (anything to the left of 

# an '='"), an '=', and a value (anything left) and leave the value to 

# _parse_value to sort out. We should be more systematic here about 

# defining what is acceptable, how quotes work, and so on. 

 

276 ↛ 280line 276 didn't jump to line 280, because the condition on line 276 was never false if '=' in line: 

(k, v) = [e.strip() for e in line.split("=", 1)] 

return (k, self._parse_value(v)) 

 

self._raise_error("Expected key=value, got: %s" % (line)) 

 

def _parse_host_definition(self, line): 

''' 

Takes a single line and tries to parse it as a host definition. Returns 

a list of Hosts if successful, or raises an error. 

''' 

 

# A host definition comprises (1) a non-whitespace hostname or range, 

# optionally followed by (2) a series of key="some value" assignments. 

# We ignore any trailing whitespace and/or comments. For example, here 

# are a series of host definitions in a group: 

# 

# [groupname] 

# alpha 

# beta:2345 user=admin # we'll tell shlex 

# gamma sudo=True user=root # to ignore comments 

 

try: 

tokens = shlex_split(line, comments=True) 

except ValueError as e: 

self._raise_error("Error parsing host definition '%s': %s" % (line, e)) 

 

(hostnames, port) = self._expand_hostpattern(tokens[0]) 

 

# Try to process anything remaining as a series of key=value pairs. 

variables = {} 

307 ↛ 308line 307 didn't jump to line 308, because the loop on line 307 never started for t in tokens[1:]: 

if '=' not in t: 

self._raise_error("Expected key=value host variable assignment, got: %s" % (t)) 

(k, v) = t.split('=', 1) 

variables[k] = self._parse_value(v) 

 

return hostnames, port, variables 

 

def _expand_hostpattern(self, hostpattern): 

''' 

Takes a single host pattern and returns a list of hostnames and an 

optional port number that applies to all of them. 

''' 

 

# Can the given hostpattern be parsed as a host with an optional port 

# specification? 

 

try: 

(pattern, port) = parse_address(hostpattern, allow_ranges=True) 

except Exception: 

# not a recognizable host pattern 

pattern = hostpattern 

port = None 

 

# Once we have separated the pattern, we expand it into list of one or 

# more hostnames, depending on whether it contains any [x:y] ranges. 

 

334 ↛ 335line 334 didn't jump to line 335, because the condition on line 334 was never true if detect_range(pattern): 

hostnames = expand_hostname_range(pattern) 

else: 

hostnames = [pattern] 

 

return (hostnames, port) 

 

@staticmethod 

def _parse_value(v): 

''' 

Attempt to transform the string value from an ini file into a basic python object 

(int, dict, list, unicode string, etc). 

''' 

try: 

v = ast.literal_eval(v) 

# Using explicit exceptions. 

# Likely a string that literal_eval does not like. We wil then just set it. 

351 ↛ 354line 351 didn't jump to line 354 except ValueError: 

# For some reason this was thought to be malformed. 

pass 

except SyntaxError: 

# Is this a hash with an equals at the end? 

pass 

return to_text(v, nonstring='passthru', errors='surrogate_or_strict') 

 

def _compile_patterns(self): 

''' 

Compiles the regular expressions required to parse the inventory and 

stores them in self.patterns. 

''' 

 

# Section names are square-bracketed expressions at the beginning of a 

# line, comprising (1) a group name optionally followed by (2) a tag 

# that specifies the contents of the section. We ignore any trailing 

# whitespace and/or comments. For example: 

# 

# [groupname] 

# [somegroup:vars] 

# [naughty:children] # only get coal in their stockings 

 

self.patterns['section'] = re.compile( 

to_text(r'''^\[ 

([^:\]\s]+) # group name (see groupname below) 

(?::(\w+))? # optional : and tag name 

\] 

\s* # ignore trailing whitespace 

(?:\#.*)? # and/or a comment till the 

$ # end of the line 

''', errors='surrogate_or_strict'), re.X 

) 

 

# FIXME: What are the real restrictions on group names, or rather, what 

# should they be? At the moment, they must be non-empty sequences of non 

# whitespace characters excluding ':' and ']', but we should define more 

# precise rules in order to support better diagnostics. 

 

self.patterns['groupname'] = re.compile( 

to_text(r'''^ 

([^:\]\s]+) 

\s* # ignore trailing whitespace 

(?:\#.*)? # and/or a comment till the 

$ # end of the line 

''', errors='surrogate_or_strict'), re.X 

)