class Puppet::ModuleTool::Applications::Installer
Public Class Methods
Puppet::ModuleTool::Applications::Application::new
# 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
# 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
# File lib/puppet/module_tool/applications/installer.rb 276 def build_dependency_graph(name, version) 277 SemanticPuppet::Dependency.query(name => version) 278 end
# 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
# 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
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
# File lib/puppet/module_tool/applications/installer.rb 264 def installed_modules 265 installed_modules_source.modules 266 end
# File lib/puppet/module_tool/applications/installer.rb 260 def installed_modules_source 261 @installed ||= Puppet::ModuleTool::InstalledModules.new(@environment) 262 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 406 def is_module_package?(name) 407 filename = File.expand_path(name) 408 filename =~ /.tar.gz$/ 409 end
# 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
# File lib/puppet/module_tool/applications/installer.rb 248 def module_repository 249 @repo ||= Puppet::Forge.new(Puppet[:module_repository]) 250 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 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