class Puppet::ModuleTool::Applications::Installer

Public Class Methods

new(name, install_dir, options = {}) click to toggle source
   # File lib/puppet/module_tool/applications/installer.rb
23 def initialize(name, install_dir, options = {})
24   super(options)
25 
26   @action              = :install
27   @environment         = options[:environment_instance]
28   @ignore_dependencies = forced? || options[:ignore_dependencies]
29   @name                = name
30   @install_dir         = install_dir
31 
32   Puppet::Forge::Cache.clean
33 
34   @local_tarball = Puppet::FileSystem.exist?(name)
35 
36   if @local_tarball
37     release = local_tarball_source.release
38     @name = release.name
39     options[:version] = release.version.to_s
40     SemanticPuppet::Dependency.add_source(local_tarball_source)
41 
42     # If we're operating on a local tarball and ignoring dependencies, we
43     # don't need to search any additional sources.  This will cut down on
44     # unnecessary network traffic.
45     unless @ignore_dependencies
46       SemanticPuppet::Dependency.add_source(installed_modules_source)
47       SemanticPuppet::Dependency.add_source(module_repository)
48     end
49 
50   else
51     SemanticPuppet::Dependency.add_source(installed_modules_source) unless forced?
52     SemanticPuppet::Dependency.add_source(module_repository)
53   end
54 end

Public Instance Methods

run() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
 56 def run
 57   name = @name.tr('/', '-')
 58   version = options[:version] || '>= 0.0.0'
 59 
 60   results = { :action => :install, :module_name => name, :module_version => version }
 61 
 62   begin
 63     if !@local_tarball && name !~ /-/
 64       raise InvalidModuleNameError.new(module_name: @name, suggestion: "puppetlabs-#{@name}", action: :install)
 65     end
 66 
 67     installed_module = installed_modules[name]
 68     if installed_module
 69       unless forced?
 70         if Puppet::Module.parse_range(version).include? installed_module.version
 71           results[:result] = :noop
 72           results[:version] = installed_module.version
 73           return results
 74         else
 75           changes = Checksummer.run(installed_modules[name].mod.path) rescue []
 76           raise AlreadyInstalledError,
 77             :module_name       => name,
 78             :installed_version => installed_modules[name].version,
 79             :requested_version => options[:version] || :latest,
 80             :local_changes     => changes
 81         end
 82       end
 83     end
 84 
 85     @install_dir.prepare(name, options[:version] || 'latest')
 86     results[:install_dir] = @install_dir.target
 87 
 88     unless @local_tarball && @ignore_dependencies
 89       Puppet.notice _("Downloading from %{host} ...") % {
 90         host: mask_credentials(module_repository.host)
 91       }
 92     end
 93 
 94     if @ignore_dependencies
 95       graph = build_single_module_graph(name, version)
 96     else
 97       graph = build_dependency_graph(name, version)
 98     end
 99 
100     unless forced?
101       add_module_name_constraints_to_graph(graph)
102     end
103 
104     installed_modules.each do |mod, release|
105       mod = mod.tr('/', '-')
106       next if mod == name
107 
108       version = release.version
109 
110       unless forced?
111         # Since upgrading already installed modules can be troublesome,
112         # we'll place constraints on the graph for each installed module,
113         # locking it to upgrades within the same major version.
114         installed_range = ">=#{version} #{version.major}.x"
115         graph.add_constraint('installed', mod, installed_range) do |node|
116           Puppet::Module.parse_range(installed_range).include? node.version
117         end
118 
119         release.mod.dependencies.each do |dep|
120           dep_name = dep['name'].tr('/', '-')
121 
122           range = dep['version_requirement']
123           graph.add_constraint("#{mod} constraint", dep_name, range) do |node|
124             Puppet::Module.parse_range(range).include? node.version
125           end
126         end
127       end
128     end
129 
130     # Ensure that there is at least one candidate release available
131     # for the target package.
132     if graph.dependencies[name].empty?
133       raise NoCandidateReleasesError, results.merge(:module_name => name, :source => module_repository.host, :requested_version => options[:version] || :latest)
134     end
135 
136     begin
137       Puppet.info _("Resolving dependencies ...")
138       releases = SemanticPuppet::Dependency.resolve(graph)
139     rescue SemanticPuppet::Dependency::UnsatisfiableGraph => e
140       unsatisfied = nil
141 
142       if e.respond_to?(:unsatisfied) && e.unsatisfied
143         constraints = {}
144         # If the module we're installing satisfies all its
145         # dependencies, but would break an already installed
146         # module that depends on it, show what would break.
147         if name == e.unsatisfied
148           graph.constraints[name].each do |mod, range, _|
149             next unless mod.split.include?('constraint')
150 
151             # If the user requested a specific version or range,
152             # only show the modules with non-intersecting ranges
153             if options[:version]
154               requested_range = SemanticPuppet::VersionRange.parse(options[:version])
155               constraint_range = SemanticPuppet::VersionRange.parse(range)
156 
157               if requested_range.intersection(constraint_range) == SemanticPuppet::VersionRange::EMPTY_RANGE
158                 constraints[mod.split.first] = range
159               end
160             else
161               constraints[mod.split.first] = range
162             end
163           end
164 
165         # If the module fails to satisfy one of its
166         # dependencies, show the unsatisfiable module
167         else
168           dep_constraints = graph.dependencies[name].max.constraints
169 
170           if dep_constraints.key?(e.unsatisfied)
171             unsatisfied_range = dep_constraints[e.unsatisfied].first[1]
172             constraints[e.unsatisfied] = unsatisfied_range
173           end
174         end
175 
176         installed_module = @environment.module_by_forge_name(e.unsatisfied.tr('-', '/'))
177         current_version = installed_module.version if installed_module
178 
179         unsatisfied = {
180           :name => e.unsatisfied,
181           :constraints => constraints,
182           :current_version => current_version
183         } if constraints.any?
184       end
185 
186       raise NoVersionsSatisfyError, results.merge(
187               :requested_name => name,
188               :requested_version => options[:version] || graph.dependencies[name].max.version.to_s,
189               :unsatisfied => unsatisfied
190       )
191     end
192 
193     unless forced?
194       # Check for module name conflicts.
195       releases.each do |rel|
196         installed_module = installed_modules_source.by_name[rel.name.split('-').last]
197         if installed_module
198           next if installed_module.has_metadata? && installed_module.forge_name.tr('/', '-') == rel.name
199 
200           if rel.name != name
201             dependency = {
202               :name => rel.name,
203               :version => rel.version
204             }
205           end
206 
207           raise InstallConflictError,
208             :requested_module  => name,
209             :requested_version => options[:version] || 'latest',
210             :dependency        => dependency,
211             :directory         => installed_module.path,
212             :metadata          => installed_module.metadata
213         end
214       end
215     end
216 
217     Puppet.info _("Preparing to install ...")
218     releases.each { |release| release.prepare }
219 
220     Puppet.notice _('Installing -- do not interrupt ...')
221     releases.each do |release|
222       installed = installed_modules[release.name]
223       if forced? || installed.nil?
224         release.install(Pathname.new(results[:install_dir]))
225       else
226         release.install(Pathname.new(installed.mod.modulepath))
227       end
228     end
229 
230     results[:result] = :success
231     results[:installed_modules] = releases
232     results[:graph] = [ build_install_graph(releases.first, releases) ]
233 
234   rescue ModuleToolError, ForgeError => err
235     results[:error] = {
236       :oneline   => err.message,
237       :multiline => err.multiline,
238     }
239   ensure
240     results[:result] ||= :failure
241   end
242 
243   results
244 end

Private Instance Methods

build_dependency_graph(name, version) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
276 def build_dependency_graph(name, version)
277   SemanticPuppet::Dependency.query(name => version)
278 end
build_install_graph(release, installed, graphed = []) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
280 def build_install_graph(release, installed, graphed = [])
281   graphed << release
282   dependencies = release.dependencies.values.map do |deps|
283     dep = (deps & installed).first
284     unless dep.nil? || graphed.include?(dep)
285       build_install_graph(dep, installed, graphed)
286     end
287   end
288 
289   previous = installed_modules[release.name]
290   previous = previous.version if previous
291   return {
292     :release          => release,
293     :name             => release.name,
294     :path             => release.install_dir.to_s,
295     :dependencies     => dependencies.compact,
296     :version          => release.version,
297     :previous_version => previous,
298     :action           => (previous.nil? || previous == release.version || forced? ? :install : :upgrade),
299   }
300 end
build_single_module_graph(name, version) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
268 def build_single_module_graph(name, version)
269   range = Puppet::Module.parse_range(version)
270   graph = SemanticPuppet::Dependency::Graph.new(name => range)
271   releases = SemanticPuppet::Dependency.fetch_releases(name)
272   releases.each { |release| release.dependencies.clear }
273   graph << releases
274 end
get_release_packages() click to toggle source

Return a Pathname object representing the path to the module release package in the `Puppet.settings`.

    # File lib/puppet/module_tool/applications/installer.rb
306 def get_release_packages
307   get_local_constraints
308 
309   if !forced? && @installed.include?(@module_name)
310     raise AlreadyInstalledError,
311       :module_name       => @module_name,
312       :installed_version => @installed[@module_name].first.version,
313       :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
314       :local_changes     => Puppet::ModuleTool::Applications::Checksummer.run(@installed[@module_name].first.path)
315   end
316 
317   if @ignore_dependencies && @source == :filesystem
318     @urls   = {}
319     @remote = { "#{@module_name}@#{@version}" => { } }
320     @versions = {
321       @module_name => [
322         { :vstring => @version, :semver => SemanticPuppet::Version.parse(@version) }
323       ]
324     }
325   else
326     get_remote_constraints(@forge)
327   end
328 
329   @graph = resolve_constraints({ @module_name => @version })
330   @graph.first[:tarball] = @filename if @source == :filesystem
331   resolve_install_conflicts(@graph) unless forced?
332 
333   # This clean call means we never "cache" the module we're installing, but this
334   # is desired since module authors can easily rerelease modules different content but the same
335   # version number, meaning someone with the old content cached will be very confused as to why
336   # they can't get new content.
337   # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
338   # but for now this is a quick fix to disable caching
339   Puppet::Forge::Cache.clean
340   download_tarballs(@graph, @graph.last[:path], @forge)
341 end
installed_modules() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
264 def installed_modules
265   installed_modules_source.modules
266 end
installed_modules_source() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
260 def installed_modules_source
261   @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment)
262 end
is_module_package?(name) click to toggle source

Check if a file is a vaild module package.


FIXME: Checking for a valid module package should be more robust and use the actual metadata contained in the package. 03132012 - Hightower +++

    # File lib/puppet/module_tool/applications/installer.rb
406 def is_module_package?(name)
407   filename = File.expand_path(name)
408   filename =~ /.tar.gz$/
409 end
local_tarball_source() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
252 def local_tarball_source
253   @tarball_source ||= begin
254     Puppet::ModuleTool::LocalTarball.new(@name)
255   rescue Puppet::Module::Error => e
256     raise InvalidModuleError.new(@name, :action => @action, :error  => e)
257   end
258 end
module_repository() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
248 def module_repository
249   @repo ||= Puppet::Forge.new(Puppet[:module_repository])
250 end
resolve_install_conflicts(graph, is_dependency = false) click to toggle source

Resolve installation conflicts by checking if the requested module or one of its dependencies conflicts with an installed module.

Conflicts occur under the following conditions:

When installing 'puppetlabs-foo' and an existing directory in the target install path contains a 'foo' directory and we cannot determine the “full name” of the installed module.

When installing 'puppetlabs-foo' and 'pete-foo' is already installed. This is considered a conflict because 'puppetlabs-foo' and 'pete-foo' install into the same directory 'foo'.

    # File lib/puppet/module_tool/applications/installer.rb
357 def resolve_install_conflicts(graph, is_dependency = false)
358   Puppet.debug("Resolving conflicts for #{graph.map {|n| n[:module]}.join(',')}")
359 
360   graph.each do |release|
361     @environment.modules_by_path[options[:target_dir]].each do |mod|
362       if mod.has_metadata?
363         metadata = {
364           :name    => mod.forge_name.tr('/', '-'),
365           :version => mod.version
366         }
367         next if release[:module] == metadata[:name]
368       else
369         metadata = nil
370       end
371 
372       if release[:module] =~ /-#{mod.name}$/
373         dependency_info = {
374           :name    => release[:module],
375           :version => release[:version][:vstring]
376         }
377         dependency = is_dependency ? dependency_info : nil
378         all_versions = @versions["#{@module_name}"].sort_by { |h| h[:semver] }
379         versions = all_versions.select { |x| x[:semver].special == '' }
380         versions = all_versions if versions.empty?
381         latest_version = versions.last[:vstring]
382 
383         raise InstallConflictError,
384           :requested_module  => @module_name,
385           :requested_version => @version || "latest: v#{latest_version}",
386           :dependency        => dependency,
387           :directory         => mod.path,
388           :metadata          => metadata
389       end
390     end
391 
392     deps = release[:dependencies]
393     if deps && !deps.empty?
394       resolve_install_conflicts(deps, true)
395     end
396   end
397 end