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  }