require 'etc' module LeapCli module Commands desc 'Apply recipes to a node or set of nodes.' long_desc 'The FILTER can be the name of a node, service, or tag.' arg_name 'FILTER' command [:deploy, :d] do |c| c.switch :fast, :desc => 'Makes the deploy command faster by skipping some slow steps. A "fast" deploy can be used safely if you recently completed a normal deploy.', :negatable => false c.switch :sync, :desc => "Sync files, but don't actually apply recipes.", :negatable => false c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false c.switch :downgrade, :desc => 'Allows deploy to run with an older platform version.', :negatable => false c.switch :dev, :desc => "Development mode: don't run 'git submodule update' before deploy.", :negatable => false c.flag :tags, :desc => 'Specify tags to pass through to puppet (overriding the default).', :arg_name => 'TAG[,TAG]' c.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT' c.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS' c.action do |global,options,args| run_deploy(global, options, args) end end desc 'Display recent deployment history for a set of nodes.' long_desc 'The FILTER can be the name of a node, service, or tag.' arg_name 'FILTER' command [:history, :h] do |c| c.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT' c.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS' c.switch :last, :desc => 'Show last deploy only', :negatable => false c.action do |global,options,args| run_history(global, options, args) end end private def run_deploy(global, options, args) require 'leap_cli/ssh' if options[:dev] != true init_submodules end nodes = manager.filter!(args, :disabled => false) if nodes.size > 1 say "Deploying to these nodes: #{nodes.keys.join(', ')}" if !global[:yes] && !agree("Continue? ") quit! "OK. Bye." end end environments = nodes.field('environment').uniq if environments.empty? environments = [nil] end environments.each do |env| check_platform_pinning(env, global) end # compile hiera files for all the nodes in every environment that is # being deployed and only those environments. compile_hiera_files(manager.filter(environments), false) log :checking, 'nodes' do SSH.remote_command(nodes, options) do |ssh, host| begin ssh.scripts.check_for_no_deploy ssh.scripts.assert_initialized rescue SSH::ExecuteError # skip nodes with errors, but run others nodes.delete(host.hostname) end end end log :synching, "configuration files" do sync_hiera_config(nodes, options) sync_support_files(nodes, options) end log :synching, "puppet manifests" do sync_puppet_files(nodes, options) end unless options[:sync] log :applying, "puppet" do SSH.remote_command(nodes, options) do |ssh, host| ssh.scripts.puppet_apply( :verbosity => [LeapCli.log_level,5].min, :tags => tags(options), :force => options[:force], :info => deploy_info, :downgrade => options[:downgrade] ) end end end end def run_history(global, options, args) require 'leap_cli/ssh' if options[:last] == true lines = 1 else lines = 10 end nodes = manager.filter!(args) SSH.remote_command(nodes, options) do |ssh, host| ssh.scripts.history(lines) end end def forcible_prompt(forced, msg, prompt) say(msg) if forced log :warning, "continuing anyway because of --force" else say "hint: use --force to skip this prompt." quit!("OK. Bye.") unless agree(prompt) end end # # The currently activated provider.json could have loaded some pinning # information for the platform. If this is the case, refuse to deploy # if there is a mismatch. # # For example: # # "platform": { # "branch": "develop" # "version": "1.0..99" # "commit": "e1d6280e0a8c565b7fb1a4ed3969ea6fea31a5e2..HEAD" # } # def check_platform_pinning(environment, global_options) provider = manager.env(environment).provider return unless provider['platform'] if environment.nil? || environment == 'default' provider_json = 'provider.json' else provider_json = 'provider.' + environment + '.json' end # can we have json schema verification already? unless provider.platform.is_a? Hash bail!('`platform` attribute in #{provider_json} must be a hash (was %s).' % provider.platform.inspect) end # check version if provider.platform['version'] if !Leap::Platform.version_in_range?(provider.platform.version) forcible_prompt( global_options[:force], "The platform is pinned to a version range of '#{provider.platform.version}' "+ "by the `platform.version` property in #{provider_json}, but the platform "+ "(#{Path.platform}) has version #{Leap::Platform.version}.", "Do you really want to deploy from the wrong version? " ) end end # check branch if provider.platform['branch'] if !is_git_directory?(Path.platform) forcible_prompt( global_options[:force], "The platform is pinned to a particular branch by the `platform.branch` property "+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.", "Do you really want to deploy anyway? " ) end unless provider.platform.branch == current_git_branch(Path.platform) forcible_prompt( global_options[:force], "The platform is pinned to branch '#{provider.platform.branch}' by the `platform.branch` property "+ "in #{provider_json}, but the current branch is '#{current_git_branch(Path.platform)}' " + "(for directory '#{Path.platform}')", "Do you really want to deploy from the wrong branch? " ) end end # check commit if provider.platform['commit'] if !is_git_directory?(Path.platform) forcible_prompt( global_options[:force], "The platform is pinned to a particular commit range by the `platform.commit` property "+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.", "Do you really want to deploy anyway? " ) end current_commit = current_git_commit(Path.platform) Dir.chdir(Path.platform) do commit_range = assert_run!("git log --pretty='format:%H' '#{provider.platform.commit}'", "The platform is pinned to a particular commit range by the `platform.commit` property "+ "in #{provider_json}, but git was not able to find commits in the range specified "+ "(#{provider.platform.commit}).") commit_range = commit_range.split("\n") if !commit_range.include?(current_commit) && provider.platform.commit.split('..').first != current_commit forcible_prompt( global_options[:force], "The platform is pinned via the `platform.commit` property in #{provider_json} " + "to a commit in the range #{provider.platform.commit}, but the current HEAD " + "(#{current_commit}) is not in that range.", "Do you really want to deploy from the wrong commit? " ) end end end end def sync_hiera_config(nodes, options) SSH.remote_sync(nodes, options) do |sync, host| node = manager.node(host.hostname) hiera_file = Path.relative_path([:hiera, node.name]) sync.log hiera_file + ' -> ' + node.name + ':' + Leap::Platform.hiera_path sync.source = hiera_file sync.dest = Leap::Platform.hiera_path sync.flags = "-rltp --chmod=u+rX,go-rwx" sync.exec end end # # sync various support files. # def sync_support_files(nodes, options) dest_dir = Leap::Platform.files_dir custom_files = build_custom_file_list SSH.remote_sync(nodes, options) do |sync, host| node = manager.node(host.hostname) files_to_sync = node.file_paths.collect {|path| Path.relative_path(path, Path.provider) } files_to_sync += custom_files if files_to_sync.any? sync.log(files_to_sync.join(', ') + ' -> ' + node.name + ':' + dest_dir) sync.chdir = Path.named_path(:files_dir) sync.source = "." sync.dest = dest_dir sync.excludes = "*" sync.includes = calculate_includes_from_files(files_to_sync, '/files') sync.flags = "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --copy-links" sync.exec end end end def sync_puppet_files(nodes, options) SSH.remote_sync(nodes, options) do |sync, host| sync.log(Path.platform + '/[bin,tests,puppet] -> ' + host.hostname + ':' + Leap::Platform.leap_dir) sync.dest = Leap::Platform.leap_dir sync.source = '.' sync.chdir = Path.platform sync.excludes = '*' sync.includes = ['/bin', '/bin/**', '/puppet', '/puppet/**', '/tests', '/tests/**'] sync.flags = "-rlt --relative --delete --copy-links" sync.exec end end # # ensure submodules are up to date, if the platform is a git # repository. # def init_submodules return unless is_git_directory?(Path.platform) Dir.chdir Path.platform do assert_run! "git submodule sync" statuses = assert_run! "git submodule status" statuses.strip.split("\n").each do |status_line| if status_line =~ /^[\+-]/ submodule = status_line.split(' ')[1] log "Updating submodule #{submodule}" assert_run! "git submodule update --init #{submodule}" end end end end # # converts an array of file paths into an array # suitable for --include of rsync # # if set, `prefix` is stripped off. # def calculate_includes_from_files(files, prefix=nil) return nil unless files and files.any? # prepend '/' (kind of like ^ for rsync) includes = files.collect {|file| file =~ /^\// ? file : '/' + file } # include all sub files of specified directories includes.size.times do |i| if includes[i] =~ /\/$/ includes << includes[i] + '**' end end # include all parent directories (required because of --exclude '*') includes.size.times do |i| path = File.dirname(includes[i]) while(path != '/') includes << path unless includes.include?(path) path = File.dirname(path) end end if prefix includes.map! {|path| path.sub(/^#{Regexp.escape(prefix)}\//, '/')} end return includes end def tags(options) if options[:tags] tags = options[:tags].split(',') else tags = Leap::Platform.default_puppet_tags.dup end tags << 'leap_slow' unless options[:fast] tags.join(',') end # # a provider might have various customization files that should be sync'ed to the server. # this method builds that list of files to sync. # def build_custom_file_list custom_files = [] Leap::Platform.paths.keys.grep(/^custom_/).each do |path| if file_exists?(path) relative_path = Path.relative_path(path, Path.provider) if dir_exists?(path) custom_files << relative_path + '/' # rsync needs trailing slash else custom_files << relative_path end end end return custom_files end def deploy_info info = [] info << "user: %s" % Etc.getpwuid(Process.euid).name if is_git_directory?(Path.platform) && current_git_branch(Path.platform) != 'master' info << "platform: %s (%s %s)" % [ Leap::Platform.version, current_git_branch(Path.platform), current_git_commit(Path.platform)[0..4] ] else info << "platform: %s" % Leap::Platform.version end if is_git_directory?(LEAP_CLI_BASE_DIR) info << "leap_cli: %s (%s %s)" % [ LeapCli::VERSION, current_git_branch(LEAP_CLI_BASE_DIR), current_git_commit(LEAP_CLI_BASE_DIR)[0..4] ] else info << "leap_cli: %s" % LeapCli::VERSION end info.join(', ') end end end