diff --git a/README.markdown b/README.markdown index 338d170a..24e2b652 100644 --- a/README.markdown +++ b/README.markdown @@ -191,6 +191,19 @@ group of the datadir and config_file, defaults to 'root' ##### `override_config_settings` Which configuration variables should be overriden. Hash, defaults to `{}` (empty hash). +##### `cluster_name` +If set, proxysql_servers with the same cluster_name will be automatically added to the same cluster and will synchronize their configuration parameters. +Defaults to undef + +##### `cluster_username` +The username ProxySQL will use to connect to the configured mysql_clusters +Defaults to 'cluster' + +##### `cluster_password` +The password ProxySQL will use to connect to the configured mysql_clusters. Defaults to 'cluster' + +##### `mysql_client_package_name` +The name of the mysql client package in your package manager. Defaults to undef ## Types #### proxy_global_variable @@ -208,6 +221,18 @@ Specifies wheter the resource should be immediately save to disk. Boolean, defau ##### `value` The value of the variable. +#### proxy_cluster +`proxy_cluster` manages an entry in the ProxySQL `proxysql_clusters` admin table. + +##### `name` +The name of the resource. + +##### `hostname` +Hostname of the server. Required. + +##### `port` +Port of the server. Required. Defaults to 3306. + #### proxy_mysql_replication_hostgroup `proxy_mysql_replication_hostgroup` manages an entry in the ProxySQL `mysql_replication_hostgroups` admin table. diff --git a/lib/puppet/provider/proxy_cluster/proxysql.rb b/lib/puppet/provider/proxy_cluster/proxysql.rb new file mode 100644 index 00000000..cd949081 --- /dev/null +++ b/lib/puppet/provider/proxy_cluster/proxysql.rb @@ -0,0 +1,128 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'proxysql')) +Puppet::Type.type(:proxy_cluster).provide(:proxysql, parent: Puppet::Provider::Proxysql) do + desc 'Manage cluster for a ProxySQL instance.' + commands mysql: 'mysql' + + def self.mysql_running + system("mysql #{defaults_file} -NBe 'SELECT 1' >out 2>&1", out: '/dev/null') + end + + # Build a property_hash containing all the discovered information about MySQL + # servers. + def self.instances + instances = [] + if mysql_running + servers = mysql([defaults_file, '-NBe', + 'SELECT `hostname`, `port` FROM `proxysql_servers`'].compact).split(%r{\n}) + + # To reduce the number of calls to MySQL we collect all the properties in + # one big swoop. + servers.each do |line| + hostname, port = line.split(%r{\t}) + query = 'SELECT `hostname`, `port`, `weight`, `comment`' + query << ' FROM `proxysql_servers`' + query << " WHERE `hostname` = '#{hostname}' AND `port` = #{port}" + + @hostname, @port, @weight, @comment = mysql([defaults_file, '-NBe', query].compact).chomp.split(%r{\t}) + name = "#{hostname}:#{port}" + + instances << new( + name: name, + ensure: :present, + hostname: @hostname, + port: @port, + weight: @weight, + comment: @comment + ) + end + end + instances + end + + # We iterate over each proxy_mysql_server entry in the catalog and compare it against + # the contents of the property_hash generated by self.instances + def self.prefetch(resources) + servers = instances + resources.keys.each do |name| + provider = servers.find { |server| server.name == name } + resources[name].provider = provider if provider + end + end + + def create + _name = @resource[:name] + hostname = @resource.value(:hostname) + port = @resource.value(:port) || 6032 + weight = @resource.value(:weight) || 0 + comment = @resource.value(:comment) || '' + + query = 'INSERT INTO proxysql_servers (`hostname`, `port`, `weight`, `comment`)' + query << " VALUES ('#{hostname}', #{port}, #{weight}, '#{comment}')" + mysql([defaults_file, '-e', query].compact) + @property_hash[:ensure] = :present + + exists? ? (return true) : (return false) + end + + def destroy + hostname = @property_hash[:hostname] + port = @property_hash[:port] + query = 'DELETE FROM `proxysql_servers`' + query << " WHERE `hostname` = '#{hostname}' AND `port` = #{port}" + mysql([defaults_file, '-e', query].compact) + + @property_hash.clear + exists? ? (return false) : (return true) + end + + def exists? + @property_hash[:ensure] == :present || false + end + + def initialize(value = {}) + super(value) + @property_flush = {} + end + + def flush + update_server(@property_flush) if @property_flush + @property_hash.clear + + load_to_runtime = @resource[:load_to_runtime] + mysql([defaults_file, '-NBe', 'LOAD PROXYSQL SERVERS TO RUNTIME'].compact) if load_to_runtime == :true + + save_to_disk = @resource[:save_to_disk] + mysql([defaults_file, '-NBe', 'SAVE PROXYSQL SERVERS TO DISK'].compact) if save_to_disk == :true + end + + def update_server(properties) + hostname = @resource.value(:hostname) + port = @resource.value(:port) + + return false if properties.empty? + + values = [] + properties.each do |field, value| + values.push("`#{field}` = '#{value}'") + end + + query = 'UPDATE proxysql_servers SET ' + query << values.join(', ') + query << " WHERE `hostname` = '#{hostname}' AND `port` = #{port}" + mysql([defaults_file, '-e', query].compact) + + @property_hash.clear + exists? ? (return false) : (return true) + end + + # Generates method for all properties of the property_hash + mk_resource_methods + + def weight=(value) + @property_flush[:weight] = value + end + + def comment=(value) + @property_flush[:comment] = value + end +end diff --git a/lib/puppet/type/proxy_cluster.rb b/lib/puppet/type/proxy_cluster.rb new file mode 100644 index 00000000..eb1926d3 --- /dev/null +++ b/lib/puppet/type/proxy_cluster.rb @@ -0,0 +1,52 @@ +# This has to be a separate type to enable collecting +Puppet::Type.newtype(:proxy_cluster) do + @doc = 'Manage a ProxySQL cluster.' + + ensurable + + autorequire(:file) { '/root/.my.cnf' } + autorequire(:class) { 'mysql::client' } + autorequire(:service) { 'proxysql' } + + validate do + raise('name parameter is required.') if (self[:ensure] == :present) && self[:name].nil? + raise('hostname parameter is required.') if (self[:ensure] == :present) && self[:hostname].nil? + raise('port parameter is required.') if (self[:ensure] == :present) && self[:port].nil? + end + + newparam(:name, namevar: true) do + desc 'name for cluster to manage.' + end + + newparam(:load_to_runtime) do + desc 'Load this entry to the active runtime.' + defaultto :true + newvalues(:true, :false) + end + + newparam(:save_to_disk) do + desc 'Perist this entry to the disk.' + defaultto :true + newvalues(:true, :false) + end + + newproperty(:hostname) do + desc 'The hostname of the server.' + newvalue(%r{\w+}) + end + + newproperty(:port) do + desc 'The port of the server.' + newvalue(%r{\d+}) + end + + newproperty(:weight) do + desc 'Currently unused, but in the roadmap for future enhancements.' + newvalue(%r{\d+}) + end + + newproperty(:comment) do + desc 'free form comment field.' + newvalue(%r{[\w+]}) + end +end diff --git a/manifests/init.pp b/manifests/init.pp index bb5b2e0a..8c645fdd 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -101,8 +101,38 @@ # * `override_config_settings` # Which configuration variables should be overriden. Hash, defaults to {} (empty hash). # +# * `cluster_name` +# If set, proxysql_servers with the same cluster_name will be automatically added to the same cluster and will +# synchronize their configuration parameters. Defaults to undef +# +# * `cluster_username` +# The username ProxySQL will use to connect to the configured mysql_clusters +# Defaults to 'cluster' +# +# * `cluster_password` +# The password ProxySQL will use to connect to the configured mysql_clusters. Defaults to 'cluster' +# +# * `mysql_client_package_name` +# The name of the mysql client package in your package manager. Defaults to undef +# +# * `cluster_name` +# If set, proxysql_servers with the same cluster_name will be automatically added to the same cluster and will +# synchronize their configuration parameters. Defaults to undef +# +# * `cluster_username` +# The username ProxySQL will use to connect to the configured mysql_clusters +# Defaults to 'cluster' +# +# * `cluster_password` +# The password ProxySQL will use to connect to the configured mysql_clusters. Defaults to 'cluster' +# +# * `mysql_client_package_name` +# The name of the mysql client package in your package manager. Defaults to undef +# class proxysql ( + Optional[String] $cluster_name = $proxysql::params::cluster_name, String $package_name = $proxysql::params::package_name, + Optional[String] $mysql_client_package_name = $proxysql::params::mysql_client_package_name, String $package_ensure = $proxysql::params::package_ensure, Array[String] $package_install_options = $proxysql::params::package_install_options, String $service_name = $proxysql::params::service_name, @@ -146,7 +176,12 @@ String $sys_owner = $proxysql::params::sys_owner, String $sys_group = $proxysql::params::sys_group, + String $cluster_username = $proxysql::params::cluster_username, + Sensitive[String] $cluster_password = $proxysql::params::cluster_password, + Hash $override_config_settings = {}, + + String $node_name = "${facts['fqdn']}:${admin_listen_port}", ) inherits ::proxysql::params { # lint:ignore:80chars @@ -162,23 +197,38 @@ monitor_password => $monitor_password.unwrap, }, } - $config_settings = deep_merge($proxysql::params::config_settings, $settings, $override_config_settings) + + if $cluster_name { + $settings_cluster = { + admin_variables => { + admin_credentials => "${admin_username}:${admin_password.unwrap};${cluster_username}:${cluster_password.unwrap}", + cluster_username => $cluster_username, + cluster_password => "${cluster_password.unwrap}", + }, + } + } else { + $settings_cluster = undef + } + + $settings_result = deep_merge($settings, $settings_cluster) + + $config_settings = deep_merge($proxysql::params::config_settings, $settings_result, $override_config_settings) # lint:endignore - anchor { '::proxysql::begin': } - -> class { '::proxysql::repo':} - -> class { '::proxysql::install':} - -> class { '::proxysql::config':} - -> class { '::proxysql::service':} - -> class { '::proxysql::admin_credentials':} - -> anchor { '::proxysql::end': } + anchor { 'proxysql::begin': } + -> class { 'proxysql::repo':} + -> class { 'proxysql::install':} + -> class { 'proxysql::config':} + -> class { 'proxysql::service':} + -> class { 'proxysql::admin_credentials':} + -> anchor { 'proxysql::end': } - Class['::proxysql::install'] - ~> Class['::proxysql::service'] + Class['proxysql::install'] + ~> Class['proxysql::service'] if $restart { - Class['::proxysql::config'] - ~> Class['::proxysql::service'] + Class['proxysql::config'] + ~> Class['proxysql::service'] } } diff --git a/manifests/install.pp b/manifests/install.pp index 9f03bd5c..2bc75e16 100644 --- a/manifests/install.pp +++ b/manifests/install.pp @@ -44,6 +44,7 @@ } class { '::mysql::client': + package_name => $proxysql::mysql_client_package_name, bindings_enable => false, } diff --git a/manifests/params.pp b/manifests/params.pp index 01fcae75..bbe1fcd3 100644 --- a/manifests/params.pp +++ b/manifests/params.pp @@ -5,6 +5,7 @@ # class proxysql::params { $package_name = 'proxysql' + $mysql_client_package_name = undef $package_ensure = 'installed' $package_install_options = [] @@ -111,7 +112,6 @@ } } - $monitor_username = 'monitor' $monitor_password = Sensitive('monitor') @@ -128,6 +128,10 @@ $load_to_runtime = true $save_to_disk = true + $cluster_name = undef + $cluster_username = 'cluster' + $cluster_password = Sensitive('cluster') + $config_settings = { datadir => $datadir, admin_variables => { diff --git a/manifests/service.pp b/manifests/service.pp index 44197bda..0ae02080 100644 --- a/manifests/service.pp +++ b/manifests/service.pp @@ -34,8 +34,8 @@ } exec { 'wait_for_admin_socket_to_open': - command => "test -S ${::proxysql::admin_listen_socket}", - unless => "test -S ${::proxysql::admin_listen_socket}", + command => "test -S ${proxysql::admin_listen_socket}", + unless => "test -S ${proxysql::admin_listen_socket}", tries => '3', try_sleep => '10', require => Service[$proxysql::service_name], diff --git a/spec/classes/proxysql_spec.rb b/spec/classes/proxysql_spec.rb index 91b6994d..d0acff44 100644 --- a/spec/classes/proxysql_spec.rb +++ b/spec/classes/proxysql_spec.rb @@ -13,14 +13,16 @@ it { is_expected.to compile.with_all_deps } it { is_expected.to contain_class('proxysql::params') } + it { is_expected.to contain_anchor('proxysql::begin').that_comes_before('Class[proxysql::repo]') } it { is_expected.to contain_class('proxysql::repo').that_comes_before('Class[proxysql::install]') } it { is_expected.to contain_class('proxysql::install').that_comes_before('Class[proxysql::config]') } it { is_expected.to contain_class('proxysql::config').that_comes_before('Class[proxysql::service]') } + it { is_expected.to contain_class('proxysql::service').that_comes_before('Class[proxysql::admin_credentials]') } + it { is_expected.to contain_class('proxysql::admin_credentials').that_comes_before('Anchor[proxysql::end]') } + it { is_expected.to contain_class('proxysql::service').that_subscribes_to('Class[proxysql::install]') } - it { is_expected.to contain_anchor('::proxysql::begin').that_comes_before('Class[proxysql::repo]') } - it { is_expected.to contain_anchor('::proxysql::end') } - it { is_expected.to contain_class('proxysql::service').that_comes_before('Anchor[::proxysql::end]') } + it { is_expected.to contain_anchor('proxysql::end') } it { is_expected.to contain_class('proxysql::install').that_notifies('Class[proxysql::service]') }