GitLab upgraded to 13.7.4-ee.0 - changelog: https://gitlab.com/gitlab-org/gitlab/blob/master/CHANGELOG-EE.md

cluster.rb 10.7 KB
Newer Older
Michael Klishin's avatar
Michael Klishin committed
1 2
# frozen_string_literal: true
#
3
# Cookbook:: rabbitmq
Michael Klishin's avatar
Michael Klishin committed
4 5 6
# Provider:: cluster
#
# Author: Sunggun Yu <sunggun.dev@gmail.com>
7
# Copyright:: (C) 2015 Sunggun Yu
Michael Klishin's avatar
Michael Klishin committed
8 9 10 11 12
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
Spring Operator's avatar
Spring Operator committed
13
#     https://www.apache.org/licenses/LICENSE-2.0
Michael Klishin's avatar
Michael Klishin committed
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
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

include RabbitMQ::CoreHelpers

# Get ShellOut
def get_shellout(cmd)
  sh_cmd = Mixlib::ShellOut.new(cmd, env: shell_environment)
  sh_cmd
end

# Execute rabbitmqctl command with args
def run_rabbitmqctl(*args)
  cmd = "rabbitmqctl #{args.join(' ')}"
  Chef::Log.debug("[rabbitmq_cluster] Executing #{cmd}")
  cmd = get_shellout(cmd)
  cmd.run_command
  begin
    cmd.error!
    Chef::Log.debug("[rabbitmq_cluster] #{cmd.stdout}")
  rescue
Michael Klishin's avatar
Michael Klishin committed
40
    raise("[rabbitmq_cluster] #{cmd.stderr}")
Michael Klishin's avatar
Michael Klishin committed
41 42 43 44 45
  end
end

# Get cluster status result
def cluster_status
46 47 48 49 50 51
  # Default formatting changed to "table" in 3.8, need to explicity specify
  # "erlang" to parse output properly.
  installed_version = Gem::Version.new(installed_rabbitmq_version)
  version_requiring_formatter = Gem::Version.new('3.8.0')
  cmd = +'rabbitmqctl -q cluster_status'
  cmd << ' --formatter erlang' if installed_version >= version_requiring_formatter
Michael Klishin's avatar
Michael Klishin committed
52 53 54 55 56 57 58
  # execute > rabbitmqctl cluster_status"
  # This removes an optional "... Done" linee that older version used to output
  Chef::Log.debug("[rabbitmq_cluster] Executing #{cmd}")
  cmd = get_shellout(cmd)
  cmd.run_command
  cmd.error!
  result = cmd.stdout.squeeze(' ').gsub(/\n */, '').gsub('...done.', '')
59
  Chef::Log.debug("[rabbitmq_cluster] #{cmd} : #{result}")
Michael Klishin's avatar
Michael Klishin committed
60 61 62 63 64 65
  result
end

# Match regex pattern from result of rabbitmqctl cluster_status
def match_pattern_cluster_status(cluster_status, pattern)
  if cluster_status.nil? || cluster_status.to_s.empty?
Michael Klishin's avatar
Michael Klishin committed
66
    raise('[rabbitmq_cluster] cluster_status should not be empty')
Michael Klishin's avatar
Michael Klishin committed
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
  end
  match = cluster_status.match(pattern)
  match && match[2]
end

# Get currently joined cluster name from result string of "rabbitmqctl cluster_status"
def current_cluster_name(cluster_status)
  pattern = '({cluster_name,<<")(.*?)(">>})'
  result = match_pattern_cluster_status(cluster_status, pattern)
  Chef::Log.debug("[rabbitmq_cluster] current_cluster_name : #{result}")
  result
end

# Get running nodes
def running_nodes(cluster_status)
  pattern = '({running_nodes,\[\'*)(.*?)(\'*\]})'
  match = match_pattern_cluster_status(cluster_status, pattern)
  result = match && match.delete("'")
  Chef::Log.debug("[rabbitmq_cluster] running_nodes : #{result}")
  result.nil? ? [] : result
end

# Get disc nodes
def disc_nodes(cluster_status)
  pattern = '({disc,\[\'*)(.*?)(\'*\]})'
  match = match_pattern_cluster_status(cluster_status, pattern)
  result = match && match.delete("'").split(',')
  Chef::Log.debug("[rabbitmq_cluster] disc_nodes : #{result}")
  result.nil? ? [] : result
end

# Get ram nodes
def ram_nodes(cluster_status)
  pattern = '({ram,\[\'*)(.*?)(\'*\]})'
  match = match_pattern_cluster_status(cluster_status, pattern)
  result = match && match.delete("'").split(',')
  Chef::Log.debug("[rabbitmq_cluster] ram_nodes : #{result}")
  result.nil? ? [] : result
end

# Get node name
def node_name
  # execute > rabbitmqctl eval 'node().'
  cmd = 'rabbitmqctl eval "node()." | head -1'
  Chef::Log.debug("[rabbitmq_cluster] Executing #{cmd}")
  cmd = get_shellout(cmd)
  cmd.run_command
  cmd.error!
  result = cmd.stdout.chomp.delete("'")
  Chef::Log.debug("[rabbitmq_cluster] node name : #{result}")
  result
end

# Get cluster_node_type of current node
def current_cluster_node_type(node_name, cluster_status)
  var_cluster_node_type = ''
  if disc_nodes(cluster_status).include?(node_name)
    var_cluster_node_type = 'disc'
  elsif ram_nodes(cluster_status).include?(node_name)
    var_cluster_node_type = 'ram'
  end
  Chef::Log.debug("[rabbitmq_cluster] current cluster node type : #{var_cluster_node_type}")
  var_cluster_node_type
end

# Parse hash string of cluster_nodes to a JSON object
def parse_cluster_nodes_string(cluster_nodes)
  JSON.parse(cluster_nodes.gsub('=>', ':'))
end

# Check whether the node has joined the cluster
def joined_cluster?(node_name, cluster_status)
  (running_nodes(cluster_status) || '').include?(node_name)
end

# Custom exception for join errors
class JoinError < StandardError
end

# Join cluster.
def join_cluster(cluster_name, type)
  cmd = "rabbitmqctl join_cluster #{type == 'ram' ? '--ram' : ''} #{cluster_name}".squeeze(' ')
  Chef::Log.debug("[rabbitmq_cluster] Executing #{cmd}")
  cmd = get_shellout(cmd)
  cmd.run_command
  begin
    cmd.error!
    Chef::Log.info("[rabbitmq_cluster] #{cmd.stdout}")
  rescue
    err = cmd.stderr
    Chef::Log.warn("[rabbitmq_cluster] #{err}")
    if err.include?('{ok,already_member}')
      Chef::Log.info('[rabbitmq_cluster] Node is already a member of the cluster, error will be ignored.')
    elsif err.include?('cannot_cluster_node_with_itself')
      Chef::Log.info('[rabbitmq_cluster] Cannot cluster node itself, error will be ignored.')
    else
      # Don't fatal here, raise JoinError so that Rabbit can be restarted for the next run
      raise JoinError, err
    end
  end
end

# Change cluster node type
def change_cluster_node_type(cluster_node_type)
  cmd = "rabbitmqctl change_cluster_node_type #{cluster_node_type}"
  Chef::Log.debug("[rabbitmq_cluster] Executing #{cmd}")
  cmd = get_shellout(cmd)
  cmd.run_command
  begin
    cmd.error!
    Chef::Log.debug("[rabbitmq_cluster] #{cmd.stdout}")
  rescue
    err = cmd.stderr
    Chef::Log.warn("[rabbitmq_cluster] #{err}")
    if err.include?('{not_clustered,"Non-clustered nodes can only be disc nodes."}')
      Chef::Log.info('[rabbitmq_cluster] Node is not clustered yet, error will be ignored.')
    else
Michael Klishin's avatar
Michael Klishin committed
184
      raise("[rabbitmq_cluster] #{err}")
Michael Klishin's avatar
Michael Klishin committed
185 186 187 188 189 190 191
    end
  end
end

########################################################################################################################
# Actions
#  :join
Michael Klishin's avatar
Michael Klishin committed
192
#  :set_cluster_name
Michael Klishin's avatar
Michael Klishin committed
193 194 195 196 197 198 199
#  :change_cluster_node_type
########################################################################################################################

# Action for joining cluster
action :join do
  Chef::Log.info('[rabbitmq_cluster] Action join ... ')

Michael Klishin's avatar
Michael Klishin committed
200
  raise('rabbitmq_cluster with action :join requires a non-nil/empty cluster_nodes.') if new_resource.cluster_nodes.nil? || new_resource.cluster_nodes.empty?
Michael Klishin's avatar
Michael Klishin committed
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

  var_cluster_status = cluster_status
  var_node_name = node_name
  var_node_name_to_join = parse_cluster_nodes_string(new_resource.cluster_nodes).first['name']
  var_node_type = parse_cluster_nodes_string(new_resource.cluster_nodes).first['type']
  var_cluster_name = new_resource.cluster_name

  if var_node_name == var_node_name_to_join
    Chef::Log.warn('[rabbitmq_cluster] This node was configured to join itself. Skipping.')
  elsif joined_cluster?(var_node_name, var_cluster_status) && current_cluster_name(var_cluster_status) == var_cluster_name
    Chef::Log.warn("[rabbitmq_cluster] Node is already member of the target cluster #{current_cluster_name(var_cluster_status)}. Skipping.")
  else
    if joined_cluster?(var_node_name, var_cluster_status) && current_cluster_name(var_cluster_status) != var_cluster_name
      unless var_cluster_name.nil?
        Chef::Log.warn("[rabbitmq_cluster] Node is already member of #{current_cluster_name(var_cluster_status)}. Rejoining the desired cluster.")
      end
    end
    run_rabbitmqctl('stop_app')

    # Catch JoinError so that we can leave Rabbit started, if possible
    begin
      join_cluster(var_node_name_to_join, var_node_type)
    rescue JoinError => exc
Michael Klishin's avatar
Michael Klishin committed
224
      raise("[rabbitmq_cluster] #{exc.message}")
Michael Klishin's avatar
Michael Klishin committed
225 226 227 228 229 230 231 232 233 234 235
    ensure
      run_rabbitmqctl('start_app')
    end

    Chef::Log.info("[rabbitmq_cluster] Node #{var_node_name} joined in #{var_node_name_to_join} with type #{var_node_type}")
    Chef::Log.info(cluster_status)
  end
end

# Action for set cluster name
action :set_cluster_name do
Michael Klishin's avatar
Michael Klishin committed
236
  raise('rabbitmq_cluster with action :set_cluster_name requires a non-nil/empty cluster_nodes.') if new_resource.cluster_nodes.nil? || new_resource.cluster_nodes.empty?
Michael Klishin's avatar
Michael Klishin committed
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
  var_cluster_status = cluster_status
  var_cluster_name = new_resource.cluster_name
  if current_cluster_name(var_cluster_status).nil?
    Chef::Log.warn('[rabbitmq_cluster] Currently not a cluster. Set cluster name will be skipped.')
  else
    unless current_cluster_name(var_cluster_status) == var_cluster_name
      unless var_cluster_name.empty?
        run_rabbitmqctl("set_cluster_name #{var_cluster_name}")
        Chef::Log.info("[rabbitmq_cluster] Cluster name has been set : #{current_cluster_name(cluster_status)}")
      end
    end
  end
end

# Action for changing cluster node type
action :change_cluster_node_type do
  Chef::Log.info('[rabbitmq_cluster] Action change_cluster_node_type ... ')

Michael Klishin's avatar
Michael Klishin committed
255
  raise('rabbitmq_cluster with action :join requires a non-nil/empty cluster_nodes.') if new_resource.cluster_nodes.nil? || new_resource.cluster_nodes.empty?
Michael Klishin's avatar
Michael Klishin committed
256 257 258 259 260 261 262

  var_cluster_status = cluster_status
  var_node_name = node_name
  var_current_cluster_node_type = current_cluster_node_type(var_node_name, var_cluster_status)
  var_cluster_node_type = parse_cluster_nodes_string(new_resource.cluster_nodes).compact.select { |node| node['name'] == var_node_name }.first['type'] # ~FC039

  if var_current_cluster_node_type == var_cluster_node_type
Michael Klishin's avatar
Michael Klishin committed
263
    Chef::Log.warn('[rabbitmq_cluster] Skip changing cluster node type: the node is already of the desired type')
Michael Klishin's avatar
Michael Klishin committed
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
    node_type_changeable = false
  else
    if var_cluster_node_type == 'ram' # rubocop:disable all
      if var_current_cluster_node_type == 'disc' && disc_nodes(var_cluster_status).length < 2
        Chef::Log.warn('[rabbitmq_cluster] At least one disc node is required for rabbitmq cluster. Changing cluster node type will be ignored.')
        node_type_changeable = false
      else
        node_type_changeable = true
      end
    elsif var_cluster_node_type == 'disc'
      node_type_changeable = true
    else
      Chef::Log.warn("[rabbitmq_cluster] Unexpected cluster_note_type #{var_cluster_node_type}. Changing cluster node type will be ignored.")
      node_type_changeable = false
    end
  end

  # Change cluster node type
  if node_type_changeable
    run_rabbitmqctl('stop_app')
    change_cluster_node_type(var_cluster_node_type)
    run_rabbitmqctl('start_app')
    Chef::Log.info("[rabbitmq_cluster] The cluster node type of #{var_node_name} has been changed into #{var_cluster_node_type}")
    Chef::Log.info(cluster_status)
  end
end