summaryrefslogtreecommitdiffstats
path: root/lib/puppet/module.rb
blob: 611cbdd4b6a74b4f584d10736445c8c8970f3f75 (plain)
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
require 'puppet/util/logging'

# Support for modules
class Puppet::Module
    class Error < Puppet::Error; end
    class MissingModule < Error; end
    class IncompatibleModule < Error; end
    class UnsupportedPlatform < Error; end
    class IncompatiblePlatform < Error; end
    class MissingMetadata < Error; end
    class InvalidName < Error; end

    include Puppet::Util::Logging

    TEMPLATES = "templates"
    FILES = "files"
    MANIFESTS = "manifests"
    PLUGINS = "plugins"

    FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS]

    # Return an array of paths by splitting the +modulepath+ config
    # parameter. Only consider paths that are absolute and existing
    # directories
    def self.modulepath(environment = nil)
        Puppet::Node::Environment.new(environment).modulepath
    end

    # Find and return the +module+ that +path+ belongs to. If +path+ is
    # absolute, or if there is no module whose name is the first component
    # of +path+, return +nil+
    def self.find(modname, environment = nil)
        return nil unless modname
        Puppet::Node::Environment.new(environment).module(modname)
    end

    attr_reader :name, :environment
    attr_writer :environment

    attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page

    def has_metadata?
        return false unless metadata_file

        FileTest.exist?(metadata_file)
    end

    def initialize(name, environment = nil)
        @name = name

        assert_validity()

        if environment.is_a?(Puppet::Node::Environment)
            @environment = environment
        else
            @environment = Puppet::Node::Environment.new(environment)
        end

        load_metadata if has_metadata?

        validate_puppet_version
        validate_dependencies
    end

    FILETYPES.each do |type|
        # A boolean method to let external callers determine if
        # we have files of a given type.
        define_method(type +'?') do
            return false unless path
            return false unless FileTest.exist?(subpath(type))
            return true
        end

        # A method for returning a given file of a given type.
        # e.g., file = mod.manifest("my/manifest.pp")
        #
        # If the file name is nil, then the base directory for the
        # file type is passed; this is used for fileserving.
        define_method(type.to_s.sub(/s$/, '')) do |file|
            return nil unless path

            # If 'file' is nil then they're asking for the base path.
            # This is used for things like fileserving.
            if file
                full_path = File.join(subpath(type), file)
            else
                full_path = subpath(type)
            end

            return nil unless FileTest.exist?(full_path)
            return full_path
        end
    end

    def exist?
        ! path.nil?
    end

    # Find the first 'files' directory.  This is used by the XMLRPC fileserver.
    def file_directory
        subpath("files")
    end

    def license_file
        return @license_file if defined?(@license_file)

        return @license_file = nil unless path
        @license_file = File.join(path, "License")
    end

    def load_metadata
        data = PSON.parse File.read(metadata_file)
        [:source, :author, :version, :license, :puppetversion].each do |attr|
            unless value = data[attr.to_s]
                unless attr == :puppetversion
                    raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}"
                end
            end
            send(attr.to_s + "=", value)
        end
    end

    # Return the list of manifests matching the given glob pattern,
    # defaulting to 'init.{pp,rb}' for empty modules.
    def match_manifests(rest)
        pat = File.join(path, MANIFESTS, rest || 'init')
        Dir.
            glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')).
            reject { |f| FileTest.directory?(f) }
    end

    def metadata_file
        return @metadata_file if defined?(@metadata_file)

        return @metadata_file = nil unless path
        @metadata_file = File.join(path, "metadata.json")
    end

    # Find this module in the modulepath.
    def path
        environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.exist?(d) }
    end

    # Find all plugin directories.  This is used by the Plugins fileserving mount.
    def plugin_directory
        subpath("plugins")
    end

    def requires(name, version = nil)
        @requires ||= []
        @requires << [name, version]
    end

    def supports(name, version = nil)
        @supports ||= []
        @supports << [name, version]
    end

    def to_s
        result = "Module #{name}"
        if path
            result += "(#{path})"
        end
        result
    end

    def validate_dependencies
        return unless defined?(@requires)

        @requires.each do |name, version|
            unless mod = environment.module(name)
                raise MissingModule, "Missing module #{name} required by #{self.name}"
            end

            if version and mod.version != version
                raise IncompatibleModule, "Required module #{name} is version #{mod.version} but #{self.name} requires #{version}"
            end
        end
    end

    def validate_puppet_version
        return unless puppetversion and puppetversion != Puppet.version
        raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}"
    end

    private

    def subpath(type)
        return File.join(path, type) unless type.to_s == "plugins"

        return backward_compatible_plugins_dir
    end

    def backward_compatible_plugins_dir
        if dir = File.join(path, "plugins") and FileTest.exist?(dir)
            warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'"
            return dir
        else
            return File.join(path, "lib")
        end
    end

    def assert_validity
        raise InvalidName, "Invalid module name; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/
    end
end