class Puppet::ModuleTool::Applications::Installer
Public Class Methods
Puppet::ModuleTool::Applications::Application::new
# 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
# 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
# File lib/puppet/module_tool/applications/installer.rb 275 def build_dependency_graph(name, version) 276 SemanticPuppet::Dependency.query(name => version) 277 end
# 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
# 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
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
# File lib/puppet/module_tool/applications/installer.rb 263 def installed_modules 264 installed_modules_source.modules 265 end
# File lib/puppet/module_tool/applications/installer.rb 259 def installed_modules_source 260 @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment) 261 end
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
# 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
# File lib/puppet/module_tool/applications/installer.rb 247 def module_repository 248 @repo ||= Puppet::Forge.new(Puppet[:module_repository]) 249 end
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