github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/network/debinterfaces/parser.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package debinterfaces 5 6 import ( 7 "io/ioutil" 8 "path/filepath" 9 "regexp" 10 "strings" 11 ) 12 13 const ( 14 unknown kind = iota 15 allow 16 auto 17 iface 18 mapping 19 noAutoDown 20 noscripts 21 source 22 sourceDirectory 23 ) 24 25 var validSourceDirectoryFilename = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) 26 27 type parser struct { 28 scanner *lineScanner 29 expander WordExpander 30 } 31 32 // Type is the set of lexical tokens that represent top-level stanza 33 // identifiers based on the description in the interfaces(5) man page. 34 type kind int 35 36 // ParseError represents an error when parsing a line of a 37 // Debian-style interfaces definition. This only covers top-level 38 // definitions. 39 type ParseError struct { 40 Filename string 41 Line string 42 LineNum int 43 Message string 44 } 45 46 // Error returns the parsing error. 47 func (p *ParseError) Error() string { 48 return p.Message 49 } 50 51 func newParseError(s *lineScanner, msg string) *ParseError { 52 return &ParseError{ 53 Filename: s.filename, 54 Line: s.line, 55 LineNum: s.n, 56 Message: msg, 57 } 58 } 59 60 func (p parser) newStanzaBase() *stanza { 61 return &stanza{ 62 location: Location{ 63 Filename: p.scanner.filename, 64 LineNum: p.scanner.n, 65 }, 66 definition: p.scanner.line, 67 } 68 } 69 70 func (p parser) parseOptions() []string { 71 options := []string{} 72 for { 73 if !p.scanner.nextLine() { 74 return options 75 } 76 line := p.scanner.line 77 if stanzaType(line) != unknown { 78 // go back a line 79 p.scanner.n-- 80 p.scanner.line = strings.TrimSpace(p.scanner.lines[p.scanner.n]) 81 return options 82 } 83 options = append(options, line) 84 } 85 } 86 87 func (p parser) parseAllowStanza() (*AllowStanza, error) { 88 words := strings.Fields(p.scanner.line) 89 if len(words) < 2 { 90 return nil, newParseError(p.scanner, "missing device name") 91 } 92 return &AllowStanza{ 93 stanza: *p.newStanzaBase(), 94 DeviceNames: words[1:], 95 }, nil 96 } 97 98 func (p parser) parseAutoStanza() (*AutoStanza, error) { 99 words := strings.Fields(p.scanner.line) 100 if len(words) < 2 { 101 return nil, newParseError(p.scanner, "missing device name") 102 } 103 return &AutoStanza{ 104 stanza: *p.newStanzaBase(), 105 DeviceNames: words[1:], 106 }, nil 107 } 108 109 func (p parser) parseIfaceStanza() (*IfaceStanza, error) { 110 s := p.newStanzaBase() 111 words := strings.Fields(p.scanner.line) 112 113 if len(words) < 2 { 114 return nil, newParseError(p.scanner, "missing device name") 115 } 116 117 options := p.parseOptions() 118 119 return &IfaceStanza{ 120 stanza: *s, 121 DeviceName: words[1], 122 HasBondMasterOption: hasBondMasterOption(options), 123 HasBondOptions: hasBondOptions(options), 124 IsAlias: isAlias(words[1]), 125 IsBridged: hasBridgePortsOption(options), 126 IsVLAN: isVLAN(options), 127 Options: options, 128 }, nil 129 } 130 131 func (p parser) parseMappingStanza() (*MappingStanza, error) { 132 s := p.newStanzaBase() 133 words := strings.Fields(p.scanner.line) 134 if len(words) < 2 { 135 return nil, newParseError(p.scanner, "missing device name") 136 } 137 138 options := p.parseOptions() 139 140 return &MappingStanza{ 141 stanza: *s, 142 DeviceNames: words[1:], 143 Options: options, 144 }, nil 145 } 146 147 func (p parser) parseNoAutoDownStanza() (*NoAutoDownStanza, error) { 148 words := strings.Fields(p.scanner.line) 149 if len(words) < 2 { 150 return nil, newParseError(p.scanner, "missing device name") 151 } 152 return &NoAutoDownStanza{ 153 stanza: *p.newStanzaBase(), 154 DeviceNames: words[1:], 155 }, nil 156 } 157 158 func (p parser) parseNoScriptsStanza() (*NoScriptsStanza, error) { 159 words := strings.Fields(p.scanner.line) 160 if len(words) < 2 { 161 return nil, newParseError(p.scanner, "missing device name") 162 } 163 return &NoScriptsStanza{ 164 stanza: *p.newStanzaBase(), 165 DeviceNames: words[1:], 166 }, nil 167 } 168 169 func (p parser) parseSourceStanza() (*SourceStanza, error) { 170 words := strings.Fields(p.scanner.line) 171 if len(words) < 2 { 172 return nil, newParseError(p.scanner, "missing filename") 173 } 174 175 pattern := words[1] 176 177 if !strings.HasPrefix(words[1], "/") { 178 pattern = filepath.Join(filepath.Dir(p.scanner.filename), words[1]) 179 } 180 181 files, err := p.expander.Expand(pattern) 182 183 if err != nil { 184 return nil, newParseError(p.scanner, err.Error()) 185 } 186 187 srcStanza := &SourceStanza{ 188 stanza: *p.newStanzaBase(), 189 Path: words[1], 190 Sources: []string{}, 191 Stanzas: []Stanza{}, 192 } 193 194 for _, file := range files { 195 stanzas, err := parseSource(file, nil, p.expander) 196 if err != nil { 197 return nil, err 198 } 199 srcStanza.Sources = append(srcStanza.Sources, file) 200 srcStanza.Stanzas = append(srcStanza.Stanzas, stanzas...) 201 } 202 203 return srcStanza, nil 204 } 205 206 func (p parser) parseSourceDirectoryStanza() (*SourceDirectoryStanza, error) { 207 words := strings.Fields(p.scanner.line) 208 if len(words) < 2 { 209 return nil, newParseError(p.scanner, "missing directory") 210 } 211 212 expansions, err := p.expander.Expand(words[1]) 213 214 if err != nil { 215 // We want file/line number information so use the 216 // Expand() error as the message but let 217 // newParseError() record on which line it happened. 218 return nil, newParseError(p.scanner, err.Error()) 219 } 220 221 var dir = words[1] 222 223 if len(expansions) > 0 { 224 dir = expansions[0] 225 } 226 227 if !strings.HasPrefix(dir, "/") { 228 // find directory relative to current input file 229 dir = filepath.Join(filepath.Dir(p.scanner.filename), dir) 230 } 231 232 files, err := ioutil.ReadDir(dir) 233 234 if err != nil { 235 return nil, newParseError(p.scanner, err.Error()) 236 } 237 238 dirStanza := &SourceDirectoryStanza{ 239 stanza: *p.newStanzaBase(), 240 Path: words[1], 241 Sources: []string{}, 242 Stanzas: []Stanza{}, 243 } 244 245 for _, file := range files { 246 if !validSourceDirectoryFilename.MatchString(file.Name()) { 247 continue 248 } 249 path := filepath.Join(dir, file.Name()) 250 stanzas, err := parseSource(path, nil, p.expander) 251 if err != nil { 252 return nil, err 253 } 254 dirStanza.Sources = append(dirStanza.Sources, path) 255 dirStanza.Stanzas = append(dirStanza.Stanzas, stanzas...) 256 } 257 258 return dirStanza, nil 259 } 260 261 func (p parser) parseInput() ([]Stanza, error) { 262 stanzas := []Stanza{} 263 264 for { 265 if !p.scanner.nextLine() { 266 break 267 } 268 269 switch stanzaType(p.scanner.line) { 270 case allow: 271 allowStanza, err := p.parseAllowStanza() 272 if err != nil { 273 return nil, err 274 } 275 stanzas = append(stanzas, *allowStanza) 276 case auto: 277 autoStanza, err := p.parseAutoStanza() 278 if err != nil { 279 return nil, err 280 } 281 stanzas = append(stanzas, *autoStanza) 282 case iface: 283 ifaceStanza, err := p.parseIfaceStanza() 284 if err != nil { 285 return nil, err 286 } 287 stanzas = append(stanzas, *ifaceStanza) 288 case mapping: 289 mappingStanza, err := p.parseMappingStanza() 290 if err != nil { 291 return nil, err 292 } 293 stanzas = append(stanzas, *mappingStanza) 294 case noAutoDown: 295 noAutoDownStanza, err := p.parseNoAutoDownStanza() 296 if err != nil { 297 return nil, err 298 } 299 stanzas = append(stanzas, *noAutoDownStanza) 300 case noscripts: 301 noScriptsStanza, err := p.parseNoScriptsStanza() 302 if err != nil { 303 return nil, err 304 } 305 stanzas = append(stanzas, *noScriptsStanza) 306 case source: 307 sourceStanza, err := p.parseSourceStanza() 308 if err != nil { 309 return nil, err 310 } 311 stanzas = append(stanzas, *sourceStanza) 312 case sourceDirectory: 313 sourceDirectoryStanza, err := p.parseSourceDirectoryStanza() 314 if err != nil { 315 return nil, err 316 } 317 stanzas = append(stanzas, *sourceDirectoryStanza) 318 default: 319 return nil, newParseError(p.scanner, "misplaced option") 320 } 321 } 322 323 return stanzas, nil 324 } 325 326 func stanzaType(definition string) kind { 327 words := strings.Fields(definition) 328 if len(words) > 0 { 329 switch words[0] { 330 case "auto": 331 return auto 332 case "iface": 333 return iface 334 case "mapping": 335 return mapping 336 case "no-auto-down": 337 return noAutoDown 338 case "no-scripts": 339 return noscripts 340 case "source": 341 return source 342 case "source-directory": 343 return sourceDirectory 344 } 345 if strings.HasPrefix(words[0], "allow-") { 346 return allow 347 } 348 } 349 return unknown 350 } 351 352 // If input is not nil, parseSource parses the source from input; the 353 // filename is only used when recording position information. The type 354 // of the argument for the input parameter must be string, []byte, or 355 // io.Reader. If input == nil, Parse parses the file specified by 356 // filename. 357 // 358 // If the source could not be read, then Stanzas is nil and the error 359 // indicates the specific failure. 360 func parseSource(filename string, src interface{}, wordExpander WordExpander) ([]Stanza, error) { 361 scanner, err := newScanner(filename, src) 362 363 if err != nil { 364 return nil, err 365 } 366 367 p := parser{ 368 expander: wordExpander, 369 scanner: scanner, 370 } 371 372 return p.parseInput() 373 } 374 375 func hasOptionIdent(ident string, options []string) bool { 376 for _, o := range options { 377 words := strings.Fields(o) 378 if len(words) > 0 && words[0] == ident { 379 return true 380 } 381 } 382 return false 383 } 384 385 func hasOptionPrefix(prefix string, options []string) bool { 386 for _, o := range options { 387 words := strings.Fields(o) 388 for _, w := range words { 389 if strings.HasPrefix(w, prefix) { 390 return true 391 } 392 } 393 } 394 return false 395 } 396 397 func hasBridgePortsOption(options []string) bool { 398 return hasOptionIdent("bridge_ports", options) 399 } 400 401 func isVLAN(options []string) bool { 402 return hasOptionIdent("vlan-raw-device", options) 403 } 404 405 func hasBondOptions(options []string) bool { 406 return hasOptionPrefix("bond-", options) 407 } 408 409 func hasBondMasterOption(options []string) bool { 410 return hasOptionIdent("bond-master", options) 411 } 412 413 func isAlias(name string) bool { 414 return strings.Contains(name, ":") 415 } 416 417 // Parse parses the definitions of a single Debian style network 418 // interfaces(5) file and returns the corresponding set of stanza 419 // definitions. 420 func Parse(filename string) ([]Stanza, error) { 421 return parseSource(filename, nil, newWordExpander()) 422 }