diff --git a/ext/emacs/puppet-mode.el b/ext/emacs/puppet-mode.el index 258db5b64..9cf9c6466 100644 --- a/ext/emacs/puppet-mode.el +++ b/ext/emacs/puppet-mode.el @@ -1,434 +1,433 @@ ;;; puppet-mode.el --- major mode for Puppet manifests ;; Copyright 2006 David Lutterkort ;; Copyright 2008 Karl Fogel ;; Copyright 2008, 2012 ;; The Board of Trustees of the Leland Stanford Junior University ;; Author: David Lutterkort ;; Russ Allbery ;; Maintainer: Russ Allbery ;; Created: 2006-02-07 ;; Version: 1.2 ;; Keywords: languages ;; This file is part of Puppet. ;; ;; Licensed under the Apache License, Version 2.0 (the "License"); you may not ;; use this file except in compliance with the License. You may obtain a copy ;; of the License at ;; ;; http://www.apache.org/licenses/LICENSE-2.0 ;; ;; Unless required by applicable law or agreed to in writing, software ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the ;; License for the specific language governing permissions and limitations ;; under the License. ;;; Code: (defconst puppet-mode-version "1.2") (defvar puppet-mode-abbrev-table nil "Abbrev table in use in puppet-mode buffers.") (define-abbrev-table 'puppet-mode-abbrev-table ()) (defcustom puppet-indent-level 2 "*Indentation of Puppet statements." :type 'integer :group 'puppet) (defcustom puppet-include-indent 2 "*Indentation of continued Puppet include statements." :type 'integer :group 'puppet) (defvar puppet-mode-map (let ((map (make-sparse-keymap))) (define-key map "\C-j" 'newline-and-indent) (define-key map "\C-m" 'newline-and-indent) map) "Key map used in puppet-mode buffers.") (defvar puppet-mode-syntax-table (let ((table (make-syntax-table))) (modify-syntax-entry ?\' "\"'" table) (modify-syntax-entry ?\" "\"\"" table) (modify-syntax-entry ?# "<" table) (modify-syntax-entry ?\n ">#" table) (modify-syntax-entry ?\\ "\\" table) (modify-syntax-entry ?$ "'" table) (modify-syntax-entry ?- "_" table) (modify-syntax-entry ?: "_" table) (modify-syntax-entry ?> "." table) (modify-syntax-entry ?= "." table) (modify-syntax-entry ?\; "." table) (modify-syntax-entry ?\( "()" table) (modify-syntax-entry ?\) ")(" table) (modify-syntax-entry ?\{ "(}" table) (modify-syntax-entry ?\} "){" table) (modify-syntax-entry ?\[ "(]" table) (modify-syntax-entry ?\] ")[" table) table) "Syntax table in use in puppet-mode buffers.") (defcustom puppet-indent-tabs-mode nil "*Indentation can insert tabs in puppet mode if this is non-nil." :type 'boolean :group 'puppet) (defcustom puppet-comment-column 32 "*Indentation column of comments." :type 'integer :group 'puppet) (defun puppet-count-matches (re start end) "The same as Emacs 22 count-matches, for portability to other versions of Emacs." (save-excursion (let ((n 0)) (goto-char start) (while (re-search-forward re end t) (setq n (1+ n))) n))) (defun puppet-comment-line-p () "Return non-nil iff this line is a comment." (save-excursion (save-match-data (beginning-of-line) (looking-at (format "\\s-*%s" comment-start))))) (defun puppet-block-indent () "If point is in a block, return the indentation of the first line of that block (the line containing the opening brace). Used to set the indentation of the closing brace of a block." (save-excursion (save-match-data (let ((opoint (point)) (apoint (search-backward "{" nil t))) (when apoint ;; This is a bit of a hack and doesn't allow for strings. We really ;; want to parse by sexps at some point. (let ((close-braces (puppet-count-matches "}" apoint opoint)) (open-braces 0)) (while (and apoint (> close-braces open-braces)) (setq apoint (search-backward "{" nil t)) (when apoint (setq close-braces (puppet-count-matches "}" apoint opoint)) (setq open-braces (1+ open-braces))))) (if apoint (current-indentation) nil)))))) (defun puppet-in-array () "If point is in an array, return the position of the opening '[' of that array, else return nil." (save-excursion (save-match-data (let ((opoint (point)) (apoint (search-backward "[" nil t))) (when apoint ;; This is a bit of a hack and doesn't allow for strings. We really ;; want to parse by sexps at some point. (let ((close-brackets (puppet-count-matches "]" apoint opoint)) (open-brackets 0)) (while (and apoint (> close-brackets open-brackets)) (setq apoint (search-backward "[" nil t)) (when apoint (setq close-brackets (puppet-count-matches "]" apoint opoint)) (setq open-brackets (1+ open-brackets))))) apoint))))) (defun puppet-in-include () "If point is in a continued list of include statements, return the position of the initial include plus puppet-include-indent." (save-excursion (save-match-data (let ((include-column nil) (not-found t)) (while not-found (forward-line -1) (cond ((bobp) (setq not-found nil)) ((looking-at "^\\s-*include\\s-+.*,\\s-*$") (setq include-column (+ (current-indentation) puppet-include-indent)) (setq not-found nil)) ((not (looking-at ".*,\\s-*$")) (setq not-found nil)))) include-column)))) (defun puppet-analyze-indent () "Analyze the identation at point and return the discovered indentation level we should use, or nil if we can't determine one." (cond ;; Comment lines are ignored unless we're at the start of the buffer. ((puppet-comment-line-p) (if (bobp) 0 nil)) ;; Closing brace or paren on a line by itself will already be indented to ;; the right level, so we can cheat and stop there. ((looking-at "^\\s-*[\)}]\\s-*$") (current-indentation)) ;; Closing brace or paren not on a line by itself will be indented one ;; level too much, but don't catch cases where the block is started and ;; closed on the same line. ((looking-at "^[^\n\({]*[\)}]\\s-*$") (- (current-indentation) puppet-indent-level)) ;; Closing brace followed by a comma ends a selector within a resource and ;; will be indented just the right amount. Take similar precautions about ;; blocks started and closed on the same line. ((looking-at "^[^\n\({]*},\\s-*$") (current-indentation)) ;; Indent by one level more than the start of our block. We lose if there ;; is more than one block opened and closed on the same line but it's still ;; unbalanced; hopefully people don't do that. ((looking-at "^.*{[^\n}]*$") (+ (current-indentation) puppet-indent-level)) ;; Indent by one level if the line ends with an open paren. ((looking-at "^.*(\\s-*$") (+ (current-indentation) puppet-indent-level)) ;; Semicolon ends a block for a resource when multiple resources are ;; defined in the same block, but try not to get the case of a complete ;; resource on a single line wrong. ((looking-at "^\\([^'\":\n]\\|\"[^\n\"]*\"\\|'[^\n']*'\\)*;\\s-*$") (- (current-indentation) puppet-indent-level)) ;; The line following the end of an array and a : should be indented one ;; level more than the indentation of the start of the array. ((looking-at "^.*\\]\\s-*:\\s-*$") (let ((array-start (puppet-in-array))) (if array-start (save-excursion (beginning-of-line) (goto-char array-start) (+ (current-indentation) puppet-indent-level)) (+ (current-indentation) puppet-indent-level)))) ;; Indent an extra level after : since it introduces a resource. ((looking-at "^.*:\\s-*$") (+ (current-indentation) puppet-indent-level)) ;; Start of buffer. ((bobp) 0))) (defun puppet-do-indent () "Internal function for puppet-indent-line. This does the indent without worrying about saving the excursion." (beginning-of-line) (if (bobp) (indent-line-to 0) ; First line is always non-indented (let ((not-indented t) (array-start (puppet-in-array)) (include-start (puppet-in-include)) (block-indent (puppet-block-indent)) cur-indent) ;; First, check if we started in an array or on a block-ending line. (cond ;; This line probably starts with an element from an array. Indent ;; the line to the same indentation as the first element in that ;; array. That is, this... ;; ;; exec { 'add puppetmaster mongrel startup links': ;; creates => [ 'string2', 'string3', ;; 'string4', 'string5', ;; 'string6', 'string7', ;; 'string8' ], ;; } ;; ;; ...should instead look like this: ;; ;; exec { 'add puppetmaster mongrel startup links': ;; creates => [ 'string2', 'string3', ;; 'string4', 'string5', ;; 'string6', 'string7', ;; 'string8' ], ;; } (array-start (save-excursion (goto-char array-start) (forward-char 1) (if (looking-at "\\s-+\n") (setq cur-indent (1+ (current-column))) (re-search-forward "\\S-") (forward-char -1) (setq cur-indent (current-column))))) ;; Inside an include. (include-start (setq cur-indent include-start)) ;; This line contains a closing brace, a closing brace followed by a ;; comma, or a closing brace followed by else or elsif and we're at ;; the inner block, so we should indent it matching the indentation ;; of the opening brace of the block. ((and (looking-at "^\\s-*}\\(,?\\s-*$\\|\\s-*els\\(e\\|if\\)\\s-\\)") block-indent) (setq cur-indent block-indent)) ;; Otherwise, we did not start on a block-ending-only line, so we ;; have to search backwards for an indentation hint. (t (save-excursion (while (not (progn (forward-line -1) (setq cur-indent (puppet-analyze-indent)))))) ;; If this line contains only a closing paren or a closing paren ;; followed by an opening brace, we added one too many levels of ;; indentation and should lose one level. (if (looking-at "^\\s-*)\\s-*\\({\\s-*\\)?$") (setq cur-indent (- cur-indent puppet-indent-level))))) ;; We've figured out the indentation, so do it. (if (< cur-indent 0) (indent-line-to 0) (indent-line-to cur-indent))))) (defun puppet-indent-line () "Indent current line as Puppet code." (interactive) (if (or (bolp) (save-excursion (beginning-of-line) (save-match-data (looking-at "\\s *\n")))) (puppet-do-indent) (save-excursion (puppet-do-indent)))) (defvar puppet-font-lock-syntax-table (let* ((tbl (copy-syntax-table puppet-mode-syntax-table))) (modify-syntax-entry ?_ "w" tbl) tbl)) ;; Stupid hack required to allow me to assign a default face to something. ;; WTF, font-lock mode? Not required on XEmacs (and breaks XEmacs). (if (not (string-match "XEmacs" emacs-version)) (defvar puppet-font-lock-default-face 'default)) (defvar puppet-font-lock-keywords (list ;; defines, classes, and nodes '("^\\s *\\(class\\|define\\|node\\)\\s +\\([^( \t\n]+\\)" 2 font-lock-function-name-face) ;; inheritence '("\\s +inherits\\s +\\([^( \t\n]+\\)" 1 font-lock-function-name-face) ;; include '("\\(^\\|\\s +\\)include\\s +\\(\\([a-zA-Z0-9:_-]+\\(,[ \t\n]*\\)?\\)+\\)" 2 font-lock-reference-face) ;; variables '("\\$[a-zA-Z0-9_:]+" . font-lock-variable-name-face) ;; usage of types '("^\\s *\\([a-z][a-zA-Z0-9_:-]*\\)\\s +{" 1 font-lock-type-face) ;; overrides, type references, and defaults '("\\(\\s \\|[\\[]\\)\\([A-Z][a-zA-Z0-9_:-]*\\)\\s *[\\[{]" 2 font-lock-type-face) ;; general delimited string '("\\(^\\|[[ \t\n<+(,=]\\)\\(%[xrqQwW]?\\([^<[{(a-zA-Z0-9 \n]\\)[^\n\\\\]*\\(\\\\.[^\n\\\\]*\\)*\\(\\3\\)\\)" 2 font-lock-string-face) ;; keywords (cons (regexp-opt '("alert" "and" "case" "class" "create_resources" "crit" "debug" "default" "define" "defined" "else" "elsif" "emerg" "err" - "extlookup" "fail" "false" "file" "filebucket" "fqdn_rand" "generate" "if" "import" "in" "include" "info" "inherits" "inline_template" "md5" "node" "not" "notice" "or" "realize" "regsubst" "require" "search" "sha1" "shellquote" "split" "sprintf" "tag" "tagged" "template" "true" "undef" "versioncmp" "warning" ) 'words) 1) ;; avoid marking require in resources as a keyword '("\\b\\(require\\)\\s-*=>" 1 puppet-font-lock-default-face t)) "*Additional expressions to highlight in puppet mode.") ;;;###autoload (defun puppet-mode () "Major mode for editing puppet manifests. The variable puppet-indent-level controls the amount of indentation. \\{puppet-mode-map}" (interactive) (kill-all-local-variables) (use-local-map puppet-mode-map) (setq mode-name "Puppet") (setq major-mode 'puppet-mode) (set-syntax-table puppet-mode-syntax-table) (set (make-local-variable 'parse-sexp-ignore-comments) t) (set (make-local-variable 'local-abbrev-table) puppet-mode-abbrev-table) (set (make-local-variable 'comment-start) "# ") (set (make-local-variable 'comment-start-skip) "#+ *") (set (make-local-variable 'comment-use-syntax) t) (set (make-local-variable 'comment-end) "") (set (make-local-variable 'comment-auto-fill-only-comments) t) (set (make-local-variable 'comment-column) puppet-comment-column) (set (make-local-variable 'indent-line-function) 'puppet-indent-line) (set (make-local-variable 'indent-tabs-mode) puppet-indent-tabs-mode) (set (make-local-variable 'require-final-newline) t) (set (make-local-variable 'paragraph-ignore-fill-prefix) t) (set (make-local-variable 'paragraph-start) "\f\\|[ ]*$\\|#$") (set (make-local-variable 'paragraph-separate) "\\([ \f]*\\|#\\)$") (or (boundp 'font-lock-variable-name-face) (setq font-lock-variable-name-face font-lock-type-face)) (set (make-local-variable 'font-lock-keywords) puppet-font-lock-keywords) (set (make-local-variable 'font-lock-multiline) t) (set (make-local-variable 'font-lock-defaults) '((puppet-font-lock-keywords) nil nil)) (set (make-local-variable 'font-lock-syntax-table) puppet-font-lock-syntax-table) (run-hooks 'puppet-mode-hook)) (provide 'puppet-mode) ;;; puppet-mode.el ends here diff --git a/lib/puppet/parser/functions/extlookup.rb b/lib/puppet/parser/functions/extlookup.rb deleted file mode 100644 index 359c9b452..000000000 --- a/lib/puppet/parser/functions/extlookup.rb +++ /dev/null @@ -1,153 +0,0 @@ -require 'csv' - -module Puppet::Parser::Functions - newfunction(:extlookup, - :type => :rvalue, - :arity => -2, - :doc => "This is a parser function to read data from external files, this version -uses CSV files but the concept can easily be adjust for databases, yaml -or any other queryable data source. - -The object of this is to make it obvious when it's being used, rather than -magically loading data in when a module is loaded I prefer to look at the code -and see statements like: - - $snmp_contact = extlookup(\"snmp_contact\") - -The above snippet will load the snmp_contact value from CSV files, this in its -own is useful but a common construct in puppet manifests is something like this: - - case $domain { - \"myclient.com\": { $snmp_contact = \"John Doe \" } - default: { $snmp_contact = \"My Support \" } - } - -Over time there will be a lot of this kind of thing spread all over your manifests -and adding an additional client involves grepping through manifests to find all the -places where you have constructs like this. - -This is a data problem and shouldn't be handled in code, and using this function you -can do just that. - -First you configure it in site.pp: - - $extlookup_datadir = \"/etc/puppet/manifests/extdata\" - $extlookup_precedence = [\"%{fqdn}\", \"domain_%{domain}\", \"common\"] - -The array tells the code how to resolve values, first it will try to find it in -web1.myclient.com.csv then in domain_myclient.com.csv and finally in common.csv - -Now create the following data files in /etc/puppet/manifests/extdata: - - domain_myclient.com.csv: - snmp_contact,John Doe - root_contact,support@%{domain} - client_trusted_ips,192.168.1.130,192.168.10.0/24 - - common.csv: - snmp_contact,My Support - root_contact,support@my.com - -Now you can replace the case statement with the simple single line to achieve -the exact same outcome: - - $snmp_contact = extlookup(\"snmp_contact\") - -The above code shows some other features, you can use any fact or variable that -is in scope by simply using %{varname} in your data files, you can return arrays -by just having multiple values in the csv after the initial variable name. - -In the event that a variable is nowhere to be found a critical error will be raised -that will prevent your manifest from compiling, this is to avoid accidentally putting -in empty values etc. You can however specify a default value: - - $ntp_servers = extlookup(\"ntp_servers\", \"1.${country}.pool.ntp.org\") - -In this case it will default to \"1.${country}.pool.ntp.org\" if nothing is defined in -any data file. - -You can also specify an additional data file to search first before any others at use -time, for example: - - $version = extlookup(\"rsyslog_version\", \"present\", \"packages\") - package{\"rsyslog\": ensure => $version } - -This will look for a version configured in packages.csv and then in the rest as configured -by $extlookup_precedence if it's not found anywhere it will default to `present`, this kind -of use case makes puppet a lot nicer for managing large amounts of packages since you do not -need to edit a load of manifests to do simple things like adjust a desired version number. - -Precedence values can have variables embedded in them in the form %{fqdn}, you could for example do: - - $extlookup_precedence = [\"hosts/%{fqdn}\", \"common\"] - -This will result in /path/to/extdata/hosts/your.box.com.csv being searched. - -This is for back compatibility to interpolate variables with %. % interpolation is a workaround for a problem that has been fixed: Puppet variable interpolation at top scope used to only happen on each run.") do |args| - - key = args[0] - - default = args[1] - datafile = args[2] - - raise ArgumentError, ("extlookup(): wrong number of arguments (#{args.length}; must be <= 3)") if args.length > 3 - - extlookup_datadir = undef_as('',self['::extlookup_datadir']) - - extlookup_precedence = undef_as([],self['::extlookup_precedence']).collect { |var| var.gsub(/%\{(.+?)\}/) { self["::#{$1}"] } } - - datafiles = Array.new - - # if we got a custom data file, put it first in the array of search files - if datafile != "" - datafiles << extlookup_datadir + "/#{datafile}.csv" if Puppet::FileSystem.exist?(extlookup_datadir + "/#{datafile}.csv") - end - - extlookup_precedence.each do |d| - datafiles << extlookup_datadir + "/#{d}.csv" - end - - desired = nil - - datafiles.each do |file| - if desired.nil? - if Puppet::FileSystem.exist?(file) - result = CSV.read(file).find_all do |r| - r[0] == key - end - - # return just the single result if theres just one, - # else take all the fields in the csv and build an array - if result.length > 0 - if result[0].length == 2 - val = result[0][1].to_s - - # parse %{}'s in the CSV into local variables using the current scope - while val =~ /%\{(.+?)\}/ - val.gsub!(/%\{#{$1}\}/, self[$1]) - end - - desired = val - elsif result[0].length > 1 - length = result[0].length - cells = result[0][1,length] - - # Individual cells in a CSV result are a weird data type and throws - # puppets yaml parsing, so just map it all to plain old strings - desired = cells.map do |c| - # parse %{}'s in the CSV into local variables using the current scope - while c =~ /%\{(.+?)\}/ - c.gsub!(/%\{#{$1}\}/, self[$1]) - end - - c.to_s - end - end - end - end - end - end - - desired || default or raise Puppet::ParseError, "No match found for '#{key}' in any data file during extlookup()" - end -end diff --git a/spec/unit/parser/functions/extlookup_spec.rb b/spec/unit/parser/functions/extlookup_spec.rb deleted file mode 100755 index 53c22a639..000000000 --- a/spec/unit/parser/functions/extlookup_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -describe "the extlookup function" do - include PuppetSpec::Files - - before :all do - Puppet::Parser::Functions.autoloader.loadall - end - - before :each do - node = Puppet::Node.new('localhost') - compiler = Puppet::Parser::Compiler.new(node) - @scope = Puppet::Parser::Scope.new(compiler) - end - - it "should exist" do - Puppet::Parser::Functions.function("extlookup").should == "function_extlookup" - end - - it "should raise an ArgumentError if there is less than 1 arguments" do - lambda { @scope.function_extlookup([]) }.should( raise_error(ArgumentError)) - end - - it "should raise an ArgumentError if there is more than 3 arguments" do - lambda { @scope.function_extlookup(["foo", "bar", "baz", "gazonk"]) }.should( raise_error(ArgumentError)) - end - - it "should return the default" do - result = @scope.function_extlookup([ "key", "default"]) - result.should == "default" - end - - it "should lookup the key in a supplied datafile" do - dir = tmpdir("extlookup_spec") - File.open("#{dir}/extlookup.csv", "w") do |t| - t.puts 'key,value' - t.puts 'nonkey,nonvalue' - end - - @scope.stubs(:[]).with('::extlookup_datadir').returns(dir) - @scope.stubs(:[]).with('::extlookup_precedence').returns(nil) - result = @scope.function_extlookup([ "key", "default", "extlookup"]) - result.should == "value" - end - - it "should return an array if the datafile contains more than two columns" do - dir = tmpdir("extlookup_spec") - File.open("#{dir}/extlookup.csv", "w") do |t| - t.puts 'key,value1,value2' - t.puts 'nonkey,nonvalue,nonvalue' - end - - @scope.stubs(:[]).with('::extlookup_datadir').returns(dir) - @scope.stubs(:[]).with('::extlookup_precedence').returns(nil) - result = @scope.function_extlookup([ "key", "default", "extlookup"]) - result.should == ["value1", "value2"] - end - - it "should raise an error if there's no matching key and no default" do - dir = tmpdir("extlookup_spec") - File.open("#{dir}/extlookup.csv", "w") do |t| - t.puts 'nonkey,nonvalue' - end - - @scope.stubs(:[]).with('::extlookup_datadir').returns(dir) - @scope.stubs(:[]).with('::extlookup_precedence').returns(nil) - lambda { @scope.function_extlookup([ "key", nil, "extlookup"]) }.should raise_error(Puppet::ParseError, /No match found.*key/) - end - - describe "should look in $extlookup_datadir for data files listed by $extlookup_precedence" do - before do - dir = tmpdir('extlookup_datadir') - @scope.stubs(:[]).with('::extlookup_datadir').returns(dir) - File.open(File.join(dir, "one.csv"),"w"){|one| one.puts "key,value1" } - File.open(File.join(dir, "two.csv"),"w") do |two| - two.puts "key,value2" - two.puts "key2,value_two" - end - end - - it "when the key is in the first file" do - @scope.stubs(:[]).with('::extlookup_precedence').returns(["one","two"]) - result = @scope.function_extlookup([ "key" ]) - result.should == "value1" - end - - it "when the key is in the second file" do - @scope.stubs(:[]).with('::extlookup_precedence').returns(["one","two"]) - result = @scope.function_extlookup([ "key2" ]) - result.should == "value_two" - end - - it "should not modify extlookup_precedence data" do - variable = '%{fqdn}' - @scope.stubs(:[]).with('::extlookup_precedence').returns([variable,"one"]) - @scope.stubs(:[]).with('::fqdn').returns('myfqdn') - result = @scope.function_extlookup([ "key" ]) - variable.should == '%{fqdn}' - end - end -end