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

Public Instance Methods

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

Private Instance Methods

build_dependency_graph(name, version) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
275 def build_dependency_graph(name, version)
276   SemanticPuppet::Dependency.query(name => version)
277 end
build_install_graph(release, installed, graphed = []) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
279 def build_install_graph(release, installed, graphed = [])
280   graphed << release
281   dependencies = release.dependencies.values.map do |deps|
282     dep = (deps & installed).first
283     unless dep.nil? || graphed.include?(dep)
284       build_install_graph(dep, installed, graphed)
285     end
286   end
287 
288   previous = installed_modules[release.name]
289   previous = previous.version if previous
290   return {
291     :release          => release,
292     :name             => release.name,
293     :path             => release.install_dir.to_s,
294     :dependencies     => dependencies.compact,
295     :version          => release.version,
296     :previous_version => previous,
297     :action           => (previous.nil? || previous == release.version || forced? ? :install : :upgrade),
298   }
299 end
build_single_module_graph(name, version) click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
267 def build_single_module_graph(name, version)
268   range = Puppet::Module.parse_range(version)
269   graph = SemanticPuppet::Dependency::Graph.new(name => range)
270   releases = SemanticPuppet::Dependency.fetch_releases(name)
271   releases.each { |release| release.dependencies.clear }
272   graph << releases
273 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
305 def get_release_packages
306   get_local_constraints
307 
308   if !forced? && @installed.include?(@module_name)
309     raise AlreadyInstalledError,
310       :module_name       => @module_name,
311       :installed_version => @installed[@module_name].first.version,
312       :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
313       :local_changes     => Puppet::ModuleTool::Applications::Checksummer.run(@installed[@module_name].first.path)
314   end
315 
316   if @ignore_dependencies && @source == :filesystem
317     @urls   = {}
318     @remote = { "#{@module_name}@#{@version}" => { } }
319     @versions = {
320       @module_name => [
321         { :vstring => @version, :semver => SemanticPuppet::Version.parse(@version) }
322       ]
323     }
324   else
325     get_remote_constraints(@forge)
326   end
327 
328   @graph = resolve_constraints({ @module_name => @version })
329   @graph.first[:tarball] = @filename if @source == :filesystem
330   resolve_install_conflicts(@graph) unless forced?
331 
332   # This clean call means we never "cache" the module we're installing, but this
333   # is desired since module authors can easily rerelease modules different content but the same
334   # version number, meaning someone with the old content cached will be very confused as to why
335   # they can't get new content.
336   # Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
337   # but for now this is a quick fix to disable caching
338   Puppet::Forge::Cache.clean
339   download_tarballs(@graph, @graph.last[:path], @forge)
340 end
installed_modules() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
263 def installed_modules
264   installed_modules_source.modules
265 end
installed_modules_source() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
259 def installed_modules_source
260   @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment)
261 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
405 def is_module_package?(name)
406   filename = File.expand_path(name)
407   filename =~ /.tar.gz$/
408 end
local_tarball_source() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
251 def local_tarball_source
252   @tarball_source ||= begin
253     Puppet::ModuleTool::LocalTarball.new(@name)
254   rescue Puppet::Module::Error => e
255     raise InvalidModuleError.new(@name, :action => @action, :error  => e)
256   end
257 end
module_repository() click to toggle source
    # File lib/puppet/module_tool/applications/installer.rb
247 def module_repository
248   @repo ||= Puppet::Forge.new(Puppet[:module_repository])
249 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
356 def resolve_install_conflicts(graph, is_dependency = false)
357   Puppet.debug("Resolving conflicts for #{graph.map {|n| n[:module]}.join(',')}")
358 
359   graph.each do |release|
360     @environment.modules_by_path[options[:target_dir]].each do |mod|
361       if mod.has_metadata?
362         metadata = {
363           :name    => mod.forge_name.tr('/', '-'),
364           :version => mod.version
365         }
366         next if release[:module] == metadata[:name]
367       else
368         metadata = nil
369       end
370 
371       if release[:module] =~ /-#{mod.name}$/
372         dependency_info = {
373           :name    => release[:module],
374           :version => release[:version][:vstring]
375         }
376         dependency = is_dependency ? dependency_info : nil
377         all_versions = @versions["#{@module_name}"].sort_by { |h| h[:semver] }
378         versions = all_versions.select { |x| x[:semver].special == '' }
379         versions = all_versions if versions.empty?
380         latest_version = versions.last[:vstring]
381 
382         raise InstallConflictError,
383           :requested_module  => @module_name,
384           :requested_version => @version || "latest: v#{latest_version}",
385           :dependency        => dependency,
386           :directory         => mod.path,
387           :metadata          => metadata
388       end
389     end
390 
391     deps = release[:dependencies]
392     if deps && !deps.empty?
393       resolve_install_conflicts(deps, true)
394     end
395   end
396 end