summaryrefslogtreecommitdiff
path: root/lib/leap_cli/remote/leap_plugin.rb
blob: e6305ae39329a7a4b79fcd0cfa68462bd4d8923a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#
# these methods are made available in capistrano tasks as 'leap.method_name'
# (see RemoteCommand::new_capistrano)
#

module LeapCli; module Remote; module LeapPlugin

  def required_packages
    "puppet rsync lsb-release locales"
  end

  def log(*args, &block)
    LeapCli.logger.log(*args, &block)
  end

  #
  # creates directories that are owned by root and 700 permissions
  #
  def mkdirs(*dirs)
    raise ArgumentError.new('illegal dir name') if dirs.grep(/[\' ]/).any?
    run dirs.collect{|dir| "mkdir -m 700 -p #{dir}; "}.join
  end

  #
  # echos "ok" if the node has been initialized and the required packages are installed, bails out otherwise.
  #
  def assert_initialized
    begin
      test_initialized_file = "test -f #{Leap::Platform.init_path}"
      check_required_packages = "! dpkg-query -W --showformat='${Status}\n' #{required_packages} 2>&1 | grep -q -E '(deinstall|no packages)'"
      run "#{test_initialized_file} && #{check_required_packages} && echo ok"
    rescue Capistrano::CommandError => exc
      LeapCli::Util.bail! do
        exc.hosts.each do |host|
          node = host.to_s.split('.').first
          LeapCli::Util.log :error, "running deploy: node not initialized. Run 'leap node init #{node}'", :host => host
        end
      end
    end
  end

  #
  # bails out the deploy if the file /etc/leap/no-deploy exists.
  # This kind of sucks, because it would be better to skip over nodes that have no-deploy set instead
  # halting the entire deploy. As far as I know, with capistrano, there is no way to close one of the
  # ssh connections in the pool and make sure it gets no further commands.
  #
  def check_for_no_deploy
    begin
      run "test ! -f /etc/leap/no-deploy"
    rescue Capistrano::CommandError => exc
      LeapCli::Util.bail! do
        exc.hosts.each do |host|
          LeapCli::Util.log "Can't continue because file /etc/leap/no-deploy exists", :host => host
        end
      end
    end
  end

  #
  # dumps debugging information
  # #
  def debug
    run "#{Leap::Platform.leap_dir}/bin/debug.sh"
  end

  #
  # dumps the recent deploy history to the console
  #
  def history(lines)
    command = "(test -s /var/log/leap/deploy-summary.log && tail -n #{lines} /var/log/leap/deploy-summary.log) || (test -s /var/log/leap/deploy-summary.log.1 && tail -n #{lines} /var/log/leap/deploy-summary.log.1) || (echo 'no history')"
    run command
  end

  #
  # This is a hairy ugly hack, exactly the kind of stuff that makes ruby
  # dangerous and too much fun for its own good.
  #
  # In most places, we run remote ssh without a current 'task'. This works fine,
  # except that in a few places, the behavior of capistrano ssh is controlled by
  # the options of the current task.
  #
  # We don't want to create an actual current task, because tasks are no fun
  # and can't take arguments or return values. So, when we need to configure
  # things that can only be configured in a task, we use this handy hack to
  # fake the current task.
  #
  # This is NOT thread safe, but could be made to be so with some extra work.
  #
  def with_task(name)
    task = @config.tasks[name]
    @config.class.send(:alias_method, :original_current_task, :current_task)
    @config.class.send(:define_method, :current_task, Proc.new(){ task })
    begin
      yield
    ensure
      @config.class.send(:remove_method, :current_task)
      @config.class.send(:alias_method, :current_task, :original_current_task)
    end
  end

  #
  # similar to run(cmd, &block), but with:
  #
  # * exit codes
  # * stdout and stderr are combined
  #
  def stream(cmd, &block)
    command = '%s 2>&1; echo "exitcode=$?"' % cmd
    run(command) do |channel, stream, data|
      exitcode = nil
      if data =~ /exitcode=(\d+)\n/
        exitcode = $1.to_i
        data.sub!(/exitcode=(\d+)\n/,'')
      end
      yield({:host => channel[:host], :data => data, :exitcode => exitcode})
    end
  end

  #
  # like stream, but capture all the output before returning
  #
  def capture(cmd, &block)
    command = '%s 2>&1; echo "exitcode=$?" 2>&1;' % cmd
    host_data = {}
    run(command) do |channel, stream, data|
      host_data[channel[:host]] ||= ""
      if data =~ /exitcode=(\d+)\n/
        exitcode = $1.to_i
        data.sub!(/exitcode=(\d+)\n/,'')
        host_data[channel[:host]] += data
        yield({:host => channel[:host], :data => host_data[channel[:host]], :exitcode => exitcode})
      else
        host_data[channel[:host]] += data
      end
    end
  end

  #
  # Run a command, with a nice status report and progress indicator.
  # Only successful results are returned, errors are printed.
  #
  # For each successful run on each host, block is yielded with a hash like so:
  #
  # {:host => 'bluejay', :exitcode => 0, :data => 'shell output'}
  #
  def run_with_progress(cmd, &block)
    ssh_failures = []
    exitcode_failures = []
    succeeded = []
    task = LeapCli.logger.log_level > 1 ? :standard_task : :skip_errors_task
    with_task(task) do
      log :querying, 'facts' do
        progress "   "
        call_on_failure do |host|
          ssh_failures << host
          progress 'F'
        end
        capture(cmd) do |response|
          if response[:exitcode] == 0
            progress '.'
            yield response
          else
            exitcode_failures << response
            progress 'F'
          end
        end
      end
    end
    puts "done"
    if ssh_failures.any?
      log :failed, 'to connect to nodes: ' + ssh_failures.join(' ')
    end
    if exitcode_failures.any?
      log :failed, 'to run successfully:' do
        exitcode_failures.each do |response|
          log "[%s] exit %s - %s" % [response[:host], response[:exitcode], response[:data].strip]
        end
      end
    end
  rescue Capistrano::RemoteError => err
    log :error, err.to_s
  end

  private

  def progress(str='.')
    print str
    STDOUT.flush
  end

end; end; end